2021年12月31日金曜日

LEGO MINDSTORMS でステートマシン

なんちゃってスレッドができるようになりましたが、プログラムを勉強していくとまずはモジュール化という考え方が必要になってきます。

Scratchではスプライトというオブジェクト毎にプログラムを作成するということで自然とモジュール化されるようになっていますし、ブロック化やメッセージ送信・受信ということで意識的にモジュール化することも可能です。

私がプログラミングを始めた頃は構造化プログラミングという言葉が流行っていましたがその後、オブジェクト指向、イベント駆動、データ駆動、ドメイン駆動にテスト駆動とプログラミング界隈でもいろいろな言葉が流行りましたが、基本は構造化プログラミングかなと個人的には思っています。

おそらくなんでもそうなんでしょうがある程度のところまでは才能だけで行くことが出来ますが、その上に行くにはやはり基本が大切なのかなと思います。

以下にMINDSTORMSのMicroPythonでのステートマシンのプログラム例を示しますが、ステートマシンは複雑な振る舞いの制御によく使われていて、ROS(Robot Operating System)ではsmachパッケージとしてPythonで利用できるようになっています。

MINDSTORMS用ステートマシン

必要に応じてuasyncioを利用した非同期処理を組み込めば複雑な処理も可能になりますが、その場合はイベント(uasyncuo.Event)によるタスク同期なども考える必要が出てきます。

from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_EXIT = "exit"
STATE_GO_STRAIGHT = "GoStraight"

# ステートクラス(基底クラス)
class State(object):
    def __init__(self, name: String):
        self.__name = name
        self.__action = None

    @property
    def name(self):
        return self.__name

    def entry(self):
        pass

    def do(self) -> String:
        return None

    def exit(self):
        pass

# ステートマシンクラス
class StateMachine(object):
    def __init__(self):
        self.__stateList = {}
        self.__now = None

    @property
    def currentState(self):
        return self.__now.name if self.__now else ""

    def add(self, state: State):
        self.__stateList[state.name] = state

    def changeTo(self, tag: String):
        if self.__now:
            self.__now.exit()
        self.__now = self.__stateList[tag]
        if self.__now:
            self.__now.entry()

    def run(self) -> String:
        if self.__now:
            return self.__now.do()
        return None

# 直進
class GoStraight(State):
    def __init__(self, name: String = STATE_GO_STRAIGHT):
        super().__init__(name)

    def entry(self):
        wheels.start(0, -20)

    def do(self) -> String:
        distance = distance_sensor.get_distance_cm()
        if distance and distance < 20: # 測定不能の場合None
            return STATE_EXIT
        return None

    def exit(self):
        wheels.stop()

# メイン処理
class Main:
    def __init__(self):
        self.__stateMachine = StateMachine()
        self.__stateMachine.add(State(STATE_EXIT))
        self.setup()
        self.loop()

    def setup(self):
        # ステートの登録
        self.__stateMachine.add(GoStraight())
        # 開始ステートへ切り替える
        self.__stateMachine.changeTo(STATE_GO_STRAIGHT)
        
    def loop(self):
        while self.__stateMachine.currentState != STATE_EXIT:
            nextState = self.__stateMachine.run()
            if nextState:
                self.__stateMachine.changeTo(nextState)
            # 外部よりのステートマシン終了
            if hub.right_button.is_pressed():
                print("right button is pressed !")
                self.__stateMachine.changeTo(STATE_EXIT)

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

2021年12月30日木曜日

LEGO MINDSTORMS Pythonでなんちゃってスレッド2

 先日はジェネレータベースのコールチンを実装してみましたが、Python 3.10からはこのジェネレータベースのコールチンは廃止が予定されているようです。
MINDSTORMSのMicroPythonは3.4.0をベースとしたもののようです。

MicroPython v1.14-893-gbae6ff2ee on 2021-11-04; LEGO Technic Large Hub with STM32F413xx
Type "help()" for more information.
>>> import sys
>>> sys.version
'3.4.0'
>>>
現在のPythonの最新バージョンが3.10なのでMINDSTORMSのMicroPythonが3.10になることはまずないとは思いますが、プログラマを目指す子供たちはasyncioベースのコールチンに慣れておくべきかなと思います。(あまり使用することはないとは思いますがw)

コールチンをしっかりと理解しておくとスレッドやタスクといった概念についても理解しやすくなるでしょう。最近ではAndroidアプリの開発でもHTTPアクセス処理などでコールチンを利用した非同期処理が推奨されているようです。

uasyncioと一緒に使うコールチン


基本的なプログラムの作り方はジェネレータベースのコールチンとほぼ同じです。
async defで定義した関数の中でawaitでコールチンの処理を待つようにプログラミングするのが基本になります。実例を見た方が早いですね。
「yield True」を「await uasyncio.sleep_ms(1)」に書き換えた感じです。尚awaitするのはコールチンであれば良いので「uasyncio.sleep_ms(1)」でなく独自のメソッドを指定することも可能です。

