//////////////////////////////////////////////////////////////////////
// Copyright (C) Hiroshi SUGIMURA 2022.06.03
//////////////////////////////////////////////////////////////////////
'use strict';
const { SerialPort } = require('serialport');
// 定数定義
/**
* USB ベンダーID (OMRON)
* @type {string}
*/
const USB_VENDOR_ID = '0590';
/**
* USB プロダクトID (2JCIE-BU)
* @type {string}
*/
const USB_PRODUCT_ID = '00D4';
/**
* パケットヘッダ (High Byte)
* @type {number}
*/
const HEADER_HIGH = 0x52;
/**
* パケットヘッダ (Low Byte)
* @type {number}
*/
const HEADER_LOW = 0x42;
/**
* 読み込みコマンド
* @type {number}
*/
const CMD_READ = 0x01;
/**
* 書き込みコマンド
* @type {number}
*/
const CMD_WRITE = 0x02;
/**
* アドレス: 最新データ (ショート)
* @type {number}
*/
const ADDR_LATEST_DATA_SHORT = 0x5022;
/**
* アドレス: LED設定
* @type {number}
*/
const ADDR_LED_SETTING = 0x5111;
/**
* アドレス: フラッシュメモリスステータス
* @type {number}
*/
const ADDR_FLASH_MEMORY = 0x5403; // flash memory status
/**
* センサーデータオブジェクトの型定義
* @typedef {object} SensorData
* @property {number} sequence_number - シーケンス番号 (0-255)
* @property {number} temperature - 温度 (degC)
* @property {number} humidity - 湿度 (%RH)
* @property {number} anbient_light - 照度 (lx)
* @property {number} pressure - 気圧 (hPa)
* @property {number} noise - 騒音 (dB)
* @property {number} etvoc - eTVOC (ppb)
* @property {number} eco2 - eCO2 (ppm)
* @property {number} discomfort_index - 不快指数
* @property {number} heat_stroke - 熱中症警戒度 (degC)
*/
/**
* OMRON USB環境センサ (2JCIE-BU) を制御するモジュール
* @namespace omron
*/
let omron = {
/**
* コールバック関数
* @type {function}
* @memberof omron
*/
callback: null,
/**
* シリアルポートの設定
* @type {object}
* @property {string} path - ポートパス (例: 'COM3')
* @property {number} baudRate - ボーレート (デフォルト: 115200)
* @property {number} dataBits - データビット (デフォルト: 8)
* @property {number} stopBits - ストップビット (デフォルト: 1)
* @property {string} parity - パリティ (デフォルト: 'none')
* @memberof omron
*/
portConfig: {
path: 'COM3',
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: 'none'
},
/**
* シリアルポートオブジェクト
* @type {SerialPort}
* @memberof omron
*/
port: null,
/**
* デバッグモードフラグ
* @type {boolean}
* @memberof omron
*/
debug: false,
/**
* 受信データバッファ (内部用)
* @type {Uint8Array}
* @memberof omron
*/
internalBuffer: new Uint8Array(0),
/**
* 空オブジェクト判定
* @param {object} obj - 判定するオブジェクト
* @returns {boolean} 空の場合はtrue
* @memberof omron
*/
isEmpty: function (obj) {
return Object.keys(obj).length === 0
},
/**
* リクエストデータ生成 (Uint8Array)
* Read Latest data short (0x5022)
* @returns {Uint8Array} 生成されたリクエストデータ
* @memberof omron
*/
createRequestData: function () {
// Header
const header_view = new Uint8Array([HEADER_HIGH, HEADER_LOW]); // fix
// Length (Payload ~ CRC-16)
const length_view = new Uint16Array([5]); // length = payload + CRC
// Payload frame; command[1], address[2], data[n]
const command_view = new Uint8Array([CMD_READ]); // 0x01: Read, 0x02: Write
const address_view = new Uint16Array([ADDR_LATEST_DATA_SHORT]); // 0x5022: Latest data short
// CRC-16 (Header ~ Payload)
const crc = omron.calcCrc16([header_view, length_view, command_view, address_view]);
const crc_view = new Uint16Array([crc]);
// 各 Typed Array を結合して 1 つの Uint8 Typed Array にする
const req_data = omron.concatTypedArrays([header_view, length_view, command_view, address_view, crc_view]);
return req_data;
},
/**
* LED設定用データ生成 (Uint8Array)
* @param {object} option - LED設定オプション
* @param {number} option.red - 赤色の輝度 (0-255)
* @param {number} option.green - 緑色の輝度 (0-255)
* @param {number} option.blue - 青色の輝度 (0-255)
* @returns {Uint8Array} 生成された設定データ
* @memberof omron
*/
createSettingLED: function (option) {
// Header
const header_view = new Uint8Array([HEADER_HIGH, HEADER_LOW]); // fix
// Length (Payload ~ CRC-16)
const length_view = new Uint16Array([10]); // length = payload + CRC
// Payload frame; command[1], address[2], data[n]
const command_view = new Uint8Array([CMD_WRITE]); // 0x01: Read, 0x02: Write
const address_view = new Uint16Array([ADDR_LED_SETTING]); // 0x5111: LED
// Helper to clamp values between min and max
const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
// setting
let r = (option.rule !== undefined) ? option.rule : 0x0001; // Default to 0x0001
r = clamp(r, 0, 0xFFFF); // Rule is 16-bit
let red = (option.red !== undefined) ? option.red : 0;
let green = (option.green !== undefined) ? option.green : 0;
let blue = (option.blue !== undefined) ? option.blue : 0;
const ruleVal = r;
const display_rule = new Uint16Array([ruleVal]);
const led_red = new Uint8Array([clamp(red, 0, 255)]);
const led_green = new Uint8Array([clamp(green, 0, 255)]); // Correct order: R, G, B
const led_blue = new Uint8Array([clamp(blue, 0, 255)]);
// CRC-16 (Header ~ Payload)
const crc = omron.calcCrc16([header_view, length_view, command_view, address_view, display_rule, led_red, led_green, led_blue]);
const crc_view = new Uint16Array([crc]);
// 各 Typed Array を結合して 1 つの Uint8 Typed Array にする
const req_data = omron.concatTypedArrays([header_view, length_view, command_view, address_view, display_rule, led_red, led_green, led_blue, crc_view]);
return req_data;
},
requestFlashMemoryStatus: function () {
// Header
const header_view = new Uint8Array([HEADER_HIGH, HEADER_LOW]); // fix
// Length (Payload ~ CRC-16)
const length_view = new Uint16Array([5]); // length = payload + CRC
// Payload frame; command[1], address[2], data[n]
const command_view = new Uint8Array([CMD_READ]); // 0x01: Read, 0x02: Write
const address_view = new Uint16Array([ADDR_FLASH_MEMORY]); // 0x5403: flash memory status
// CRC-16 (Header ~ Payload)
const crc = omron.calcCrc16([header_view, length_view, command_view, address_view]);
const crc_view = new Uint16Array([crc]);
// 各 Typed Array を結合して 1 つの Uint8 Typed Array にする
const req_data = omron.concatTypedArrays([header_view, length_view, command_view, address_view, crc_view]);
return req_data;
},
/**
* Typed Array オブジェクトのリストを 1 つの Uint8Array に連結
* @param {Array<TypedArray>} typed_array_list - 連結したいTyped Arrayのリスト
* @returns {Uint8Array} 連結されたUint8Array
* @memberof omron
*/
concatTypedArrays: function (typed_array_list) {
let byte_list = [];
for (let typed_array of typed_array_list) {
let uint8_view = new Uint8Array(typed_array.buffer, typed_array.byteOffset, typed_array.byteLength);
for (let byte of uint8_view) {
byte_list.push(byte);
}
}
return new Uint8Array(byte_list);
},
/**
* Typed Array のリストから CRC-16 を算出
* @param {Array<TypedArray>} typed_array_list - 計算対象のTyped Arrayリスト
* @returns {number} 計算されたCRC-16値
* @memberof omron
*/
calcCrc16: function (typed_array_list) {
let byte_list = omron.concatTypedArrays(typed_array_list);
let reg = 0xffff;
for (let i = 0; i < byte_list.length; i++) {
reg = reg ^ byte_list[i];
let bit_shift = 0;
while (true) {
let last_bit = reg & 1;
reg = reg >>> 1;
if (last_bit === 1) {
reg = reg ^ 0xA001;
}
bit_shift++;
if (bit_shift >= 8) {
break;
}
}
}
return reg;
},
/**
* レスポンスデータをパースしてオブジェクトに変換
* (呼び出し元で完全なパケットフレームであることを保証すること)
* @param {Uint8Array} recvData - 受信データ (1パケット分)
* @returns {SensorData|object|undefined} パースされたセンサーデータオブジェクト、またはundefined
* @memberof omron
*/
parseResponse: function (recvData) {
if (recvData.length < 4) return; // 最低限ヘッダと長さフィールドが必要
let data_view = new DataView(recvData.buffer, recvData.byteOffset, recvData.length);
// Header check
if (data_view.getUint8(0) != HEADER_HIGH || data_view.getUint8(1) != HEADER_LOW) {
return;
}
// Length check (Packet Size = Length Value + 4 bytes header)
// Note: The 'Length' field in the packet is (Payload + CRC).
// Packet = Header(2) + Length(2) + Payload + CRC(2)
// Thus, Length Value = Payload + CRC
// Total Bytes = 4 + Length Value
let len = data_view.getUint16(2, true);
if (recvData.length !== len + 4) {
omron.debug ? console.log('パケット長不一致: Expected ' + (len + 4) + ', Got ' + recvData.length) : 0;
return;
}
// CRC Check
let dataForCrc = recvData.subarray(0, recvData.length - 2);
let expectedCrc = data_view.getUint16(recvData.length - 2, true);
let actualCrc = omron.calcCrc16([dataForCrc]);
if (actualCrc !== expectedCrc) {
omron.debug ? console.log('CRC Error: Expected ' + expectedCrc.toString(16) + ', Got ' + actualCrc.toString(16)) : 0;
return;
}
let command = data_view.getUint8(4);
let address = data_view.getUint16(5, true);
if (address == ADDR_LATEST_DATA_SHORT) {
let sequence_number = data_view.getUint8(7);
let temperature = data_view.getInt16(8, true) / 100; // degC
let humidity = data_view.getInt16(10, true) / 100; // %RH
let anbient_light = data_view.getInt16(12, true); // lx
let pressure = data_view.getInt32(14, true) / 1000; // hPa
let noise = data_view.getInt16(18, true) / 100; // dB
let etvoc = data_view.getInt16(20, true); // ppb
let eco2 = data_view.getInt16(22, true); // ppm
let discomfort_index = data_view.getInt16(24, true) / 100;
let heat_stroke = data_view.getInt16(26, true) / 100; // degC
return {
'sequence_number': sequence_number,
'temperature': temperature, 'humidity': humidity, 'anbient_light': anbient_light, 'pressure': pressure, 'noise': noise,
'etvoc': etvoc, 'eco2': eco2, 'discomfort_index': discomfort_index, 'heat_stroke': heat_stroke
};
} else if (address == ADDR_LED_SETTING) {
omron.debug ? console.log('LED setting [normal state].') : 0;
let d = data_view.getUint8(7);
return { 'data': d };
} else if (address == ADDR_FLASH_MEMORY) {
omron.debug ? console.log('read Flash memory status.') : 0;
let d = data_view.getUint8(7);
return { 'data': d };
}
omron.debug ? console.log('other address:', address) : 0;
return;
},
/**
* 利用可能なシリアルポートのリストを取得
* @async
* @returns {Promise<Array>} シリアルポート情報の配列
* @memberof omron
*/
getPortList: async function () {
let portList = [];
try {
portList = await SerialPort.list();
} catch (err) {
omron.debug ? console.log(err, "e") : 0;
}
return portList;
},
/**
* センサーデータのリクエストを送信
* @memberof omron
*/
requestData: function () {
if (!omron.port) { // まだポートがない
if (omron.callback) {
omron.callback(null, 'Error: usb-2jcie-bu.requestData(): port is not found.');
} else {
console.error('@usb-2jcie-bu Error: usb-2jcie-bu.requestData(): port is not found.');
}
return;
}
const b = omron.createRequestData();
// console.log('req:', b);
omron.port.write(b);
},
/**
* LEDの設定を行う
* @async
* @param {object} option - LED設定オプション
* @memberof omron
*/
settingLED: async function (option) {
// console.log(option);
if (!omron.port) { // まだポートがない
if (omron.callback) {
omron.callback(null, 'Error: usb-2jcie-bu.settingLED(): port is not found.');
} else {
console.error('@usb-2jcie-bu Error: usb-2jcie-bu.settingLED(): port is not found.');
}
return;
}
const b = omron.createSettingLED(option);
// console.log('led:', b);
await omron.port.write(b);
},
/**
* フラッシュメモリの状態を確認する
* @async
* @memberof omron
*/
flashMemoryStatus: async function () {
// console.log('flashMemoryStatus');
await omron.port.write(omron.requestFlashMemoryStatus());
},
//////////////////////////////////////////////////////////////////////
// entry point
/**
* モジュールの開始処理
* シリアルポートを探索し、接続を確立してデータ受信の準備を行う
* @async
* @param {function} callback - データ受信時またはエラー発生時に呼ばれるコールバック関数 (data, err)
* @param {object} [options={}] - オプション設定
* @param {boolean} [options.debug=false] - デバッグモードの有効化
* @memberof omron
*/
start: async function (callback, options = {}) {
omron.debug = options.debug == true ? true : false;
if (omron.port) { // すでに通信している
if (omron.callback) {
omron.callback(null, 'Error: usb-2jcie-bu.start(): port is used already.');
} else {
console.error('@usb-2jcie-bu Error: usb-2jcie-bu.start(): port is used already.');
}
return;
}
omron.portConfig = { // default config set
path: 'COM3',
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: 'none'
};
omron.port = null;
if (callback) {
omron.callback = callback;
} else {
omron.debug ? console.log('Error: usb-2jcie-bu.start(): responceFunc is null.') : 0;
return;
}
// 環境センサーに接続
// ユーザーにシリアルポート選択画面を表示して選択を待ち受ける
let portList = await omron.getPortList();
// 同期処理なのでawait不要
let com = portList.filter((p) => {
if (p.vendorId == USB_VENDOR_ID && p.productId == USB_PRODUCT_ID) {
return p;
}
});
if (com.length == 0) { // センサー見つからない
if (omron.callback) {
omron.callback(null, 'Error: usb-2jcie-bu.start(): Sensor (2JCE-BU) is not found.');
} else {
console.error('@usb-2jcie-bu Error: usb-2jcie-bu.start(): Sensor (2JCE-BU) is not found.');
}
return;
}
omron.portConfig.path = com[0].path; // センサー見つかった
omron.port = new SerialPort(omron.portConfig, function (err) {
if (err) {
if (omron.callback) {
omron.callback(null, err);
} else {
console.error('@usb-2jcie-bu ' + err);
}
return;
}
});
// 通信エラーイベントのハンドリング
omron.port.on('error', function (err) {
if (omron.callback) {
omron.callback(null, 'Error: ' + err.message);
} else {
console.error('@usb-2jcie-bu Error: ' + err.message);
}
});
// データ受信イベントのハンドリング強化
// バッファリング処理を追加し、パケットの断片化や結合に対応
omron.internalBuffer = new Uint8Array(0);
omron.port.on('data', function (chunk) {
// バッファ肥大化防止 (Defense: Buffer Overflow Protection)
const MAX_BUFFER_SIZE = 1024 * 4; // 4KB
if (omron.internalBuffer.length + chunk.length > MAX_BUFFER_SIZE) {
omron.debug ? console.error('Warning: Buffer overflow protected. Resetting buffer.') : 0;
omron.internalBuffer = new Uint8Array(0);
}
// バッファに追加
let newBuffer = new Uint8Array(omron.internalBuffer.length + chunk.length);
newBuffer.set(omron.internalBuffer);
newBuffer.set(chunk, omron.internalBuffer.length);
omron.internalBuffer = newBuffer;
while (omron.internalBuffer.length >= 4) { // ヘッダ(2) + 長さ(2) の最小4バイトが必要
// ヘッダ検索
let headerIndex = -1;
for (let i = 0; i < omron.internalBuffer.length - 1; i++) {
if (omron.internalBuffer[i] === HEADER_HIGH && omron.internalBuffer[i + 1] === HEADER_LOW) {
headerIndex = i;
break;
}
}
if (headerIndex === -1) {
// ヘッダが見つからない場合、バッファをクリア(ただし最後の1バイトの可能性を考慮)
if (omron.internalBuffer[omron.internalBuffer.length - 1] === HEADER_HIGH) {
omron.internalBuffer = omron.internalBuffer.slice(omron.internalBuffer.length - 1);
} else {
omron.internalBuffer = new Uint8Array(0);
}
break; // データ不足で待機
}
// ヘッダより前のゴミデータを破棄
if (headerIndex > 0) {
omron.internalBuffer = omron.internalBuffer.slice(headerIndex);
continue; // 再度チェック
}
// 長さフィールドの読み取り
let view = new DataView(omron.internalBuffer.buffer, omron.internalBuffer.byteOffset, omron.internalBuffer.length);
let payloadLen = view.getUint16(2, true); // Payload + CRC
let packetSize = 4 + payloadLen; // Header(2) + Length(2) + Payload + CRC
if (omron.internalBuffer.length < packetSize) {
// パケット全体がまだ揃っていない
break;
}
// パケット抽出
let packet = omron.internalBuffer.slice(0, packetSize);
// バッファから抽出分を削除
omron.internalBuffer = omron.internalBuffer.slice(packetSize);
// パース実行
let r = omron.parseResponse(packet);
if (r) {
if (omron.callback) {
omron.callback(r, null);
} else {
omron.debug ? console.dir(r) : 0;
}
}
// パースエラーでもバッファからは削除済みなのでループ継続
}
});
// USB外したりしたとき
omron.port.on('close', function () {
if (omron.port) {
omron.port.close();
omron.port = null;
}
if (omron.callback) {
omron.callback(null, 'INF: port is closed.');
omron.callback = null;
}
});
},
/**
* モジュールの停止処理
* シリアルポートを閉じる
* @memberof omron
*/
stop: function () {
if (omron.port) {
omron.port.close();
omron.port = null;
}
if (omron.callback) {
omron.callback(null, 'INF: port is closed.');
omron.callback = null;
}
}
};
module.exports = omron;