Adafruit LED matrixラジオ体操

Last updated on

秋月電子で売っているきれいなLEDマトリクスでいろいろニュースを表示しつつ、毎朝ラジオ体操をできるようにしてみた。

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のフォントを入れておく必要あり。色々調整したが、以下が好み:
    – 16px向け -> mplus-2c-medium
    – 8px向け -> 美咲フォント

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

  • nhk.sh
     rtmpdump + mplay2 で NHK第一を聞くシェルスクリプト。
     上記モジュールがapt-getで導入済みであればOK

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

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

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

以下、コードです。


crawler.py

crawler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/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

feeder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/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

nhk.sh
1
2
3
4
5
6
7
8
9
#!/bin/sh
#Copyright (c) 2017 Tomohiko Araki
#Released under the MIT license
#http://opensource.org/licenses/mit-license.php
URL1="rtmpe://netradio-r1-flash.nhk.jp"
URL2="http://www3.nhk.or.jp/netradio/files/swf/rtmpe.swf"
PLAYPATH="NetRadio_R1_flash@63346"
rtmpdump --rtmp ${URL1} --playpath ${PLAYPATH} --app "live" -W ${URL2} --live | mplayer - &

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