見出し画像

【SALZmini2022作製日誌】完成まであともう一息

はじめに

前回の記事

でひとまず使ってみて様子を見ることにしました。
現在の所、気に入らないのは以下の点です。

BOWLについて

  • html、cssの整理

  • 全体のデザインがやぼったい。

  • 「今すぐ水やり」ボタンをもう少し何とかする。

  • SALZmini2022が近くになくても、Connect状態になる。

  • 値の更新が遅い。

  • Connectボタンを押さなくても、つながるようにならないか?

SALZmini2022について

  • BOWLでは赤色なのに、常に青色点滅している。

  • どれぐらいの頻度で水やりしているかわからない。

早速改善に取り掛かります。

BOWL:html、cssの整理

色んなサンプルを取りこんできちんと理解もせず突き進んできたので、ソースがぐちゃぐちゃです。

こんな感じで、bodyの中身がid="AAA" class="AAA"となっています。idとclassの使い分けがよくわかっていなかったのと、JavaScriptからアクセスする際にはidを指定しているし、cssではclassなので、両方書いておいた。という感じになっています。

調べてみると、どうやら

 id ⊂ class

のようです。

例えば、class「標準ボタン」の中にid「コネクト」、id「info」があって、class「標準ボタン」のスタイルはこんな感じで。。。と書いていくようです。

確かにFigmaでも元のパーツをリンクを保ったままコピーする機能があったりしたので、このような構造を最初から意識しながらデザインしていればよかったのだと思います。

おおよそはわかったので、対策をしていきます。

classとしてまとめるべきものがそもそもあるのか?という疑問があります。Figmaから出力されたものが、

.class名 {
}

という書き方で出力されていましたが、

#id名 {
}

で書けるようなので、一度classを整理するのがよいと思いました。

すっきりしました。

HTMLやCSSの仕掛けが少しわかるようになってきたので、Chromeで[Ctrl]+[Shift]+i キーでディベロッパーツールをよく開くようになりました。

いままで何気なく使っていたWebページの仕掛けがどんどん理解できます。以前仕事で、RPAが使えないか検討したり、PythonからSeleniumを使ってスクレイピングできないかかじってみたことがありました。

確かその時もわからないなりにタグを探していましたが、今ならきちんと理解できるような気がします。

話を戻して、ソースを見ていると"<div class=..."が結構多いことに気づきます。

ちょっと気になって調べてみました。

ここで分かったことは、div自体がHTMLコードを束ねるために使うもの。ということです。divで束ねた後、どれぐらいの大きさの名前を付けたいかということを考えたとき、今回の取り組みではdiv idで問題ないことがありました。

こんなちょっとした疑問にも答えてくれるページが用意されているのは、SEO対策が世の中に普及しているおかげなんですね。とてもありがたいです。

BOWL:全体のデザインがやぼったい。

について考えてみます。

本当はわかっているんです。デザイナーさんにヘルプすれば、いくらでも素晴らしいデザインができるということを。ツテを頼ってもいいし、今なら、ココナラのようなサービスに頼ってもいいかもしれません。

でも、ここはもう少し自分が納得するまで頑張ってみたいところです。表示すべきデータ、必要な機能については、だいぶん洗練されてきていると思います。

もっと良いデザインを求めて、今風の考え方を取り入れてみたいと思います。

  • シングルカラムレイアウト

  • 背景に写真を入れてみる。

に挑戦します。

図書館で借りてきた本「HTML&CSSとWebデザインが1冊できちんと身につく本 服部 雄樹/著 技術評論社」を元に使えそうなことろを取りこんでいきます。

<body id="index">
    (略)
</body>
<style>
    (略)
  #index {
        background-image: url(./images/background.jpg);
        background-repeat: no-repeat;
        background-position: center top;
        background-attachment: fixed;
        background-size: cover;
    }
</style>

bodyにidを振り、スタイルシートに記述すると、簡単に背景画像を指定することができました。

背景を入れるだけでそれっぽくなってきました。

シングルカラムレイアウトに関しては、まだちょっとわかっていない所が多いのですが、次に進むことにします。

BOWL:「今すぐ水やり」ボタンをもう少し何とかする。

