Rust serialport ③

e-tipsmemo.hatenablog.com
以前このようなプログラムを書いて、
ワットチェッカーからシリアルポート(over Bluetooth)経由で電力のログを取っていたが、
まともにエラーハンドリングしていなかったので、
不具合が起きており、その修正を行った。

問題の処理

pub fn communicate_command(&mut self, cmd:Vec<u8>, rcount:usize)->Result<Vec<u8>, std::io::Error>
{
	let mut resp : Vec<u8> = vec![];

	match self.port.write(&cmd) {
		Ok(_) => (),
		Err(e) => eprintln!("{:?}", e),
	}

	let mut buffer : [u8; 1] = unsafe{mem::uninitialized()};

	loop{
		match self.port.read(&mut buffer)
		{
			Ok(bytes) => if bytes == 1
			{
				resp.push(buffer[0]);
				if resp.len() == rcount 
				{
					break;
				}
			},
			Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (),
			Err(e) => return Err(e),
		}
	}
	
	Ok(resp)
}

指定したバイト数を受信するまでループするが、何らかの理由で受信バイト数が来ずにずっとループが回っていた。

Err(ref e) if e.kind() == io::ErrorKind::TimedOut => {
	eprintln!("{:?}", e);
	timeoutbreak = timeoutbreak + 1;
	
	if timeoutbreak == 200 {
		return Err(Error::new(ErrorKind::Other, "timeout 200 times"));
	}
	
	()
},

一回ごとのタイムアウトの時間は10ミリ秒
連続して、200回タイムアウトしたら、Errでループを抜けることにした。
(計測の間隔が2秒であることと、RTCを設定するのに1300ミリ秒ぐらいかかるので)

f:id:katakanan:20191017061210p:plain
処理を継続させる代わりにstd::process::exitで止めたところ。

止めずに再び新しい測定値を得るようにしたところどうやら動き続けているのでこれでとりあえず放置。

あと、mem::uninitialized()が非推奨になっていた。

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]

ThingsBoardを利用する 3.

Thingsboardに送る適当なプログラムは書いたので
Thingsboardのグラフ画面を作成する。

とは言ってもここの動画で行っていることをアレンジするだけ
Getting Started | ThingsBoard


複数のセンサーが増えてきたときのために
動画内では、
defaultダッシュボードにはセンサーの現在状態だけを一覧表示するようなサマリー画面にして
それぞれのセンサーの行をクリックしたときの動作を定義して、詳細を見に行くという設定にするようにしている。

サマリー表示ウィジェットの編集から。
f:id:katakanan:20191010040415p:plain

行をクリックしたときの動作を定義する。
f:id:katakanan:20191010040530p:plain

できたもの

Chartウィジェットには最初から平均値などを表示してくれていた。
f:id:katakanan:20191010040642p:plain

その他

おそらく上級設定のJavascriptのところをどうにかすれば、電力の積分値を計算して電力料金なども算出することができるだろう。
いつかやる
f:id:katakanan:20191010040909p:plain

とりあえずやりたいことの9割程度はできたので、
Rustの電力を取ってくるソフトウェアをdaemon化するかどうにかしようと検討している。
(あとはthingsboard serviceが時々勝手に落ちてしまっている原因などを調査する?(メモリが少ないからのような気がする))

おまけ

起動してから一時間程度視聴した。
f:id:katakanan:20191010051216p:plain

PLEX 地上デジタル・BS・CS対応TVチューナー PX-W3PE4

PLEX 地上デジタル・BS・CS対応TVチューナー PX-W3PE4

ThingsBoardを利用する 2.

今回は、Rustのプログラムを書いて、
実際のデータを使って、何秒か一回にデータが送られるようにする。

reqwest

e-tipsmemo.hatenablog.com
前回はreqwestを使って単発で送ったので
今回は昔の電力測定プログラムを合わせるだけ。

fn main() {
    println!("Hello, world!");
    let mut wc = WattChecker::default();
    wc.init();
    wc.start_measure();

    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();
    ctrlc::set_handler(move || {
        r.store(false, Ordering::SeqCst);
    }).expect("Error setting Ctrl-C handler");

    let token = " ************** "; //デバイスのアクセストークンキーを入れる
    let str_url = format!("http://192.168.0.14:8080/api/v1/{}/telemetry", token);
    let url = reqwest::Url::parse(&str_url).unwrap();
    let client = reqwest::Client::new();
   
    while running.load(Ordering::SeqCst)
    {
        match wc.request_measure() {
            Ok(cmd) => {
                disp_data(&cmd);
                let map = parse_data(&cmd);

                let res = client
                    .post(url.clone())
                    .json(&map)
                    .send()
                    .unwrap();

                println!("{}", res.status());
            
            },
            Err(e) => eprintln!("{:?}", e),
        }

        thread::sleep(Duration::from_millis(2000));
    }

    println!("stop measuring");
    wc.stop_measure();
}

