「えだまめ」しているラズパイ

30年ぶりに半田ごて握ってラズパイ勉強中。

モーションセンサー(人感センサー)の電池長寿命化 (2) Wroom02スケッチ編

◯やりたいこと

(1)で作成したモーションセンサーのWroom02(WR02)用のスケッチです。


◯やったこと

以下がWR02用のスケッチです。
ATtiny85(AT85)と同様にデータ保存にリングバッファ(RB)を取り入れWR02特有のものとしてエンキュー・デキュー用のアドレスポインタにRTCメモリを使っています。RTCメモリは通電さえしていればディープスリープしても内容が保持されるRAMなので書込み制限を気にしなくて済み気が楽です。またWiFi周りでノンブロッキング化を少々進めてWiFi接続性能の向上を図っています。

/**
*
*    WR02_MOS v0.1.2 (ok)
*      AT85からUARTで送られてくるデータをWiFiでSQLに送信する
*
**/
// BSP(Board Support Package)
//   ESP8266 Boards v3.1.2
//     FlushSize ESP01:1MB Wroom023D:2MB Wroom02:4MB

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <time.h>
#include            <EEPROM.h>
#include            <HardwareSerial.h>

// BSP Built-in Library
#include            <ESP8266WiFi.h>
extern "C" {
  #include          "user_interface.h"
}

// Installed Library

// Global Variable
/* WiFi */
#define WIFI_SSID   "YOUR_SSID"
#define WIFI_PASS   "YOUR_PASS"

/* TCP/IP */
IPAddress ip        (YOUR_IP);            // ex. (192,168,0,2)
IPAddress gw        (YOUR_GW);
IPAddress dns       (YOUR_DNS);           // Google:(8,8,8,8)
IPAddress mask      (YOUR_MASK);          // ex. (255,255,255,0)

/* UART */
#define UART_TX     1
#define UART_RX     3
HardwareSerial      hs(0);

/* Reset */
#define RES_EN      2

/* SQL Server IP */
#define SQL_HOST    "SQL_IP"
#define SQL_PORT    SQL_PORT

/* NTP DATA */
#define NTP_SRV1    "133.243.238.244"     // "ntp.nict.jp"
#define NTP_SRV2    "ntp.jst.mfeed.ad.jp"

/* RTC Memory */
#define RTC_CODE    1234
#define RTC_TOP     64                    // 
#define RTC_END     192                   // 
#define RTC_SIZE    RTC_END-RTC_TOP       // 128個(512byte)

#define ADR_CODE    RTC_TOP
#define ADR_RBT     RTC_TOP+1
#define ADR_RBE     RTC_TOP+2

/* EEPROM */
#define EEP_SIZE    2048                  // EEPROM size
#define RB_MAX      EEP_SIZE/MD_SIZE      // RB(リングバッファ)最大値

/* Measurement DATA */
int     MD_SIZE =   sizeof(time_t);       // time_t(4byte)
#define MD_MAX      125                   // UART受信可能なデータ数
time_t  MDATA       [MD_MAX];

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

/* Function Prototype */

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // pinモード設定
  digitalWrite(RES_EN, LOW);
  pinMode     (RES_EN, OUTPUT);

  // IRセンサー リセット禁止
  ir_resDE();

  // UART
  /* シリアル起動, ESP8266起動時レポートの送信待ち */
  hs.begin(19200); delay(200);

  /* ESP8266起動時レポート用の改行送信 */
  hs.print('\n'); delay(5);

  // EEPROM
  EEPROM.begin(EEP_SIZE);

  // RTC memory
  rtcm_chk();
}

