私の入門記録であって、入門者向け解説サイトではありません。

リッチUI化

  • 投稿日:
  • Category:

Web画面があまりにも素っ気ないので、多少のリッチUI化を試みる。使用するのは ExtJS。RIA構築の為の JavaScriptライブラリで、過去に使用経験があるのでこれを採用する事にした。

スケッチ側は基本的に変わりないが、若干の調整もあるので再掲する。

[ スケッチ:SRR_Point_System_Ver21 ]

// SRR_Point_System
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <ArduinoJson.h>
// for Servo
#include <ESP32Servo.h>
#include <ServoEasing.hpp>
// for OTA
#include <ESPmDNS.h>
#include <NetworkUdp.h>
#include <ArduinoOTA.h>
// SPIFFS
#include "FS.h"
const char ssid[] = "********";
const char pass[] = "********";
const IPAddress ip(192,168,3,17);
const IPAddress gateway(192,168,1,1);  // デフォルトゲートウェイ
const IPAddress subnet(255,255,255,0);
const int pnt_max = 8;
int rel_pins[pnt_max];  // リレーパラメータ用配列
int srv_pins[pnt_max];  // サーボパラメータ用配列
int srv_deg0[pnt_max];  // サーボL側微調整用配列
int srv_deg1[pnt_max];  // サーボR側微調整用配列
int srv_defo[pnt_max];  // サーボデフォ位置用配列
//const int SIZEOF_REL_PINS = sizeof(rel_pins)/sizeof(rel_pins[0]);
const int srv_min = 1450;
const int srv_max = 2400;
const int srv_degL = -20;
const int srv_degR = 170;
const int srv_sec = 4000;
const uint32_t BUF_SIZE = 255;  // SPIFFS
String file_name = "/config.json";  // ファイル名の指定
String readStr;
AsyncWebServer server(80);      // ポート設定
// Jsonオブジェクトの初期化
StaticJsonDocument<512> doc;
// ブラウザから受信する変数
const char* command;  // コマンド
uint8_t pnt_number;  // ポイント配置番号
uint8_t pnt_status;  // ポイント状態制御(0=L / 1=R)
// for Servo
ServoEasing servo[pnt_max]; // オブジェクト配列定義
void setup() {
  Serial.begin(115200);
  Serial.println("***** SRR Point System Start *****");
  // SPIFFSのセットアップ
  if(!SPIFFS.begin(true)){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  WiFi.config(ip, gateway, subnet);
  WiFi.mode(WIFI_STA);  // for OTA
  WiFi.begin (ssid, pass);
  // for OTA
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH) {
        type = "sketch";
      } else {  // U_SPIFFS
        type = "filesystem";
      }
      // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) { Serial.println("Auth Failed"); }
      else if (error == OTA_BEGIN_ERROR) { Serial.println("Begin Failed"); }
      else if (error == OTA_CONNECT_ERROR) { Serial.println("Connect Failed"); }
      else if (error == OTA_RECEIVE_ERROR) { Serial.println("Receive Failed"); }
      else if (error == OTA_END_ERROR) { Serial.println("End Failed"); }
    });
  ArduinoOTA.begin();
  // 各種情報を表示
  Serial.print("SSID: ");
  Serial.println(ssid);
  Serial.print("AP IP address: ");
  Serial.println(ip);
  server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
  server.onNotFound(notFound);
  // Read Config
  Serial.println("Config read.");
  if (SPIFFS.exists(file_name)) {  // ファイルが存在すれば
    File fr = SPIFFS.open(file_name.c_str(), "r");  //データ読み込み
    readStr = fr.readStringUntil('\0');  //EOFまで読み出し
    fr.close();
  } else {
    Serial.println("Error : File not found.");
  }
  StaticJsonDocument<512> docjson;
  DeserializationError error = deserializeJson(docjson, readStr);
  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }
  else {
    for (JsonObject data_item : docjson["data"].as()) {
      int data_item_pn = data_item["pnt_number"];
      rel_pins[data_item_pn] = data_item["rel_pin"];
      srv_pins[data_item_pn] = data_item["srv_pin"];
      srv_deg0[data_item_pn] = data_item["srv_deg0"];
      srv_deg1[data_item_pn] = data_item["srv_deg1"];
      srv_defo[data_item_pn] = data_item["srv_defo"];
    }
  }
  // for Servo
  Serial.println("Initialize start.");
  for(int i =1; i < pnt_max; i++) {
    pinMode(rel_pins[i], OUTPUT); // pinを出力設定に
    pinMode(srv_pins[i], OUTPUT); // pinを出力設定に
    servo[i].attach(srv_pins[i], srv_min, srv_max); // attach(int pin, int min, int max)
    resetPnt(i);  // ResetPoint
  }
  setEasingTypeForAllServos(EASE_CUBIC_IN_OUT); // EASE_LINEAR is default
  // Pointの制御変数の変更リクエスト
  server.on(
    "/arduino/post",
    HTTP_POST,
    [](AsyncWebServerRequest * request){},
    NULL,
    [](AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total) {
      String resjson = "";
      for (size_t i = 0; i < len; i++) {
        resjson.concat(char(data[i]));
      }
      Serial.println(resjson);
      DeserializationError error = deserializeJson(doc, resjson);
      if(error){
        Serial.println("deserializeJson() faild");
        request->send(400);
      }
      else{
        command = doc["COMMAND"];
        pnt_number = doc["PNT_NUMBER"];
        pnt_status = doc["PNT_STATUS"];
        // Reset Points
        if (strcmp(command, "reset") == 0) {
          for(int i =1; i < pnt_max; i++) {
            resetPnt(i);
          }
        }
        // Set Point
        if (strcmp(command, "set") == 0) {
          if(pnt_number > 0) {
            if (srv_pins[pnt_number] > 0) servoGo(pnt_number, pnt_status, srv_sec);   // Servo
            if (rel_pins[pnt_number] > 0) relayGo(pnt_number, pnt_status);            // Relay
            command = "";
          }
        }
        request->send(200, "text/plain", "OK"); // Response to xhr
      }
  });
  command = "";
  pnt_number = 0;
  pnt_status = 0;
  // サーバースタート
  Serial.println("Server start.");
  server.begin();
  Serial.println("***** SRR Point System Ready *****");
} // End setup()
void loop() {
  ArduinoOTA.handle();  // for OTA
  delay(100);
} // End loop()
void servoGo(int pnt_number, int pnt_status, int srv_sec) {
  int deg = 0;
  if (pnt_status == 0) { deg = srv_degL - srv_deg0[pnt_number]; }
  else { deg = srv_degR + srv_deg1[pnt_number]; }
  Serial.println(String(pnt_number) + " : deg=" + String(deg));
  servo[pnt_number].setEaseToD(deg, srv_sec);
  synchronizeAllServosStartAndWaitForAllServosToStop();
}
void relayGo(int pnt_number, int pnt_status) {
  if(pnt_status == 0){
    digitalWrite(rel_pins[pnt_number], LOW);
  } else {
    digitalWrite(rel_pins[pnt_number], HIGH);
  }
}
void resetPnt(int pnt_number){
    servoGo(pnt_number, srv_defo[pnt_number], 500);  // initialize
    relayGo(pnt_number, srv_defo[pnt_number]);  // initialize
}
void notFound(AsyncWebServerRequest *request){
  if (request->method() == HTTP_OPTIONS) request->send(200);
  else request->send(404);
}