結果

f:id:katakanan:20191006054339p:plain
どうやらHTTPのリクエストが遅くなって前の測定から2秒以上かかってしまうことがあるらしいので、
これは非同期に処理しなければいけないのかもしれない。
あとリクエストを投げ続けていると、とまってしまう。

とりあえず次は、thingsboardのログ表示部分を作る。

ThingsBoardを利用する 1.

e-tipsmemo.hatenablog.com
前回設定したThingsboardを
以前構築した録画サーバーの電力測定とつなげることを考える。

Thingsboardにデータを送るためのAPIドキュメント
ThingsBoard API reference | ThingsBoard

今回はデータを受ける側を準備して、データを投げる側はあまり深入りせずに
最もシンプルな方法で行く。

adminを変更する

Mail Settings | ThingsBoard

デモモードでインストールしなくてもsysadminのアカウントは最初から存在する。
sysadminで入って、右上のプロフィールからパスワードを変更する。
テナントを作ってから、テナントのユーザーアカウントを作る。
この時アクティベーションURLというのが出てくるので、そのURLにログインすると
今作ったテナントアカウントのパスワードを設定することができる。

そして、テナントでログインしなおす。

以下の動画を参考にやっていく。
Getting Started | ThingsBoard

以降はテナントアカウントでログインしている。

アセットを登録する

「資産」と書いてある。
アセット名は、デバイスと紐づけるのでどこにそれがあるかわかったほうが良いのかもしれない。
(普通の部屋だけど)

f:id:katakanan:20191005034831p:plain

バイスを登録する

今回はワットチェッカーだけ。あとあと温度センサー、湿度センサーなどを追加したい。
f:id:katakanan:20191005034924p:plain

紐づけする

アセットの画面からプロパティを変更する画面で「関係」タブで設定する。
先ほど定義したものをここで紐づける。
f:id:katakanan:20191005035031p:plain

Thingsboardにデータを投げる

とりあえずコマンドラインからcurlで投げたデータがThingsboardの画面に出るかを確認する。
ACCESS_TOKENはデバイス画面からコピーする
以下のjsonを送る

{
	"time":2019/01/14 02:50:07,
	"voltage":102.396,
	"current":108.74219,
	"wattage":5.335
}

timeはStringで送らないとダメだった

curl -v -X POST -d "{\"time\":\"2019/01/14 02:50:07\", \"voltage\":102.396, \"current\":108.74219, \"wattage\":5.335}" http://192.168.0.14:8080/api/v1/$ACCESS_TOKEN/telemetry --header "Content-Type:application/json"

これはOK
f:id:katakanan:20191004023829p:plain

ワットチェッカーのデータを投げる

reqwestを使った。

extern crate reqwest;

use std::collections::HashMap;
use reqwest::*;

fn main() {

    let time = "2019/01/14 02:50:07";
    let v = "102";
    let c = "108.9";
    let w = "5";

    let token = " ^^^^^^^^^^^^^ "; //アクセストークンを書く。

    let str_url = format!("http://192.168.0.14:8080/api/v1/{}/telemetry", token);
    let url = Url::parse(&str_url).unwrap();

    let mut map = HashMap::new();
    map.insert("time", time);
    map.insert("voltage", v);
    map.insert("current", c);
    map.insert("wattage", w);

    let client = reqwest::Client::new();

    let mut res = client
        .post(url)
        .json(&map)
        .send()
        .unwrap();

    println!("{}", res.status());
}

とりあえずこれで行けたので
次は、前にRustで作ったワットチェッカーBluetooth経由で取りに行くプログラムを改造する。
時間があればRustのMQTT crateなどを利用して別のプロトコルでも行えることを試してみようかと思う。

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]

Raspberry piにThingsBoardのインストールと試用

家においてあるパソコンが集まっている部屋の温度とか湿度とか電力とか監視するために
Raspberry piとその他もろもろを設置したのはいいが、
それらを統合して視覚化できたほうが便利

そこでIoT Platformと銘打っているThingsBoardをRaspberry pi上に入れて自分向けに運用する(予定)
ThingsBoard - Open-source IoT Platform