void loop() {
  // ESP8266起動時レポートのATtiny側での処理待ち
  delay(1000);

  // 初期値
  String  line = "";

  // Xon送信後 ATからの NTP,SOH待ち
  //while (hs.available()>0) {char d = hs.read();}
  unsigned long tout = millis();
  while (millis() - tout < 3000) {
    hs.print("Xon\n");
    line = read_line(2000);

    /* データ転送 mode */
    if (line.equals("SOH")) break;

    /* 時刻転送 mode */
    if (line.equals("NTP")) {
      /* WiFi接続 */;
      if (wifi_connect(60000)) {
        if (ntp_connect(10000)) {
          /* NTP時刻 ->UART ->AT85 */
          UXTM = time(NULL);
        }
      }
      hs.println(UXTM);    // NTP->AT85
      delay(5);      wifi_end();
    }
    delay(100);
  }

  // 返信なしはスリープ
  if (line.length()<=0) wifi_end();

  // 受信開始(受信データはメモリに格納->高速処理)
  int mdcnt = 0;
  unsigned long timeout = millis();
  while (millis() - timeout < 5000) {
    line = read_line(1500);

    /* 受信タイムアウト */
    if      (line.length()<=0)   break;

    /* 全データ受信終了 */
    else if (line.equals("EOT")) break;

    /* 受信データ格納 */
    else MDATA[mdcnt] = line.toInt();
    mdcnt ++;
  }

  // MD->RB保存(EEPROMにゆっくり格納)
  for (int i=0; i<mdcnt; i++) rb_eque(MDATA[i]);

  // RBにデータがあればデータ送信へ
  int num = rb_num();
  if (num>0) {
    // WiFi接続チェック(接続成功は データ送信)
    if (wifi_connect(60000)) {
      /* リングバッファにデータがあれば送信 */
      while (true) {
        /* データがなければ終了 */
        UXTM = rb_peek();
        if (!UXTM)          break;
        /* 送信失敗は終了 */
        if (!md_send(UXTM)) break;
        /* 送信成功でキューポインタをinc */
        rb_dque();
        delayy(100);
     }
    }
  }
  // 送信終了
  wifi_end();
}

///// IR Control ////
// IRセンサー許可
inline void ir_resEN() { digitalWrite(RES_EN, HIGH);}

// IRセンサー禁止
inline void ir_resDE() { digitalWrite(RES_EN,  LOW);}

///// UART Control /////
// 1行読込(タイムアウト付)
String read_line(int timeout) {
  String line = "";
  unsigned long tout = millis();
  while (millis() - tout < timeout) {
    if (hs.available() > 0) {
      line = hs.readStringUntil('\n');
      line.trim();
      return line;
    }
  }
  return "";
}

///// WiFi Control /////
/* WiFi接続開始処理
 *   <引数> Timeout: 接続制限時間(0 or 省略は接続するまで無限ループ)
 *   <戻値> 接続:true 未接続:False
 */
boolean wifi_connect() { return wifi_connect(0); }
boolean wifi_connect(int timeout) {
  unsigned long start = millis();
  unsigned long now   = start;
  //int retry = 0;

  // WiFi 初期化
  wifi_init();

  // WiFiが未接続の場合のみbeginする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    delayy(500);
  }

  // WiFi接続(指定時間内) 接続:true 接続失敗:false
  while (WiFi.status() != WL_CONNECTED) {
    if (timeout>0 && millis()-start >= timeout) return false;
    delayy(500);
  }
  return true;
}

/* WiFi 初期化処理 */
boolean wifi_init() {
  // WiFi初期化 (順番大事)
  //   disconnect(true)されて再接続の場合 mode,configの再設定が必須
  WiFi.mode(WIFI_STA);                      // 1.WiFi モード設定
  WiFi.persistent(false);                   // 2.WiFi設定情報 保存しない
  if (!WiFi.config(ip, gw, mask, dns)) {    // 3.固定ip 設定
    delayy(100);
    return false;
  }
  delayy(100);
  return true;  
}

/* WiFi stop処理 */
void wifi_stop() {
  // WiFiが接続の場合のみdisconnectする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.disconnect(true);
  }
}

/* 終了処理 */
void wifi_end() {
  // WiFi切断
  wifi_stop();
  // Uptime 表示
//@@  hs.print("WR02(");
//@@  hs.print((millis()-STTM)/1000.0); 
//@@  hs.println("sec)");

  // Reset 許可
  ir_resEN();
  
  // sleep
  ESP.deepSleep(0);
  //ESP.deepSleep(60*1000*1000);
  delay(100);
}