なんだかよくわからなくなって、一番シンプルなボタンに戻していたのですが、何が分かっていないかを整理しました。

現行のボタン
        <input type="button" id="btn_watering_now" value="今すぐ水やり">

<input type="button"...>を用いてます。
ところが、本のサンプル、ネット情報を尋ねると
<a href="...">を用いており、この<a>タグをCSSで修飾を行っていました。

ということは、この流れに逆らわず、<a href="...">で書き換えができるか検討してみます。

その後、ネット情報も合わせてさまよっているうちに、今の所このような感じで落ち着きました。

デザインが違うため、正にコピペ感たっぷりのボタンとなってしまいました。しっかり勉強しようと勢い込んだものの、html&cssの世界は奥が深すぎて沼ですね。及第点で満足することにしました。

次の課題に取り掛かります。

BOWL:SALZmini2022が近くになくても、Connect状態になる。

これは、不具合というよりも作製途中という感じです。遷移状態(ステートマシン)に応じた画面更新が行われていないために起こります。

別の言い方をすると、エラーチェックをきちんと行っておらず、接続することを前提として処理をしているので、接続の有無にかかわらず、アイコンを変更していることが問題です。

データとUIの関係。いつの時代も悩ましい問題のままです。
エレガントに書けるようになればなるほど、汚い処理が浮かび上がってきます。

と、ぼやいていても始まりません。
泥臭く結果を出すことを優先させて取り組んでみます。

まずは、問題を見える化しなければなりません。今起きていることが分かるように表示を増やします。

SALZmini2022の表示を増やし、シリアルモニタでの表示を見える化しました。

SALZmini2022のシリアルモニタの様子

これで、プログラムの内部で何が起きているかわかるようになりました。特に予想外の動きはしていません。

併せてソースの整理も行いました。
だらだら触っていると、気になるところがぽつぽつ出てきます。
不具合もいくつか見つかり、ある程度すっきりしてきました。

BOWL:値の更新が遅い。

BOWLで設定値が見えるようになったのはよいことなのですが接続してから値が返ってくるまでの時間が長く待ちきれません。

  • BLEのための通信時間確保は必要か?

  • マルチタスクの適切な利用について考える。

という課題について整理してみようと思いましたが、もう少し後回しにします。当面の回避策としてサイクルを短くして試してみたいと思います。

マルチタスクについては、現在、LEDをぼわーっと光らせるために、別タスクで処理を行っています。SALZmini2022の仕事が増えてくれば、別タスクを本来の仕事のために割り当てるべきではないかと思うのですが、今の所はこのままで進めることといたします。

BOWL:Connectボタンを押さなくても、つながるようにならないか?

SALZmini2022側のプログラムは整理できたので、今度はレシーバー側のBOWLの整理にかかります。

私の作りたいものは、このページを表示する際にコネクト処理を行い、必要な値を取得し表示するようにしたいと考えています。

なのですが、なかなかうまくできません。
私はJavaScriptでイベントハンドリングが苦手で、意図した通りに上手く動かせたよい思い出がありません。時々つぶやいている「コールバック地獄」に陥ったこともあります。

なので、この部分はあまり深入りしない方がいいのかなと。もう少しこのままで置いておくことにしました。

SALZmini2022についての課題も整理していきます。

もうすでにBOWLの改良過程でSALZmini2022も手を入れています。ですが、まだまだ気になる点がいくつかあります。

SALZmini2022:BOWLでは赤色なのに、常に青色点滅している。

こちらはSALZmini2022の方の問題でした。
青色点滅しかしない状態になっていました。ソースを修正し確認しました。

int
GetDispLevel(void)
{
  int ret;
  ret = (int)(((float)salzData.currentWaterLevel / 100.0) * (float)NCOLORS);
  ret = (ret > NCOLORS - 1) ? (NCOLORS - 1) : ret;
  return ret;
}

SALZmini2022:どれぐらいの頻度で水やりしているかわからない。

これをどのように処理しようか少し悩んでいました。
今作っている、BOWLというものがSALZmini2022の状態をモニタリングし設定をするものです。
であれば、BOWLができる限りの情報を手にして表示できればよいのですが、色々作っているうちにちょっと自信がなくなってきました。
いまやりたい「どれぐらいの頻度で水やりしているか」を表示するには、過去のログをどこかで保持して見せる必要があります。

