#初めに
業務開始・終了を記録するときに、うっかり忘れてしまうことないでしょうか?
私はあります。パソコンを起動するとすぐに仕事のことが気になって。。
そこで、業務開始・終了、昼食開始・終了を記録したら、”少しだけ”メリットをもらえる、つい押してみたくなるボタンを作りました。
- 業務開始:業務開始時刻と今日の天気予報を教えてくれる。(+励ましてくれる)
- 昼食開始:休憩開始時刻と今日のニュースを教えてくれる。(+いたわってくれる)
- 昼食終了:業務再開の時刻を教えてくれる。(+励ましてくれる)
- 業務終了:業務終了時刻と明日の天気予報を教えてくれる。(+いたわってくれる)
工夫した、新規性のある部分は以下かと思います。
- BLEビーコンボタンを使用して、ボタンのアクションごとに業務開始・終了 etc.を記録。
- 天気予報を入手するための位置情報として、ソラコムの接続先基地局の位置情報を利用。
#システム構成
システム構成を以下に示します。
ラズパイはビーコンの信号を受信したら、ソラコムの基地局情報や、OpenWeatheMapの天気情報、NHKのニュース情報を入手して、読み上げてくれます。
同時に、業務開始・終了、昼食開始・終了の情報をクラウドにアップします。今回は暫定で、ソラコムのメタデータサービスが持っているタグの領域に情報を書き込んでいます。出力先をAWS etc.にしてプログラムを組めば、本当の業務システムでの記録も可能と思います。
結果、ラズパイがBLEビーコンのゲートウェイになっている(とも言えなくもない)構成です。
#使用環境
##ハードウェア
- Raspberry Pi 4 Model B
- [BLE ボタン式ビーコン] (https://www.braveridge.com/product/archives/22)
- SORACOM Onyx LTE USB ドングル
- ソラコムSIM (今回はplan-DのSIMを使用)
- スピーカー ステレオミニジャックを使うもの。例えばBUFFALOのPC用スピーカー
##ソフトウェア
- OS: Raspberry Pi OS
- Node.js (v8.11.4)
- Python3 (v3.7.3)
#事前準備
##アカウントの取得
OpenWeatheMapのAPI Key
天気予報の情報は、OpenWeatheMapから取得します。
情報取得にはユーザー登録が必要ですので、下記を参考に、ユーザー登録とAPI Keyの入手を行ってください。
無料天気予報APIのOpenWeatheMapを使ってみる
ソラコムアカウントの取得
ソラコムのSIMを持っていればもうアカウントは持っていると思いますが、参考にするならこのページ。
ハードウェアのセットアップ
ドングルのセットアップ
こちらのページをもとに、setup_air.shの実行とネットワークへの接続を行ってください。
SORACOM Onyx LTE USB ドングルをセットアップする
スピーカー接続
スピーカーの端子をラズパイの3.5mmジャックに刺して音が再生されるようにしてください。
BUFFALOのスピーカーの場合は、さらに電源供給のために、USBに接続します。
ネットワークのセットアップ
メタデータサービスの設定
- ソラコムのコンソールにログインする
- ラズパイで使用しているSIMを選択 → 詳細
- SIM詳細の画面で、グループをクリック。既存のグループを割り当てるか、「新しいグループを作成」でグループを作る。
- 「タグ」のタグを選択。「+」ボタンを押して、タグの編集画面を開く。名前"working"、値"0"を入力して、保存。もう一度、「+」ボタンを押してタグの編集画面を開いて、名前"lunch"、値"0"を入力して、保存。「閉じる」ボタンで、SIM詳細画面を閉じる。
- SIMの画面でグループ名を選択して、SIMグループ画面を開く。SORACOM Air for Cellular設定→ メタデータサービス設定を"on"、読み取り専用のチェックを"off"。下のほうにスクロールして、「保存」ボタンを押す。
ソフトウェアのセットアップ
今回は、ビーコン受信にNode.jsのbleacon、Webとの通信にPython3を使います。
とくに、bleaconは特定バージョンのNode.jsしか動かないので、注意してください。
node.jsと関連パッケージのインストール
ラズパイのホームディレクトリで実行
$sudo apt-get update
$sudo apt-get install -y libbluetooth-dev
libbluetoothをインストールしたときに、ラズパイのBLEが停止することがあります。その時は再起動してください。
node.jsのv8.11.4のインストールと関連パッケージのインストール
$sudo apt-get install -y nodejs npm
(実行後、apt-get updateしろとのメッセージが表示された場合は、
sudo apt-get updateして、再度、sudo apt-get install -y nodejs npmを実行)
$sudo npm install n -g
$sudo n 8.11.4
(新しいshellを開けとのメッセージが出た場合は新しいshellを起動)
$sudo npm install bleacon -unsafe-perm
$sudo npm install sleep -unsafe-perm
pythonの関連パッケージのインストール
$sudo pip3 install feedparser
作業用ディレクトリの作成
今回の作業用に"workrecorder"というフォルダを作ります。
実行ファイルなどはここに置くようにします。
$mkdir /home/pi/workrecorder
$cd /home/pi/workrecorder
BLEのUUID、major番号の調査
今回使用するBLEビーコンボタンは、iBeacon規格のアドバタイズ信号を発信します。
アクションとして、
- 1クリック
- 2クリック
- 長押し
三つの動作が区別できて、それぞれ同一UUID+異なるmajor番号を発信します。
あらかじめ各アクションのUUID、major番号を調べておいて、後のプログラムに使用します。
調査用の下記のプログラムを作成、保存してください。
var Bleacon = require("bleacon");
function ExitProcess(){
Bleacon.stopScanning();
process.exit();
}
Bleacon.startScanning();
Bleacon.on("discover",function(bleacon) {
console.dir(bleacon);
ExitProcess();
});
ファイルを置いたフォルダで以下のコマンドを実行
$sudo node checkUUID_MajorMinor.js
ここでビーコンボタンを押下すると、受信した信号の情報が表示されます。
私の場合は、1クリック:1、2クリック:12289、長押し:4097のmajor番号が発信されていました。
{ uuid: 'XXXXXXXXXXXXXXXXX', ← 製品によって一意。控えておく
major: 1, ← アクションによって一意
minor: 140,
measuredPower: -59,
rssi: -58,
accuracy: 0.9387786164580845,
proximity: 'near' }
読み上げソフトのインストール
読み上げソフトはAquesTalkを使用します。
セットアップの仕方や、天気予報・ニュース入手~読み上げまでのスクリプトはほとんど下記のサイトの情報を使用させていただきました。ありがとうございます!!
-
以下のサイトからラズパイ用のAquesTalkのパッケージをダウンロード、解凍する。
本稿執筆時点の最新版は、aquestalkpi-20201010.tgz
AquesTalk Pi -
解凍されたフォルダ(aquestalkpi)をそのまま、workrecoderフォルダ直下に置く。
-
以下のシェルスクリプト"atalk-hp.sh"を作り、workrecoredrフォルダ直下に置き、実行権限をつける。元の技術ブログ記載のシェルスクリプトに対して、音声をヘッドホン出力するところだけ、変更しています。
#!/bin/bash
aquestalkpi=/home/pi/workrecorder/aquestalkpi/AquesTalkPi
var=`$aquestalkpi "$@" | base64; echo ":${PIPESTATUS[0]}"`
ret=(${var##*:})
data=${var%:*}
if [ $ret -eq 0 ]; then
echo $data | base64 --decode --ignore-garbage | aplay -Dhw:Headphones -q
else
echo $data | base64 --decode --ignore-garbage
exit $ret
fi
実行権限の付与
$ chmod a+x atalk-hp.sh
#プログラム
ソフトウェア構成
今回の使用するソフトウェアとその関係図です。
昔作っていたコードなどを集めてきたので複雑です(適材適所ともいう)。
これらのファイルはすべて、作業ディレクトリ(/home/pi/workrecorder)においてください。
beacon_start.sh
ビーコン受信プログラム(beacon_discover.js)をキックします。
beacon_discover.jsは一度信号を受信したら終了するスクリプトなので、終了したらこのシェルで再度キックします。
実行権限もつけておきます。
#!/bin/sh
WORKDIR=/home/pi/workrecorder
echo start ibeacon
while true
do
sudo node $WORKDIR/beacon_discover.js
sleep 2
echo restart ibeacon
done
実行権限の付与
$ chmod a+x beacon_start.sh
beacon_discover.js
node.jsのライブラリ"bleacon"を使ってiBeacon信号を受信し、受信したMajor番号にしたがって、異なる引数でpythonスクリプトを起動するプログラムです。
こちらのスクリプトに、先に調べておいたビーコンボタンのUUID, Major番号(3種類)を記載してください。
var Bleacon = require("bleacon");
var sleep = require("sleep");
var exec = require('child_process').exec,
child;
var UUID = 'XXXXXXXXXXXXXXXXXXXXXX'; <-ここに調査したビーコンのUUIDを記載
var Major1 = 0x0001; ←1クリックのMajor番号を記載
var Major2 = 0x3001; ←2クリックのMajor番号を記載
var Major3 = 0x1001; ←長押しのMajor番号を記載
var Timeoutperiod = 1000*60*60*1;
function ExitProcess(){
Bleacon.stopScanning();
process.exit();
}
process.on('uncaughtException',function(err){
console.log(err);
ExitProcess();
});
setTimeout(function(){
console.log('timeout');
ExitProcess();
},Timeoutperiod);
Bleacon.startScanning(UUID);
Bleacon.on("discover",function(bleacon) {
console.dir(bleacon);
if(bleacon.major == Major1){
child = exec('python3 working_recorder.py 0');
sleep.sleep(60)
}else if(bleacon.major == Major2) {
child = exec('python3 working_recorder.py 2');
sleep.sleep(60)
}else{
child = exec('python3 working_recorder.py 1');
sleep.sleep(120)
}
ExitProcess();
});
working_recorder.py
引数にしたがって、業務開始、昼食開始・終了、業務業務終了の処理を行います。
ソラコムのAPIとOpenWeatheMapのAPIを使用するため、
・ソラコムのアカウント、パスワード
・前掲の、OpenWeatheMapのAPIキー
をスクリプト内に記載してください。
#!/usr/bin/env python
import sys
import shlex
import subprocess
from datetime import datetime
import requests
import feedparser
import json
import deg_speed
import celllib
#login for soracom
EMAIL = 'XXXXXXXXXXXXX' ←ソラコムのAPIを使用するための、アカウント名を記入
PASS = 'XXXXXXXXXXXX' ←ソラコムのAPIを使用するための、パスワードを記入
#API key for OpenWeatheMap
API_KEY = "XXXXXXXXXXXXXX" ← OpenWeatheMapのAPIキーを記入
CMD_SAY = "/home/pi/workrecorder/atalk-hp.sh -b -s 90"
CMD_SAY2 = "/home/pi/workrecorder/atalk-hp.sh"
da = datetime.now()
def main():
if sys.argv[1] == "0":
start_work()
elif sys.argv[1] == "1":
check_lunch()
else:
end_work()
return
def start_work():
res = celllib.putTagValue("working", "1")
res = celllib.putTagValue("lunch" , "0")
api = celllib.getApiKey(EMAIL, PASS)
cell = celllib.getCellInfo()
pos = celllib.getPosition(api, cell)
say_weather_today(pos)
return
def end_work():
res = celllib.putTagValue( "working", "0")
res = celllib.putTagValue("lunch" , "0")
api = celllib.getApiKey(EMAIL, PASS)
cell = celllib.getCellInfo()
pos = celllib.getPosition(api, cell)
say_weather_tommorrow(pos)
return
def say_weather_today(pos):
url = "https://api.openweathermap.org/data/2.5/onecall?lat=%s&lon=%s&appid=%s&units=metric&lang=ja&exclude=hourly" % (pos["lat"], pos["lon"], API_KEY)
print(url)
weather_text = u'%sは%sの風、風力%s。%s。天気は%s。降水確率は、%s%です。'
now_text = u'現在の気温は%s度、湿度%s%で体感温度は、%s度です。'
temperature_text = u'%sの予想最高気温は、%s度。予想最低気温は、%s度、湿度は、%s%です。'
try:
obj = requests.get(url).json()
c = obj["current"]
d = obj["daily"]
#today
Nt = d[0]["temp"]
pop = round(d[0]["pop"] * 100)
wdn = c["wind_deg"]
wdn = deg_speed.wdeg(wdn)
wsn = c["wind_speed"]
(wsn,wsna) = deg_speed.wspeed(wsn)
today_w = c["weather"][0]["description"]
today_w_txt = weather_text % (u"今日", wdn, wsn, wsna, today_w, pop)
today_t_txt = temperature_text % (u"今日", round(Nt["max"], 1), round(Nt["min"], 1), d[0]["humidity"])
today_n_txt = now_text % (round(c["temp"], 1), c["humidity"], round(c["feels_like"], 1))
#say
opening_str = "業務開始時間は、%s時%s分です。今日も、がんばりましょう。" % (da.hour, da.minute)
weather_str = opening_str + "今日の天気です。" + today_w_txt + ' ' + today_n_txt + ' ' + today_t_txt
weather_str = weather_str.replace("-", "マイナス")
text = '''%s '%s' ''' % (CMD_SAY, weather_str)
print(text)
proc = subprocess.Popen(shlex.split(text))
proc.communicate()
except:
return False
return True
def say_weather_tommorrow(pos):
url = "https://api.openweathermap.org/data/2.5/onecall?lat=%s&lon=%s&appid=%s&units=metric&lang=ja&exclude=hourly" % (pos["lat"], pos["lon"], API_KEY)
print(url)
weather_text = u'%sは%sの風、風力%s。%s。天気は%s。降水確率は、%s%です。'
temperature_text = u'%sの予想最高気温は、%s度。予想最低気温は、%s度、湿度は、%s%です。'
try:
obj = requests.get(url).json()
d = obj["daily"]
#tommorow
Nt = d[1]["temp"]
pop = round(d[1]["pop"] * 100)
wdt = d[1]["wind_deg"]
wdt = deg_speed.wdeg(wdt)
wst = d[1]["wind_speed"]
(wst,wsna) = deg_speed.wspeed(wst)
tommorow_w = d[1]["weather"][0]["description"]
tommorow_w_txt = weather_text % (u"明日", wdt, wst, wsna, tommorow_w, pop)
tommorow_t_txt = temperature_text % (u"明日", round(Nt["max"], 1), round(Nt["min"], 1), d[1]["humidity"])
#say
opening_str = "業務終了時間は、%s時%s分です。今日も、おつかれさまでした。" % (da.hour, da.minute)
weather_str = opening_str + "明日の天気です。" + tommorow_w_txt + ' ' + tommorow_t_txt
weather_str = weather_str.replace("-", "マイナス")
text = '''%s '%s' ''' % (CMD_SAY, weather_str)
print(text)
proc = subprocess.Popen(shlex.split(text))
proc.communicate()
except:
return False
return True
def check_lunch():
lunch = celllib.getTagValue("lunch")
if lunch == 0:
res = celllib.putTagValue("working", "1")
res = celllib.putTagValue("lunch" , "1")
say_news()
else:
res = celllib.putTagValue("working", "1")
res = celllib.putTagValue("lunch" , "0")
say_endlunch()
return
def say_news():
RSS_URL = 'https://www.nhk.or.jp/rss/news/cat0.xml'
d = feedparser.parse(RSS_URL)
for i, entry in enumerate(d.entries):
newstime = entry.published_parsed
newstime = datetime(newstime[0],newstime[1],newstime[2],newstime[3],newstime[4],newstime[5])
if i == 0:
opening_str = "%s時%s分です。昼休みです。おつかれさまです。" % (da.hour, da.minute)
text = opening_str + "ニュースです。" + entry.summary
else:
text = "次のニュースです。" + entry.summary
text = CMD_SAY2 + ' '+ text
if da.timestamp() - newstime.timestamp() <= 172800:
print(text)
proc = subprocess.Popen(shlex.split(text))
proc.communicate()
return
def say_endlunch():
opening_str = "%s時%s分です。昼休み終了です。残りもがんばりましょう。" % (da.hour, da.minute)
text = CMD_SAY + ' ' + opening_str
print(text)
proc = subprocess.Popen(shlex.split(text))
proc.communicate()
return
### Execute
if __name__ == "__main__":
main()
celllib.py
ソラコムのAPIにアクセスする関数群です。
#!/usr/bin/env python
import requests
#get API key and token
def getApiKey(email, password):
headers = {
'accept': 'application/json',
'Content-Type': 'application/json',
}
data = '{ "email": "' + email + '", "password": "' + password + '" }'
url = 'https://api.soracom.io/v1/auth'
try:
res = requests.post(url, headers=headers, data=data)
res.raise_for_status()
return res.json()
except:
return None
#get cell ID (use metadata service)
def getCellInfo():
url = 'http://metadata.soracom.io/v1/subscriber.sessionStatus.cell'
try:
res = requests.get(url)
res.raise_for_status()
return res.json()
except:
return None
#get lat and lot
def getPosition(api, cell):
if api is None or cell is None:
return None
headers = {
'accept': 'application/json',
'X-Soracom-API-Key': api['apiKey'],
'X-Soracom-Token': api['token'],
}
params = (
('mcc' , cell['mcc']),
('mnc' , cell['mnc']),
('tac' , cell['tac']),
('ecid', cell['eci']),
)
url = 'https://api.soracom.io/v1/cell_locations'
try:
res = requests.get(url, headers=headers, params=params)
res.raise_for_status()
return res.json()
except:
return None
#get tags (use metadata service)
def getTagValue(name):
url = 'http://metadata.soracom.io/v1/subscriber.tags.' + name
try:
res = requests.get(url)
res.raise_for_status()
return res.json()
except:
return None
#put Value to Tags
def putTagValue(name, value):
if name is None or value is None:
return None
headers = {
'Content-Type': 'application/json',
}
data = '[ { "tagName": "' + name + '", "tagValue": "' + value + '" }]'
url = 'http://metadata.soracom.io/v1/subscriber/tags'
try:
res = requests.put(url, headers=headers, data=data)
res.raise_for_status()
return res.json()
except:
return None
deg_speed.py
OpenWeatherMapの風の情報を日本語に置き換えるプログラムモジュールです。
下記のサイトに掲載されているプログラムををそのまま使わせていただいていますので、下記のサイトより引用してください。
Raspberry Piに現在時刻、天気予報、ニュースを喋らせる
プログラム実行
beacon_start.shを実行してください。ラズパイのBLE受信が始まります。
業務開始(ボタン1クリック)
ビーコンボタンを1クリックすると、業務開始時間、励まし、今日の天気予報の順にスピーカーから出力されます。
同時に、ソラコムのコンソールのタグ情報が、"working"="1"に書き換わります。
昼休み開始(ボタン長押し その1)
ビーコンボタンを長押しすると、現在時間、いたわり、今日のニュース一覧の順にスピーカーから出力されます。
同時に、ソラコムのコンソールのタグ情報が、”lunch”="1"に書き換わります。
昼休み終了(ボタン長押し その2)
再度ビーコンボタンを長押しすると、現在時間を伝えた後、昼からも仕事を頑張るよう、励ましてくれます。
同時に、ソラコムのコンソールのタグ情報が、”lunch”="0"に書き換わります。
業務終了(ボタン2クリック)
ビーコンボタンを2クリックすると、業務終了時間、いたわり、明日の天気予報の順にスピーカーから出力されます。
同時に、ソラコムのコンソールのタグ情報が、"working"="0"に書き換わります。
#プログラムの説明(デバイスの位置情報の取得)
OpenWeatheMapは、取得する天気予報の地点として、都市のID、Zipコード、緯度・経度などの指定が可能です。
よくあるプログラム例ではこの値をあらかじめ調べておいてから、データ取得を行っています。
今回は、SORACOMのAPIを使用して、デバイスが接続している基地局の緯度・経度の情報を自動的に入手しました。
基地局の位置情報取得API
上記ブログに書かれているように、この緯度・経度は必ずしも厳密な値ではありませんが、OpenWeatheMapで必要なのは都市のサイズの位置情報ですので、これで充分だと思います。
手順としては、下記の二段階で情報を取得します。
・メタデータサービスの情報から、基地局IDほかの値の入手。
#get cell ID (use metadata service)
def getCellInfo():
url = 'http://metadata.soracom.io/v1/subscriber.sessionStatus.cell'
try:
return requests.get(url).json()
except:
return None
・SORACOM API (cell_locations) で基地局の緯度・経度の情報入手。
引数のcellが先ほど取得した基地局IDほかの情報。apiは別途ほかの関数(getApiKey)で取得した、API-keyとTokenの情報です。
#get lat and lot
def getPosition(api, cell):
if api is None or cell is None:
return None
headers = {
'accept': 'application/json',
'X-Soracom-API-Key': api['apiKey'],
'X-Soracom-Token': api['token'],
}
params = (
('mcc' , cell['mcc']),
('mnc' , cell['mnc']),
('tac' , cell['tac']),
('ecid', cell['eci']),
)
url = 'https://api.soracom.io/v1/cell_locations'
try:
return requests.get(url, headers=headers, params=params).json()
except:
return None
今後の改善案
次は以下の情報を参考にして、稼働情報をslackに反映するようにしたいです。
SORACOM レシピ:IoTで在席状況の自動更新
→ 2021/7/18にリリースしました!! 詳しくはこちら
BLEビーコンとソラコムAPIでつい押してみたくなる出退勤記録ボタンを作る(Slack通知機能追加版)
#参考リンク
- beacon受信
部屋に入るとファミマの入店音が鳴るアプリ
[Raspberry PiでiBeaconを受信する]
(https://qiita.com/yuyakato/items/739443960fac2668f4a3)
-
天気予報・ニュース入手・読み上げ
Raspberry Piに現在時刻、天気予報、ニュースを喋らせる
raspberry piの音周り -
基地局の位置情報入手
SORACOM メタデータサービスの説明
Cell ID ほかの説明
基地局の位置情報取得API