Skip to content

Commit

Permalink
feat: 複数モデルに対応
Browse files Browse the repository at this point in the history
  • Loading branch information
nana4rider committed Jan 17, 2025
1 parent bf6ed4a commit b1ee0c0
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 48 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

## 使い方

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
80 changes: 50 additions & 30 deletions src/connector/BP35C2Connector.ts → src/connector/BP35Connector.ts
Original file line number Diff line number Diff line change
@@ -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^<DURATION> + 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<string, string>([
// ER01-ER03 Reserved
["ER04", "指定されたコマンドがサポートされていない"],
["ER05", "指定されたコマンドの引数の数が正しくない"],
["ER06", "指定されたコマンドの引数形式や値域が正しくない"],
// ER07-ER08 Reserved
["ER09", "UART 入力エラーが発生した"],
["ER10", "指定されたコマンドは受付けたが、実行結果が失敗した"],
]);
Expand All @@ -27,26 +37,31 @@ type Events = {
error: [err: Error]; // エラーイベント
};

/**
* https://www.furutaka-netsel.co.jp/maker/rohm/bp35c2
*/
export class BP35C2Connector extends Emitter<Events> implements WiSunConnector {
export class BP35Connector extends Emitter<Events> 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");
Expand All @@ -60,33 +75,37 @@ export class BP35C2Connector extends Emitter<Events> implements WiSunConnector {
return;
}

// ERXUDP の場合、データ長い部分を抽出
const byteLengthStartIndex = 118;
const byteLengthEndIndex = byteLengthStartIndex + 4;
const lengthString = textData.substring(
byteLengthStartIndex,
byteLengthEndIndex,
const commandMatcher = textData.match(
/^ERXUDP (?<sender>.{39}) (?<dest>.{39}) (?<rport>.{4}) (?<lport>.{4}) (?<senderlla>.{16}) (?<secured>.) (?<side>.) (?<datalen>[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)}<HEX:${message.toString("hex")}>`,
`SerialPort response: ${commandMatcher.input}<HEX:${message.toString("hex")}>`,
);

// 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);
});

// シリアルポートのエラーハンドリング
Expand Down Expand Up @@ -128,7 +147,8 @@ export class BP35C2Connector extends Emitter<Events> 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";
Expand Down Expand Up @@ -174,7 +194,7 @@ export class BP35C2Connector extends Emitter<Events> 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",
);

Expand All @@ -199,7 +219,7 @@ export class BP35C2Connector extends Emitter<Events> 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"),
);
Expand All @@ -218,8 +238,8 @@ export class BP35C2Connector extends Emitter<Events> implements WiSunConnector {

private async scanInternal(): Promise<PanInfo | undefined> {
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,
);
Expand Down
29 changes: 24 additions & 5 deletions src/connector/WiSunConnector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BP35C2Connector } from "@/connector/BP35C2Connector";
import { BP35Connector } from "@/connector/BP35Connector";

export type PanInfo = {
[name in string]: string;
Expand Down Expand Up @@ -57,21 +57,40 @@ export interface WiSunConnector {
close(): Promise<void>;
}

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)}`);
}
}
14 changes: 7 additions & 7 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { WiSunConnectorModels } from "@/connector/WiSunConnector";
import { cleanEnv, num, port, str } from "envalid";

const env = cleanEnv(process.env, {
Expand Down Expand Up @@ -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",
}),
Expand Down
4 changes: 2 additions & 2 deletions src/service/smartMeter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ export default async function initializeSmartMeterClient(): Promise<SmartMeterCl

async function initializeWiSunConnector(): Promise<[WiSunConnector, PanInfo]> {
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);

Expand Down

0 comments on commit b1ee0c0

Please sign in to comment.