コールチンの処理を終了させる方法はジェネレータベースのコールチンと同じ方法(コメントにしてあります)とuasyncio.Taskクラスが提供するcancel()メソッドを利用する方法があります。

uasyncioと一緒に使う方法ではメイン処理もコールチンになっているので適時awaitで処理を切り替えることが可能です。

from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math
import uasyncio

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_START = 1
STATE_EXIT = -1

# コールチンクラス
class Coroutine1:
    def __init__(self):
        self.__distance = -1
        self.__loop = True

    @property
    def distance(self):
        return self.__distance

    def stop(self):
        self.__loop = False

    async def run(self):
        try:
            while self.__loop:
                await self.__done()
        finally:
            print("Coroutine1 end!")

    async def __done(self):
        self.__distance = distance_sensor.get_distance_cm()
        print(self.__distance)
        await uasyncio.sleep_ms(1)
        if self.__distance and self.__distance < 15:
            print("The wall is near!")
            await uasyncio.sleep_ms(1)

# メイン処理
class Main:
    def __init__(self):
        self.state = STATE_START
        self.setup()
        uasyncio.run(self.run())

    def setup(self):
        self.__coroutines = []
        self.__coroutines.append(Coroutine1())

    async def run(self):
        self.__tasks = []
        for c in self.__coroutines:
            self.__tasks.append(uasyncio.create_task(c.run()))
        while self.state != STATE_EXIT:
            if hub.right_button.is_pressed():
                print("right button is pressed !")
                #for task in self.__coroutines:
                #    task.stop()
                for task in self.__tasks:
                    task.cancel()
                await uasyncio.sleep_ms(30)
                self.state = STATE_EXIT
            else:
                self.__done()
                await uasyncio.sleep_ms(1) # コールチンに処理を譲る

    def __done(self):
        print("Main keep alive!")

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

2021年12月29日水曜日

LEGO MINDSTORMS Pythonでなんちゃってスレッド処理

LEGO MINDSTORMS Robot Inventor


こちらに紹介ページはあるのですが「今すぐ購入」ボタンを押すとページは存在せず公式サイトで購入することはできません。日本では教育用のSPIKEプライムセットしか購入できません。SPIKEは教育用なのでパーツを入れるプラスチックケース入りなのに対してInventorはホビー向けなのでケースは紙製となっています。ただ、標準でモータが4つついていたりLEGOパーツの数が多かったりと個人で遊ぶのにはInventorの方が良いので、米国Amazonで購入することにしました。

本体の色がSPIKEは黄色なの対してInventorは青系の色となっています。またプログラム作成用ツールもSPIKE用とInventor用は別になっています。SPIKE用は日本語化されているのですがInventor用は日本で発売されていないこともあってか日本語化はされていません。
最近は小学校でも英語教育が必須化されたりしていますが、遊びの中で英語に触れる(LEGOなので殆ど言葉を利用することはありませんがw)ことが自然と英語を学ぶ良い方法なのかと思ったりします。

プログラミング用言語としてはScratchベースのブロックプログラミングとPythonが利用できます。PythonはMicroPythonなのでthreadingは入っていませんし、ローレベルのAPIである_threadも利用することはできません。利用できるのは非同期 I/O スケジューラuasyncio(非同期処理を扱うための標準ライブラリasyncioのサブセット)となるのでコールチンを利用した非同期処理のコードを書くことは可能となります。

ただ、ここで大切なのはマルチタスクやマルチスレッド、非同期処理といった概念ではなく複数の処理があたかも同時に処理されているように見えることが子供達にとっては大切だということだと思います。

MINDSTORMSでPythonは必要か?


Scratchでプログラムを作成する場合、まずはゲーム的なプログラムを作ることが多いと思いますが、その場合それぞれのスプライト用にプログラムを作成する考え方は直感的ですし、それを動かす場合に非同期処理(ScratchはNode.jsをベースに開発されているのでシングルスレッド・シングルプロセスが基本)を意識せずにマルチスレッド的な動作が自然と実践できるScratchの考え方は非常に優れていると思います。

さてそれではMINDSTORMSをPythonでプログラムする場合マルチスレッドは必要になるのかな?という疑問が出てきます。
ロボット工学は畑違いですが、MINDSTORMSのプログラムを作っていて感じるのはマルチスレッドよりもステートマシンでプログラムした方がすっきりとできるかなという感じです。

MINDSTORMS用のScratchは必要なブロックもきめ細かく用意されているのであえてPythonでプログラムを作成する必要はないかなと思います。Pythonを使ってプログラムしたい方はプログラミング言語を勉強したい方が取り組むべきかなと感じました。

Pythonジェネレータベースのコールチン


ということでまずはジェネレータベースの非同期処理を作ってみました。

Scratchの場合はスプライトという単位でプログラムが分けられますが、MINDSTORMS用のPythonではファイルの分割は出来なそうです。ある程度の規模のプログラムになるとできればファイル分割できた方が管理がやりやすいのですがね。

ノンプリエンティブなマルチタスクになりますので適時「yield True」でCPUを解放するのがポイントになります。またMainの__done()は非同期処理の対象外になっているので、ここで長時間CPUを占有すると非同期処理に影響を与えるので極力小さい処理にする必要があります。

