家庭菜園ロガーの製作:マイコンソフトウェアの改良

MicroPython から Arduino IDE へ移行

 Pico W のソフトウェアを MicroPython で書いていたのだが、原因不明の動作停止が時々起きる。MicroPython がランタイムエラーを起こしているのではないか?と疑っている。実際、PC につないで開発中に何度か MemoryError 例外で落ちてしまった。PC につないでいるときならシリアルモニタで例外発生を見つけられるが、庭に設置した時に例外で落ちてしまうと、黙って停止してしまう。

 そこで、MicroPython を使わずにソフトウェアを書いてみることにした。前に検討した時は、ディープスリープで行き詰まっていたのだが、最近改めて調べてみたところ、実装例があることがわかった。

 実は、Arduino Pico は C/C++ SDK を内包していて、SDK の関数をそのまま使うことができる。

 Arduino Pico を使って、家庭菜園ロガーのソフトウェアを書き直してみた。

Arduino Pico のインストール

土壌水分センサー・温湿度センサー

 温湿度センサー DHT22 は、DHT22ライブラリを使う。Arduino IDEでライブラリマネージャを開き、"DHT Sensor Library" Adafruit をインストールする。Adafruit Unified Sensor に依存するのでインストールするか?と聞かれるので、合わせてインストールする。

 土壌水分センサーは、アナログ入力を読むだけなので、特にライブラリなどは必要ない。Pico W の ADC は 12ビット分解能だが、analogRead() はデフォルトでは10ビット分解能なので、注意する。また、A0, A1を入力モードに切り替えてから少し時間をおいて取得しないと、正しい値が読み出せない。

#include <DHT.h>
#include <DHT_U.h>

//  Global variables for storing the sensor output
float temperature;
float humidity;
float core_temp;
float voltage;
int moist1;
int moist2;

//  Pin assignments
const int PIN_DHT = 20;
const int PIN_MOISTURE_VCC = 18;

DHT dht(PIN_DHT, DHT22);

void led_on() {
  digitalWrite(LED_BUILTIN, HIGH);
}

void led_off() {
  digitalWrite(LED_BUILTIN, LOW);
}

//  Read sensor data
void read_sensors() {
  int i;
  //  Prepare for the sensor readout
  pinMode(PIN_MOISTURE_VCC, OUTPUT);
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  dht.begin();
  digitalWrite(PIN_MOISTURE_VCC, HIGH);
  for (i = 0; i < 10; i++) {
    delay(250);
    led_off();
    delay(250);
    led_on();
    humidity = dht.readHumidity();
    temperature = dht.readTemperature();
    if (isfinite(humidity) && isfinite(temperature))
      break;
  }
  if (!isfinite(humidity))
    humidity = 0;
  if (!isfinite(temperature))
    temperature = -10;
  for (; i < 10; i++) {
    delay(375);
    led_off();
    delay(125);
    led_on();
  }
  moist1 = analogRead(A0);
  moist2 = analogRead(A1);
  core_temp = analogReadTemp();
}

電源電圧

 A3による電源電圧の読み出しは少しクセがあるのだが、下のようなコードで取得できた。起動直後にこの関数を実行する。

//  Read VSYS/3 from ADC3
void read_vsys() {
  pinMode(25, OUTPUT);
  digitalWrite(25, HIGH);
  pinMode(29, INPUT);
  delay(500);
  voltage = analogRead(A3) * 3 * 3.3 / 1024;
  pinMode(29, OUTPUT);
  pinMode(25, OUTPUT);
  digitalWrite(25, LOW);
}

無線LANへの接続

 無線LANへの接続は、WiFi クラスを使う。無限ループにならないように注意する。channel という定数は、複数のシステムを走らせる時にサーバ側が区別するためのもの。本番環境とテスト環境を分けるためにも使っている。

#include <WiFi.h>

//  Server information
const char *ssid = "SSID";
const char *password = "PASSWORD";
int status = WL_IDLE_STATUS;
const char *server = "xxx.xxx.xxx.xxx";
const int channel = 1;

int connect_wifi() {
  int i, j;
  for (j = 10; j >= 0; j--) {
    WiFi.begin(ssid, password);
    Serial.println("Connecting to WiFi...");
    for (i = 10; i >= 0; i--) {
      if (WiFi.status() == WL_CONNECTED)
        break;
      delay(250);
      led_off();
      delay(250);
      led_on();
      Serial.print(".");
    }
    if (i >= 0)
      break;
    Serial.println("retry.");
    WiFi.disconnect();
    delay(250);
    led_off();
    delay(250);
    led_on();
  }
  if (j < 0) {
    Serial.println();
    Serial.println("Wifi connection failed.");
    return -1;  //  Connection not established
  }
  Serial.println();
  Serial.println("Wifi connection established.");
  return 0;
}

