Node.js環境で動作する宣言的UIによるハードウェア制御ライブラリ「Edison」
やっとできた。
import React, { useState } from "react";
import { renderToString } from "react-dom/server";
import { Board, Led } from "edison";
const App: React.FC = () => {
const [isOn, setIsOn] = useState(false);
if (!isOn) {
setIsOn(true);
}
return (
<Board port={"/dev/ttyUSB0"}>
<Led pin={13} isOn={isOn} />
</Board>
);
};
renderToString(<App />);
renderToStringじゃ値のインタラクティブな監視が無理。
一回しかレンダリングできないから値の保持ができない。やばいどうしよう
子コンポーネントへの値の受け渡しはcreateContextでいける。
inputモード系も同様の原理で行ける。
import { SerialPort, board } from "edison";
import React, { createContext } from "react";
export const BoardContext = createContext<SerialPort | null>(null);
type BoardProps = {
children: React.ReactNode;
port: string;
// onReady: (port: SerialPort) => void /* こういう風に書くと引数増やせる */
};
export const Board: React.FC<BoardProps> = ({ children, port }) => {
board.connectManual(port);
return <BoardContext.Provider value={null}>{children}</BoardContext.Provider>;
};
当たり前だけどReactフレームワークは動作しないか。
不要部分を書き直してみるか。他の物を試してみる
サーバーサイドで動作する状態管理のための新しいhookを作らなきゃいけない。
方法があまりわからない
全体を再レンダリングする実験は成功。
<Board>コンポーネントを二回呼ぶとportのロックでエラーになるので部分的な再レンダリングが必要
import { renderToString } from "react-dom/server";
import { FC } from "edison";
import { Button } from "./input/Button";
import { Board } from "./lib/Board";
import { Buzzer } from "./lib/Buzzer";
import { useCustomHook, subscribeToStateChange } from "./lib/state";
import React from "react";
const [someState, setSomeState] = useCustomHook(false);
const Test: FC = () => {
const handlePress = () => {
setSomeState(true);
};
const handleRelease = () => {
setSomeState(false);
};
return (
<Board port={"/dev/ttyUSB1"}>
<Button pin={8} onPress={handlePress} onRelease={handleRelease}>
<Buzzer pin={13} isOn={someState} />
</Button>
</Board>
);
};
// サーバーサイドレンダリングではなく、Node.js環境での実行を想定
console.log(renderToString(<Test />));
subscribeToStateChange((newValue: any) => {
console.log("new value: ", newValue);
console.log("State changed to: }");
console.log(renderToString(<Test />));
});
ライフサイクルを作成しなければいけない?用語が合ってるのかはわからないけれどこれは最初の宣言的UIの実装に近い過ちを感じる。
他のアプローチを思いつかないと。
要はCan not lock port を乗り越えればいいのだから既につながっているという事を把握し読み飛ばすことができればいい?
エラーで分岐する方法が必要かもしれない
レンダリングエンジンを作る必要があるようで
ここは避けては通れなさそう
まだ楽そうなシリアルポートがロック時にport接続しないロジックを組んでみる
①状態管理
②イベントドリブンなハードウェア制御(済)
③レンダリングエンジンの実装
上記が多分必要になるもの
Inkやreact-three-fiberを読むしかない。
Inkは一度挫折してしまったけど今度は仲間もいる。大丈夫できる
js-domとかで仮想的にDOMを制御している風にして開発者から見てわかりやすく実装はできるかもしれないけれど、ロジック的には冗長で綺麗なものではない。
edison専用のレンダリングロジックを実装する必要があるっぽい。
ts-nodeの代替を探す。
vite-nodeかBunかDenoかな
vite-node採用した
えっぐいの見つけた。超絶参考になる
supportMutationかpersistanceか。
appendChild的なノードの変更はないけど、useState的なプロパティの変更は実装される予定。
でもappendしたらロボットの腕がガンタイプからソードタイプに変更されるみたいなのを考えるとMutationModeでいいかな。多分名前的にpersistanceの方がルールが厳しいから動作するならこっちにしよう。
fablicっていうのはsupportPersistanceを使用しているレンダラーなのか
仕事だと無能すぎてつら。
これで実装なんてできるのだろうか。
春休み終わるまでにどこまで作れるかな。
初めてのWASM
Inkのレンダリングエンジンに乗せてLチカできた!
状態の保持ができた。コンソールには値が更新されて表示されるが、Ledコンポーネントに反映されない。なぜ?
classを使用しなくてはいけない場面というのはレンダリングが実装されていない部分で、状態の保持などをhook使わずに書くと面倒なので使える。
これでLチカができた。Inkを参考にしすぎたので最小構成を作ってからEdisonに必要な実装に変更していってる。
無能の書き方だなぁ...
import React from 'react'
import { useState, useEffect } from 'react'
import { Board } from './utils/Board'
import { Led } from './component/output/uniqueDevice/Led'
import render from './rendere/render'
const App: React.FC = () => {
const [isOn, setIsOn] = useState(true)
useEffect(() => {
setInterval(() => {
setIsOn((preIson) => !preIson)
}, 200)
}, [])
return (
<Board port={'/dev/ttyUSB0'}>
<Led
pin={13}
isOn={isOn}
/>
</Board>
)
}
render(<App />)
やっとサーボモーターのコンポーネント化できたンゴ
useEffect × useStateで状態の変更を行えるが、新しい値としてsetするのと、関数の返り値としてsetするので挙動が違う?
ふわっとわかるような気がせんでもないけど明日以降の俺任せた!
フロントで使う場合は挙動同じだったハズ
変わらず動いたなぁ。兎に角これで問題ない。
てか面白すぎる!
いけたと思ったけどlinkでローカルインストールして呼び出してみたら下記のエラーが発生した。
allen@MSI:~/Github/EdisonRun$ npx vite-node tsx/ServoRun.tsx
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
The above error occurred in the <App> component:
at App (/home/allen/Github/EdisonRun/tsx/ServoRun.tsx:8:51)
at App (/home/allen/Github/Edison/dist/esm/declarative/components/App.js:10:1)
React will try to recreate this component tree from scratch using the error boundary you provided, InternalApp.
CJS消したからなのか...?それともライブラリの中でuseEffectを使用したからなのか。
useEffectはトップレベルでしか呼べないのはもちろんだけれど条件によって呼び出してるわけではないので使えるのかな?過去に使えなかったのがrenderの問題で今回はカスタムレンダラーだから普通にCJSビルドしたら解決するのかどうかわからん。仕事しなきゃ
シリアルポート通信を介さずにリアルタイム通信でEsp32の操作ができそう
ずっとESP32にfirmataをアップロードしようとしているけどできない...
firmataをアップロードする方法があまり見当たらず、StandardFirmataはまず無理で、Configulable Firmataいけるみたいなことも書いてあったけど無理っぽい
数日前にあげたエラーはReactのhookの呼び出し方が悪いってエラーだけれど友人から引き続き修正開始。
わからないけれどClassではなくFunctionコンポーネントで書き直すと使えるようになるらしい...?わからん
あーnpm linkしてるからReactを二つ発見している可能性があるのか
多分直る気がしてきた
レンダラーでClassを使用しているからっぽい。
難しいなReact
違った。わからん
できたー!!
原因:Reactが複数存在していると認識された
何故?:npm link
をテストに使用していたが、シンボリックリンクでのインストールになる為?本番環境と異なる状況になっていたっぽい
解決方法:verdaccioで試したらいけた。
何故か is-in-ciのみEdison側でインストールしてもパッケージ見つからないといわれた。EdisonRunでインストールしたら動いた
ちなみにそのあとにモジュールのimport, exportの末尾にjs拡張子付けてないから駄目だったりcjs対応していたころの名残で.mjs拡張子をmoduleに設定していた為に読み込めないなどした
angle 30
App
angle 30
App
angle 40
App
angle 40
実行結果に表示されるのにサーボモーターが動かない時は心がポキッっていったけど普通にServoコンポーネント書いてなかっただけでよかった。泣くかと思った。
input系が二つ以上存在すると互いに干渉しあって正しく動作しないというバグと戦った
現在わかっているのは
①inputが二種あれば、片方だけ押してももう片方のバイナリも受信する。
②受信したバイナリのピン番号が融合している。
③片方を押した状態でもう片方を押したり離したりすると干渉せず動作する
押したときにinputPortのon, offが同時に受信する場合がある
if (preBinary && preBinary === buffer) return
これ比較に比較にひっかからないのってC言語のポインタ的な事?
JSにもそんなことあるんか!?
ストリーム処理を混在させない為にrxjsに責務を全て渡した。
import type { SerialPort } from 'serialport'
import { bufferWrite } from '../Utils/bufferWrite'
import type { Observable } from 'rxjs'
import { fromEventPattern } from 'rxjs'
import { filter, map, scan } from 'rxjs/operators'
import { Buffer } from 'node:buffer'
const SET_PIN_MODE = 0xf4
const INPUT_MODE = 0x00
const DIGITAL_CHANGE_MESSAGE = 0xd0
export const setInputState = (
pin: number,
port: SerialPort,
): Observable<boolean> => {
bufferWrite(port, Buffer.from([SET_PIN_MODE, pin, INPUT_MODE]))
bufferWrite(
port,
Buffer.from([DIGITAL_CHANGE_MESSAGE + Math.floor(pin / 8), 1]),
)
return fromEventPattern<Buffer>(
(handler) => port.on('data', handler), // イベントを追加する関数
(handler) => port.removeListener('data', handler), // イベントを削除する関数
).pipe(
map((data) => (data.length <= 3 ? data : data.subarray(0, 3))),
scan(
(
acc: { preBinary?: Buffer; lastState?: boolean; emit: boolean },
currentBinary: Buffer,
) => {
if (acc.preBinary && acc.preBinary.equals(currentBinary)) {
return { ...acc, emit: false }
}
if (
currentBinary.length === 0 ||
(currentBinary[0] !== 0x90 && currentBinary[0] !== 0x91)
) {
return { ...acc, emit: false }
}
const currentState = !!(currentBinary[1] & (1 << pin % 8))
// console.log(
// 'acc:',
// acc,
// 'currentBinary:',
// currentBinary,
// 'currentState:',
// currentState,
// 'lastState:',
// acc.lastState,
// 'emit:',
// acc.lastState !== currentState,
// )
if (acc.emit) {
console.log('acc', acc)
}
return {
preBinary: currentBinary,
lastState: currentState,
emit: acc.lastState !== currentState, // 状態が変わった場合のみemitをtrueに
}
},
{ emit: false },
),
filter((acc) => acc.emit),
map((acc) => acc.lastState as boolean),
)
}
import type { SerialPort } from 'serialport'
import { setInputState } from '../../helper/Input/setInputState'
import type { Sensor } from '../../types/analog/analog'
export const inputPort = (port: SerialPort) => {
return (pin: number) => {
let prevValue: boolean | null = null
return {
read: (
method: Sensor,
func: () => Promise<void> | Promise<number> | void | number,
): void => {
const observable = setInputState(pin, port)
observable.subscribe((value: boolean) => {
if (method === 'change') {
if (value !== prevValue) {
prevValue = value
func()
}
} else if (method === 'off' && value && prevValue !== value) {
prevValue = value
func()
} else if (method === 'on' && !value && prevValue !== value) {
prevValue = value
func()
}
})
},
}
}
}
上記のコードでいけたー!
pinごとに前回のデジタルメッセージの情報を保存しておかないと混ざるのか。よかった解決して
センサー固有のラグに対してのdelayTimeをオプショナルで付与する。
デバイスを変えたときも参照透過性を持たせるために必要
書くの久しぶり
ESP32の対応などをしている
Servo.hライブラリがESP32では動作しないのでStandardFirmataではなくConfigurableFirmataをUploadする。
接続時にデータを送信してくれないからUDP的にBoard状態を送り続けることで接続を確認する
baulateがデフォルトで違うから動かないのかぁ
引数で渡そう。仕方がない
baulateが違うとそもそもportが見つからないエラーにはならずサクセスになる。
しかしpinへの操作が正しくならない
現状ではFREENOVE ESP32 WROOMをESP-32-WROOM-DEVで動かしているが書き込み成功後タイムアウトする。(onDataで通信が届けられない)
connecting中にぶっこぬいて port is closedになってからまた刺して動作すると動いた。
なぜだ...
FREENOVE ESP32 WROOMは5GHz対応していないためconfigurable firmataで動作しない?どの層かの理解はないが仮説としてマイコン本体の問題とする
TCPとWebSocket両対応