LEGOでの注意事項としてはモータ制御はstart、stopで行うようにしてmoveは(同期処理になるので、短い距離では問題ないと思いますが)極力使用しないように、またセンサーのwait系も使用しないようにします。
from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_START = 1
STATE_EXIT = -1

# ジェネレータクラス
class Generator1:
    def __init__(self):
        self.__distance = -1
        self.__loop = True

    @property
    def distance(self):
        return self.__distance

    def stop(self):
        self.__loop = False

    # ジェネレータ関数本体
    def loop(self):
        while self.__loop:
            yield from self.__done()
        yield False

    def __done(self):
        self.__distance = distance_sensor.get_distance_cm()
        print(self.__distance)
        yield True
        if self.__distance and self.__distance < 15:
            print("The wall is near!")
            yield True

# メイン処理
class Main:
    def __init__(self):
        self.setup()
        self.loop()

    def setup(self):
        # ジェネレータの登録
        self.generators = []
        self.distance_sensor = Generator1()
        self.generators.append(self.distance_sensor)
        # ジェネレータ関数の登録
        self.generatorLoops = []
        for g in self.generators:
            self.generatorLoops.append(g.loop())

    def loop(self):
        while self.state != STATE_EXIT:
            if hub.right_button.is_pressed():
                for i, g in enumerate(self.generators):
                    g.stop()
                    while (next(self.generatorLoops[i])):
                        pass
                self.state = STATE_EXIT
            else:
                # ジェネレータの次の要素を取得(処理の再開)
                for loop in self.generatorLoops:
                    next(loop)
                # 自身の処理
                self.__done()
        
    def __done(self):
        print("Main keep alive!")

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

2021年9月23日木曜日

Raspberry Pi ZeroでHeadphonesが無くなった

最近こんなの

Adeept Robotic Arm Kit for Arduino

こんなの

「Petoi Bittle」(ペトイ・ビトル)

を購入して遊んでいました。








両方共にArduino互換ボードでサーボモーターを駆動する物ですが基本的なプログラムは提供されるのでとりあえず組み立てるだけで動かすことができます。

Arm Kitの方は仕組みは簡単なので工夫すればいろいろとプログラムをして遊べそうです。ただバッテリーが18650を2本とちょっと特殊ですが、バッテリーがなくてもUSBからの給電でも動くので無理してバッテリーを購入する必要はないとおもいます。なをバッテリーを購入するときは充電器も専用なものが必要となるのでご注意下さいね。

Bittleの方は赤外線リモコンもついていて組み立てればすぐに遊ぶことができます。
提供されるプログラムですぐに動かすことができますが、スムーズに歩くようにさせるには調整が必要になるようです。ビデオ等ではスムーズに動いているようですが、この写真のように顔をつけていない物がほとんどです。
ちょっと調整をしてみましたが思ったようにうまく動かすことができません。想像ですが提供されるプログラムは顔の部分がついていない状態で調整されているのではないかなと思われます。顔がつくことで重量バランスが違ってくるので、単純なキャリブレーションで調整できずにプログラム側の調整も必要になるのではないかなと。

ということでBittleのプログラムをざっと眺めてみましたが、各サーボモータの角度を持つフレームを複数用意してループさせることで動作をさせるという形式でした。
うまく歩かない場合はこの角度を微調整していくことでよりスムーズに動作させることが可能になります。とはいえ、なかなか時間がかかる作業になりますので今回は見送り、よたよたと動くことで満足することにしました。

といったことをやっていて、そういえばRAPIROをしばらく動かしていないということに気が付きました。

引っ張り出して来て1年ぶり程に動かしたら、どうも調子が悪いようです。

MicroSDカードでエラーが発生


原因を調べてみると組み込んだRaspberry Pi ZeroのMicroSDカードでエラーが発生しているようです。あちこちのデモで使用した時にきちんとShutdownせずに電源を切っていたのが原因かもしれません。せっかくShutdownスイッチを付けたのですが撤収時にバタバタしてついつい電源をプチンと切ることが多かったのかもしれません。
fsckで修復してみましたがやはりファイルシステムが破損しているようです、ログインがパスワードエラーで弾かれてしまいます。

出来る範囲で必要なファイルをサルベージしてからMicroSDカードをフォーマットしなおしてRaspberry Pi OSからインストールです。

bcm2835 Headphonesがなくなった


Raspberry Pi Zeroにはヘッドフォン用ジャックは付いていませんがHDMIで音声出力も可能にはなっています。また、RAPIROに組み込んで使うときはPWM Audioを利用してスピーカーから音声を出力していました。
ということでOSインストール後音声の出力先を3.5mm Jackに変更しようとraspi-configを実行してみたのですがHDMIしか表示されません。どうやらOSがStretchからBusterに変わったのが原因のようです。ハードウエア的についていない物は表示しないということでしょうか?

config.txtの修正で解決


