中華のRAK3172を使って温湿度基板を作って可視化してみた(1)

# はじめに
こんにちは　５年前からLoRaWANというものを知りつつ、デバイスの入手性やコスト、技適、頭の能力不足などから放置していましたが、ここ数が月ネットサーフィンをしていると安価にLoRaWANが買えそうではありませんか！！！

他の記事でも同じように考えている方がいて誠に勝手ながら親近感が湧きました。

と言うことでAliexpressから技適登録済みの製品を購入し可視化までできたので共有したいと思います。

# 購入品
https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172?variant=40014759461062

・適当な温湿度センサ(AHT20)
・Seeed studio SenseCAP M2　ゲートウェイ

# 技適について
このRAK3172ですが、一瞬、技適も表示もとれてないんじゃないかと思うんですがしっかり登録されていました。
![スクリーンショット 2023-11-27 18.51.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2180934/844c8425-788d-4d00-344f-fb41ac054a19.png)

後は表示の問題なんですが...直接メーカに云々言ったところシールを貼ってくれると..!
![スクリーンショット 2023-11-27 19.00.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2180934/9c0bf086-49d3-3553-1ef5-0f3b52ded5ee.png)
これで完璧です。※もし不都合があれば教えてください...

# 基板起こし
昔は使えるお金が乏しかったので回路を引いて中華基板屋さんに生基板作って自分で実装などやっていましたが、今は楽ですね。JLCPCBに頼めば実装までしてくれて日本に届く頃には検品して終わりですから。
チップ部品やICもJLCPCBで在庫を持っていて無い部品はJLCPCB宛に送れば実装してくれます（RAK3172もJLCPCBに送って実装してもらいました）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2180934/556a8b73-0670-8f36-e998-9cde6a7feada.png)
この画像を元にUART2に接続すれば書き込みできます。
専用の書き込みツールに見えますが適当なUSB-UART 3.3Vで問題ありません。

# プログラム書き込み(Arduino IDE)
このRAKWirelessはドキュメントが見やすくArduino IDEの開発方法など書いてあり、もはや私の方で解説不要です
https://docs.rakwireless.com/Product-Categories/WisDuo/RAK3172-Module/Quickstart/#what-do-you-need

適当にペイロードに温度と湿度を乗せて飛ばしたところ問題なく通信しました。
![スクリーンショット 2023-11-27 19.45.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2180934/ff0623da-ae56-c6d7-80f9-394e82750f21.png)

~~~
/***
 *  This example shows LoRaWan protocol joining the network in OTAA mode, class A, region EU868.
 *  Device will send uplink every 20 seconds.
***/
#include <Wire.h>
#include <AHT20.h>

AHT20 aht;
#define OTAA_PERIOD   (20000)
/*************************************

   LoRaWAN band setting:
     RAK_REGION_EU433
     RAK_REGION_CN470
     RAK_REGION_RU864
     RAK_REGION_IN865
     RAK_REGION_EU868
     RAK_REGION_US915
     RAK_REGION_AU915
     RAK_REGION_KR920
     RAK_REGION_AS923

 *************************************/
#define OTAA_BAND     (RAK_REGION_AS923)
#define OTAA_DEVEUI   {0x}
#define OTAA_APPEUI   {0x}
#define OTAA_APPKEY   {0x}

/** Packet buffer for sending */
uint8_t collected_data[128] = { 0 };

void recvCallback(SERVICE_LORA_RECEIVE_T * data)
{
    if (data->BufferSize > 0) {
        Serial.println("Something received!");
        for (int i = 0; i < data->BufferSize; i++) {
            Serial.printf("%x", data->Buffer[i]);
        }
        Serial.print("\r\n");
    }
}

void joinCallback(int32_t status)
{
    Serial.printf("Join status: %d\r\n", status);
}

/*************************************
 * enum type for LoRa Event
    RAK_LORAMAC_STATUS_OK = 0,
    RAK_LORAMAC_STATUS_ERROR,
    RAK_LORAMAC_STATUS_TX_TIMEOUT,
    RAK_LORAMAC_STATUS_RX1_TIMEOUT,
    RAK_LORAMAC_STATUS_RX2_TIMEOUT,
    RAK_LORAMAC_STATUS_RX1_ERROR,
    RAK_LORAMAC_STATUS_RX2_ERROR,
    RAK_LORAMAC_STATUS_JOIN_FAIL,
    RAK_LORAMAC_STATUS_DOWNLINK_REPEATED,
    RAK_LORAMAC_STATUS_TX_DR_PAYLOAD_SIZE_ERROR,
    RAK_LORAMAC_STATUS_DOWNLINK_TOO_MANY_FRAMES_LOSS,
    RAK_LORAMAC_STATUS_ADDRESS_FAIL,
    RAK_LORAMAC_STATUS_MIC_FAIL,
    RAK_LORAMAC_STATUS_MULTICAST_FAIL,
    RAK_LORAMAC_STATUS_BEACON_LOCKED,
    RAK_LORAMAC_STATUS_BEACON_LOST,
    RAK_LORAMAC_STATUS_BEACON_NOT_FOUND,
 *************************************/

void sendCallback(int32_t status)
{
    if (status == RAK_LORAMAC_STATUS_OK) {
        Serial.println("Successfully sent");
    } else {
        Serial.println("Sending failed");
    }
}

void setup()
{
    Serial.begin(115200, RAK_AT_MODE);
    delay(2000);
    Wire.begin(); //Join I2C bus
    //Check if the AHT20 will acknowledge
    if (aht.begin() == false)
    {
      Serial.println("AHT20 not detected. Please check wiring. Freezing.");
      while (1);
    }
    Serial.println("AHT20 acknowledged.");
  
    Serial.println("RAKwireless LoRaWan OTAA Example");
    Serial.println("------------------------------------------------------");
  
    if(api.lorawan.nwm.get() != 1)
    {
        Serial.printf("Set Node device work mode %s\r\n",
            api.lorawan.nwm.set(1) ? "Success" : "Fail");
        api.system.reboot();
    }

    // OTAA Device EUI MSB first
    uint8_t node_device_eui[8] = OTAA_DEVEUI;
    // OTAA Application EUI MSB first
    uint8_t node_app_eui[8] = OTAA_APPEUI;
    // OTAA Application Key MSB first
    uint8_t node_app_key[16] = OTAA_APPKEY;
  
    if (!api.lorawan.appeui.set(node_app_eui, 8)) {
        Serial.printf("LoRaWan OTAA - set application EUI is incorrect! \r\n");
        return;
    }
    if (!api.lorawan.appkey.set(node_app_key, 16)) {
        Serial.printf("LoRaWan OTAA - set application key is incorrect! \r\n");
        return;
    }
    if (!api.lorawan.deui.set(node_device_eui, 8)) {
        Serial.printf("LoRaWan OTAA - set device EUI is incorrect! \r\n");
        return;
    }
  
    if (!api.lorawan.band.set(OTAA_BAND)) {
        Serial.printf("LoRaWan OTAA - set band is incorrect! \r\n");
        return;
    }
    if (!api.lorawan.deviceClass.set(RAK_LORA_CLASS_A)) {
        Serial.printf("LoRaWan OTAA - set device class is incorrect! \r\n");
        return;
    }
    if (!api.lorawan.njm.set(RAK_LORA_OTAA))	// Set the network join mode to OTAA
    {
        Serial.printf("LoRaWan OTAA - set network join mode is incorrect! \r\n");
        return;
    }
    if (!api.lorawan.join())	// Join to Gateway
    {
        Serial.printf("LoRaWan OTAA - join fail! \r\n");
        return;
    }
  
    /** Wait for Join success */
    while (api.lorawan.njs.get() == 0) {
        Serial.print("Wait for LoRaWAN join...");
        api.lorawan.join();
        delay(10000);
    }
  
    if (!api.lorawan.adr.set(true)) {
        Serial.printf("LoRaWan OTAA - set adaptive data rate is incorrect! \r\n");
        return;
    }
    if (!api.lorawan.rety.set(1)) {
        Serial.printf("LoRaWan OTAA - set retry times is incorrect! \r\n");
        return;
    }
    if (!api.lorawan.cfm.set(1)) {
        Serial.printf("LoRaWan OTAA - set confirm mode is incorrect! \r\n");
        return;
    }
  
    /** Check LoRaWan Status*/
    Serial.printf("Duty cycle is %s\r\n", api.lorawan.dcs.get()? "ON" : "OFF");	// Check Duty Cycle status
    Serial.printf("Packet is %s\r\n", api.lorawan.cfm.get()? "CONFIRMED" : "UNCONFIRMED");	// Check Confirm status
    uint8_t assigned_dev_addr[4] = { 0 };
    api.lorawan.daddr.get(assigned_dev_addr, 4);
    Serial.printf("Device Address is %02X%02X%02X%02X\r\n", assigned_dev_addr[0], assigned_dev_addr[1], assigned_dev_addr[2], assigned_dev_addr[3]);	// Check Device Address
    Serial.printf("Uplink period is %ums\r\n", OTAA_PERIOD);
    Serial.println("");
    api.lorawan.registerRecvCallback(recvCallback);
    api.lorawan.registerJoinCallback(joinCallback);
    api.lorawan.registerSendCallback(sendCallback);
}