これをどうするか考えていたのですが、色んな方法は思い付くものの、実験する気力が湧いてきません。
私が今獲得している技術は、BLEを通じてWi-fiでネットに接続し、Ambientへ値を書き込むことです。以前、Switchbot温湿度計で身につけたゲートウェイ機能を使って、様子を見てみることにしました。

まずはターゲットとなる機器選びです。

手持ちのM5シリーズたち(左下は「黄金のAtomLite」)

M5Stack GRAY、M5StackC、AtomMatrix、AtomLite、M5Stamp C3 Mateなど、うちの電子工作ブルペンで待機しているマイコンを集めてみました。

デバッグ用のモニタリング装置なので、BLEとWi-fiが使えれば何でもよいのですが、以前作ったTHGatewayがAtomMatrix用だったので、M5シリーズであれば、あとは何とかなるかなと。

だからと言って、ではAtomMatrixで。というのも面白くなかったもので、あれこれ考えておりましたが。。。

M5 Core-Inkに決定しました。

選考理由は今の所一番使い方が分からない機種であること。
電子ペーパーは低電力で表示できるとのことで屋外用のデバイスによいのではないかと思っておりましたが、長時間紫外線に当ててはいけないようなので積極的には使っておりませんでした。

まあ、THGatewayがAtomMatrixで動くということは、表示をしなければ十分動くと思われます。

使用環境も屋内なのでおそらく問題ないはずです。

将来的には内蔵バッテリを有効に使った遠隔データ収集ゲートウェイとして使いたいという夢があるのですが、今はとりあえず動けばよい状況なので練習がてら使ってみることにしました。

M5CoreInkへの環境設定については、先人のみなさまの有益な情報を元にスムーズに進めることができました。

M5CoreInkの表示サンプルを取りこみ、初期化部分を作成し、不要なコードをどんどんコメントアウトし、必要なコードの書き換えを行いました。

SMGatewayと名付けました。

一晩動作させた結果

それなりに動いているようです。

夜の間、水分量%が上昇しています。センサが気温など別の要因に反応している可能性がありますが、今の段階では詳しいことはわかりません。水やりカウントは0のままなので、給水されたわけではないようです。夜に雨が降ったからか。ともかくあやふやです。もう少し様子を見ることにします。

その後の様子

SALZmini2022から値が飛んで来なくなっている様子です。
値が空白です。
定期的に項目があるということは、SMGatewayからの定期的なデータ送信は行われています。BLEの受信が上手く行っていないようです。

さらに、その後の様子

SALZmini2022の問題ととらえていましたが、SALZmini2022をリセットしても解決できず、SMGatewayをリセットすると直る状態なので、この辺りはきちんと調べてみる必要がありそうです。

確かにこの問題は、歴代のSALZシリーズで発生していました。今までは2週間~1カ月しばらく放っておいても、それなりに使えていて、止まっていることに気が付けば、電源を抜き差しする、雑な対応で乗り切ってきました。

この部分はこだわってなんとか原因究明し解決したい。
という所で、今回も力尽きそうです。

さいごにSMGatewayのソースコードを掲載します。
コメントだらけのソースです。

// SMGateway bbd
// SALZmini2022ゲートウェイ
// ArduinoIDEスケッチ例ESP32 BLE ArduinoのBLE_scanを参考に
// THGatewayAMをもとに作成
// M5 CORE INK 用
//
//SALZmini2022から発せられるBLE信号をキャッチし、Ambientへデータを送ります。
//[ファイル]-[環境設定]-[追加のボードマネージャのURL]に
//「https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json」を追加する。
//[スケッチ]-[ライブラリをインクルード]-「ライブラリを管理...」で「M5Core-Ink」を追加する。
//[ツール]-[ボード]-[ボードマネージャ]で「M5Stack」をインストールし、
//[ツール]-[ボード]-[M5Stack Arduino]-[M5Stack-CoreInk]を選択する。
//Partition Scheme:"NO OTA(Large APP)"にする。