インストール

Installing ThingsBoard on Raspberry Pi 3 Model B | ThingsBoard
これに沿って設定する。
f:id:katakanan:20190929042256j:plain

sudo service thingsboard start

をするのだが、Thingsboardのサービスを起動するまえに再起動したほうがいい気がする。
(しないでいたら、とてつもなくRaspberry piの動作が遅くなった)


インストールしなおすならば
一端DATABASEを消さなければ上書きインストールすることができなかった
(すでにデータベースにデモモードで使うユーザーが登録されているので「失敗」といったメッセージが出る)

データベースを消す方法

psql -U postgres -d postgres -h 127.0.0.1 -W

でログインしてから

SELECT * FROM pg_database;

インストール手順で作ったthingsboardデータベースがあるのが確認できる

データベースを消す

DROP DATABASE thingsboard;

そのあと、またthingsboardデータベースを作成する。

デモ

ログイン画面
f:id:katakanan:20190929051110p:plain
インストールしたRaspiに8080ポートでアクセスする
f:id:katakanan:20190929045026j:plain
インストール手順にあるアドレスとパスワードでログインできる。

動画

Getting Started | ThingsBoard
デモモードで用意されたtenant administratorでログインする。
途中で温度センサーの値を投げるときに、curlを利用しているので、
今ある電力測定プログラムを対応させるのはそれが一番簡単かもしれない。
(MQTTなどにも対応しているらしいので、ESP32などがセンサー値を集めるときは,そちらのほうが良さそう)

チュートリアルの途中の状態
f:id:katakanan:20190929062352p:plain
おおよそやりたいこと(以前作った録画サーバーの電力ログを取る)はできそうだと分かったので
つぎはThingsboardにデータを投げる、またはThingsboardに取りに行かせることができるのかを検討して、
それを実装する。

その他

今回はRaspberry pi 3 B+を利用しているが、
拡張性を考えるとメモリが多く積んであるRaspberry pi 4がよいかもしれない

Rustのparserを試す

ちょっと必要になったきがするのでparserをかくためのものを探したり
サンプルを書いてみたりした。
個人的なメモ
What is the difference between parser generators and parser combinators? - Quora
compiler construction - What is the difference between LALR and LR parsing? - Stack Overflow
専門と離れているので適当に下調べ。

nom - Rust
combine - Rust
pest. The Elegant Parser
LALRPOP -
syntax-cli - npm

最初はlex(情報がたくさん),bison(情報がたくさん)に似てると書いてある
syntax-cliがいいかもしれないと思ったけど、
パーサーを生成するコマンドがbuild.rsでうまく自動化できなかったのでやめた。

結局最初から生成もセットで提供されている
lalrpopを使うことにした。

とりあえず式をパースするサンプルを書いた。
f:id:katakanan:20190706151544p:plain

use std::str::FromStr;
use crate::ast::Node;

grammar;

NUM_INTEGER: i32 = r"[0-9]+" => i32::from_str(<>).unwrap();
IDENTIFIER: String = r"[a-zA-Z_][a-zA-Z0-9_$]*" => <>.to_string();

pub Expr: Node = {
  <e:Expr> "+" <f:Factor> => Node::Binary{
                                            op: "+",
                                            left: Box::new(e),
                                            right: Box::new(f),
                                          },
  Factor,
};

pub Factor: Node = {
  <f:Factor> "*" <t:Term> => Node::Binary{
                                            op: "*",
                                            left: Box::new(f),
                                            right: Box::new(t),
                                          },
  Term,
};

pub Term: Node = {
  <n:NUM_INTEGER> => Node::Literal(n),
  <ident:IDENTIFIER> => Node::Ident(ident),
  "(" <Expr> ")",
};
#[derive(Debug)]
pub enum Node {
    Literal(i32),
    Ident(String),
    Binary {
        op: &'static str,
        left: Box<Node>,
        right: Box<Node>,
    },
}
#[macro_use]
extern crate lalrpop_util;

pub mod ast;
pub mod parser;

fn main() {
    println!("Hello, world!");
    println!("{:?}", parser::ExprParser::new().parse("((22))"));
    println!("{:?}", parser::ExprParser::new().parse("(a_A0)"));
    println!("{:?}", parser::ExprParser::new().parse("(2+a)*4"));
    println!("{:?}", parser::ExprParser::new().parse("2+a*4"));
    println!("{:?}", parser::ExprParser::new().parse("5*4+b"));
    println!("{:?}", parser::ExprParser::new().parse("5*(4+b)"));
}
    Finished dev [unoptimized + debuginfo] target(s) in 1.68s
     Running `target/debug/calc3`
