MG5130をネットワークプリンターとして使う

もう少しプリンターを使いたいがドライバーの更新がない。

印刷場所がパソコンから遠い

ラズパイを使ってネットワークプリンターとして使う

 

プリンターの利用

# CUPSとプリンタードライバー(Gutenprint)のインストール
sudo apt install cups gutenprint-locales printer-driver-gutenprint -y

# 現在のユーザーを lpadmin グループに追加
sudo usermod -a -G lpadmin ユーザー名

# ネットワーク内の他のPCから管理画面を開けるようにする
sudo cupsctl --remote-admin --remote-any --share-printers

#webサービスを許可

sudo cupsctl WebInterface=yes

# 再起動

sudo systemctl restart cups

#CUPS web設定にアクセス

https//***.***.***.***:631

・セキュリティ警告が出るけど無視して入る

・今回は自動認識されている

 (プリンターにCannon MG5100 Seriesと出ている。型式が違うけど問題なし)

・表示文字Cannon MG5100 Seriesを押してSet Default OptionsのColor ModelをRGBからKCMYに変更すると黒文字印刷ができるようになる

・Windowsパソコンからプリンターの追加にCannon MG5100 Series@ラズパイ名が表示されるので追加する

 

スキャナーの利用

# スキャナー関連パッケージのインストール
sudo apt install sane-utils libsane1 -y

# 物理的にUSBが認識されているか
sudo sane-find-scanner

レスポンスの中に

found possible USB scanner (vendor=0x04a9 [Canon], product=0x1748 [MG5100 series]) at libusb:001:004 could not open USB device 0x1d6b/0x0002 at 001:001: Input/output error

見つけているけど開けない→認識OK

# スキャナードライバー(pixma)が割り当たっているか

sudo scanimage -L