#include <M5CoreInk.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include "Ambient.h"

#define SECONDS(s)    ((s) * 1000)
#define MINUTES(m)    SECONDS(m * 60)

#define SALZ_MINI_2022_DATA_SIZE 8

enum {
  stError,
  stWatering,
  stWaitRed,
  stWaitYellow,
  stWaitBlue
};

typedef struct {
  uint16_t  manufactureID;
  uint8_t   wateringLevel; // 水分量%
  uint8_t   st;
  uint16_t  count;
  uint16_t  wCount;
} SALZmini2022ADData;

int scanTime = 5; //In seconds
int waitTime = 2; //In seconds
int intervalTime = 20 * 60; //In seconds
int skipResetCount = intervalTime / (scanTime + waitTime);
int skipCount = skipResetCount;

BLEScan* pBLEScan;

#define MAX_ADDRESS 8

int nAddresses = 0;
std::string addresses[MAX_ADDRESS];

Ink_Sprite InkPageSprite(&M5.M5Ink);

boolean fConnect = false;
WiFiClient client;
// Wi-Fi
const char * ssid     = "ssid";
const char * password = "password";

Ambient ambient;
unsigned int channelId = 0; // AmbientのチャネルID
const char* writeKey = "XXXX"; // ライトキー

void
printAddressPos(void)
{
  Serial.printf("***** printAddressPos(%d) *****\n", nAddresses);
  int loopCount = (nAddresses < MAX_ADDRESS) ? nAddresses : MAX_ADDRESS;
  for (int i = 0; i < loopCount; i++)
    Serial.printf("address[%d]:%s\n", i, addresses[i]);
  Serial.printf("*****\n");
}

int getAddressPos(const std::string tAddress)
{
  // 見つけたMACアドレス毎に番号を振り、その番号でAmbientへデータを書き込む。
  // 最大数を超えた場合、-1を返す。
  bool fFound = false;
  int pos = -1;
  int loopCount = (nAddresses < MAX_ADDRESS) ? nAddresses : MAX_ADDRESS;
  for (int i = 0; i < loopCount; i++) {
    if (tAddress == addresses[i]) {
      fFound = true;
      pos = i;
      continue;
    }
  }
  if ((nAddresses < MAX_ADDRESS) && !fFound) {
    pos = nAddresses;
    addresses[nAddresses++] = tAddress;
  }
  return pos;
}

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    //activeサイン
    static int ff = 1;
//    M5.dis.drawpix(4, 4, (ff ^= 1) ? RGB(0x40, 0x00, 0x00) : 0);

    Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());

    BLEAddress address = advertisedDevice.getAddress();

/*
    if (advertisedDevice.haveServiceData()) {
      std::string d = advertisedDevice.getServiceData();
      Serial.printf("ServiceData(%d):", d.size());
      for (int i = 0; i < d.size(); i++) {
        Serial.printf("%02X", d[i]);
      }
      Serial.print("\n");
    }
*/
    if (advertisedDevice.haveManufacturerData()) {
      std::string d = advertisedDevice.getManufacturerData();
      Serial.printf("ManufacturerData(%d):", d.size());
      for (int i = 0; i < d.size(); i++) {
        Serial.printf("%02X", d[i]);
      }
      Serial.print("\n");
    }

    // SALZmini2022を特定する。
    if (advertisedDevice.haveManufacturerData() == true
        && advertisedDevice.getManufacturerData().length() == SALZ_MINI_2022_DATA_SIZE) {
      SALZmini2022ADData sd;
      memcpy(&sd, advertisedDevice.getManufacturerData().data(),
          advertisedDevice.getManufacturerData().length());

      if (sd.manufactureID == 0xffff) {
        Serial.printf("manufactureID:%04X\n", sd.manufactureID);
        Serial.printf("wateringLevel:%d\n", sd.wateringLevel);
        Serial.printf("st:%d\n", sd.st);
        Serial.printf("count:%ld\n", sd.count);
        Serial.printf("wCount:%ld\n", sd.wCount);
 
        if (fConnect) {
          int pos = getAddressPos(address.toString());
          if (pos >= 0) {
            ambient.set(pos * 4 + 1, sd.wateringLevel);
            Serial.printf("ambient.set(%d, %d);\n", pos * 3 + 1, sd.wateringLevel);
            ambient.set(pos * 4 + 2, sd.st);
            Serial.printf("ambient.set(%d, %ld);\n", pos * 3 + 2, sd.st);
                          ambient.set(pos * 4 + 3, sd.count);
            Serial.printf("ambient.set(%d, %ld);\n", pos * 3 + 3, sd.count);
            ambient.set(pos * 4 + 4, sd.wCount);
            Serial.printf("ambient.set(%d, %ld);\n", pos * 3 + 4, sd.wCount);
          }
        }
      }
    }
  }
};

