Node-RED MCU用のノードを作成してみる その2(Servoノード)

はじめに

 以前Node-RED MCU用のServoノードの作成について、Qiitaに記事を投稿しました。その時点ではPWM outノードと一緒に使う前提で、msg.payloadの加工程度のことしかしていませんでした。

▼こちらの記事です。

https://qiita.com/background/items/9b820251aa9dda5a3167

 今回はソースコードをさらに読み込んで、Servoノードのみで使えるようにしてみました。PWM outノードを参考にしています。

 まだJavaScriptについて完全に理解しているわけではないので、間違い等がありましたらご指摘いただけると幸いです。

現状ではサーボモーターは動作するものの、UI Dashboardの画面が更新されないという不具合が起きています。

エラーが出るわけでもないので、また時間があるときに調査する予定です....

▼プログラムはGitHubにも公開しています。

https://github.com/404background/node-red-mcu-servo/tree/develop

▼以前の記事はこちら

Node-RED MCU用のノードを作成してみる その1(LINE Notify)

はじめに  以前の記事にて、ハッカソンで使っていたNode-RED関連の情報について紹介しました。その後もNode-REDで作成したフローをマイコンに書き込める、Node-RED MCUを…

ソースコードを読む

ファイル構成

▼Node-RED MCUに特有のノードはこちら

https://github.com/phoddie/node-red-mcu/tree/main/nodes/mcu

 例えばpwmノードのフォルダには、以下の4つのファイルが入っています。

  • manifest.json : ビルド時にノードを含めるために必要。node_types.jsonにパスを記述する。
  • mcu_pwm.html : Node-REDでの表示の設定
  • mcu_pwm.js : Node-REDに表示するノードの作成
  • pwm.js : ノードの具体的な処理について記述

 今回は特にpwm.jsファイルを読みます。

▼リポジトリではこちら

https://github.com/phoddie/node-red-mcu/blob/main/nodes/mcu/pwm/pwm.js

pwm.jsの中身

 "nodered"からNodeをインポートしています。

import {Node} from "nodered";

▼Nodeクラスはnode-red-mcu/nodered.jsでexportされています。REDクラスもこちらにありますね。

https://github.com/phoddie/node-red-mcu/blob/6855b565dc65ae36921b700df35bb9ae3ede2bf2/nodered.js#L316

 pwm.jsではNodeクラスを継承しています。PWMOutNodeクラスで具体的な処理が記述されています。

class PWMOutNode extends Node { ...

 configが使われていますね。htmlファイルのinputから入力を受け付けたdefaultsの値は、jsファイルではconfig.変数名で受け取ることができます。

▼プロパティについてはこちら

https://nodered.jp/docs/creating-nodes/properties

 なお、credentialsに設定した値はフローを書き出す際に含まれないためご注意ください。Node-RED MCUでビルドするときも含まれません。