Created directory: /var/lib/snmp/cert_indexes
device `pixma:04A91748_10FC33' is a CANON Canon PIXMA MG5100 multi-function peripheral→割り当てられた

#saneの設定ファイルを開く

sudo nano /etc/sane.d/saned.conf

ネットワーク範囲を追加***.***.***.*/24

#共有サービスを起動・反映

sudo systemctl enable saned.socket
sudo systemctl restart saned.socket

 

Windows側の設定

Windows PCで SaneWinDS をダウンロードしてインストール

SaneWinDS Configurationがなくスキャンを押すとwindowが出るのでHOST名を入れる。

 

NAPS2をインストールしてダウンロードして

実行すると左側のwindowの新規プロファイルを選択 

ここでTWINドライバーにSANEWinDSが出るので選んでスキャンを試行

スキャンした画像の背景が白にどうしてもならないので、詳細設定で「スキャン後に明度、コントラスト処理をする」 にチェックを入れる

plotly dashを利用して炭酸濃度をグラフ化する

測定したcvsデータを10分毎に読み込みプロットするapp.pyを作成

 

# -*- coding: utf-8 -*-
from datetime import datetime
import glob
import csv
import subprocessimport os
import dash
import plotly.graph_objects as go
from dash import dcc, html
from dash.dependencies import Input, Output

def get_date():
    return datetime.now().strftime("%Y-%m-%d")

def get_path(d):
    return f'data/{d}.csv'

def get_csv_dates():
    files = glob.glob('data/*.csv')
    if not files:
        return
    files.sort(reverse=True)
    dates =

    for f in files:
        basename = os.path.basename(f)
        date_str = os.path.splitext(basename)[0]
        dates.append(date_str)
    return dates

def read_csv(path):
    try:
        if not os.path.exists(path):
            return
        with open(path) as f:
            reader = csv.reader(f, delimiter=',')
            # ヘッダー(timestamp)を除外して正常な行のみ抽出
            rows = [row for row in reader if row and 'timestamp' not in row[0]]
            if not rows:
                return

            
            # 時間の古い順に並び替え
            rows.sort(key=lambda x: x[0])
            data_t = [list(x) for x in zip(*rows)]
        return data_t
    except Exception as e:
        print(f"Error reading {path}: {e}")
        return []

def sampling():
    dates_list = get_csv_dates()
    datetime_now = datetime.now().strftime("%Y/%m/%d - %H:%M:%S")
    if not dates_list:
        return "データがありません", datetime_now
    
    path = get_path(dates_list[0])
    try:
        with open(path, 'r') as f:
            lines = f.readlines()
            valid_lines = [l.strip() for l in lines if l.strip() and 'timestamp' not in l]
            if not valid_lines:
                return "データがありません", datetime_now
            
            last_line = valid_lines[-1].split(',')
            count = last_line[1]
            b_co2 = last_line[4]
            b_sol = last_line[5]
            c_co2 = last_line[6]
            c_sol = last_line[7]
            
            main_display = f"添加: {count}回 | B機: {b_co2}ppm/{b_sol}W  |  C機: {c_co2}ppm/{c_sol}W"
            return main_display, f"最終更新: {datetime_now}"
    except Exception:
        return "データ読み取りエラー", datetime_now

def shutdown():
    subprocess.Popen(['bash', 'shutdown.sh'])

def safe_float(val):
    try:
        return float(val)
    except (ValueError, TypeError):
        return None

def get_combined_fig(path, target_date):
    data_t = read_csv(path)
    layout = go.Layout(plot_bgcolor='WhiteSmoke', paper_bgcolor='WhiteSmoke')
    fig = go.Figure(layout=layout)

    # 💡 解決策: グラフのデータ(X軸)には元の「長い日時データ」を100%そのまま渡す
    if data_t and len(data_t) >= 8:
        timestamps = data_t[0]
        co2_b = [safe_float(i) for i in data_t[4]]
        sol_b = [safe_float(i) for i in data_t[5]]
        co2_c = [safe_float(i) for i in data_t[6]]
        sol_c = [safe_float(i) for i in data_t[7]]

        # --- B機系統 (赤系) ---
        fig.add_trace(go.Scatter(x=timestamps, y=co2_b, name='B機 CO2', yaxis='y1', line=dict(width=3, color='Crimson')))
        fig.add_trace(go.Scatter(x=timestamps, y=sol_b, name='B機 日射', yaxis='y2', line=dict(width=1.5, color='LightPink')))

        # --- C機系統 (青系) ---
        fig.add_trace(go.Scatter(x=timestamps, y=co2_c, name='C機 CO2', yaxis='y1', line=dict(width=3, color='RoyalBlue')))
        fig.add_trace(go.Scatter(x=timestamps, y=sol_c, name='C機 日射', yaxis='y2', line=dict(width=1.5, color='LightSkyBlue')))

    # 24時間の固定表示枠を「日時型」として裏側で計算する
    start_time = f"{target_date} 00:00:00"
    end_time = f"{target_date} 23:59:59"

    fig.update_layout(
        xaxis=dict(
            type='date',                 # X軸の性質を「日付・時間型」に指定
            title='時間',
            range=[start_time, end_time], # 24時間の幅に固定
            tickformat='%H:%M',          # 見た目(説明)だけを「時:分」の表示にする
            dtick=1800000,               # 目盛り間隔は「30分刻み」にする(1800000ミリ秒)
            tickangle=45
        ),
        yaxis=dict(title='CO2 (ppm)', range=[400, 1200]),
        yaxis2=dict(title='日射 (W/m²)', range=[0, 600], overlaying='y', side='right'),
        margin=dict(l=60, r=60, t=40, b=60),
        showlegend=True,
        legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
        height=450
    )
    return fig

# --- Dash App Initial Settings ---
app = dash.Dash(__name__, meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}])
app.title = 'Sensor Monitor 2026'

csv_dates = get_csv_dates()
initial_date = csv_dates[0] if csv_dates else get_date()

app.layout = html.Div(children=[
    html.Br(),
    html.H3('Raspberry Pi Sensor Monitor', style={'fontFamily': 'Arial Black'}),
    html.H3(id='container-sample-main', style={'fontFamily': 'Arial Black'}),
    html.H6(id='container-sample-sub'),
    html.Hr(),
    
    html.Div([
        html.Div([
            html.Label('Date '),
            dcc.Dropdown(csv_dates, value=initial_date, id='dropdown_date')
        ], className="custom-dropdown"),
    ], style={'display': 'flex', 'flexWrap': 'wrap', 'justifyContent': 'center'}),

    html.Div([
        dcc.Graph(id='combined-graph', config={'displayModeBar': False, 'responsive': True}),
    ], className="custom-figure"),

    html.Hr(),
    html.Div([
        html.Button('Shutdown', id='shutdown', n_clicks=0),
        html.H6(id='shutdown_message', style={'fontSize': 16}),
    ]),
    dcc.Interval(id='interval', interval=600000, n_intervals=0)
], style={'textAlign': 'center', 'backgroundColor': 'WhiteSmoke', 'color': '#2F3F5C'})

@app.callback(
    [Output('container-sample-main', 'children'),
     Output('container-sample-sub', 'children'),
     Output('combined-graph', 'figure'),
     Output('dropdown_date', 'options'),
     Output('dropdown_date', 'value')],
    [Input('interval', 'n_intervals'),
     Input('dropdown_date', 'value')]
)
def update_dashboard(n, selected_date):
    dates_list = get_csv_dates()
    co2_val, time_str = sampling()
    
    target_date = selected_date if selected_date in dates_list else (dates_list[0] if dates_list else get_date())
    fig = get_combined_fig(get_path(target_date), target_date)
    
    return co2_val, time_str, fig, dates_list, target_date

@app.callback(Output('shutdown_message', 'children'), Input('shutdown', 'n_clicks'))
def handle_shutdown(n_clicks):
    if n_clicks > 0:
        shutdown()
        return 'Shutdown signal sent...'
    return ''

if __name__ == '__main__':
    app.run(debug=False, host="***.***.***.***")

 

 

PCとスマートホンで見られるように表示を変える

/assets/style.css

/* 全体共通 */
body {
    margin: 0;
    padding: 0;
    font-family: Arial, sans-serif;
}

/* PC向けスタイル */
@media (min-width: 1001px) {
    #container-sample-main {
        font-size: 32px; /* 少し小さくして一行に収まりやすく */
        color: #2F3F5C;
        margin: 20px 0;
    }
}

/* モバイル向けスタイル */
@media (max-width: 1000px) {
    #container-sample-main {
        font-size: 18px; /* スマホではさらに小さく */
        font-weight: bold;
    }
}

 

 

source myenv/bin/acivate

cd pi/sensor

python app.py

 

 

 

ラズパイ起動時に自動的に起動する

 

sudo nano /etc/systemd/system/sensormonitor.service

 

Description=Run sensor monitor
After=network.target

[Service]
ExecStart=/home/akihiro/myenv/bin/python3 /home/akihiro/pi/sensor/app.py
WorkingDirectory=/home/akihiro/pi/sensor
Restart=always

[Install]
WantedBy=multi-user.target

 

sudo systemctl daemon-reload

sudo systemctl enable sensormonitor.service

sudo systemctl start sensormonitor.service

sudo systemctl status sensormonitor.service

MQTTを使って離れた場所のデータを集める

子機のプログラム

import paho.mqtt.client as mqtt
import time
import json
import mh_z19
import board
import adafruit_bh1750

# --- 設定 ---
BROKER_HOST = "***.***.**.*"
COMMAND_TOPIC = "sensor/control/command"
DATA_TOPIC = "sensor/data/response"
CLIENT_ID = "B_machine_Multi_sensor"
MQTT_USER = "***"
MQTT_PASS = "***"

# --- センサー初期化 ---
# 起動時に一度だけ実行
i2c = board.I2C()
light_sensor = adafruit_bh1750.BH1750(i2c)

def get_combined_data():
    """CO2と照度を両方取得する"""
    co2_value = None
    for _ in range(3):
        try:
            res = mh_z19.read(serial_console_untouched=True)
            if res and 'co2' in res:
                co2_value = res['co2']
                break
        except:
            pass
        time.sleep(1)

    lux_value = None
    try:
        lux_value = round(light_sensor.lux, 2)
    except Exception as e:
        print(f"照度センサー読み取りエラー: {e}")

    return co2_value, lux_value

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        client.subscribe(COMMAND_TOPIC, qos=1)
        print("接続成功。指令を待機しています...")

def on_message(client, userdata, msg):
    try:
        command_data = json.loads(msg.payload.decode('utf-8'))
        if command_data.get("command") == "get_data":
            co2, lux = get_combined_data()
            
            response_payload = {
                "status": "success" if (co2 is not None) else "error",
                "device_id": "B_machine_Multi_sensor",
                "co2_ppm": co2,
                "lux": lux,
                "timestamp_b": time.time(),
                "received_timestamp_a": command_data.get("timestamp_a")
            }
            
            client.publish(DATA_TOPIC, json.dumps(response_payload), qos=1)
            print(f"[{time.strftime('%H:%M:%S')}] 送信: CO2={co2}, Lux={lux}")
    except Exception as e:
        print(f"処理エラー: {e}")

def main():
    try:
        client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, CLIENT_ID)
        client.username_pw_set(MQTT_USER, MQTT_PASS)
        client.on_connect = on_connect
        client.on_message = on_message
        client.reconnect_delay_set(min_delay=1, max_delay=120)

        print(f"ブローカー({BROKER_HOST})に接続を試みています...")
        client.connect(BROKER_HOST, 1883, 60)
        
        print("ループを開始します。指令待機中...")
        client.loop_forever()
    except Exception as e:
        print(f"!!! 致命的なエラーが発生しました: {e}")

if __name__ == "__main__":
    main()

 

親機のプログラム

import paho.mqtt.client as mqtt
import time
import json
import csv
import os
import numpy as np # 平均計算用
from datetime import datetime
from gpiozero import LED, OutputDevice

# --- センサー用ライブラリ ---
import mh_z19
import board
import adafruit_bh1750

# --- 設定 ---
BROKER_HOST = "***.***.***.***"
COMMAND_TOPIC = "sensor/control/command"
DATA_TOPIC = "sensor/data/response"
CLIENT_ID = "A_machine_commander"
MQTT_USER = "***"
MQTT_PASS = "***"

INTERVAL = 60      # 1分毎に測定
WAIT_TIME = 5      # レンスポンス待機
LOG_INTERVAL = 10  # 10回ごとにCSV保存
SAVE_DIR = "./data"

# --- 新しいGPIOピン設定 ---
relay = OutputDevice(4, active_high=True, initial_value=False) # リレースイッチ
led_green = LED(23)                                            # 緑LED
led_red = LED(24)                                              # 赤LED

# --- センサー初期化 ---
i2c = board.I2C()
light_sensor = adafruit_bh1750.BH1750(i2c)

# データ蓄積用
current_batch = {"B": None, "C": None}
history = {
    "A_co2": , "A_lux": ,
    "B_co2": , "B_lux": ,
    "C_co2": , "C_lux": ,
    "relay_on_count": 0
}

if not os.path.exists(SAVE_DIR):
    os.makedirs(SAVE_DIR)

def get_a_sensor_data():
    """A機自身のCO2と照度を取得する"""
    co2_value = None
    for _ in range(3):
        try:
            res = mh_z19.read(serial_console_untouched=True)
            if res and 'co2' in res:
                co2_value = res['co2']
                break
        except:
            pass
        time.sleep(1)

    lux_value = None
    try:
        lux_value = round(light_sensor.lux, 2)
    except Exception as e:
        print(f"A機 照度センサー読み取りエラー: {e}")

    return co2_value, lux_value

def get_daily_file_path():
    today_str = datetime.now().strftime("%Y-%m-%d")
    return os.path.join(SAVE_DIR, f"{today_str}.csv")

def save_to_csv():
    """10分間の統計データをCSVに保存"""
    file_path = get_daily_file_path()
   
    # 平均値の計算(データがない場合は0)
    def get_avg(lst):
        return round(float(np.mean(lst)), 2) if lst else None

    # luxのリストから直接W/m2の平均値を算出する
    def get_wm2_avg(lux_list):
        if not lux_list:
            return None
        # 平均を求めてから掛け算処理
        return round(float(np.mean(lux_list)) * 0.00833, 2)

    row = {
        'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:00"),
        'relay_score': history["relay_on_count"],
        'A_co2_avg': get_avg(history["A_co2"]),
        'A_wm2_avg': get_wm2_avg(history["A_lux"]), # W/m2のみ記録
        'B_co2_avg': get_avg(history["B_co2"]),
        'B_wm2_avg': get_wm2_avg(history["B_lux"]), # W/m2のみ記録
        'C_co2_avg': get_avg(history["C_co2"]),
        'C_wm2_avg': get_wm2_avg(history["C_lux"])  # W/m2のみ記録
    }

    # CSVの列名から lux_avg を削除し、wm2_avg だけに整理
    fieldnames = [
        'timestamp', 'relay_score',
        'A_co2_avg', 'A_wm2_avg',
        'B_co2_avg', 'B_wm2_avg',
        'C_co2_avg', 'C_wm2_avg'
    ]

    try:
        file_exists = os.path.isfile(file_path)
        with open(file_path, 'a', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            if not file_exists:
                writer.writeheader()
            writer.writerow(row)
        print(f"【10分統計保存】 A_WM2:{row['A_wm2_avg']}, B_WM2:{row['B_wm2_avg']}, C_WM2:{row['C_wm2_avg']}")
    except Exception as e:
        print(f"CSV保存エラー: {e}")

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        client.subscribe(DATA_TOPIC, qos=1)
        print("MQTT v5 接続成功")

def on_message(client, userdata, msg):
    global current_batch
    try:
        data = json.loads(msg.payload.decode('utf-8'))
        dev_id = data.get("device_id")

        if dev_id == "B_machine_Multi_sensor":
            current_batch["B"] = data
        elif dev_id == "C_machine_Multi_sensor":
            current_batch["C"] = data

    except Exception as e:
        print(f"受信エラー: {e}")

def main():
    global current_batch, history
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, CLIENT_ID, protocol=mqtt.MQTTv5)
    client.username_pw_set(MQTT_USER, MQTT_PASS)
    client.on_connect = on_connect
    client.on_message = on_message

    client.connect(BROKER_HOST, 1883, 60)
    client.loop_start()

    print(f"2026年 運用開始 (W/m2記録限定版 1分測定 / 10分集計)")

    # 同期待機
    now = datetime.now()
    wait_start = 60 - now.second
    print(f"同期待機中... 次の00秒からスタートします(約{wait_start}秒)")
    time.sleep(wait_start)

    try:
        sample_count = 0
        while True:
            # 1. リセット
            current_batch = {"B": None, "C": None}
           
            # 2. 指令送信
            payload = json.dumps({"command": "get_data", "timestamp_a": time.time()})
            client.publish(COMMAND_TOPIC, payload, qos=1)
           
            # 3. A機自体のセンサーデータ取得
            a_co2, a_lux = get_a_sensor_data()
            if a_co2 is not None:
                history["A_co2"].append(a_co2)
            if a_lux is not None:
                history["A_lux"].append(a_lux)

            # 4. 残りの応答待機時間
            time.sleep(1)
           
            # 5. 1分毎の集計処理と周辺機器制御
            if current_batch["B"]:
                history["B_co2"].append(current_batch["B"].get("co2_ppm"))
                history["B_lux"].append(current_batch["B"].get("lux"))
            if current_batch["C"]:
                history["C_co2"].append(current_batch["C"].get("co2_ppm"))
                history["C_lux"].append(current_batch["C"].get("lux"))

            # --- ハードウェア制御ロジック ---
           
            # 条件A: 赤LEDの制御
            if current_batch["B"] is None or current_batch["C"] is None:
                led_red.on()
                print(" -> [警告] B機またはC機のデータが取得できません。赤LED点灯")
            else:
                led_red.off()

            # 条件B: 緑LEDとリレースイッチの制御
            if a_co2 is not None and a_co2 <= 600:
                led_green.on()
                relay.on()
                print(f" -> [正常] A機CO2 {a_co2}ppm (<=600): 緑LED点灯 / リレー5秒ON開始")
               
                time.sleep(5)
                relay.off()
               
                history["relay_on_count"] += 1
                extra_wait = 5
            else:
                led_green.off()
                relay.off()
                extra_wait = 0
                if a_co2 is not None:
                    print(f" -> A機CO2 {a_co2}ppm (>600): 緑LED消灯 / リレー停止")
           
            sample_count += 1
            print(f"[{datetime.now().strftime('%H:%M:%S')}] 測定 {sample_count}/{LOG_INTERVAL}完了")

            # 6. 10回(10分)たまったら保存
            if sample_count >= LOG_INTERVAL:
                save_to_csv()
                # 履歴リセット
                history = {
                    "A_co2": , "A_lux": ,
                    "B_co2": , "B_lux": ,
                    "C_co2": , "C_lux": ,
                    "relay_on_count": 0
                }
                sample_count = 0
           
            # 7. 次の1分まで待機
            time.sleep(INTERVAL - WAIT_TIME - extra_wait)
               
    except KeyboardInterrupt:
        print("停止します")
    finally:
        relay.off()
        led_green.off()
        led_red.off()
        client.loop_stop()
        client.disconnect()

if __name__ == "__main__":
    main()
 
実行方法
sourse myenv/bin/activate
cd pi/sensor
python main_sensor.py

環境データの整頓

#!/bin/bash

# パスの設定
SOURCE_DIR="/home/****/pi/sensor/data"
ARCHIVE_DIR="/home/****/pi/sensor/archive"

# アーカイブ先がなければ作成
mkdir -p "$ARCHIVE_DIR"

# 基準日(4日前)を計算 (2025-12-15 のような形式)
# ※今日を含めて5日分残すため、4日前より「古い」ものを対象にする
THRESHOLD_DATE=$(date -d "4 days ago" +"%Y-%m-%d")

echo "基準日: $THRESHOLD_DATE 以前のファイルをアーカイブします。"

#ファイルが存在しない場合にループを回さない設定
shopt -s nullglob

# ファイルをループでチェック
for file in "$SOURCE_DIR"/*.csv; do
    # ファイル名(パスなし)を取得
    filename=$(basename "$file")

    # ファイル名から日付部分 (YYYY-MM-DD) を抽出
    # 例: 2025-12-20.csv -> 2025-12-20
    file_date=$(echo "$filename" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')

    # 日付が抽出できなかった場合はスキップ
    if [ -z "$file_date" ]; then
        continue
    fi

    # 基準日より古い(日付文字列として小さい)場合に移動
    if "$file_date" < "$THRESHOLD_DATE" ; then
        echo "アーカイブ中: $filename"
        mv "$file" "$ARCHIVE_DIR/"
    fi
done

echo "完了しました。"

 

  • chmod + x mv_sensor_data.sh
  • crontab -e

50 *** \bin/bash /home/****/pi/sensor/mv_sensor_data.sh

BH1750を使って照度測定

作業

https://zenn.dev/kotaproj/books/raspberrypi-tips/viewer/220_kiso_bh1750

を参考にラズパイZero 2wでBH1750を使ってみた。

 

チップは同じでもメーカーによって端子の順番やIDが違うので注意する。

 

VCC (3.3V), GND, SDA, SCL, ADDR(IDを変えるジャンパピン GND) SDAは2番ピンSCLは3番ピンに接続I2C通信でやり取りをする。

 

sudo raspi-configからInterface OptionでI2Cを使えるようにする。

 

pipでモジュールを導入する

$ python3 -m venv env
$ source env/bin/activate
(env) $ pip install adafruit-circuitpython-bh1750

pipは仮想環境で使うか、--break-system-packagesで強制的に導入するようになった。

 

BH1750照度センサーのID確認

(env) $ i2cdetect -y 1

 

testコードの作成

import time
import board
import adafruit_bh1750


def main ():
    #初期化
    i2c = board.I2C()
    light_sensor = adafruit_bh1750.BH1750(i2c)

    #ルックスの表示
    for _ in range(5):
        time.sleep(1)
        print("%.2f LUX"%light_sensor.lux)
    return


if __name__ == "__main__":
    main()
 
実行 5回照度測定
(env) $ python sample_bh1750.py 

 

Root権限不要でのmh_z19ライブラリの活用方法

ラズパイ 止めたはずのagettyが動きシリアルポートを使えない。

原因

mh_z19ライブラリのデフォルト設定は、センサー値を読み取る前にgettyを自動的に停止し、mh_z19のシリアルコンソールを開く。センサー値を読み取った後、gettyを再起動する。これでagettyにシリアルポートを占領されていた。

 

作業した事

raspi-configの3 Interface Optionsで、Would you like a login shell accessible over serial?→No、Would you like the serial port hardware to be enabled?→Yesにしてもagettyがシリアルポートを占有してしまう。

 

sudo lsof /dev/serial0

systemctl status serial-getty@ttyS0.serviceでenableになっている。

 

/boot/firmware/cmdline.txt

systemd.mask=serial-getty@serial0.service

agettyをmaskすると、mh_z19がagettyを再起動しようとするのを阻止する(mh_z19の--serial_consol_untouchedオプションで良かった)。

 

シリアルポートのユーザー権限を持たせるためにユーザーをdialoutグループに所属させる。

sudo usermod -aG dialout ユーザー名

python3 -m mh_z19 --serial_console_untouchedで読める。ここまではOK。

 

次にプログラムからmh_z19.read()を使うとportが開けない。

$ sudo lsof /dev/serial0
lsof: WARNING: can't stat() fuse.gvfsd-fuse file system /run/user/1000/gvfs
      Output information may be incomplete.
lsof: WARNING: can't stat() fuse.portal file system /run/user/1000/doc
      Output information may be incomplete.
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
agetty  2333 root    0u   CHR   4,64      0t0  126 /dev/ttyS0
agetty  2333 root    1u   CHR   4,64      0t0  126 /dev/ttyS0
agetty  2333 root    2u   CHR   4,64      0t0  126 /dev/ttyS0

 

$ systemctl status serial-getty@ttyS0.service
○ serial-getty@ttyS0.service - Serial Getty on ttyS0
     Loaded: loaded (/lib/systemd/system/serial-getty@.service; disabled; prese>
     Active: inactive (dead)

 

 となっていて、systemctl status では inactive (dead)(停止中) となっているのに、lsof では agetty (PID 2333) がポートを掴んでいる。

 

$ ps -fj --ppid 1 | grep agetty
root      2333     1  2333  2333  0 17:52 ttyS0    00:00:00 /sbin/agetty -o -p -- \u --keep-baud 115200,57600,38400,9600 - vt220

 

PPID 1(systemd)/sbin/agetty が --keep-baud などのオプション付きで動いている。

 

How to use without root permission. · UedaTakeyuki/mh-z19 Wiki · GitHub

プログラムから使用

mh_z19.read(serial_console_untouched=True)

これが必要だった

 

MQTTブローカーのセットアップ手順

Brokerサービスの構築

 
brokerのインストール

sudo apt update

sudo apt install mosquitto mosquitto-clients

 

brokerサービスの自動起動確認

sudo systemctl status mosquitto

 

brokerサービスの自動起動

sudo systemctl enable mosquitto

 

broker認証設定

sudo mosquitto_passwd -c /etc/mosquitto/passwd ユーザー名

# 他のユーザーを追加する場合の例 -cオプション無し
sudo mosquitto_passwd /etc/mosquitto/passwd anotheruser

 

Mosquitto設定ファイルの編集

sudo nano /etc/mosquitto/mosquitto.conf

# 認証を必須にする設定
allow_anonymous false

# パスワードファイルの場所を指定
password_file /etc/mosquitto/passwd

sudo systemctl restart mosquitto

 

ポートの確認

sudo ufw status verbose

1883番がなければ

sudo ufw allow 1883/tcp
sudo ufw reload

publisherとsubscriberに必要なライブラリーをインストール

pip install paho-mqtt

 

テストのためにpublisherにmosquitto-clientsのインストール

sudo apt update
sudo apt install mosquitto-clients

 

テストしてみる

 

version1からversion2へon connectにpropertiesの引数が必要。

エラーメッセージが変で、version1にpropertiesを間違って追加設定してもclient.loop_forever()のエラーでon connectにprotertiesの引数がないと言われる。

 

subscriberのプログラム

import paho.mqtt.client as mqtt

# VERSION2のシグネチャに修正
def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"接続失敗: {reason_code}")
    else:
        print("接続成功")
        client.subscribe("test/topic")

def on_message(client, userdata, msg):
    print("Message received:", msg.payload.decode())

# CallbackAPIVersion.VERSION2 を明示的に指定
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, "test_client")
client.username_pw_set("****", "****")
client.on_connect = on_connect
client.on_message = on_message

client.connect("***.***.***.***", 1883, 60)
client.loop_forever()

publisher

mosquitto_pub -h ***.***.***.*** -t test/topic -m "Hello MQTT" -u **** -P ****

でメッセージ確認