データのポスト

 クラス WiFiClient を使う。サーバからの応答は基本的に無視するが、Date: で始まる行は現在の時刻を含んでいるので、それを RTC に保存しておく。(現バージョンでは、この情報は特に使っていない)

#include <time.h>

int connect_client(WiFiClient &client) {
  int i;
  for (i = 10; i >= 0; i--) {
    if (client.connect(server, 80) == 0) {
      delay(500);
      led_off();
      delay(500);
      led_on();
      Serial.print(".");
    } else break;
  }
  if (i >= 0)
    return 0;
  else return -1;
}

int post_data() {
  int i, j, result;
  result = 0;
  Serial.print("Trying to connect to server...");
  WiFiClient client;
  result = connect_client(client);
  if (result < 0)
    return result;
  if (i >= 0) {
    char buf[80];
    Serial.println();
    Serial.println("Connected to server.");
    sprintf(buf, "?a0=%d&a1=%d&a2=%.2f&temp=%.1f&humid=%.1f&ch=%d&comment=coretemp:%.2f",
      moist1, moist2, voltage, temperature, humidity, channel, core_temp);
    Serial.println(buf);
    client.print("GET /iot/cgi-bin/post.py");
    client.print(buf);
    client.println(" HTTP/1.0\r\n\r\n");
    for (j = 10; j >= 0; j--) {
      if (client.available() != 0)
        break;
      delay(750);
      led_off();
      delay(250);
      led_on();
    }
    if (j >= 0) {
      struct tm tm = {0};
      while (client.available()) {
        String line = client.readStringUntil('\n');
        Serial.print(line);
        Serial.print("\n");
        //  If the line begins with "Date:", then update the clock
        int idx = line.indexOf("Date: ");
        if (idx >= 0) {
          // Date: Sun, 20 Oct 2024 00:35:42 GMT
          string_to_tm(line.c_str() + idx + 11, tm);
          char buf[100];
          strftime(buf, 100, "=== %Y/%m/%d %H:%M:%S\n", &tm);
          Serial.print(buf);
        }
      }
      Serial.println();
      //  Set the RTC
      if (tm.tm_year > 0) {
        datetime_t dt;
        tm_to_datetime(&tm, &dt);
        rtc_set_datetime(&dt);
      }
    } else {
      Serial.println("Server timeout.");
      result = -2;
    }
    //  Close connection
    Serial.println("Closing connection");
    client.stop();
  }
  return result;
}

自動アップデート

 HTTPUpdate クラスを使うと、ソフトウェアの更新を HTTP サーバから行うことができる。公式ドキュメントはこちら:「OTA Updates」。ただし、コード例は一部修正が必要だった。update()はクラスメソッドではなくインスタンスメソッドなので、HTTPUpdateのインスタンスを作っておかないといけない。これは HTTPUpdate.h を読まないとわからなかった。Arduino IDE 上でヘッダファイルを開くには、クラス名 HTTPUpdate の上で右クリックして “Go To Definition” を選べばよい。

 OTA update では、ダウンロードされたバイナリは LittleFSファイルシステムに保存される。ファイルシステムを作っておく必要がある。Arduino IDE でTool → Flash Size → 2MB (Sketch 1536 KB, FS 512 KB) を選ぶ。

 バイナリを作成するには、Arduino IDE で「Sketch → Export Compiled Binary」を実行する。スケッチと同じフォルダに build というフォルダが作られ、この中に bin, elf, map, uf2 ファイルが格納される。

 また、サーバ上のバイナリのバージョンは下のような json ファイルで示す。

{ "version":1.00 }

 ラズピコ側では、json として解釈するのではなく、単純に1行ずつ読んで "version:" を見つけ、atof() で数値に変換する。また、「現在のバージョン」を覚えておくために、EEPROM を使う。

#include <EEPROM.h>
#include <HTTPUpdate.h>

//  A struct to store the version information
struct {
  char sig[4];
  float version;
} st;
float new_version;