 onStartではピンや周波数の設定が行われています。statusはデバッガに表示されるものです。

onStart(config) { ...
	try {
			const options = {
				pin: config.pin,
			};
			if (config.hz)
				options.hz = config.hz;
			this.#io = io = new device.io.PWM(options);
			cache.set(config.pin, io);
		}
		catch {
			this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"});
		}

 onMessageでmsg.payloadの値をもとに処理が行われています。

onMessage(msg, done) {
		if (this.#io) {
			this.#io.write(msg.payload * ((1 << this.#io.resolution) - 1));
			this.status({fill:"green", shape:"dot", text: msg.payload.toString()});
		}
		done();
	}

 計算にresolutionプロパティが使われていますが、これはModdable側で設定されているようです。サンプルにもありました。

▼詳しくはこちらをご覧ください。PWMクラスもこちらにあります。

https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/io/io.md#pwm

その他:JavaScriptの書き方

 「??=」はNull合体代入演算子というものだそうです。nullまたはundefinedの場合に代入されます。

▼こちらに書かれています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_assignment

 ioに#記号がついていますが、これはES2022の書き方だと思います。プライベートなプロパティになるようです。

▼こちらに書かれていました。

https://jsprimer.net/basic/class/#private-class-fields

ノードを作成する

 ここからはServoノードを作成していきます。

 まずhtmlファイルについて、PWM outノードでも使われていたmcuHelperを取り入れています。共通の処理をまとめたものです。

▼mcu_servo.htmlはこちら。PWM outノードに繋げないので、outputsは0にしました。

<script type="text/javascript">
	RED.nodes.registerType('mcu_servo',{
		category: mcuHelper.category,
		color: mcuHelper.color,
		defaults: {
			name: { value:"SG90" },
			pin: { validate: RED.validators.number() },
			hz: { value:"50" },
			pulseMin: {value:"0.5"},
			pulseMax: {value:"2.4"},
			angleMin: {value:"0"},
			angleMax: {value:"180"},
			moddable_manifest: {value: {include: "./manifest.json"}},
		},
		inputs:1,
		outputs:0,
		icon: "serial.svg",
		paletteLabel: 'Servo',
		label: function() {
			return this.name || "Servo " + this.pin;
		},
		oneditprepare: function() {
			const div = $("#node-mcu-rows");
			mcuHelper.appendProperties.PWM(div, "io", {});
			mcuHelper.restoreProperties.PWM(this, "io");
		},
		oneditsave: function() {
			mcuHelper.saveProperties.PWM(this, "io");
		},
	});
</script>

<script type="text/html" data-template-name="mcu_servo">
	<div class="form-row">
		<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
		<input type="text" id="node-input-name" placeholder="Servo">
	</div>
	<div id="node-mcu-rows">
	</div>
	<div class="form-row">
        <label for="node-input-pulse"><i class="fa fa-arrows-h"></i> pulse width(us)</label>
        <span for="node-input-pulseMin">min</span>
        <input type="number" id="node-input-pulseMin" style="width:60px" step="0.1">
        <span for="node-input-pulseMax" style="margin-left:22px;">max</span>
        <input type="number" id="node-input-pulseMax" style="width:60px" step="0.1">
    </div>
	<div class="form-row">
        <label for="node-input-angle"><i class="fa fa-arrows-h"></i> angle</label>
        <span for="node-input-angleMin">min</span>
        <input type="number" id="node-input-angleMin" style="width:60px" step="0.1">
        <span for="node-input-angleMax" style="margin-left:22px;">max</span>
        <input type="number" id="node-input-angleMax" style="width:60px" step="0.1">
    </div>
</script>

<script type="text/markdown" data-help-name="mcu_servo">

</script>

▼ノードの入力欄はこんな感じ。

 設定値をもとに、計算はservo.jsで行います。数値計算でエラーが出ることがあったのですが、Number関数に渡すと解決しました。数値に変換しておくのが確実なのかもしれません。

▼Number関数についてはこちら

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number

▼プログラムはこちら

import {Node} from "nodered";
let cache;

class ServoNode extends Node {
	#io; #cycle; #pulseWidth; #angleWidth; #pulseMin;

	onStart(config) {
		super.onStart(config);

		if (!globalThis.device?.io?.PWM)
			return void this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"});

		cache ??= new Map;
		let io = cache.get(config.pin);
        this.#cycle = 1 / Number(config.hz) * 10 ** 3;
        this.#pulseWidth = Number(config.pulseMax) - Number(config.pulseMin);
        this.#angleWidth = Number(config.angleMax) - Number(config.angleMin);
		this.#pulseMin = Number(config.pulseMin);

		if (io) {
			this.#io = io;
		}
		else {
			try {
				const options = {
					pin: config.pin,
				};
				if (config.hz)
					options.hz = config.hz;
				this.#io = io = new device.io.PWM(options);
				cache.set(config.pin, io);
			}
			catch {
				this.status({fill: "red", shape: "dot", text: "node-red:common.status.error"});
			}
		}
	}
	onMessage(msg, done) {
        let ratio = (Number(msg.payload) / this.#angleWidth * this.#pulseWidth + this.#pulseMin) / this.#cycle;
		if (this.#io) {
			this.#io.write(ratio * ((1 << this.#io.resolution) - 1));
			this.status({fill:"green", shape:"dot", text: ratio.toString()});
		}
		done();
	}

	static type = "mcu_servo";
	static {
		RED.nodes.registerType(this.type, this);
	}
}

 PWM outノードでは0~1の数値で出力を調整します。周期とパルス幅から、サーボモーターを制御するための値を計算しています。

▼SG90の場合は0.5ms~2.4msです。0.5msの場合は0.025になります。

 他のファイルはPWM outノードとほとんど同じです。

▼manifest.jsonはこちら

{
	"modules": {
		"servo": "./servo"
	},
	"preload": [
		"servo"
	]
}

▼mcu_servo.jsはこちら

module.exports = function(RED) {
    function ServoNode(config) {
        RED.nodes.createNode(this, config);
		console.log(config)
    }
    RED.nodes.registerType("mcu_servo", ServoNode);
}

ノードをインストールする

 以下のコマンドでフォルダを指定してパッケージをインストールすることができます。Node-RED MCUの環境を構築したときのディレクトリで実行してください。node_modulesフォルダに追加されます。

 node_modules/@ralphwetzel/node-red-mcu-plugin/node-red-mcuにある、node_types.jsonにmanifest.jsonのパスを追加します。今回は相対パスで指定します。

▼同じnode_modulesフォルダに追加されるので、そこまで遡っています。

▼パスが適切に設定されていない、またはmanifest.jsonでの名前が適切でない場合、以下のようなエラーが出ます。

 Node-REDを起動して、ノードが追加されているかどうかを確認してください。

▼インストール後、uiノードのsliderに繋いでみました。

▼実際の動作はこちら

最後に

 node-red-mcuとmoddableリポジトリのどちらに書かれているのか、そもそもJavaScript特有の書き方なのかなど、知らないことがまだまだ多いので読むのに時間がかかりました。

 サーボモーターは動作しているものの、UI画面が更新されていない状態です。エラーが出ていないので、原因調査に時間がかかりそうなんですよね。また時間があるときに調べてみます...

 Moddable側のプログラムが使えるなら、WiFi関連のノードが欲しいなと思っているところです。アクセスポイントにしたり、SSIDを設定したりできるようにしたいですね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です