5 minute read

秋月電子で売っているきれいなLEDマトリクスでいろいろニュースを表示しつつ、毎朝ラジオ体操をできるようにしてみました。  ※ 2017/9月からNHKのラジオ配信がHLSになったので、記事を修正しました

1) 物品の準備

2) Raspberry Piのセットアップ

  • OSなど
    • この方の記事が分かりやすかったです
    • SDカードに焼いて差し込んで起動するだけなので、自分で当初調べながらやったところ、デフォルトでsshdが上がっていなかったのでだいぶイライラ切り分けに時間を要した。丁寧な方のノウハウを頂きましょう。
  • USBスピーカを有効にして、内蔵の音声出力ジャックを無効化する。  今回LED Matrix制御に利用するHenner Zeller氏作成のライブラリは、Raspi内臓の音声モジュールとは共存できない。このため、USBスピーカをつないで内臓の音声出力ジャックを無効化してやる必要がある。実は若干忘れ気味。USBスピーカを鳴らす方法は、こちらの記事がよさそう。   Raspberry Pi でUSBスピーカーを動かす

    内蔵の音声出力ジャックを無効化する方法はこちらがよさそう。   Disable the Built-in Sound Card of Raspberry Pi

sudo vi alsa-blacklist.conf
でファイルを開いて、以下を入力
blacklist snd_bcm2835

3) Raspiへの結線、およびサンプルコードの稼働

  • LED Matrix と電源アダプタの接続  ここが慣れなくて一番厄介だったはずだがあまり記憶になく。。。とにかく+/ー間違えずに繋げばよい。。。
  • LED Matrix とRaspiの接続
    • Henner Zeller氏の解説のようにやる。「😄」マークを結線のみでよい。 https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/wiring.md
    • 他のアイコンの結線は、もっとLED Matrixを大量に並べるときのためのもの。出来てみるとさほど難しくないのだが、ミスなくraspiとLED側と突合するのが厄介だった

4) Henner Zeller氏製のrpi-rgb-led-matrixのセットアップ

  • Raspiへログインし、ここからコードをすべて持ってくる

https://github.com/hzeller/rpi-rgb-led-matrix

  • その後、以下の手順に準拠して、rpi-rgb-led-matrixのライブラリを導入する

https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/python/README.md

cd (持ってきたコードを展開したディレクトリ)
cd python
sudo apt-get update && sudo apt-get install python2.7-dev python-pillow -y
make build-python
sudo make install-python  
  • 実はCのサンプルコードも同梱されており、そちらの方が速い、というようなことが書いてあるが、やっぱりデータを手軽にいじるにはPythonのような言語がよい。サンプルを走らせて楽しむ。
cd samples
sudo ./runtext.py --led-rows=16 --led-brightness=20

5) コードを作って動かす

  • crawler.py

rss or 某ニュースサイトからニュースを取得してきて配下の「newsimg」フォルダに画像にして保存するもの。予め画像にしておいた方がLED matrixに食わせやすい。なお、予め配下の「font」フォルダにTrueType or OpenTypeのフォントを入れておく必要あり。色々調整したが、以下が好み:

crawler.pyが集めてきた画像をランダムに表示する。画像がなければ待って、見つかった時点で表示を再開する。

  • nhk.sh

livestreamer + mplay2 で NHK第一を聞くシェルスクリプト。上記モジュールがapt-getで導入済みであればOK。こんな風にしてラジオ第一を再生する

livestreamer hls://nhkradiobkr1-i.akamaihd.net/hls/live/512291/1-r1/1-r1-01.m3u8 best -p mplayer

omxplayerのみで行けるという情報もあるのだが、omxplayerはUSB音声やHDMI音声に対応していないのでダメであった

  • 動かし方

上記をBlynkのようなIoTフレームワークから叩くか、cronで時間がきたら実行するなどすると、ラジオで目覚ましー>ニュースを見ながらラジオ体操 ができる。

2A + 5Vが出るモバイルバッテリ2個を組み合わせると持ち出しもできるので、実は昔懐かし夏休みラジオ体操ができるのだが、近所の公園で息子の同級生の親御さんに会う勇気はまだ、持ち合わせておらず。。。一旦は家の中でこんな風に動かしましょう。。。

./crawler.py &
./nhk.sh &
sudo ./feeder.py

以下、コードです

  • crawler.py
#!/usr/bin/env python
# -*- encoding:utf8 -*-
#Copyright (c) 2017 Tomohiko Araki
#Released under the MIT license
#http://opensource.org/licenses/mit-license.php