int check_update() {
  int j, result;
  char buf[80];
  //  Read version.json and EEPROM info, and compare
  EEPROM.begin(256);
  EEPROM.get(0, st);
  if (strncmp(st.sig, "VERS", 4) != 0) {
    strncpy(st.sig, "VERS", 4);
    st.version = 0.0;
  }
  Serial.print("Check update: present version is ");
  Serial.println(st.version);
  //  Get the latest version number from the server
  Serial.print("Check update: getting latest version info...");
  WiFiClient client;
  result = connect_client(client);
  if (result < 0)
    return result;
  Serial.println();
  sprintf(buf, "%d", channel);
  client.print("GET /iot/");
  client.print(buf);
  client.println("/version.json HTTP/1.0\r\n\r\n");
  for (j = 10; j >= 0; j--) {
    if (client.available() != 0)
      break;
    delay(250);
    led_off();
    delay(100);
    led_on();
    delay(250);
    led_off();
    delay(200);
    led_on();
  }
  if (j < 0) {
    Serial.println("Timeout before fetching version.json.");
    return -1;
  }
  result = 0;
  while (client.available()) {
    String line = client.readStringUntil('\n');
    Serial.print(line);
    Serial.print("\n");
    //  If the line includes "version": then read the version string
    int idx = line.indexOf("\"version\":");
    if (idx >= 0) {
      new_version = atof(line.c_str() + idx + 10);
      Serial.print("Old version: ");
      Serial.print(st.version);
      Serial.print("  New version: ");
      Serial.println(new_version);
      if (new_version > st.version) {
        //  We need update
        result = 1;
      }
      break;
    }
  }
  client.stop();
  return result;
}

int fetch_update() {
  char buf[80];
  int j, result;
  //  Try to update
  WiFiClient client;
  HTTPUpdate httpUpdate;
  httpUpdate.rebootOnUpdate(false);
  Serial.println("Auto update: getting the new firmware...");
  sprintf(buf, "/iot/%d/arduino.bin", channel);
  t_httpUpdate_return ret = httpUpdate.update(client, server, 80, buf);
  client.stop();
  if (ret == HTTP_UPDATE_OK) {
    float old_version = st.version;
    Serial.println("Auto update ok.");
    st.version = new_version;
    EEPROM.put(0, st);
    EEPROM.commit();
    //  Post version update notice
    result = connect_client(client);
    if (result >= 0) {
      sprintf(buf, "?ch=%d&comment=Firmware%%20updated:%%20%.2f%%20->%%20%.2f",
      channel, old_version, new_version);
      client.print("GET /iot/cgi-bin/post.py");
      client.print(buf);
      client.println(" HTTP/1.0\r\n\r\n");
      while (client.available()) {
        String line = client.readStringUntil('\n');
        Serial.print(line);
        Serial.print("\n");
      }
      client.stop();
    }
    return 0;  //  The new version will run after wakeup from deep sleep
  } else {
    Serial.println("Auto update failed.");
    return -1;
  }
}

 

ディープスリープ

 matthias-bs/arduino-pico-sleep をダウンロードして、src 以下をディレクトリごと、このプロジェクトのディレクトリにコピーする。pico_sleep(秒数) でディープスリープさせることができる。ディープスリープから復帰したあとは、再起動して、スケッチを先頭から実行する。

//  RTC support
#include <ESP32Time.h>
#include <hardware/rtc.h> // from Pico SDK
#include "src/pico_rtc/pico_rtc_utils.h"

const int sleep_time = 60*5;

void setup() {
  unsigned long start_time = millis();
  int result;
  Serial.begin(9600);
  delay(10);
  //  Initialize pico rtc
  rtc_init();
  Serial.println("Reading Vsys");
  read_vsys();
  pinMode(LED_BUILTIN, OUTPUT);
  led_on();
  Serial.println("Reading Sensors");
  read_sensors();
  if (connect_wifi() == 0) {
    Serial.println("Posting Data");
    if (post_data() == 0) {
      Serial.println("Checking Update");
      if (check_update() > 0) {
        fetch_update();
      }
    }
    WiFi.disconnect();
    Serial.println("Wifi disconnected.");
  }
  led_off();
  digitalWrite(PIN_MOISTURE_VCC, LOW);
  digitalWrite(23, LOW);
  digitalWrite(25, LOW);
  int eseconds = (millis() - start_time) / 1000;
  if (eseconds > sleep_time - 10)
    eseconds = sleep_time - 10;
  pico_sleep(sleep_time - eseconds);
  rp2040.restart();
}

void loop() {
}

 ディープスリープ中の消費電力を測定してみた(DHT22接続、水分センサーは1本だけ接続)。スリープ時は1.6〜1.8 mAになった。MicroPython 版と比べても、遜色はない。

 スケッチをコンパイルしたサイズは下の通り。十分余裕はありそう。

Sketch uses 427468 bytes (27%) of program storage space. Maximum is 1568768 bytes.
Global variables use 73028 bytes (27%) of dynamic memory, leaving 189116 bytes for local variables. Maximum is 262144 bytes.