ネットを検索すると同じような現象でこまっていた方もいるようで、こちらのサイトに解決策が上がっていました。
以下の2行を/boot/config.txtの最後に追加することでGPIO 12と13でPWM Audioが利用できます。(一般的にはGPIO 13と18のペアが利用されますがGPIO 18がRAPIROとの接続ケーブルで使用しているのでペアを変更して利用しています)
dtoverlay=audremap,enable_jack=on
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4

2021年6月3日木曜日

QNAPでPostgreSQLを動かす

 PostgreSQL公式では起動できない

QNAPでPostgreSQLを動かす場合はContainerSenterを導入してDockerで動かすようになっています。
標準でサポートされているデータベースはMariaDBなのですがバージョンが5.5.57(最新版は10.5.10)と古くこちらも最新版を動かす場合はDockerで動かすことになります。
今回は仕事の関係もあってPostgreSQLの11.12を動かす必要があったのでインストールを行ったのですがここでちょっとハマったのでアップしておきます。

ContainerSenterを導入して作成でPostgreSQLを検索するれば「公式」も表示されるのでこちらをインストールすれば簡単かなと思ったのですが・・・

/usr/lib/postgresql/11/bin/postgres: error while loading shared libraries: libsystemd.so.0: ELF load command alignment not page-aligned        
no data was returned by command ""/usr/lib/postgresql/11/bin/postgres" -V"                                                                     
The program "postgres" is needed by initdb but was not found in the                                                                            
same directory as "/usr/lib/postgresql/11/bin/initdb".                                                                                         
Check your installation.    

ライブラリのエラーで起動できないようです。

QNAPのCPUには2種類ある

私が購入したのはTS-231P3ですが使っているCPUは


とあるようにArmを使ってますが、Arm以外にもIntel CPUを使用している機種も存在します。QNAPのサポートでもPostgreSQLのインストール方法は記載されていますが、情報が古く現在のバージョンでは「Odoo」として表示されます。

またサポートでは注意書きとして「x86ベースのデバイスのみに対応しています。」とありますが現在のサンプルで使用しているイメージは「postgresql_armhf」となっているのでちょっとチグハグになっていました。

インストールバージョンでX.X.X-alpineを選択

結論としてはベースOSとしてalpineを選択することで問題なく起動することができました。
「ELF load command alignment not page-aligned」問題はQNAPだけでなく発生しているようですが参考になれば幸いです。


2021年4月22日木曜日

Vega-liteで温度測定グラフを描いてみた

ここ数日はVegaにハマっています。
IoTシステムでよく使う稼働状況グラフは表示できたので次に測定結果をグラフにすることにチャレンジしました。Node-REDのDashboardを使えば簡単に表示できますが、同じことをVega-liteで出来るようにしてみました。

mark: lineのTips


折れ線グラフはVega-liteのサンプルにもあるので簡単に表示できましたが、以下2点がちょっと不満なのであれこれと格闘してみました。
  • 表示範囲にちょっと余白を持ちたい
    • Vegaではデータからグラフの表示範囲(domain)を自動的に計算してくれるのですが、数値軸の場合は0から最大値までとなり、時間軸の場合はデータの時間範囲になります。
      domainを指定することでその範囲を指定することが可能ですが、データの値を元に指定したい。さらに、domainを最小値と最大値に設定すると上下(あるいは左右)に余白がなくなってグラフとしてちょっと見辛いという問題がありました。
  • 時間軸の表示方法をHH:MMとしたい
    • 時間軸の場合timeUnitをhoursminutesに設定すると軸ラベルはHH:MMで表示してくれるようになりますが、データも分単位に集約されてジャギーなグラフになってしまいます。測定が1秒毎のデータを表示しつつラベル表示はHH:MMとしたい時にどうしたら良いかでちょっとハマリました。

Dashboardでは何気に配慮してくれているので見やすいグラフに仕上がっています。

結果は以下のようになりました、Vega-Editorで確認してください。
ポイントはtransformのcalculateを使って軸データから上下(あるいは左右)の余白分を加減算した列を追加してdomainで指定することです。

{
    "description": "Line Chart Sample",
    "width": 600,
    "height": 600, 
    "data": {
		"values": [
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.094,"H":33.53},"sensed_at":"2021-04-21T01:35:38.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.096,"H":33.53},"sensed_at":"2021-04-21T01:35:43.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.105,"H":33.53},"sensed_at":"2021-04-21T01:35:48.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.35,"P":980.116,"H":33.531},"sensed_at":"2021-04-21T01:35:53.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.099,"H":33.531},"sensed_at":"2021-04-21T01:35:58.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.138,"H":33.52},"sensed_at":"2021-04-21T01:36:03.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.133,"H":33.52},"sensed_at":"2021-04-21T01:36:08.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.152,"H":33.52},"sensed_at":"2021-04-21T01:36:13.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.36,"P":980.138,"H":33.52},"sensed_at":"2021-04-21T01:36:18.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.37,"P":980.143,"H":33.509},"sensed_at":"2021-04-21T01:36:23.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.37,"P":980.118,"H":33.509},"sensed_at":"2021-04-21T01:36:28.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.124,"H":33.51},"sensed_at":"2021-04-21T01:36:33.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.14,"H":33.509},"sensed_at":"2021-04-21T01:36:38.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.126,"H":33.51},"sensed_at":"2021-04-21T01:36:43.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.16,"H":33.51},"sensed_at":"2021-04-21T01:36:48.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.126,"H":33.51},"sensed_at":"2021-04-21T01:36:53.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.116,"H":33.51},"sensed_at":"2021-04-21T01:36:58.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.144,"H":33.51},"sensed_at":"2021-04-21T01:37:03.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.39,"P":980.128,"H":33.486},"sensed_at":"2021-04-21T01:37:08.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.11,"H":33.51},"sensed_at":"2021-04-21T01:37:13.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.11,"H":33.51},"sensed_at":"2021-04-21T01:37:18.000Z"},
			{"machine_id":"machine01","unit_address":"FC:F5:C4:1A:53:64","sensor_number":2,"sensor_type":"BME280","data":{"T":22.38,"P":980.094,"H":33.497},"sensed_at":"2021-04-21T01:37:23.000Z"}
		]
    },
    "transform": [
        {"calculate": "datum.data.T-0.01", "as": "minY"},
        {"calculate": "datum.data.T+0.01", "as": "maxY"}
    ],
    "mark": {"type":"line", "clip": true},
    "encoding": {
        "x": {
            "field": "sensed_at",
            "timeUnit": "hoursminutesseconds",
            "axis": { "format": "%H:%M" }
        },
        "y": {
            "field": "data.T",
            "type": "quantitative",
            "scale": {
                "domain": {"data": "data_0", "fields": ["minY", "maxY"]}
            }
        }
    }
  }

2021年4月17日土曜日

Vega-liteで稼働状況グラフを作成してみた

現在ESP32とRaspberry Piを使ってNC工作機械の稼働監視を行おうとしています。
稼働監視その物はESP32のNC工作機械主軸の電流を計測することで可能なのですが、問題はその稼働状況をグラフ化するところになります。

データはRaspberry Pi上でNode-redを動かしているのでDashboardを使えば可視化はある程度できましすが、よく見かける稼働監視グラフ(横方向の積み上げグラフあるいはガントチャートの応用)を表示しようとしてつまづきました。
Dashboardそのものはchartでグラフ化は可能ですが、積み上げグラフには対応していないようです。

node-red-node-ui-vega

ライブラリを検索しているとnode-red-node-ui-vegaというノードを発見しました。
Vega(「可視化を記述する言語」を目指した物で描画ライブラリとしてはD3を使用しているようです)及びVega-lite(Vegaの文法を簡略化した物でVega用の定義ファイルを出力して描画はVegaで行います)をNode-redから使えるようにしてくれるノードのようです。非常に高機能なグラフ化ツールのようです。積み上げグラフを表示する程度ならVega-liteで十分なようです。

サンプルも豊富に用意されていますが、残念ながらx軸を時間でとった横方向の積み上げグラフのサンプルはありませんでした。(横方向の積み上げ棒グラフのサンプルはありました)
これを修正して横軸を時間にできれば出来そうです。

ところがこれでハマってしまいました。Vegaに関する日本語の解説本もなく情報が少ない上にVegaの概念がちょっと複雑なのが原因です。基本的にはデータをチャネルという概念でデータ変換(データドメインから表示ドメインへ変換)して表示用の修飾を行って可視化(グラフ表示)するという考え方のようです。

Vega-liteによる稼働状況グラフ

結局数日かけた結果が以下のソースになります。Vega-Editorをブラウザで表示してコピペをするとどのように表示されるか見ることが出来ます。
このサイトを使えば自分で表示したいグラフのテストが簡単に出来るので開発もスムーズに出来ます。

{
  "config": {
      "legend": { "disable": true }
  },
  "width": 600,
  "height": 100,
  "data": {
    "values": [
      {"machine": "machine01", "start": "2021-04-01 08:30:00", "end": "2021-04-01 10:00:00", "color": "#ff0000"},
      {"machine": "machine01", "start": "2021-04-01 10:00:00", "end": "2021-04-01 11:15:00", "color": "#00ff00"},
      {"machine": "machine01", "start": "2021-04-01 12:00:00", "end": "2021-04-01 13:00:00", "color": "#ff00ff"},
      {"machine": "machine01", "start": "2021-04-01 14:00:00", "end": "2021-04-01 16:42:00", "color": "#0000ff"}
    ]
  },
  "mark": "bar",
  "encoding": {
    "y": {"field": "machine", "title": null},
    "x": {
      "field": "start",
      "timeUnit": "hoursminutes",
      "scale": {
        "domain": [{"hours": 8}, {"hours": 19}]
      },      
      "title": null
    },
    "x2": {
      "field": "end",
      "timeUnit": "hoursminutes"
    },
    "color": {
      "legend": {"disable": true, "title": null, "values": [ "" ]},
      "field": "color",
      "scale": {
          "range": {"field": "color"}
      }
    }
  }
}

2021年4月8日木曜日

PowerBI 32bitでMariaDBに接続できなかった件

