調整の手間を省こう

この記事はMice Advent Calendar 7日目の記事です。

全日本について

更新してなかったので、この場で。
まずは、2017年度お疲れ様でした。全日本ではクラシック競技で我々Bustersの圧倒的勝利でしたね。

自分は4位とMice系トップは守りました。U宮さんとは今年こそ、日本人が優勝取り返しましょう話していましたが、ふがいない結果・・・。これで晴れて地球人に戻させていただけたと思います。いやしかし、10/19に基板が届いてから丁度1か月で4位なので、実は相当良いのでは??

あとakoちゃんは個人的にバスター対象にしたので、来年はシーズン通して完封します。


さて、今日の記事ですが、 web系わからない人には初見バイバイです。さようなら。

パラメータ調整の手間を軽減しよう。

もう、ただただ調整がツライ。 いうことで。ツライ要因を上げる。

  • 理論がいまいち
  • 調整パラメータはすべてハードコーディングされているため都度、ビルド⇒書き込みが発生していた。

前者は皆努力しても無駄なので、後者を専用システム構築をして改善する。

構成

f:id:nao1288stusj:20171205004258p:plain

図にはデータフラッシュとか書いてあるので、RX系マイコンを前提に話をしますが、ロボット側に不揮発性メモリがあればよいです。

socket.ioは双方向通信のために使用しているのは高速な更新と、サーバーからのレスポンスを反映を容易にするためです。

Node.jsを利用した理由は環境構築がものすごく楽だから。OS依存ないし。

コンソールGUI

コンソールGUIはWebアプリで作成。どの道どんなGUIでも内部でXML使っているし、HTML5でそこらへんに転がってるサンプルを使えば、おしゃれに作れる。

ここら辺使うのはどうだろうか

フレームワークにAngular.JSなど、双方向データバインディングがあると楽です。

  • Angular.JS/Angular (2+)
  • React
  • Vue
  • Riot

jQueryは不要。以下を参照。
You might not need jQuery.

socket.ioから送信するデータの体裁

{
 "type":"update",
 "id":123456,
 "name":"TIRE",
 "value":24.5,
 "descript":"タイヤ直径[mm]"
}

ロボットにはid(マイコン上の書き込み先アドレス)とvalueだけ渡せばよいですが、コンソール画面上には、変数名とそれが何なのかを示すため、nameとdescriptも送信。

サーバー

今回Node.jsにはwebサーバーはいらないです。expressを使って、socket.ioサーバーと同時に建てられますが、サーバーからhtml貰う必要ないので、webリソースは全てローカルファイル参照でいいでしょう。HTTPリクエストは飛ばさないので、CORSの考慮も不要。 コンソールとの通信はsocket.ioにすべて任せます。

以下、必要なnode_modulesのインストール。

npm install --save-dev socket.io serialport

上の2ついればOKです。serialportはpython前提になります。こちらを参照。

サーバーにはコンソールGUIからリクエストしたファイルをローカルファイルに書き込む。わざわざDB立てるのももったいない上、別環境での再構築や復元が面倒です。

var fs = require('fs');
function readFile(path, success) {
    fs.readFile(path, 'utf8', function (err, data) {
        if (err) {
            throw err;
        }
        success(data);
    });
}
function writeFile(path, data) {
    fs.writeFile(path, data, function (err) {
        if (err) {
            throw err;
        }
    });
}

コンソールGUIから来たデータをファイルに書き込んだら、いよいよシリアル通信です。 自分は以下のようにデータを送信してます。コンソールからはnameなど色々送信してますが、ロボット上での解析が面倒なので、必要なデータだけ送ります。(json-cがあればもっと複雑な形式で送信できますね。必要なのかは別として)

// {key:value}
 {123456:24.5}

ロボット側の処理

サーバーからの送信データは、接頭に{ 、接尾に}、Key-Valueのセパレータにコロンを使用しています。
ロボット側には接頭文字を検知したら、受信バッファを初期化、以降のコロンと接尾文字を除き、バッファに貯める。接尾を検知したら、バッファから送信データを構築します。

key(id)には4バイト以内の自然数valueには浮動小数として認識させます。

#define MAX_BUFFER 20
typedef struct {
    int index;
    int length;
    char buffer[MAX_BUFFER];
} s_key;
volatile s_key keys, values;

void detectChar() {
    SCI1.SSR.BIT.RDRF = 0; // 受信フラグを解除
    char recieveData = SCI1.RDR;
    switch (recieveData) {
    case '{':
        recieveMode = KEY;
        flushData();
        break;
    case '}':
        recieveMode = END;
        applyRecieveData(recieveMode, recieveData);
        break;
    case ':':
        recieveMode = VALUE;
        break;
    default:
        applyRecieveData(recieveMode, recieveData);
        break;
    }
}
void applyRecieveData(char type, char data) {
    if (type == KEY) {
        pushKey(data);
    } else if (type == VALUE) {
        pushValue(data);
    } else if (type == END) {
        mapping();
    } else if (type == STAY) {
    }
}
void mapping() {
    const char* keyBuffer = &(keys.buffer);
    const char* valueBuffer = &(values.buffer);
    long key = atoi(keyBuffer);
    double value = atof(valueBuffer);
    save(key, value);
}
void pushKey(char key) {
    if (keys.length < MAX_BUFFER) {
        keys.buffer[keys.length] = key;
        keys.length++;
    }
}
void pushValue(char val) {
    if (values.length < MAX_BUFFER) {
        values.buffer[values.length] = val;
        values.length++;
    }
}
void flushData() {
    for (char i = 0; i < MAX_BUFFER; i++) {
        keys.buffer[i] = values.buffer[i] = 0;
    }
    keys.index = keys.length = values.index = values.length = 0;
}

かつて、CとJSのソースを1つの記事に書いたヤツがいるだろうか・・・。

画面イメージ

途中まで作ってみました。左の写真には各センサー値を表示、値を見ながら、閾値を調整するという形式にしてみました。 f:id:nao1288stusj:20171207000129p:plain

今後の課題。

  • パラメータの調整だけではなく、テストシナリオも同様にバイナリ書き込みではなく、フラッシュからの読み出しから実行したい。
  • json-cを使う。
  • 大量のデータを一度に更新できるようにする。