void uplink_routine()
{
  /** Payload of Uplink */
  uint8_t data_len = 0;
  
  // AHT20からデータを取得
  float temperature = aht.getTemperature();  // Reading temperature
  float humidity = aht.getHumidity();  // Reading humidity
  
  Serial.print("Temperature: ");
  Serial.println(temperature);
  
  Serial.print("Humidity: ");
  Serial.println(humidity);

  if (!isnan(temperature) && !isnan(humidity)) {
    collected_data[data_len++] = (uint8_t) 't'; // "t" for temperature
    collected_data[data_len++] = (uint8_t)temperature; // 仮に整数部分のみ

    collected_data[data_len++] = (uint8_t) 'h'; // "h" for humidity
    collected_data[data_len++] = (uint8_t)humidity; // 仮に整数部分のみ
  } else {
    // データが取得できなかった場合の処理
    collected_data[data_len++] = (uint8_t) 'e'; // "e" for error
    collected_data[data_len++] = 0xFF; // Error code to indicate sensor disconnection
  }

  // デバッグ出力
  Serial.println("Data Packet:");
  for (int i = 0; i < data_len; i++) {
    Serial.printf("0x%02X ", collected_data[i]);
  }
  Serial.println("");

  /** Send the data package */
  if (api.lorawan.send(data_len, (uint8_t *) & collected_data, 2, true, 1)) {
    Serial.println("Sending is requested");
  } else {
    Serial.println("Sending failed");
  }
}

void loop()
{
    static uint64_t last = 0;
    static uint64_t elapsed;
  
    if ((elapsed = millis() - last) > OTAA_PERIOD) {
        uplink_routine();
  
        last = millis();
    }
    //Serial.printf("Try sleep %ums..", OTAA_PERIOD);
    api.system.sleep.all(OTAA_PERIOD);
    //Serial.println("Wakeup..");
}

~~~

テンプレートにあるLoRaWAN_OTAAを元にAHT20が使えるように変更しました。

# データ蓄積(ラズパイ上にMySQLを設置)
可視化にあたってエンドデバイスからのデータを受け取り、蓄積する必要があります。
今回はThe Things Network(TTN)のMQTTを使ってデータをMySQLに溜め込みそれをGrafanaで可視化をしたいと思います。

TTN consoleにあるMQTTを使ってラズパイ上で受け取ります！
![スクリーンショット 2023-11-27 20.06.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2180934/bfa757e9-f0ed-09c0-487a-e7d8c4397f33.png)

ラズパイ上でMySQLを構築し適当にテーブルを作成します。

https://knt60345blog.com/mysql/

MySQLの構築が終わったらMQTTを受け取ったらMySQLに格納するパイソンコードをラズパイ上で実行します。
~~~
import paho.mqtt.client as mqtt
import base64
import binascii
import json
import mysql.connector
from datetime import datetime
import pytz
import struct
import paho.mqtt.client as mqtt     # MQTTのライブラリをインポート
host = 'au1.cloud.thethings.network'

port = 1883
keeparrive = 60
mqtt_id = 'TTNで表示されているMQTT_ID'

mqtt_pw = 'TTNで表示されているMQTT_PW'

# ブローカーに接続できたときの処理
def on_connect(client, userdata, flag, rc):
  print("Connected with result code " + str(rc))  # 接続できた旨表示
  client.subscribe("#")  # すべてをsubする  # subするトピックを設定

# ブローカーが切断したときの処理
def on_disconnect(client, userdata, rc):
  if  rc != 0:
    print("Unexpected disconnection.")

def on_message(client, userdata, msg):
    try:
        payload = msg.payload.decode("utf-8")
        data = json.loads(payload)

        # Additional fields
        device_id = data["end_device_ids"]["device_id"]
        application_id = data["end_device_ids"]["application_ids"]["application_id"]
        frequency = data["uplink_message"]["settings"]["frequency"]
        bandwidth = data["uplink_message"]["settings"]["data_rate"]["lora"]["bandwidth"]
        spreading_factor = data["uplink_message"]["settings"]["data_rate"]["lora"]["spreading_factor"]
        coding_rate = data["uplink_message"]["settings"]["data_rate"]["lora"]["coding_rate"]
        rssi = data["uplink_message"]["rx_metadata"][0]["rssi"]
        snr = data["uplink_message"]["rx_metadata"][0]["snr"]
        received_time_str = data["received_at"]

        # デコードされたBase64バイトデータ
       frm_payload = base64.b64decode(data["uplink_message"]["frm_payload"])

        # Convert received_time to datetime
        modified_time_str = received_time_str[:-4] + 'Z'
        received_time = datetime.strptime(modified_time_str, '%Y-%m-%dT%H:%M:%S.%fZ')

        # Print the extracted fields
        print(f"Device ID: {device_id}, Decoded frm_payload: {frm_payload}, Time: {received_time}")
        print(f"Application ID: {application_id}, Frequency: {frequency}, Bandwidth: {bandwidth}")
        print(f"Spreading Factor: {spreading_factor}, Coding Rate: {coding_rate}")
        print(f"RSSI: {rssi}, SNR: {snr}, Received Time: {received_time}")
        print(f"payload: {frm_payload.hex()}")

        # ここでlen()を呼び出す前にNoneチェック
        if frm_payload is not None:
            max_length = 255
            if len(frm_payload) > max_length:
                frm_payload = frm_payload[:max_length]  # Truncate the payload
            hex_payload = binascii.hexlify(frm_payload).decode('utf-8')  # バイト列を16進数文字列に変換
            print(f"Hex Payload: {hex_payload}")
        else:
            print("frm_payload is None. Skipping length check.")

        # 温度と湿度の変数を初期化
        temperature = None
        humidity = None

        # frm_payload_hexで温度と湿度を特定
        temperature_hex = None
        humidity_hex = None 

        # frm_payload_hexを16進数文字列に変換
        frm_payload_hex = binascii.hexlify(frm_payload).decode('utf-8')
        if "74" in frm_payload_hex:
            temp_idx = frm_payload_hex.index("74")
            temperature_hex = frm_payload_hex[temp_idx+2:temp_idx+4]
        if "68" in frm_payload_hex:
            hum_idx = frm_payload_hex.index("68")
            humidity_hex = frm_payload_hex[hum_idx+2:hum_idx+4]
        if temperature_hex:
            temperature = int(temperature_hex, 16)
            print(f"抽出された温度: {temperature}°C")
        else:
           print(f"温度のヘックス値が不正です: {temperature_hex}")

        if humidity_hex:
            humidity = int(humidity_hex, 16)
            print(f"抽出された湿度: {humidity}%")
        else:
            print(f"湿度のヘックス値が不正です: {humidity_hex}")
                # バイト列を16進数文字列に変換
        hex_payload = frm_payload.hex()
       
        # Database connection
        mydb = mysql.connector.connect(
            host="localhost",
            user="ユーザー名",
            password="パスワード",
            database="データベース名",
            port=3306
        )
        mycursor = mydb.cursor()

        # SQL文（温度と湿度も含む）
        sql = "INSERT INTO fcc_ondo (device_id, application_id, frequency, bandwidth, spreading_factor, coding_rate, rssi, snr, receive_time, hex_payload, temperature, humidity) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"

        val = (device_id, application_id, frequency, bandwidth, spreading_factor, coding_rate, rssi, snr, received_time, frm_payload.hex(), temperature, humidity)
        
        mycursor.execute(sql, val)
        mydb.commit()
        if mycursor.rowcount > 0:
                print(f"Successfully inserted {mycursor.rowcount} record(s).")
        else:
                print("No record inserted.")          
        mycursor.close()
        mydb.close()

    except mysql.connector.Error as err:
        print(f"Something went wrong with the database operation: {err}")
    except Exception as e:
        print(f"An error occurred: {e}")

# MQTTの接続設定
client = mqtt.Client()                 # クラスのインスタンス(実体)の作成
client.on_connect = on_connect         # 接続時のコールバック関数を登録
client.on_disconnect = on_disconnect   # 切断時のコールバックを登録
client.on_message = on_message         # メッセージ到着時のコールバック

client.username_pw_set(mqtt_id, mqtt_pw) #IDとPWを設定
client.connect(host, port, keeparrive)  # 接続先は自分自身
client.loop_forever()
~~~
実はChatGPTに書かせました。うまく動かなかったらChatGPTに書き直せば動くと思います。
これでMQTTからMySQLにデータを蓄積するシステムが完成しました。
後はGrafanaで可視化するだけです！

# 可視化ツール(Grafana)
ラズパイ上にGrafanaを設置します。

ほとんどここに書かれていますね

https://qiita.com/airpocket/items/61209b40f0cddf8f90ed

ChatGPTの登場からコードをめっきり自分で書かなくなりました。
構想と制約を伝えれば自動で生成してくれるので楽ですね。

今回は、ラズパイ上にMySQLとGrafanaを設置しましたが、ローカルエリアからの接続しか可視化できないので次はクラウド化を目指していきたいと思います。

※初めて投稿しましたが、皆さんの記事はきっちり丁寧に書かれていてすばらしいですね。私は途中で断念してしまいます...