import datetime
import time
import argparse
import sys
import os
import random
import feedparser
import hashlib
from glob import glob
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
import urllib2
from bs4 import BeautifulSoup
#load Logger
from logging import getLogger, StreamHandler, DEBUG
logger = getLogger(__name__)
handler = StreamHandler()
handler.setLevel(DEBUG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..'))

def isOkToCrawl():
    crawl_interval = 60 #sec.
    crawl_interval_file = "./lastcrawl"
    now = time.time()

    if os.path.isfile(crawl_interval_file):
        if os.stat(crawl_interval_file).st_mtime > now - 60:
            return False
    
    f = open(crawl_interval_file, 'w')
    f.write(str(now) + "\n")
    f.close()
    return True

def getImageFromFile(path):
    image = Image.open(path).convert('RGB')
    return image

def saveImgFromText(text, imgdir, fontsize):
    path = os.path.abspath(os.path.dirname(__file__))
    if fontsize == 8:
        font   = [ImageFont.truetype(path + '/font/misaki_gothic.ttf', fontsize),1]
    else:
        fontsize = 16
        font  = [ImageFont.truetype(path + '/font/mplus-2c-medium.ttf', fontsize),-2]
        #font  = [ImageFont.truetype(path + '/font/Makinas-Scrap-5.otf', fontsize),-2]
        #font  = [ImageFont.truetype(path + '/font/PixelMplus10-Regular.ttf', fontsize),1]

    color  = [(255,0,255),
             (0,255,255),
             (255,255,0),
             (0,255,0),
             (255,255,255)]

    width, ignore = font[0].getsize(text)
    im = Image.new("RGB", (width + 30, fontsize), "black")
    draw = ImageDraw.Draw(im)
    draw.text((0, font[1]), text, random.choice(color), font=font[0])
    imgname = imgdir+"/"+str(fontsize)+str(hashlib.md5(text.encode('utf_8')).hexdigest())+".ppm"
    if not os.path.exists(imgname):
        im.save(imgname)

def removeOldImg(imgdir):
    #remove ppm files more than 1 days before.
    if not(imgdir=="") and not(imgdir=="/")and not(imgdir=="."): 
        now = time.time()
        for f in os.listdir(imgdir):
            if f[-4:] == '.ppm':
                f = os.path.join(imgdir, f)
                if os.stat(f).st_mtime < now - 0.5 * 86400:
                    if os.path.isfile(f):
                        os.remove(f)

def getNewsFromFeed():
    news  = []
    url = ['https://news.yahoo.co.jp/pickup/economy/rss.xml']
    for tg in url:
        fd = feedparser.parse(tg)
        for ent in fd.entries:
            news.append(u"          "+unicode(ent.title))
    return news

def getNewsFromNikkei():
    news  = []
    url = ['http://www.nikkei.com/',
            'http://www.nikkei.com/news/category/?at=ALL&bn=1',
            'http://www.nikkei.com/news/category/?bn=21']
    for tg in url:
        html = urllib2.urlopen(tg)
        soup = BeautifulSoup(html, "html.parser")
        tags = soup.find_all("span", class_="cmnc-large")
        for tag in tags:
            news.append(u"          "+tag.text)
        tags = soup.find_all("span", class_="cmnc-middle")
        for tag in tags:
            news.append(u"          "+tag.text)
        tags = soup.find_all("span", class_="cmnc-small")
        for tag in tags:
            news.append(u"          "+tag.text)
        tags = soup.find_all("span", class_="cmnc-xsmall")
        for tag in tags:
            news.append(u"          "+tag.text)
        time.sleep(2.0)
        
    return news

parser = argparse.ArgumentParser()
#parser.add_argument("-r", "--led-rows", action="store", help="Display rows. 16 for 16x32, 32 for 32x32. Default: 32", default=16, type=int)

if isOkToCrawl():
    print ("I gonna crawl.")
    
    imgdir = os.path.abspath(os.path.dirname(__file__)) + "/newsimg"
    if not os.path.isdir(imgdir):
        os.mkdir(imgdir)

    #clean up old news
    removeOldImg(imgdir)

    #get from RSS feed
    for text in getNewsFromFeed():
        saveImgFromText(text, imgdir, 8)

    #get from Nikkei
    for text in getNewsFromNikkei():
        saveImgFromText(text, imgdir, 8)

else:
    print ("You need to wait for 1min before next crawl.")
  • feeder.py
#!/usr/bin/env python
# -*- encoding:utf8 -*-

# Copyright (C) 2013 Henner Zeller <h.zeller@acm.org> for original work.
# Copyright (C) 2017 Tomohiko Araki <arakitomohiko@gmail.com> for delivertive work.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://gnu.org/licenses/gpl-2.0.txt>

import time
import argparse
import sys
import os
import random
import feedparser
from PIL import Image
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
#load Logger
from logging import getLogger, StreamHandler, DEBUG
logger = getLogger(__name__)
handler = StreamHandler()
handler.setLevel(DEBUG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..'))
from rgbmatrix import RGBMatrix, RGBMatrixOptions


def run(image, matrix):
    print("Running")
    image.resize((matrix.width, matrix.height), Image.ANTIALIAS)
    double_buffer = matrix.CreateFrameCanvas()
    img_width, img_height = image.size
    # let's scroll
    xpos = 0
    while True:
        xpos += 1
        if (xpos > img_width):
            xpos = 0
            break
            
        double_buffer.SetImage(image, -xpos)
        double_buffer.SetImage(image, -xpos + img_width)
    
        double_buffer = matrix.SwapOnVSync(double_buffer)
        time.sleep(0.04) #===========modifled

def prepareMatrix(parser):
    args    = parser.parse_args()
    options = RGBMatrixOptions()
    if args.led_gpio_mapping != None:
      options.hardware_mapping = args.led_gpio_mapping
    options.rows = args.led_rows
    options.chain_length = args.led_chain
    options.parallel = args.led_parallel
    options.pwm_bits = args.led_pwm_bits
    options.brightness = args.led_brightness
    options.pwm_lsb_nanoseconds = args.led_pwm_lsb_nanoseconds
    if args.led_show_refresh:
      options.show_refresh_rate = 1
    if args.led_slowdown_gpio != None:
        options.gpio_slowdown = args.led_slowdown_gpio
    if args.led_no_hardware_pulse:
      options.disable_hardware_pulsing = True
    return RGBMatrix(options = options)

def getImageFromFile(path):
    image = Image.open(path).convert('RGB')
    return image

parser = argparse.ArgumentParser()
parser.add_argument("-r", "--led-rows", action="store", help="Display rows. 16 for 16x32, 32 for 32x32. Default: 32", default=16, type=int)
parser.add_argument("-c", "--led-chain", action="store", help="Daisy-chained boards. Default: 1.", default=1, type=int)
parser.add_argument("-P", "--led-parallel", action="store", help="For Plus-models or RPi2: parallel chains. 1..3. Default: 1", default=1, type=int)
parser.add_argument("-p", "--led-pwm-bits", action="store", help="Bits used for PWM. Something between 1..11. Default: 11", default=11, type=int)
parser.add_argument("-b", "--led-brightness", action="store", help="Sets brightness level. Default: 100. Range: 1..100", default=10, type=int)
parser.add_argument("-m", "--led-gpio-mapping", help="Hardware Mapping: regular, adafruit-hat, adafruit-hat-pwm" , choices=['regular', 'adafruit-hat', 'adafruit-hat-pwm'], type=str)
parser.add_argument("--led-scan-mode", action="store", help="Progressive or interlaced scan. 0 Progressive, 1 Interlaced (default)", default=1, choices=range(2), type=int)
parser.add_argument("--led-pwm-lsb-nanoseconds", action="store", help="Base time-unit for the on-time in the lowest significant bit in nanoseconds. Default: 130", default=130, type=int)
parser.add_argument("--led-show-refresh", action="store_true", help="Shows the current refresh rate of the LED panel")
parser.add_argument("--led-slowdown-gpio", action="store", help="Slow down writing to GPIO. Range: 1..100. Default: 1", choices=range(3), type=int)
parser.add_argument("--led-no-hardware-pulse", action="store", help="Don't use hardware pin-pulse generation")
parser.add_argument("-i", "--image", help="The image to display", default="./news.ppm")

imgdir = os.path.abspath(os.path.dirname(__file__)) + "/newsimg"
matrix = prepareMatrix(parser)

if not os.path.isdir(imgdir):
    print("Error: no img to display, no such directory.")
    sys.exit(0)
else:
    while True:
        files = os.listdir(imgdir)
        if len(files)==0:
            print("Warning: no img to display, I am going to wait news to come.")
            time.sleep(5.0)
        else:
            frnd = random.sample(files,len(files))
            for f in frnd:
                if f[-4:] == '.ppm':
                    f = os.path.join(imgdir, f)
                    try:
                        if os.path.exists(f):
                            run(getImageFromFile(f), matrix)
                        else:
                            print("Warning: no such file, next please...")
                    except IOError:
                        print("Warning: no such file, next please...")
                    except KeyboardInterrupt:
                        print("Exiting\n")
                        sys.exit(0)
                else:
                    printf("Warning: Please do not include non-ppm files.")
                    sys.exit(0)
  • nhk.sh
#!/bin/sh
#Copyright (c) 2017 Tomohiko Araki
#Released under the MIT license
#http://opensource.org/licenses/mit-license.php
URL="hls://nhkradioakr1-i.akamaihd.net/hls/live/511633/1-r1/1-r1-01.m3u8"
livestreamer --yes-run-as-root $URL best -p mplayer &

参考にさせていただいたサイト

Updated: