From b1ee0c04c72ba1ef9f2964b9684d709cce9b927a Mon Sep 17 00:00:00 2001 From: Shunichiro Aki Date: Sat, 18 Jan 2025 05:49:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A4=87=E6=95=B0=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- package-lock.json | 4 +- package.json | 2 +- .../{BP35C2Connector.ts => BP35Connector.ts} | 80 ++++++++++++------- src/connector/WiSunConnector.ts | 29 +++++-- src/env.ts | 14 ++-- src/service/smartMeter.ts | 4 +- 7 files changed, 91 insertions(+), 48 deletions(-) rename src/connector/{BP35C2Connector.ts => BP35Connector.ts} (77%) diff --git a/README.md b/README.md index c6f0259..54bb08c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,11 @@ ECHONET Liteプロトコルを使用して、Wi-SUN対応スマートメータ ## サポートデバイス -- [BP35C2](https://www.furutaka-netsel.co.jp/maker/rohm/bp35c2) +- ROHM BP35C2(動作確認済み) +- ROHM BP35C2 +- ROHM BP35A1 +- JORJIN WSR35A1-00 +- ラトックシステム RS-WSUHA-P ## 使い方 diff --git a/package-lock.json b/package-lock.json index cd09067..ec2eaa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wisun2mqtt", - "version": "1.0.4", + "version": "1.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wisun2mqtt", - "version": "1.0.4", + "version": "1.0.8", "license": "ISC", "dependencies": { "@serialport/parser-readline": "^13.0.0", diff --git a/package.json b/package.json index 080ebe4..3ba6921 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wisun2mqtt", - "version": "1.0.7", + "version": "1.0.8", "main": "dist/index.js", "type": "module", "homepage": "https://github.com/nana4rider/wisun2mqtt", diff --git a/src/connector/BP35C2Connector.ts b/src/connector/BP35Connector.ts similarity index 77% rename from src/connector/BP35C2Connector.ts rename to src/connector/BP35Connector.ts index b9680b6..57ad341 100644 --- a/src/connector/BP35C2Connector.ts +++ b/src/connector/BP35Connector.ts @@ -1,23 +1,33 @@ import { PanInfo, WiSunConnector } from "@/connector/WiSunConnector"; import logger from "@/logger"; import { ReadlineParser } from "@serialport/parser-readline"; +import assert from "assert"; import { pEvent, TimeoutError } from "p-event"; import { DelimiterParser, SerialPort } from "serialport"; import { Emitter } from "strict-event-emitter"; import { setTimeout } from "timers/promises"; +/** ボーレート */ const BAUDRATE = 115200; -const SCAN_DURATION = 6; // スキャンのデフォルト期間 -const SCAN_TIMEOUT = 30000; // スキャンのタイムアウト (ms) -const JOIN_TIMEOUT = 30000; // ジョインのタイムアウト (ms) -const COMMAND_TIMEOUT = 5000; // スキャンを除くコマンドのタイムアウト +/** スキャン間隔 */ +const SCAN_DURATION = 6; +/** コマンドのタイムアウト */ +const COMMAND_TIMEOUT = 3000; +/** SKSCAN 2のタイムアウト スキャン時間 0.0096 sec * (2^ + 1) */ +const SCAN_TIMEOUT = + 0.0096 * (2 ^ (SCAN_DURATION + 1)) * 28 * 1000 + COMMAND_TIMEOUT; +/** SKJOINのタイムアウト */ +const JOIN_TIMEOUT = 38000 + COMMAND_TIMEOUT; + const CRLF = "\r\n"; -const HEX_PORT = "0E1A"; // 3610 +const HEX_ECHONET_PORT = "0E1A"; // 3610 const ErrorMessages = new Map([ + // ER01-ER03 Reserved ["ER04", "指定されたコマンドがサポートされていない"], ["ER05", "指定されたコマンドの引数の数が正しくない"], ["ER06", "指定されたコマンドの引数形式や値域が正しくない"], + // ER07-ER08 Reserved ["ER09", "UART 入力エラーが発生した"], ["ER10", "指定されたコマンドは受付けたが、実行結果が失敗した"], ]); @@ -27,26 +37,31 @@ type Events = { error: [err: Error]; // エラーイベント }; -/** - * https://www.furutaka-netsel.co.jp/maker/rohm/bp35c2 - */ -export class BP35C2Connector extends Emitter implements WiSunConnector { +export class BP35Connector extends Emitter implements WiSunConnector { private serialPort: SerialPort; private parser: ReadlineParser; private ipv6Address: string | undefined; + private extendArg: string; /** * BP35C2Connector クラスのインスタンスを初期化します。 * * @param device シリアルポートのパス + * @param side B面:0 HAN面:1 + * @param */ - constructor(device: string) { + constructor(device: string, side: 0 | 1 | undefined = undefined) { super(); + + this.extendArg = side ? ` ${side}` : ""; this.serialPort = new SerialPort({ path: device, baudRate: BAUDRATE }); this.parser = this.serialPort.pipe( new DelimiterParser({ delimiter: Buffer.from(CRLF, "utf-8") }), ); + this.setupSerialEventHandlers(); + } + private setupSerialEventHandlers() { // シリアルポートからのデータ受信 this.parser.on("data", (data: Buffer) => { const textData = data.toString("utf8"); @@ -60,33 +75,37 @@ export class BP35C2Connector extends Emitter implements WiSunConnector { return; } - // ERXUDP の場合、データ長い部分を抽出 - const byteLengthStartIndex = 118; - const byteLengthEndIndex = byteLengthStartIndex + 4; - const lengthString = textData.substring( - byteLengthStartIndex, - byteLengthEndIndex, + const commandMatcher = textData.match( + /^ERXUDP (?.{39}) (?.{39}) (?.{4}) (?.{4}) (?.{16}) (?.) (?.) (?[0-9A-F]{4}) /, ); - const dataLength = parseInt(lengthString, 16); - if (isNaN(dataLength) || dataLength < 0) { - console.error("Invalid data length in ERXUDP message:", lengthString); + if (!commandMatcher) { + logger.error(`Invalid data format in ERXUDP message: ${textData}`); return; } + assert(commandMatcher.input && commandMatcher.groups); // バイナリデータを切り出し - const binaryDataStartIndex = byteLengthEndIndex + 1; + const binaryDataStartIndex = commandMatcher.input.length + 1; + const binaryDataLength = parseInt(commandMatcher.groups.datalen, 16); const message = data.subarray( binaryDataStartIndex, - binaryDataStartIndex + dataLength, + binaryDataStartIndex + binaryDataLength, ); logger.debug( - `SerialPort response: ${textData.substring(0, binaryDataStartIndex)}`, + `SerialPort response: ${commandMatcher.input}`, ); - // ECHONET Liteメッセージならイベントを発火 - if (message.readUInt16BE(0) === 0x1081) { - this.emit("message", message); + // ポートとヘッダを確認 + if ( + commandMatcher.groups.rport !== HEX_ECHONET_PORT || + commandMatcher.groups.lport !== HEX_ECHONET_PORT || + message.readUInt16BE(0) !== 0x1081 + ) { + // ECHONET Liteのデータではない + return; } + + this.emit("message", message); }); // シリアルポートのエラーハンドリング @@ -128,7 +147,8 @@ export class BP35C2Connector extends Emitter implements WiSunConnector { responses.push(textData); if (expected(textData)) { - return true; // 条件に一致したら解決 + // 条件に一致したら待機終了 + return true; } else if (textData.startsWith("FAIL")) { const errorCode = textData.substring(5); const errorMessage = ErrorMessages.get(errorCode) ?? "Unknown"; @@ -174,7 +194,7 @@ export class BP35C2Connector extends Emitter implements WiSunConnector { .toUpperCase() .padStart(4, "0"); const commandBuffer = Buffer.from( - `SKSENDTO 1 ${this.ipv6Address} ${HEX_PORT} 1 0 ${hexDataLength} `, + `SKSENDTO 1 ${this.ipv6Address} ${HEX_ECHONET_PORT} 1${this.extendArg} ${hexDataLength} `, "utf8", ); @@ -199,7 +219,7 @@ export class BP35C2Connector extends Emitter implements WiSunConnector { logger.info("Configuring Wi-SUN connection..."); await this.sendCommand(`SKSREG S2 ${panInfo["Channel"]}`); await this.sendCommand(`SKSREG S3 ${panInfo["Pan ID"]}`); - const [, ipv6Address] = await this.sendCommand( + const [_echo, ipv6Address] = await this.sendCommand( `SKLL64 ${panInfo["Addr"]}`, (data) => !data.startsWith("SKLL64"), ); @@ -218,8 +238,8 @@ export class BP35C2Connector extends Emitter implements WiSunConnector { private async scanInternal(): Promise { logger.info("Starting PAN scan..."); - const [, , ...responses] = await this.sendCommand( - `SKSCAN 2 FFFFFFFF ${SCAN_DURATION} 0`, + const [_echo, _ok, ...responses] = await this.sendCommand( + `SKSCAN 2 FFFFFFFF ${SCAN_DURATION}${this.extendArg}`, (data) => data.startsWith("EVENT 22"), SCAN_TIMEOUT, ); diff --git a/src/connector/WiSunConnector.ts b/src/connector/WiSunConnector.ts index a1feec4..193f6fa 100644 --- a/src/connector/WiSunConnector.ts +++ b/src/connector/WiSunConnector.ts @@ -1,4 +1,4 @@ -import { BP35C2Connector } from "@/connector/BP35C2Connector"; +import { BP35Connector } from "@/connector/BP35Connector"; export type PanInfo = { [name in string]: string; @@ -57,21 +57,40 @@ export interface WiSunConnector { close(): Promise; } +export const WiSunConnectorModels = [ + // https://www.rohm.co.jp/products/wireless-communication/specified-low-power-radio-modules/bp35a1-product + "BP35A1", + // https://www.rohm.co.jp/products/wireless-communication/specified-low-power-radio-modules/bp35c0-product + "BP35C0", + "BP35C2", + // https://www.incom.co.jp/products/detail.php?company_id=3166&product_id=64453 + "WSR35A1-00", + // https://www.ratocsystems.com/products/wisun/usb-wisun/rs-wsuha/ + "RS-WSUHA-P", +] as const; + +type WiSunConnectorModel = (typeof WiSunConnectorModels)[number]; + /** * 指定したモデルのWi-SUNコネクタのインスタンスを取得します * - * @param model + * @param model Wi-SUNコネクタのモデル * @param device シリアルポートのパス * @returns */ export default function createWiSunConnector( - model: string, + model: WiSunConnectorModel, device: string, ): WiSunConnector { switch (model) { + case "BP35C0": case "BP35C2": - return new BP35C2Connector(device); + case "RS-WSUHA-P": + return new BP35Connector(device, 0); + case "BP35A1": + case "WSR35A1-00": + return new BP35Connector(device); default: - throw new Error(`Unsupported model: ${model}`); + throw new Error(`Unsupported model: ${String(model)}`); } } diff --git a/src/env.ts b/src/env.ts index 38508e5..89569aa 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,3 +1,4 @@ +import { WiSunConnectorModels } from "@/connector/WiSunConnector"; import { cleanEnv, num, port, str } from "envalid"; const env = cleanEnv(process.env, { @@ -29,15 +30,14 @@ const env = cleanEnv(process.env, { }), ECHONET_GET_TIMEOUT: num({ desc: "GET要求のタイムアウト", - default: 10000, // 4000msくらいはかかる + default: 8000, // 4000msくらいはかかる }), - WISUN_CONNECTOR: str({ - desc: "Wi-SUNコネクタ", - default: "BP35C2", - choices: ["BP35C2"], + WISUN_CONNECTOR_MODEL: str({ + desc: "Wi-SUNコネクタのモデル", + choices: WiSunConnectorModels, }), - WISUN_DEVICE: str({ - desc: "デバイス名", + WISUN_CONNECTOR_DEVICE_PATH: str({ + desc: "Wi-SUNコネクタのデバイスパス", default: "/dev/ttyUSB0", example: "/dev/ttyUSB0 or COM3", }), diff --git a/src/service/smartMeter.ts b/src/service/smartMeter.ts index bbb520c..929202c 100644 --- a/src/service/smartMeter.ts +++ b/src/service/smartMeter.ts @@ -170,8 +170,8 @@ export default async function initializeSmartMeterClient(): Promise { const wiSunConnector = createWiSunConnector( - env.WISUN_CONNECTOR, - env.WISUN_DEVICE, + env.WISUN_CONNECTOR_MODEL, + env.WISUN_CONNECTOR_DEVICE_PATH, ); await wiSunConnector.setAuth(env.ROUTE_B_ID, env.ROUTE_B_PASSWORD);