HTML側は基本的に中身なし。ヘッダ部分にライブラリへのリンクを設けておき、後は JavaScript内でパネルやフォームの描画を行なうのだ。

[ index.html ]

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" charset="UTF-8">
    <title>SRR_Point_System</title>
    <link rel="icon" href="./image/favicon.ico">
    <link rel="stylesheet" type="text/css" href="./style.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
    <link rel="stylesheet" type="text/css" href="https://hkuma.com/ext620/build/classic/theme-aria/resources/theme-aria-all.css" />
    <script type="text/javascript" src="https://hkuma.com/ext620/build/ext-all.js"></script>
    <script type="text/javascript" src="./Main.js"></script>
    <script type="text/javascript" src="./MainController.js"></script>
  </head>
  <body>
  </body>
</html>

[ style.css ]

.x-mask {
  background-color: rgba(128, 128, 128, 0.1) !important;
}
.whiteIcon {
  color: #fff;
  margin-top: 3px;
}
.fa-spin {
  font-size: 14px !important;
}

描画や処理の為の JavaScriptがこちら。

[ Main.js ]

// Main.js
var setFlg = true;
const spinner = '<i class="fas fa-spinner fa-spin whiteIcon"></i>';
async function fetchFunc() {
  try {
    const res = await fetch("./config.json");
    if (!res.ok) {
      throw new Error("fetch failure");
    }
    const jdata = await res.json();
    return jdata;
  } catch (error) {
    console.error("fetchFunc error:", error);
  }
}
Ext.onReady(function(){
  fetchFunc().then(
    (jdat) => {
    let pmax = jdat["data"].length;
    let pdef = [];
    for (let i = 0; i <= pmax; i++) pdef.push(Array(2));
    for (let i = 0; i <= pmax-1; i++) {
      const pt = jdat["data"][i]["pnt_number"];  // ポイント番号取得
      const lr = jdat["data"][i]["srv_defo"];    // デフォルト方向取得
      pdef[pt][lr] = 1;
    }
    var radioGroup = {
      xtype: 'fieldset',
      title: 'Point Control',
      layout: 'form',
      id: 'pntForm',
      defaults: {
        labelStyle: 'padding-left:4px;',
      },
    };
    function setJson(radObj){
      if (setFlg == false) return;
      var pntNumber = radObj.items.items[0].name;
      var pntStatus = radObj.items.items[1].getValue();
      send_status('set', Number(pntNumber), pntStatus);
    }
    // combine all that into one huge form
    var fp = Ext.create('Ext.FormPanel', {
      title: 'Point Panel',
      frame: true,
      fieldDefaults: {
        labelWidth: 110
      },
      id: 'pointPanel',
      width: 520,
      margin: '20, 0',
      bodyPadding: 10,
      items: [
        radioGroup
      ],
      buttons: [{
        text: 'Reset',
        iconCls: 'fas fa-sync-alt whiteIcon',
        margin: '5px',
        handler: function(){
          setFlg = false;
          fp.getForm().reset();
          setFlg = true;
          send_status('reset', 0, 0);
        }
      },{
        xtype: 'displayfield',
        id: 'btn_msg',
        margin: '10px',
        value: spinner
        }
      }]
    });
    var vp = Ext.create('Ext.Viewport', {
      extend: 'Ext.container.Viewport',
      autoScroll: true,
      layout: {
        type: 'vbox',
        align: 'center'
      },
      items: [ fp ],
      listeners: {
        afterrender:  function() { msgDisp('Ready', '#0f0'); }
      }
    });
    var radios;
    for (let i = 1; i <= pmax; i++) {
      radios = Ext.create('Ext.form.RadioGroup',{
        xtype: 'radiogroup',
        fieldLabel: 'Pnt'+i,
        columns: 1,
        onChange: function(){ setJson(this); },
        items: [
          {boxLabel: 'L', name: i, inputValue: 0, itemId: i+'0', checked: pdef[i][0]},
          {boxLabel: 'R', name: i, inputValue: 1, itemId: i+'1', checked: pdef[i][1]}
        ]
      });
      Ext.getCmp('pntForm').insert(radios);
    }
  });
});