Hello, world!
Ok(Literal(22))
Ok(Ident("a_A0"))
Ok(Binary { op: "*", left: Binary { op: "+", left: Literal(2), right: Ident("a") }, right: Literal(4) })
Ok(Binary { op: "+", left: Literal(2), right: Binary { op: "*", left: Ident("a"), right: Literal(4) } })
Ok(Binary { op: "+", left: Binary { op: "*", left: Literal(5), right: Literal(4) }, right: Ident("b") })
Ok(Binary { op: "*", left: Literal(5), right: Binary { op: "+", left: Literal(4), right: Ident("b") } })

うーんよさそう?

ある文法をパースしたかったら
既存のプロジェクトを参考にできる
GitHub - RustPython/RustPython: A Python Interpreter written in Rust
GitHub - tcr/rust-verilog: Verilog parsing and generator crate.

diagram-js customize 6. CustomizeRenderer

今回はBase Renderを継承した自作クラスに切り替える

bpmn-jsを参考にすると、lib/drawに主な3つのファイルがあるので
それを作る。(今回はまだMyRenderUtilは使わない)
f:id:katakanan:20190629234033p:plain

四角をつなげる線を引いたりするのに
lib/util/RenderUtilが必要で、
diagram-jsからそれを含めてコピーした。
f:id:katakanan:20190629234431p:plain

bpmn-jsのBPMNRendererを参考にdiagram-exampleと同じような動作になるように
必要な関数だけを実装した。

import {
    componentsToPath,
    createLine
  } from '../util/RenderUtil';
  
  import {
    append as svgAppend,
    attr as svgAttr,
    create as svgCreate
  } from 'tiny-svg';
  
  // apply default renderer with lowest possible priority
  // so that it only kicks in if noone else could render
  var HIGH_PRIORITY = 1500;
  import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
  import { pick } from 'min-dash';
  
  export default class MyRenderer extends BaseRenderer {
      constructor(eventBus, styles){
          super(eventBus, HIGH_PRIORITY);
          this.CONNECTION_STYLE = styles.style([ 'no-fill' ], { strokeWidth: 5, stroke: '#000' });
          this.SHAPE_STYLE = styles.style({ fill: 'white', stroke: '#000', strokeWidth: 2 });
      }
  
      canRender(element){
          return true;
      }
  
      drawShape(visuals, element){
        var rect = svgCreate('rect');
        svgAttr(rect, {
          x: 0,
          y: 0,
          width: element.width || 0,
          height: element.height || 0
        });
        svgAttr(rect, this.SHAPE_STYLE);
      
        svgAppend(visuals, rect);

        return rect;
      }
  
      drawConnection(visuals, connection){
          var type = connection.type;
 
          var line = createLine(connection.waypoints, this.CONNECTION_STYLE);
  
          svgAppend(visuals, line);
          return line;
      }
}
  
  MyRenderer.$inject = [
      'eventBus',
      'styles' 
  ];

適当なコミットの時点のものをコピーしてきたので
もしかしたら使ってないメンバ関数もあるかもしれない。
そしてここで、drawShape関数がcanvasに追加する
svgタグの要素を生成しており、svgAttrでスタイル(線の色とか)を決定している。
最後にsvgAppendで親(visuals)に対する子要素として追加される。

e-tipsmemo.hatenablog.com
の記事の2の方法。

import MyRenderer from './MyRenderer';

export default {
  __init__: [ 'MyRenderer' ],
  MyRenderer: [ 'type', MyRenderer ],
};

index.jsはいつも通り

Renderができたので、
Modeler.jsにモジュールを追加する。

import DrawModule from './lib/draw';
....
export default class Modeler extends Viewer{
    constructor(options){
        var options = {
            canvas:{
                container: options.container
            },
            modules:[
                .....
                DrawModule,
                .....
            ]
        };
        super(options);
    }
}

そしてapp.jsのスタイルを上書きするところはもういらない

// defaultRenderer.CONNECTION_STYLE = styles.style([ 'no-fill' ], { strokeWidth: 5, stroke: '#000' });
// defaultRenderer.SHAPE_STYLE = styles.style({ fill: 'white', stroke: '#000', strokeWidth: 2 });

見た目は変わらない。
f:id:katakanan:20190629235554p:plain