【IoT×キャンプ|機能実装編】意地でもラズパイとキャンプへ!らずキャン△プロジェクト第8回

2021/03/06

IoT Python ガジェット らずキャン△ ラズパイ 開発

らずキャンプロジェクト機能実装編

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から実行しないといけません。ということで、次回は電源投入後にアプリが自動で走るように設定して仕上げます。

記事まとめ

スポンサーリンク

フォロワー

Labels

Amazon (3) Apache (3) Apple (9) AppleSilicon (7) Bloggerカスタマイズ (12) EchoShow15 (1) IoT (25) Jetson (1) MySQL (1) PHP (3) Python (20) Web (3) アウトドア (11) アメリカ生活 (19) ガジェット (35) キャンプ (9) ディープラーニング (1) らずキャン△ (11) ラズパイ (24) 暗号資産 (5) 開発 (31) 旅行 (8)

QooQ