Microsoft.PowerBI.OleDb is not registered

 最近IoT関係で計測データの分析をする必要があったのでPowerBIを使ってみました。
とりあえず自分のPCで分析できたので、顧客先のPCで同じことをやろうとしたところ上記のようなエラーになって分析できませんでした。
違いといえば自分のWindows10が64bitのProバージョンなのに対して、顧客のWindows10は32bitのHomeであることでした。

PowerBI及びMariaDB Connector/ODBCについては32bit版をインストールしたので問題ないと思ったのですが・・・
テーブルの選択は可能なのでMariaDBへの接続まではできているようです、その後内部的にデータ処理用にOleDBのオブジェクトに変換しているようで、そこでクラスが登録されていないというエラーのようです。

いつものようにGoogleさんにお伺いを立てるとこちらのサイトに行き当たりました。

内容的には64bitのPowerBIから32bitのAccessに接続しようとして発生したエラーの話のようですが、結論としてはMicrosoft Access Database Engineをインストールすれば解決するよという事のようです。OLE DBがAccess Database Engineに含まれているのかな?

一件落着


早速こちらから32bitのAccess Database Engineをダウンロードしてインストールして再度テストしましたが・・・

う・ご・か・な・い・・・

同じくOleDBが無いというエラーです。
PCを再起動してテストしても同じです。

そこで再度PowerBIも含めてインストールをやり直してみました。
  1. Microsoft Access Database Engineのインストール
  2. MariaDB Connector/ODBCのインストール
  3. PowerBIのインストール
今度はちゃんとアクセスすることができました。

とすると、インストールされているエンジンに合わせてPowerBIが必要なモジュールをインストールしているのかもしれませんね。

いずれにしろ一件落着でした。

2021年2月9日火曜日

MicroSDカードアダプタでWindows10がクラッシュ

事の初め 

Raspberry Pi 4のスタータキットをAmazonで購入したところ、MicroSDカードをUSB-TypeAとTypeCで利用できるようにするアダプタがついてきました。非常にコンパクトで良さげです。


TypeAの方ではチョコチョコと使っていましたが、特に問題はありませんでした。WindowsPCでもMacでも問題なく使えていました。


WindowsPCがクラッシュ

しばらくは問題なく使用していたのですが、開発用に使用しているWindowsPC(mouse X4-i5)を使っているときに、ちょうどTypeAが全て塞がっている状態でファイルのコピーが必要となり困っているとTypeCの口も1つ持っていることに気がつきました。それではと早速MicroSDカードを入れてTypeCに差し込んだ瞬間に・・・

なんとWindows10の画面が真っ黒になりノイズだけが鳴り渡りました・・・

調べてみるとmouse X4-i5のUSB-TypeCは「DisplayPort Alternate Mode」には対応していないので画像周りでトラブルにはなりそうにないのですが・・・

とは言え、事すでに遅しで電源を抜いてしばらく放置後再度電源を入れてもブートさえしません。


マウスコンピュータやるじゃない!

まだ保証期間中だったこともあり、早速修理依頼を行いました。保証書などのコピーを用意してこちらのサイトから修理依頼シートを作成して、購入時の箱も保管してあってのでヤマトで埼玉サービスセンターへ送りました。

休日を挟むこともあって10日程は掛かるかなと覚悟していましたが、なんと4日後に手元に帰ってきました。発送翌日にセンターへ到着(さすがヤマトさん)したとメールが送られてきました。土曜日だったので翌週末ぐらいになるかなと思っていたら、なんと翌日(日曜日)には発送したとのメールがきました。メールの翌日(家を出てから4日目ですね)には手元に帰ってきたのです。

唯一残念だったのはwindowsも初期化されていたので今まで設定した環境はまた1から構築し直しでした。データ保全の為、基本ハードディスクは初期化のようです。幸いデータは全てクラウドにバックアップ保存してあったので少しの手間で元に復旧することができました。

修理シートにはマザーボード破損で交換した旨記載されていました。


皆さんもご注意あれ!

気になったのでアダプタを分解してみましたが特に問題はなさそうでしたが、いづれにしろこのアダプターはゴミ箱へ投げ入れました。

分解してみた時の写真が以下です。



2021年1月6日水曜日

ESP32-POE-ISOとNETGEAR GS305Pで配線すっきり

 ESP32とW5500で有線LANで接続することはできました。ただ、これだと電源とLANケーブルの2本を引きずり回すことになってちょっと面倒です。

実はPoEでネットワーク経由で電源も供給できれば1本で取り廻すことができるなというのは以前から考えていました。ESP32をPoE化している方もネットでは見受けられましたが、できれば製品として提供されている方が現場で利用する場合にも簡単・安心です。

ネットを検索したところESP32-POE-ISOという製品が見つかったので早速取り寄せてみました。

ESP32-POE-ISO仕様


仕様について詳細は英文ではありますがこちらを参照していただければと思います。
概要としては
  • ESP32-WROOM-32
  • Micro USBコネクタ(ESP32-DevKiCと同様にプログラムのアップロードもこれでOK)
  • MicroSD card(GPIOを3つ使用、カードスロットは背面に装着済)
  • LiPo battery(コネクタ有)