boolean md_send(time_t uxtm) {

  ~ ここでデータを加工し送信する

}

///// NTP Control /////
/* NTP 同期処理(ノンブロッキング風)
 *   <引数> Timeout[ms]:同期制限時間(0 or 省略は同期するまで無限ループ)
 *   <戻値> Unixtime or 0(エラー)
 */
time_t ntp_connect() { return ntp_connect(0); }
time_t ntp_connect(int timeout) {
  // WiFi接続している時のみ時間取得する
  if (WiFi.status() != WL_CONNECTED) {
    return 0;
  }

  // 初期化
  unsigned long now;
  unsigned long start;
  unsigned long itvl = 250; // NTP再接続時のインターバル時間
  unsigned long last = 0;   // ノンブロッキング用時間変数

  // タイムゾーン,NTPサーバー設定
  configTzTime("JST-9", NTP_SRV1, NTP_SRV2, NTP_SRV3);
  /* 最初に一定時間待機すると同期時にループが省かれる(短時間化) */
  delayy(50);

  // NTPサーバー接続 (timeout秒間 接続確認)
  start = millis();
  while (millis()-start < timeout) {
    // NTP接続はノンブロッキング風(非同期)で
    now = millis();
    if (now-last >= itvl) {
      last = now;
      // 現在時刻の取得
      time_t uxtm  = time(NULL);        // unix時間
 
      // NPT同期チェック
      if(uxtm>1755000000) {
        return uxtm;
      }
      // 未接続の場合,0.3-0.5sec おきに再接続
      itvl = random(300, 500);
    }
    delayy(1);
  }

  // 同期不可 エラーリターン
//@@  hs.println("Connection failed");
  return 0;
}

///// Time Controle /////
// 日時文字列"yyyy-mm-dd hh:mm:ss"を unixtimeに変換
time_t dat2uxt(String str) {
  // String->char 変換
  int len = str.length()+1;
  char chr[len];
  str.toCharArray(chr, len);

  // 日時文字列を tm構造に格納
  struct tm t;
  sscanf(chr, "%d-%d-%d %d:%d:%d",
    &t.tm_year, &t.tm_mon, &t.tm_mday,
    &t.tm_hour, &t.tm_min, &t.tm_sec);
  t.tm_year -= 1900;
  t.tm_mon -= 1;

  // 上記 tm構造から unixtimeを求めてリターン
  return mktime(&t);
}

// unixtimeを日時文字列"yyyy-mm-dd hh:mm:dd"に変換
String uxt2dat(time_t t) {
  char date[64];
  // フォーマット作成
  size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

  // unixtime->日時文字列
  strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", localtime(&t));
  return date;
}

///// Ringbuffer Control /////
// RBポインタ inc
inline int rb_inc(int idx) {
  if (++idx >= RB_MAX) return 0;
  else return idx;
}

// RB内のデータ数を求める
int rb_num() {
  /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt > rbe) rbe += RB_MAX;
  return rbe - rbt;
}

// RBポインタから EEPROMのアドレスを求める
inline int rb_idx2adr(int idx) {return idx * MD_SIZE;}

// エンキュー(enqueue) val->EEP
void rb_eque(time_t val) {
  /* RTC->RBend */
  int rbe = rtcm_get(ADR_RBE);

  /* val->EEP */
  int adr = rb_idx2adr(rbe);
  EEPROM.put(adr, val);
  EEPROM.commit();
  /* RBend inc */
  rbe = rb_inc(rbe);
  rtcm_put(ADR_RBE, rbe);
}

// デキュー(dequeue) EEP->val
time_t rb_dque() {
  /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt==rbe) return false;

  /* EEPROM->val */
  time_t val;
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr, val);
  /* RBtop inc */
  rbt = rb_inc(rbt);
  rtcm_put(ADR_RBT, rbt);
  return val;
}