[ MainController.js ]

// MainController.js
// json返り値のテンプレート
var json_tpl = {
  COMMAND: "",
  PNT_NUMBER: 1,
  PNT_STATUS: 0
}
// 送信ステータス表示
function msgDisp(msg_txt, txt_color){
  var obj = Ext.getCmp('btn_msg');
  obj.setValue('<span style="color: ' + txt_color + ';">' + msg_txt + '</span>');
}
// タイムスタンプ文字列の作成
function getTime() {
  var now_time = new Date();
  var now_time_val = ('0' + now_time.getHours()).slice(-2) + ':' + ('0' + now_time.getMinutes()).slice(-2) + ':' + ('0' + now_time.getSeconds()).slice(-2);
  var time_stamp = now_time_val + " ";
  return time_stamp;
}
// Pointの状態変更リクエストの送信
function send_status(cmd, pnt, send_status){
  var str_status = cmd;
  if (cmd == "set") {
    str_status += " Point" + pnt + " > ";
    if(send_status == 0) str_status += "L";
    else str_status += "R";
  }
  // 送信コマンド用JSONデータ作成
  var send_json = json_tpl;
  send_json.COMMAND = cmd;
  send_json.PNT_NUMBER = pnt;
  send_json.PNT_STATUS = send_status;
  send_json = JSON.stringify(send_json);
  console.log(send_json);
  msgDisp(spinner);  // disp spinner
  Ext.Ajax.timeout = 5000;  // ms
  formDisable(true);
  Ext.Ajax.request({
    url: '/arduino/post',  // post url
    method: 'post',
    headers: { 'Content-Type': 'application/json' },
    success: function (response, opts) {
      var res = response.responseText;
      console.log(res);
      msgDisp(getTime() + str_status + " : " + res, "#0f0");
      formDisable(false);
    },
    failure: function (response, opts) {
      var stat = response.status;
      console.log(stat);
      msgDisp(getTime() + str_status + " : NG <small>(" + stat + ")</small>", "#f00");
      formDisable(false);
    },
    params: send_json  // send json data
  });
}
function formDisable(flag) {
  var obj = Ext.getCmp('pointPanel');
  if (flag) obj.disable();
  else obj.enable();
}

生成した画面の状況をサンプルとして以下に埋め込んでおく。実際に操作可能だがあくまでもデモ用であり、ESP32回路には繋いでいないので画面のみの紙芝居状態だ。

なお最終的にモジュールへ設置の際は、cssで絶対座標を指定し、路線図上に各ラジオ釦を配置する予定である。