Adafruit LED matrixラジオ体操
秋月電子で売っているきれいなLEDマトリクスでいろいろニュースを表示しつつ、毎朝ラジオ体操をできるようにしてみました。 ※ 2017/9月からNHKのラジオ配信がHLSになったので、記事を修正しました
1) 物品の準備
- Raspi関連
- LED Matrix関連
- Adafruit LED matrix 16x32 贔屓目だと思いますがすごく綺麗です。街中で普段見かけるもので、これに匹敵する綺麗さのものはなかなかないように思います。
- それ以外電源周り色々は、Adafruitのサイト参照。。。 LED matrix用の電源アダプタとLED Matrix側の電源端子をつなぐジャッンパワイヤ、ジャックなど色々ありました。Adafruitのサイトの情報が正しいのはあくまでもLED Matrixに電源をつなぐところくらいまででしたから、GPIOの結線以降は本記事をご覧いただいた方が良いかもしれません。
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
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 &
参考にさせていただいたサイト
- ラズパイ2でLED電光掲示板を作る
- Raspberry Pi コマンドラインで音を鳴らす
- raspberry pi で radiko
- Raspberry Pi でUSBスピーカーを動かす
- Raspberry Piでradikoの再生、録音
- ラジコ: FreeBSD で録音する radiko.sh アップデート
- らじるらじる m3u8 を ffmpeg で録音する(8放送局)2017/9 以降対応
- Some livecoding.tv streams only play audio even though there’s video on the website
- How to play Akamai adaptive HLS stream in fullscreen mode with auto start on boot