// peek(非破壊読み出し)
time_t rb_peek() {
 /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt==rbe) return false;

  /* EEPROM->val */
  time_t val;
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr, val);
  return val;
}

///// RTCmemory(ESP8266) Control /////
// RTCメモリ 初期化チェック
void rtcm_chk() {
  int code = rtcm_get(ADR_CODE);
  if (code!=RTC_CODE) {
//@@    ss.println("RTC"); delay(2);
    /* RTCメモリ ヘッダクリア */
    rtcm_put(ADR_CODE, RTC_CODE);
    rtcm_put(ADR_RBT , 0);
    rtcm_put(ADR_RBE , 0);
  }
}

// RTCメモリに書込む
bool rtcm_put(uint32_t addr, uint32_t val) {
  return system_rtc_mem_write(addr, &val, sizeof(val));
}

// RTCメモリから読出す
uint32_t rtcm_get(uint32_t addr) {
  uint32_t val = 0;
  system_rtc_mem_read(addr, &val, sizeof(val));
  return val;
}

///// Other Control /////
/* yield delay(ノンブロッキング ディレイ) */
inline void delayy(unsigned long tm) {
  unsigned long now = millis();
  while (millis()-now < tm) { yield();}
}

<初期定義>
WR02の起動はAT85のデータFullによるリセット信号により行われます。
初期定義では必要なモジュールのインクルードや各種定数・グローバル変数を定義、別途インストールが必要なモジュールは<Installed Library>にまとめてあります。ちなみにボードの指定が間違っていると<BSP Built-in Library>でエラーが発生します。

<setup>
WR02、シリアル、EEPROM、RTCメモリの設定を行います。シリアル設定では厄介なESP8266の起動メッセージを送信し終わるまで200msほど無駄に待機させています。さらにその起動メッセージをAT85が処理し新規に1行待ち状態にするために追加で"\n"を送信しています。実を言うとこれに気がつくまでどうしてもハンドシェイクがうまくいかず結構ハマってしまっていました。

<loop>
loopは
・ハンドシェイク用にAT85にXonを送信、一定時間返信がなければスリープ
・AT85からの返信がNTPならばNTPから得たUnixtimeを送信しスリープ
・ 〃 SOHならばデータ受信モードに入る
・受信ロストを避けるためEOTまでのデータを一旦メモリに格納
・受信したデータをRBに格納
WiFiに接続しデータを送信
・データ送信に失敗した場合は次回送信用にRBデータは未消去のまま
・スリープ
という流れになっています。 モーターや高周波ノイズなどで一時的にWiFi通信ができなかった場合の再送信に備えデータは非破壊読み出しで取得しています。RBに残っていれば次回WiFi接続時に全送信されます。もちろん送信に成功すればデキューされデータは削除されます。RBはこの辺の処理が簡単なので助かります。
実はこの方式にしてからロガーからのデータ欠損率が劇的に減少していて、データの取得率はほぼ100%に上昇しています。ノンブロッキング化との相乗効果かもしれません。なんだもっと早く気がついていればよかったと少々後悔していたところです。

<関数>
関数は以下のグループでまとめてあります。
・IR(赤外線センサー)
・UART
WiFi
・NTP
・Datatime
・RB(リングバッファ)
・RTCメモリ
・その他
何だかんだいってスケッチがかなり長文化。関数は他のスケッチと共用できるものも多くライブラリ化すればいいのでしょうがIDEのライブラリ化はいまだ挑戦したことがありません。サブルーチンの感覚で本体に付随させているのでダラダラと長いスケッチになっています。スケッチの読みにくさ、素人ということで御勘弁です。

〇やってみて

yield()を使ったノンブロッキング化は結構いい感じで動いてくれています。RBもZ80時代を思い出しながら楽しくスケッチの組換えに取り組むことができました。何十年たっても若いころ取り組んでいたものはちょっとやるとすぐに思い出します。やはり若い頃は無理してでもいろいろやっておいた方がいいもんだといまさらながら感じてます。