となかなかの優れものです。
LiPo batteryが接続できるので、PoEで充電しつつ電源断(=ネットワーク断)となった場合にはLiPo batteryから電源を取ってMicro SDカードにデータを保存しネットワークが復旧したら(電源も復旧になりますね)その間のデータをアップロードするといったシステムもこれ一つで構築できます。

ただ欠点は機能が豊富な分、ユーザが利用できるGPIOの数が少なくなっているという点でしょうか。利用できるGPIOは以下の通りです。

EXT1
  • 5V
  • 3.3V
  • GND
  • ESP_EN
  • GPIO0
  • GPIO1
  • GPIO2(MicroSD cardで使用)
  • GPIO3
  • GPIO4
  • GPIO5
EXT2
  • GPIO39(入力専用)
  • GPIO36(入力専用)
  • GPIO35(入力専用)
  • GPIO34(入力専用)
  • GPIO33
  • GPIO34
  • GPIO16
  • GPIO15(MicroSD cardで使用)
  • GPIO14(MicroSD cardで使用)
  • GPIO13

開発環境


VSCode + PlatformIOで開発していますが、ボードとして「OLIMEX ESP32-PoE-ISO」を指定可能になっています。サンプルプログラムも標準的なものは用意されているので、サンプルを参考に開発も簡単にできそうです。

サンプルはESP32-POE用ですが、ESP32-POEとESP32-POE-ISOの違いはアイソレーション(insulation:絶縁)がされているか、いないかです。この辺りは電気的なお話なので私も詳しくは理解していませんが、ハブ側とESP32側が電機的に直接接続されているか、間接的なのかの違いです。安全性の面から絶縁されていた方が良いのですが、ESP32-POEを使うときはそのあたりは自分で考えてねということになります。

さて、platformio.iniは

[env:esp32-poe-iso]
platform = espressif32
board = esp32-poe-iso
framework = arduino
monitor_speed = 115200

プログラムはESP32_PoE_Ethernet_Arduinoを参考にすればOKです。
使用しているETHはWiFiをベースに開発されているのでESPmDNSがそのまま利用できます。

            // Get server ip address by mDNS
            if (!MDNS.begin(client_name)) {
                Serial.println("Error setting up MDNS(ETH) responder!");
                while(true) { 
                    delay(1000);
                }
            }
            mqtt_server_address = MDNS.queryHost(MQTT_SERVER);

            Serial.print("Server");
            Serial.print("(");
            Serial.print(MQTT_SERVER);
            Serial.print(") IP address : ");
            Serial.println(mqtt_server_address);

プログラムを書込み後、USB接続を外してから、この為に購入したNETGEAR GS305Pに接続すると問題なくサーバ側でデータを受信することができました。

これからの目論見


ESP32を
  1. Wi-Fiでネットワークに接続+AC電源(一般的な利用方法)
  2. 有線LANでネットワークに接続+AC電源(Wi-Fiが不安定な場合)
  3. PoEでネットワークに接続&電源
という3種類の設置方法が確認できました。

現在製造業におけるIoTの導入支援をお手伝いしていますが、サーバー側を含めて標準的なベースシステムを作っていければなと考えています。ある程度形になった所でソースも公開したいと思いますのでしばらくお待ちください。

2021年1月5日火曜日

ESP32をW5500で有線LAN接続してmDNSしてみた

 最近はESP32で遊んでいますがWi-Fiの環境が不安定な場合、有線LANで接続できるとうれしいかなということでW5500を積んだボードを見つけて早速やってみました。

ESP32にセンサーをつないでMQTTでサーバへデータを投げるんですが、Wi-Fiでつなげた場合はmDNSを使ってサーバーのhostnameで接続できるんですが、有線LANでちょっとトラブったので覚書としてアップしておきます。

開発環境


Windows10
VSCode + PlatformIO
ESP32-DevkitC

