Open52

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モード系も同様の原理で行ける。

Board.tsx
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のロックでエラーになるので部分的な再レンダリングが必要

index.tsx
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専用のレンダリングロジックを実装する必要があるっぽい。

新谷アレン新谷アレン

https://github.com/facebook/react/blob/main/packages/react-reconciler/README.md

supportMutationかpersistanceか。
appendChild的なノードの変更はないけど、useState的なプロパティの変更は実装される予定。
でもappendしたらロボットの腕がガンタイプからソードタイプに変更されるみたいなのを考えるとMutationModeでいいかな。多分名前的にpersistanceの方がルールが厳しいから動作するならこっちにしよう。

新谷アレン新谷アレン

仕事だと無能すぎてつら。
これで実装なんてできるのだろうか。
春休み終わるまでにどこまで作れるかな。

新谷アレン新谷アレン

状態の保持ができた。コンソールには値が更新されて表示されるが、Ledコンポーネントに反映されない。なぜ?

新谷アレン新谷アレン

classを使用しなくてはいけない場面というのはレンダリングが実装されていない部分で、状態の保持などをhook使わずに書くと面倒なので使える。

新谷アレン新谷アレン

これでLチカができた。Inkを参考にしすぎたので最小構成を作ってからEdisonに必要な実装に変更していってる。
無能の書き方だなぁ...

App.tsx
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を二つ発見している可能性があるのか
多分直る気がしてきた

新谷アレン新谷アレン

できたー!!
原因: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が二種あれば、片方だけ押してももう片方のバイナリも受信する。
②受信したバイナリのピン番号が融合している。
③片方を押した状態でもう片方を押したり離したりすると干渉せず動作する

新谷アレン新谷アレン
  if (preBinary && preBinary === buffer) return

これ比較に比較にひっかからないのってC言語のポインタ的な事?
JSにもそんなことあるんか!?

新谷アレン新谷アレン

ストリーム処理を混在させない為にrxjsに責務を全て渡した。

inputState.ts
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),
  )
}
新谷アレン新谷アレン
inputPort.ts
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をオプショナルで付与する。
デバイスを変えたときも参照透過性を持たせるために必要

新谷アレン新谷アレン

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で動作しない?どの層かの理解はないが仮説としてマイコン本体の問題とする