void dispLCD(int skipCount)
{
  //Wifiの接続状況
//  int c = (fConnect) ? RGB(0x00, 0x80, 0x00) : 0;
/*
  for (int x = 0; x < 4; x++)
    M5.dis.drawpix(x, 4, c);
*/

  //アドレス登録の状態
//  c = RGB(0x20, 0x20, 0x20);
/*
  for (int i = 0; i < nAddresses; i++)
    M5.dis.drawpix((i > 4) * 3 + (i % 2), (i % 4), c);
*/

  //更新までの残り時間
/*
  M5.dis.drawpix(2, 0, (skipCount > skipResetCount / 4 * 3) ? RGB(0x80, 0x80, 0x00) : 0);
  M5.dis.drawpix(2, 1, (skipCount > skipResetCount / 4 * 2) ? RGB(0x80, 0x80, 0x00) : 0);
  M5.dis.drawpix(2, 2, (skipCount > skipResetCount / 4) ? RGB(0x80, 0x80, 0x00) : 0);
  M5.dis.drawpix(2, 3, (skipCount > 0) ? RGB(0x80, 0x80, 0x00) : 0);
*/
}

void setup() {
  M5.begin(); //Init CoreInk.  初始化 CoreInk
  if( !M5.M5Ink.isInit()) //Init CoreInk screen.  初始化 CoreInk 屏幕
  {
    Serial.printf("Ink Init faild");    //如果初始化失败,打印信息
    while (1) delay(100);
  }
  M5.M5Ink.clear();   //Clear screen.  清屏
  delay(1000);
  if( InkPageSprite.creatSprite(0,0,200,200,true) != 0)   //Create a sprite.  创建一个画布
  {
    Serial.printf("Ink Sprite creat faild");
  }
  InkPageSprite.drawString(10,50,"SALZmini2022 Gateway"); //Draw a string.  绘制一个字符串
  InkPageSprite.pushSprite(); //Push the sprite to the screen.  将画布绘制到屏幕上
 
  Serial.begin(115200);
  Serial.println("Scanning...");

  BLEDevice::init("M5CoreInk");
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);  // less or equal setInterval value

  WiFi.begin(ssid, password);  //  Wi-Fi APに接続
  for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++) {  //  Wi-Fi AP接続待ち
    Serial.print(".");
    delay(100);
  }
  Serial.print("\n");
  fConnect = (WiFi.status() == WL_CONNECTED);

  if (fConnect) {
    // チャネルIDとライトキーを指定してAmbientの初期化
    ambient.begin(channelId, writeKey, &client);
  }

  delay(SECONDS(1));
}

void loop() {
  dispLCD(skipCount);
  if (--skipCount < 0) {
    skipCount = skipResetCount;
    if (fConnect)
      ambient.send();
  }

  BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
  Serial.print("Devices found: ");
  Serial.println(foundDevices.getCount());
  Serial.println("Scan done!");
  pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory

  printAddressPos();

  delay(SECONDS(waitTime));
}

※参照注意:テスト中のソースコードです。

さいごに

次回は完成までたどり着きたい。と思いながら、今、この作っている瞬間が最高に面白いと感じています。最後までお付き合いいただき、誠にありがとうございました。

#自動潅水装置 #盆栽 #園芸 #電子工作 #WebBluetoothAPI #BlueJelly #IoT #Javascript #HTML5 #css #Figma #Webデザイン #M5Stack #M5CoreInk #AruduinoIDE

この記事が気に入ったらサポートをしてみませんか?