platformio.iniは
[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino


有線LANの接続


W5500についてはAmazonでW5500イーサネット ネットワークモジュールを購入しました。W5500は3.3V駆動なんですがこのボードは5Vでも動くようにDCDCコンバータが搭載されているのが特徴でしょうか。
  • 5V = 5V
  • GND = GND
  • RST = GPIO4
  • MISO = GPIO19
  • MOSI = GPIO23
  • SCS = GPIO5
  • SCLK = GPIO18 

ネットで検索するとESP32でW5500を使って有線LANを行っている記事は多数みうけられますが、基本はSPI接続をすればライブラリがあるのでそれ程戸惑うことはないと思います。
ポイントとしてはEthernetライブラリではなくEthernet2ライブラリを使用することでしょうか。
後、多くの記事ではリセットピンについては接続していないようですが、ESP32がリセットされた場合にW5500側もリセットした方がいいかなと思い接続してみました。

プログラムはこんな感じです。
    byte mac[] = { 0xDE0xAD0xBE0xFE0xFE0x01 };    
    pinMode(4OUTPUT);
    digitalWrite(40);
    delay(25);
    digitalWrite(41);
    delay(500);
    Ethernet.init(5);
    Ethernet.begin(mac);
    Serial.print("IP address: ");
    Serial.println(Ethernet.localIP());

後はMQTTライブラリでサーバに接続すれば完了です。


mDNSでサーバーのIPアドレスを取得する


ここまでは問題なかったのですが、Wi-Fiで接続しているときはサーバーのIPアドレスはESPmDNSを利用して取得していました。問題はESPmDNSはWi-Fi接続を前提として作成されているのでW5500(Ethernet2 )では利用できません。
そこでW5500でmDNSを利用できるMDNS_Genericを使用することにしました。
PlatformIOでライブラリをインストールすればサンプルにResolvingHostNamesもあるので簡単かなと・・・

まずはREADMEのLibraries' Patchesの「4. To fix Ethernet2 library」の指示に従いEthernet2のソースにパッチ(ファイルの置き換えと追加)を当てます。
後はサンプルを参考にソースをちょいちょいと作成してコンパイルすると「beginMulticast」の未定義エラーになってしまいました。
ソースを眺めてみると確かにパッチを当てたEthernetUdp2.cppにbeginMulticastはあります。

問題はMDNS_Generic.hにありました。

MDNSクラスのプライベート変数として_udpがUDPクラスの変数として定義されていますが、beginMulticastはEthernetUDP2.hで定義されているEthernetUDPクラスのメソッドとして定義されています。サンプルでもEthernetUDPクラスのインスタンスをMDNSのコンストラクタで渡しています。EthernetUDPクラスはUDPクラスを継承しているのでエラーになりませんが、beginMulticastメソッドを使うところでUDPクラスでは未定義になってしまいます。

ということでMDNS_Generic.hの以下の2か所の修正を行います。

class MDNS
{
  private:
    EthernetUDP*              _udp;

  public:
    MDNS(EthernetUDP& udp);

さらにMDNS_Generic_Impl.hも以下の2か所の修正を行います。

//#include <Udp.h> <-コメントアウト
#include <EthernetUDP2.h>

MDNS::MDNS(EthernetUDPudp)
{

これで無事にコンパイルは通りました。


hostnameの長さが固定?


コンパイルも無事に通り実行してみると、なぜかリブートを繰り返します。
仕方がないのでソースを眺めてみると、検索に失敗した場合に発生しているようです。

検索結果を返すコールバック関数を呼び出すメソッド_finishedResolvingNameメソッドでアドレス例外が発生しているようです。検索できなかった場合、IPアドレスがNULLで_finishedResolvingNameが呼び出されるのですが、それが原因でした。文字列のIPアドレスをIPAddressクラスのインスタンスに変換してコールバックを呼び出していますが、ここで文字列がNULLなのでアドレス例外が発生していました。

    this->_nameFoundCallback((const char*)nameipAddr == NULL ? INADDR_NONE : IPAddress(ipAddr));

これでリブートは収まりましたがまだ検索できていないようです。

    #define _MDNS_LOGLEVEL_ 4
    #include <Ethernet2.h>
    #include <MDNS_Generic.h>

とするとデバッグログをSerialに出力してくれるのでログを確認すると、アドレス解決ははできているようですが、なぜか解決不可となってしまいます。

またソースを見る羽目に・・・
本当にデバッグしてあるのかな?

                  //KH, to report name Resolve only UDP packet has corect size of 48
                  if (48 == udp_len)
                  {
                    // KH debug
                    MDNS_LOGINFO1("::_processMDNSQuery: to report IP, buf ="String((uint8_tbuf[0]));
                  
                    this->_finishedResolvingName((char*)this->_resolveNames[0], (const byte*)buf);
                  }

とudp_lenが48バイト固定の判定している部分が原因でした。
応答のUDPパケットのサイズを48バイト固定にしているので、hostnameの長さが14バイト(ドメインは.localで固定)でないと駄目なようです。

ということでパケットサイズのチェックは以下のように修正しました。

                  uint16_t l = sizeof(DNSHeader_t) + (strlen((const char *)this->_resolveNames[0])+2) + 4 + 6 + dataLen;
                  if (l == udp_len)
                  {
                    // KH debug
                    MDNS_LOGINFO1("::_processMDNSQuery: to report IP, buf ="String((uint8_tbuf[0]));
                  
                    this->_finishedResolvingName((char*)this->_resolveNames[0], (const byte*)buf);
                  }


ちなみにdataLenはIPアドレスの長さですが、これも4バイト固定(この部分の少し上でチェックしていました)になっているのでIPv4限定となります。いまのところIPv4も割り当てているのでこれで良しとしました。

bullseyeで初期設定後画面が真っ黒になって困った

あれ、画面が真っ黒に  Raspberry Pi OSとして最近はbullseyeを主に使っています。 今まではIoT用途が多かったのでヘッドレスでのセットアップがばかりでしたが久々にディスプレイ+キーボード+マウスの構成でインストールをしたところ初期設定後の再起動で画面が真っ黒...