IoTの代表格Raspberry Pi(ラズベリーパイ)を何とかキャンプで使ってやろうというこのプロジェクト、題して「らずキャン△プロジェクト」。
手段が目的化してしまっていますが、そんなことは気にしない!らずキャン△プロジェクトスタート!
連載第8回は【機能実装編】。Raspberry Pi OS(旧Raspbian)とSense HATを使ってキャンプに使える機能を実装していきますよ~
第7回【Sense HATテスト編】はこちら。
【IoT×キャンプ|Sense HATテスト編】意地でもラズパイとキャンプへ!らずキャン△プロジェクト第七回この記事でわかること
この記事では以下の簡単なPythonアプリ開発方法がわかります。コピペで行けますんで、この機会にPython × IoTにチャレンジしてみたいよ、という方でも大丈夫です。
- Sense HATデータ入出力。
- ファイル出力。
- ログ出力。
- 月毎にフォルダ作成。
- 日毎にファイル作成。
- CSVファイルヘッダ有無確認し付与。
実際に実装していく機能一覧はこちら。
- 1分ごとに環境データをCSV出力する機能。
- ジョイスティックを使ったモード切替機能。
- LEDランタンモード。
- 炎っぽくゆらぐLEDランタンモード。
私の環境
- Raspberry Pi 4 Model B 8GB
- Raspberry Pi OS 32bit (Released:2021-01-11)
- Visual Studio Code ver 1.53.2
- Python 3.7.3
準備物
第7回までで準備してきたラズパイを使用します。Sense HATを取り付けて、VSCodeでPython3とSense HATを使えるようにしてある状態です。
モニター、マウス、キーボード接続のラズパイ直接操作を前提としており、SSHやVNCは不要なのでONにはしません。
コーディング
以下の順に進めます。- サンプルコード
- 要変更箇所
- 機能解説
- 注意事項
サンプルコード
細かい解説はすっ飛ばして、サンプルコード全文です。
VSCodeでPythonファイルを作り、丸ごとコピペしてください。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sense_hat
import time
import numpy as np
import random
import math
import datetime
import os
import csv
from logging import getLogger, StreamHandler, DEBUG, INFO, FileHandler, Formatter
def main():
#センスハット表示初期化
sense.clear()
try:
while True:
#1分ごとに環境データをCSV出力
if compare_min(previous_min) == True:
output_env_data()
#run_modeに応じて毎ループごとに処理
if run_mode == 0:
#モード選択表示
sense.show_letter(str(select_mode))
elif run_mode == 1:
#LEDランタンモード
lantern_single()
elif run_mode == 2:
#炎っぽくゆらぐLEDランタンモード
lantern_yuragi()
elif run_mode == 3:
return
elif run_mode == 4:
return
elif run_mode == 5:
return
except KeyboardInterrupt: #ターミナルCtrl+C強制終了
logger.debug(f"KeyboardInterrupt")
except Exception as e:
logger.error(f"main: {e}")
finally:
sense.show_message("bye", scroll_speed=0.05)
sense.clear()
def clamp(value, min_value, max_value):
"""valueの計算結果を最小値から最大値までの間の値で返す。"""
return min(max_value, max(min_value, value))
def pushed_up(event):
"""上を押したときのイベント"""
global select_mode
global run_mode
if event.action != "released":
run_mode = 0
select_mode = clamp(select_mode + 1, MODE_MIN, MODE_MAX)
sense.clear()
logger.info(f"pushed_up: {select_mode}")
def pushed_down(event):
"""下を押したときのイベント"""
global select_mode
global run_mode
if event.action != "released":
run_mode = 0
select_mode = clamp(select_mode - 1, MODE_MIN, MODE_MAX)
sense.clear()
logger.info(f"pushed_down: {select_mode}")
def pushed_middle(event):
"""真ん中で押したときのイベント"""
global select_mode
global run_mode
if event.action == "pressed":
sense.clear()
run_mode = select_mode
logger.info(f"pushed_middle: run_mode {run_mode}")
def lantern_single():
"""ランタンモード:電球色LED"""
try:
sense.clear(BULB3400k)
except Exception as e:
logger.error(f"lentern_mode: {e}")
sense.clear()
return ""
def lantern_yuragi():
"""ランタンモード:電球色LEDゆらぎ"""
try:
#色減衰用フィルター
n = 2 #設定用:数値を大きくしたほうが、かさ上げが小さくなる
filter_array = np.random.rand(64,1) #64x1の配列を明示しないとブロードキャストエラーになる
filter_array = filter_array * (1-(1/n)) + (1/n) #ランダム数値への下駄履かせ。例えば0~1のランダム値に0.5かけて0.5足すことにより0.5~1にかさ上げできる
#ベースとなる電球色配列 64x3のLED配列
base_array = np.full((64,3),BULB3400k)
#64x3のベースLED配列に対し64x1のランダムフィルターをブロードキャストで乗算
display_array = base_array * filter_array
#floatではLED出力できないのでintへキャスト
display_array = display_array.astype(np.int32)
#センスハットLED出力
sense.set_pixels(display_array)
#次処理までのタイマーをsin偏りでランダム制御。
timer = math.sin(math.radians(int(random.uniform(5,90)))) #短すぎると表示が途切れるので5~90度
timer = timer * 0.2 #数値調整
time.sleep(timer)
except Exception as e:
logger.error(f"lantern_yuragi: {e}")
sense.clear()
return ""
def output_env_data():
try:
#データ取得し小数点1桁まで表示
temp = round(sense.get_temperature(),1)
humd = round(sense.get_humidity(),1)
pres = round(sense.get_pressure(),1)
comp = round(sense.get_compass(),1)
#日付取得
dt_now = datetime.datetime.now()
#書き込みデータ
rowdata = [dt_now.strftime("%Y-%m-%d %H:%M:%S"),temp,humd,pres,comp]
#フォルダとファイル作成
# フォルダ:月ごと
# ファイル:日ごと
month_folder = dt_now.strftime("%Y%m")
folder_path = DATA_PATH + month_folder
os.makedirs(folder_path, exist_ok=True)
filename = folder_path + "/" + dt_now.strftime("%Y%m%d") + "_camplog.csv"
header = ["日時","温度(℃)","湿度(%)","気圧(hPa)","方角(°)"]
#データ書き込み
write_csv_data(filename, header, rowdata)
except Exception as e:
logger.error(f"output_env_data: {e}")
def write_csv_data(filename, header, rowdata):
"""CSVデータを一行書き込む。ファイルがない場合はヘッダーを加えて作成。"""
try:
#ファイル確認
file_exists = os.path.isfile(filename)
#CSVデータ書き込み
with open(filename, "a") as f:
writer = csv.writer(f, delimiter=",")
#ファイルがない場合はファイル作ってヘッダー書き込み
if not file_exists:
writer.writerow(header)
#書き込み
writer.writerow(rowdata)
except Exception as e:
logger.error(f"write_csv_data: {e}")
def compare_min(prev):
"""前回と現在の分を比較して異なる場合はTrueを返し、Global変数のprevious_minを現在分に更新する。"""
global previous_min
dt_now = datetime.datetime.now()
now = dt_now.minute
if prev == now:
return False
else:
previous_min = now
return True
#################
# グローバル変数など
#################
#ファイルパス 個人環境に応じて変更してください
DATA_PATH = "/home/pi/Python/RaspCamp/Data/" #CSVデータ保存先
LOG_PATH = "/home/pi/Python/RaspCamp/Log/" #Logファイル保存先
#動作モード
select_mode = 0
run_mode = 0
#動作モード上下限
MODE_MAX = 5
MODE_MIN = 0
#1分ごとに処理するための比較用変数
dt_now = datetime.datetime.now()
previous_min = dt_now.minute
current_min = dt_now.minute
#色の定義 RGB
WHITE = [255,255,255]
RED = [255,0,0]
ORANGE = [255,127,0]
YELLOW = [255,255,0]
GREEN = [0,255,0]
SKYBLUE = [0,255,255]
BLUE = [0,0,255]
BULB3400k = [255,129,58] #3400k電球色
###################
#センスハットモジュール
###################
sense = sense_hat.SenseHat()
#ダークモード選択
sense.low_light = False
#センスハットイベントハンドラ
sense.stick.direction_up = pushed_up
sense.stick.direction_down = pushed_down
sense.stick.direction_middle = pushed_middle
###################
############
#ログ設定
#loggingとloggerがややこしいのでloggingが候補に出てこないようimport指定
############
#Log出力先フォルダ
logfolder = LOG_PATH + dt_now.strftime("%Y%m") + "/"
os.makedirs(logfolder, exist_ok=True)
logger = getLogger(__name__)
#handler1 ターミナル用
handler1 = StreamHandler()
handler1.setLevel(DEBUG)
handler1.setFormatter(Formatter("%(asctime)s %(levelname)8s %(message)s"))
#handler2 ファイル出力用
handler2 = FileHandler(filename = logfolder+dt_now.strftime("%Y%m%d")+".log")
handler2.setLevel(INFO)
handler2.setFormatter(Formatter("%(asctime)s %(levelname)8s %(message)s"))
#loggerにハンドラ設定
logger.setLevel(DEBUG)
logger.addHandler(handler1)
logger.addHandler(handler2)
logger.propagate = False
########
if __name__ == "__main__":
main()
ライブラリのリファレンスはこちら
Sense HAT API Reference要変更箇所
ご自身の環境に合わせてファイルパスを指定してください。コードの下の方にあります。サンプルは初期ユーザpiの状態です。
指定したフォルダにデータが保存されます。ユーザ名のとこさえちゃんと合わせれば、フォルダ自体は自動生成されます。
#ファイルパス 個人環境に応じて変更してください
DATA_PATH = "/home/pi/Python/RaspCamp/Data/" #CSVデータ保存先
LOG_PATH = "/home/pi/Python/RaspCamp/Log/" #Logファイル保存先
機能解説
先程のコードにはこれらの機能が含まれています。
- ソフト実行後の初期モードは0。
- ジョイスティックの上下でモード選択。
- ジョイスティック中央押込みでモード決定。
- モード0: モード選択
- モード1: LEDランタン
- モード2: 炎っぽくゆらぐLEDランタン
- モード3~5: 予備。決定でソフト終了。
- モードに関係なく1分に1回環境データをCSVファイル出力。
- INFO以上のログをファイル出力。
- 各フォルダは月毎に自動作成。
- 各ファイルは日毎に自動作成。
注意事項
まずキャンプ用のため連続常時使用を想定していません。CSVは起動しっぱなしでも自動的にフォルダとファイル作成しますが、ログはしません。起動時に自動で作って終わりです。またファイル自動消去も未実装です。
あとジョイスティックが地味に危険。
上下左右がキーボード、中央押し込みがエンターの働きをします。なので、デバッグ中にエディタ上でポチポチやってるといつの間にか改行されてたりします。1番危険なのがターミナル。上、上、押し込み、とかやると過去のコマンドがシレっと実行されます。パスワードかかってないとsudoコマンドが意図せず走る危険性があるので要注意ですね。
入力禁止にする方法ないんかな〜。
最後に
いかがでしたか?ついにラズパイをキャンプに持って行けるとこまで来ました!
こいつで暗闇を照らしつつキャンプデータを取りますよ〜。来週グランドキャニオン行くのでそこで使います!!
ただこのままだと電源起動後にVSCodeから実行しないといけません。ということで、次回は電源投入後にアプリが自動で走るように設定して仕上げます。
0 件のコメント:
コメントを投稿