@jiangtaste/baiwei-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/core/BaiweiClient.d.ts +38 -0
- package/dist/core/BaiweiClient.js +94 -0
- package/dist/core/BaiweiSession.d.ts +56 -0
- package/dist/core/BaiweiSession.js +229 -0
- package/dist/core/Client.d.ts +74 -0
- package/dist/core/Client.js +127 -0
- package/dist/core/MessageAssembler.d.ts +17 -0
- package/dist/core/MessageAssembler.js +76 -0
- package/dist/core/PendingRequestStore.d.ts +18 -0
- package/dist/core/PendingRequestStore.js +33 -0
- package/dist/core/Session.d.ts +43 -0
- package/dist/core/Session.js +330 -0
- package/dist/core/SessionConnector.d.ts +14 -0
- package/dist/core/SessionConnector.js +62 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/services/BaseService.d.ts +11 -0
- package/dist/services/BaseService.js +17 -0
- package/dist/services/ControlService.d.ts +33 -0
- package/dist/services/ControlService.js +67 -0
- package/dist/services/DeviceService.d.ts +26 -0
- package/dist/services/DeviceService.js +51 -0
- package/dist/services/GatewayService.d.ts +11 -0
- package/dist/services/GatewayService.js +33 -0
- package/dist/services/RoomService.d.ts +11 -0
- package/dist/services/RoomService.js +27 -0
- package/dist/services/SceneService.d.ts +12 -0
- package/dist/services/SceneService.js +39 -0
- package/dist/services/UserService.d.ts +21 -0
- package/dist/services/UserService.js +70 -0
- package/dist/transport/TcpClient.d.ts +38 -0
- package/dist/transport/TcpClient.js +315 -0
- package/dist/types/device-catalog.d.ts +42 -0
- package/dist/types/device-catalog.js +16 -0
- package/dist/types/device-state.d.ts +56 -0
- package/dist/types/device-state.js +2 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +24 -0
- package/dist/types/messages.d.ts +53 -0
- package/dist/types/messages.js +32 -0
- package/dist/types/options.d.ts +27 -0
- package/dist/types/options.js +7 -0
- package/dist/types/room.d.ts +9 -0
- package/dist/types/room.js +2 -0
- package/dist/types/scene.d.ts +22 -0
- package/dist/types/scene.js +2 -0
- package/dist/types/user.d.ts +17 -0
- package/dist/types/user.js +2 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.js +50 -0
- package/dist/utils/IdGenerator.d.ts +19 -0
- package/dist/utils/IdGenerator.js +49 -0
- package/dist/utils/MessageIdGenerator.d.ts +4 -0
- package/dist/utils/MessageIdGenerator.js +12 -0
- package/dist/utils/logger.d.ts +33 -0
- package/dist/utils/logger.js +115 -0
- package/dist/utils/time.d.ts +2 -0
- package/dist/utils/time.js +14 -0
- package/package.json +45 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GatewayService = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const BaseService_1 = require("./BaseService");
|
|
6
|
+
/** 网关级操作封装,例如允许设备入网。 */
|
|
7
|
+
class GatewayService extends BaseService_1.BaseService {
|
|
8
|
+
constructor(session) {
|
|
9
|
+
super(session);
|
|
10
|
+
}
|
|
11
|
+
discovery() {
|
|
12
|
+
// TODO: Implement device discovery
|
|
13
|
+
}
|
|
14
|
+
/** 打开入网窗口,允许新 Zigbee 设备在指定时间内加入。 */
|
|
15
|
+
async permitZigbeeJoin(time = 60) {
|
|
16
|
+
return this.request({
|
|
17
|
+
msgClass: types_1.MsgClass.GATEWAY_MGMT,
|
|
18
|
+
msgName: types_1.MsgName.ZB_NET_OPEN,
|
|
19
|
+
msgType: types_1.MsgType.SET,
|
|
20
|
+
payload: { time },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** 提前关闭入网窗口。 */
|
|
24
|
+
async stopZigbeeJoin() {
|
|
25
|
+
return this.request({
|
|
26
|
+
msgClass: types_1.MsgClass.GATEWAY_MGMT,
|
|
27
|
+
msgName: types_1.MsgName.ZB_NET_OPEN,
|
|
28
|
+
msgType: types_1.MsgType.SET,
|
|
29
|
+
payload: { time: 0 },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.GatewayService = GatewayService;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaiweiSession } from "../core/BaiweiSession";
|
|
2
|
+
import { Room, RoomList } from "../types";
|
|
3
|
+
import { BaseService } from "./BaseService";
|
|
4
|
+
/** 房间查询封装。 */
|
|
5
|
+
export declare class RoomService extends BaseService {
|
|
6
|
+
constructor(session: BaiweiSession);
|
|
7
|
+
list(): Promise<RoomList>;
|
|
8
|
+
/** 返回拍平后的房间数组,便于业务层直接使用。 */
|
|
9
|
+
listRooms(): Promise<Room[]>;
|
|
10
|
+
getRoomById(roomId: number): Promise<Room | undefined>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RoomService = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const BaseService_1 = require("./BaseService");
|
|
6
|
+
/** 房间查询封装。 */
|
|
7
|
+
class RoomService extends BaseService_1.BaseService {
|
|
8
|
+
constructor(session) {
|
|
9
|
+
super(session);
|
|
10
|
+
}
|
|
11
|
+
async list() {
|
|
12
|
+
return this.request({
|
|
13
|
+
msgClass: types_1.MsgClass.ROOM_MGMT,
|
|
14
|
+
msgName: types_1.MsgName.ROOM_QUERY,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** 返回拍平后的房间数组,便于业务层直接使用。 */
|
|
18
|
+
async listRooms() {
|
|
19
|
+
const response = await this.list();
|
|
20
|
+
return response.room_list ?? [];
|
|
21
|
+
}
|
|
22
|
+
async getRoomById(roomId) {
|
|
23
|
+
const rooms = await this.listRooms();
|
|
24
|
+
return rooms.find((room) => room.id === roomId);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.RoomService = RoomService;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaiweiSession } from "../core/BaiweiSession";
|
|
2
|
+
import { Scene, SceneList } from "../types";
|
|
3
|
+
import { BaseService } from "./BaseService";
|
|
4
|
+
/** 场景查询与触发封装。 */
|
|
5
|
+
export declare class SceneService extends BaseService {
|
|
6
|
+
constructor(session: BaiweiSession);
|
|
7
|
+
list(): Promise<SceneList>;
|
|
8
|
+
/** 触发指定场景。 */
|
|
9
|
+
call(sceneId: number): Promise<unknown>;
|
|
10
|
+
listScenes(): Promise<Scene[]>;
|
|
11
|
+
getSceneById(sceneId: number): Promise<Scene | undefined>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SceneService = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const BaseService_1 = require("./BaseService");
|
|
6
|
+
/** 场景查询与触发封装。 */
|
|
7
|
+
class SceneService extends BaseService_1.BaseService {
|
|
8
|
+
constructor(session) {
|
|
9
|
+
super(session);
|
|
10
|
+
}
|
|
11
|
+
async list() {
|
|
12
|
+
return this.request({
|
|
13
|
+
msgClass: types_1.MsgClass.SCENE_MGMT,
|
|
14
|
+
msgName: types_1.MsgName.SCENE_LIST,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** 触发指定场景。 */
|
|
18
|
+
async call(sceneId) {
|
|
19
|
+
return this.request({
|
|
20
|
+
msgClass: types_1.MsgClass.SCENE_MGMT,
|
|
21
|
+
msgName: types_1.MsgName.SCENE_CALL,
|
|
22
|
+
msgType: types_1.MsgType.SET,
|
|
23
|
+
payload: {
|
|
24
|
+
scene: {
|
|
25
|
+
id: sceneId,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async listScenes() {
|
|
31
|
+
const response = await this.list();
|
|
32
|
+
return response.scene_list ?? [];
|
|
33
|
+
}
|
|
34
|
+
async getSceneById(sceneId) {
|
|
35
|
+
const scenes = await this.listScenes();
|
|
36
|
+
return scenes.find((scene) => scene.id === sceneId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.SceneService = SceneService;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaiweiSession } from "../core/BaiweiSession";
|
|
2
|
+
import { AdminLoginOptions, UserLoginOptions, UserPasswordLoginOptions, UserResponse } from "../types";
|
|
3
|
+
import { BaseService } from "./BaseService";
|
|
4
|
+
/** 用户登录封装,主要职责是换取并保存 token。 */
|
|
5
|
+
export declare class UserService extends BaseService {
|
|
6
|
+
constructor(session: BaiweiSession);
|
|
7
|
+
/**
|
|
8
|
+
* 统一登录入口。
|
|
9
|
+
* - 传字符串或空值:按管理员密码登录
|
|
10
|
+
* - 传 `{ userName, userPwd }`:按普通用户账号密码登录
|
|
11
|
+
* - 传 `{ userPwd }`:按管理员密码登录
|
|
12
|
+
*/
|
|
13
|
+
login(options?: string | UserLoginOptions): Promise<UserResponse>;
|
|
14
|
+
/** 管理员登录:当前协议只要求管理员密码。 */
|
|
15
|
+
loginAsAdmin(options?: AdminLoginOptions): Promise<UserResponse>;
|
|
16
|
+
/** 普通用户登录:显式传用户名和密码。 */
|
|
17
|
+
loginAsUser(options: UserPasswordLoginOptions): Promise<UserResponse>;
|
|
18
|
+
/** 登录成功后把 token 写回 session,供后续请求复用。 */
|
|
19
|
+
private storeToken;
|
|
20
|
+
private isUserPasswordLogin;
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UserService = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
const BaseService_1 = require("./BaseService");
|
|
6
|
+
/** 用户登录封装,主要职责是换取并保存 token。 */
|
|
7
|
+
class UserService extends BaseService_1.BaseService {
|
|
8
|
+
constructor(session) {
|
|
9
|
+
super(session);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 统一登录入口。
|
|
13
|
+
* - 传字符串或空值:按管理员密码登录
|
|
14
|
+
* - 传 `{ userName, userPwd }`:按普通用户账号密码登录
|
|
15
|
+
* - 传 `{ userPwd }`:按管理员密码登录
|
|
16
|
+
*/
|
|
17
|
+
async login(options) {
|
|
18
|
+
if (typeof options === "string" || options === undefined) {
|
|
19
|
+
return this.loginAsAdmin({ userPwd: options });
|
|
20
|
+
}
|
|
21
|
+
if (this.isUserPasswordLogin(options)) {
|
|
22
|
+
return this.loginAsUser(options);
|
|
23
|
+
}
|
|
24
|
+
return this.loginAsAdmin(options);
|
|
25
|
+
}
|
|
26
|
+
/** 管理员登录:当前协议只要求管理员密码。 */
|
|
27
|
+
async loginAsAdmin(options = {}) {
|
|
28
|
+
const res = await this.request({
|
|
29
|
+
msgClass: types_1.MsgClass.USER_MGMT,
|
|
30
|
+
msgName: types_1.MsgName.USER_LOGIN,
|
|
31
|
+
payload: {
|
|
32
|
+
user: {
|
|
33
|
+
appId: types_1.DEFAULT_APP_ID,
|
|
34
|
+
user_pwd: options.userPwd ?? "888888",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
this.storeToken(res);
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
41
|
+
/** 普通用户登录:显式传用户名和密码。 */
|
|
42
|
+
async loginAsUser(options) {
|
|
43
|
+
const res = await this.request({
|
|
44
|
+
msgClass: types_1.MsgClass.USER_MGMT,
|
|
45
|
+
msgName: types_1.MsgName.USER_LOGIN,
|
|
46
|
+
payload: {
|
|
47
|
+
user: {
|
|
48
|
+
appId: types_1.DEFAULT_APP_ID,
|
|
49
|
+
user_name: options.userName,
|
|
50
|
+
user_pwd: options.userPwd,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
this.storeToken(res);
|
|
55
|
+
return res;
|
|
56
|
+
}
|
|
57
|
+
/** 登录成功后把 token 写回 session,供后续请求复用。 */
|
|
58
|
+
storeToken(res) {
|
|
59
|
+
if (res?.user?.token) {
|
|
60
|
+
this.session.updateToken(res.user.token);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new Error("Login failed: no token received");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
isUserPasswordLogin(options) {
|
|
67
|
+
return "userName" in options;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.UserService = UserService;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import EventEmitter from "events";
|
|
2
|
+
import { BaiweiTcpOptions } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* TCP 传输层。
|
|
5
|
+
* 负责 socket 生命周期、帧编解码、断线重连和协议级容错。
|
|
6
|
+
*/
|
|
7
|
+
export declare class TcpClient extends EventEmitter {
|
|
8
|
+
private options;
|
|
9
|
+
private socket;
|
|
10
|
+
private reconnectTimer?;
|
|
11
|
+
private destroyed;
|
|
12
|
+
private connected;
|
|
13
|
+
private connecting;
|
|
14
|
+
private manualClose;
|
|
15
|
+
private cache;
|
|
16
|
+
private log;
|
|
17
|
+
constructor(options: BaiweiTcpOptions);
|
|
18
|
+
/** 建立底层 socket 连接;如果已连接或正在连接则直接复用当前状态。 */
|
|
19
|
+
connect(): void;
|
|
20
|
+
isConnected(): boolean;
|
|
21
|
+
/** 把 JSON 对象编码后写入 TCP 连接。 */
|
|
22
|
+
send(data: Record<string, any>): Promise<void>;
|
|
23
|
+
/** 处理背压:write 返回 false 时等待 drain。 */
|
|
24
|
+
private writeAsync;
|
|
25
|
+
/** 按百微协议格式封包:MAGIC + 长度 + JSON body。 */
|
|
26
|
+
private encode;
|
|
27
|
+
/** 从字节流中持续切出完整协议帧,并恢复成 JSON 消息。 */
|
|
28
|
+
private decode;
|
|
29
|
+
/** 主动断开连接时关闭自动重连语义。 */
|
|
30
|
+
disconnect(): void;
|
|
31
|
+
/** 协议解析失败时清空缓存并销毁连接,回到干净状态。 */
|
|
32
|
+
private handleProtocolError;
|
|
33
|
+
/** 彻底销毁客户端实例,通常只在进程退出或彻底弃用时调用。 */
|
|
34
|
+
destroy(): void;
|
|
35
|
+
/** 异常关闭后按配置延迟重连。 */
|
|
36
|
+
scheduleReconnect(): void;
|
|
37
|
+
private clearReconnectTimer;
|
|
38
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TcpClient = void 0;
|
|
7
|
+
const events_1 = __importDefault(require("events"));
|
|
8
|
+
const node_net_1 = require("node:net");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
const MAGIC = "@#$%";
|
|
11
|
+
const HEADER_SIZE = MAGIC.length + 4;
|
|
12
|
+
/**
|
|
13
|
+
* TCP 传输层。
|
|
14
|
+
* 负责 socket 生命周期、帧编解码、断线重连和协议级容错。
|
|
15
|
+
*/
|
|
16
|
+
class TcpClient extends events_1.default {
|
|
17
|
+
options;
|
|
18
|
+
socket = null;
|
|
19
|
+
reconnectTimer;
|
|
20
|
+
destroyed = false;
|
|
21
|
+
connected = false;
|
|
22
|
+
connecting = false;
|
|
23
|
+
manualClose = false;
|
|
24
|
+
cache = Buffer.alloc(0);
|
|
25
|
+
log;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super();
|
|
28
|
+
this.log = (0, logger_1.createLogger)(options.logger);
|
|
29
|
+
this.options = {
|
|
30
|
+
autoReconnect: true,
|
|
31
|
+
reconnectInterval: 5_000,
|
|
32
|
+
maxPacketSize: 0xffff, // 64KB (protocol uses 2-byte length)
|
|
33
|
+
maxCacheSize: 0xffff * 4, // 256KB (4x max packet)
|
|
34
|
+
keepAlive: true,
|
|
35
|
+
keepAliveInitialDelay: 15_000,
|
|
36
|
+
noDelay: true,
|
|
37
|
+
...options
|
|
38
|
+
};
|
|
39
|
+
// Protocol length field is 2 bytes (encoded as 4 ASCII hex chars). Hard cap at 0xFFFF.
|
|
40
|
+
if (!this.options.maxPacketSize || this.options.maxPacketSize <= 0) {
|
|
41
|
+
this.options.maxPacketSize = 0xffff;
|
|
42
|
+
}
|
|
43
|
+
if (this.options.maxPacketSize > 0xffff) {
|
|
44
|
+
this.options.maxPacketSize = 0xffff;
|
|
45
|
+
}
|
|
46
|
+
if (!this.options.maxCacheSize || this.options.maxCacheSize <= 0) {
|
|
47
|
+
this.options.maxCacheSize = this.options.maxPacketSize * 4;
|
|
48
|
+
}
|
|
49
|
+
// Ensure cache cap is always >= max packet size.
|
|
50
|
+
if (this.options.maxCacheSize < this.options.maxPacketSize) {
|
|
51
|
+
this.options.maxCacheSize = this.options.maxPacketSize;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** 建立底层 socket 连接;如果已连接或正在连接则直接复用当前状态。 */
|
|
55
|
+
connect() {
|
|
56
|
+
if (this.destroyed)
|
|
57
|
+
return;
|
|
58
|
+
if (this.connected || this.connecting)
|
|
59
|
+
return;
|
|
60
|
+
if (this.socket)
|
|
61
|
+
return;
|
|
62
|
+
this.connecting = true;
|
|
63
|
+
const socket = new node_net_1.Socket();
|
|
64
|
+
this.socket = socket;
|
|
65
|
+
if (this.options.noDelay)
|
|
66
|
+
socket.setNoDelay(true);
|
|
67
|
+
if (this.options.keepAlive) {
|
|
68
|
+
socket.setKeepAlive(true, this.options.keepAliveInitialDelay);
|
|
69
|
+
}
|
|
70
|
+
socket.on("connect", () => {
|
|
71
|
+
this.connecting = false;
|
|
72
|
+
this.connected = true;
|
|
73
|
+
this.clearReconnectTimer();
|
|
74
|
+
this.log.info("Tcp Client: connected");
|
|
75
|
+
this.emit("connected");
|
|
76
|
+
});
|
|
77
|
+
socket.on("error", (err) => {
|
|
78
|
+
this.connecting = false;
|
|
79
|
+
this.log.info("Tcp Client: error", err);
|
|
80
|
+
this.emit("error", err);
|
|
81
|
+
if (!this.destroyed && !this.manualClose && this.options.autoReconnect) {
|
|
82
|
+
this.scheduleReconnect();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
socket.on("close", (hadError) => {
|
|
86
|
+
this.connecting = false;
|
|
87
|
+
this.connected = false;
|
|
88
|
+
this.socket = null;
|
|
89
|
+
this.log.info("TCP closed: ", hadError);
|
|
90
|
+
this.emit("disconnected", hadError);
|
|
91
|
+
const shouldReconnect = !this.destroyed && !this.manualClose && this.options.autoReconnect;
|
|
92
|
+
this.manualClose = false;
|
|
93
|
+
if (shouldReconnect) {
|
|
94
|
+
this.scheduleReconnect();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
socket.on("data", (chunk) => {
|
|
98
|
+
const messages = this.decode(chunk);
|
|
99
|
+
for (const message of messages) {
|
|
100
|
+
this.emit("message", message);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
socket.connect({
|
|
104
|
+
host: this.options.host,
|
|
105
|
+
port: this.options.port,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
isConnected() {
|
|
109
|
+
return this.connected;
|
|
110
|
+
}
|
|
111
|
+
/** 把 JSON 对象编码后写入 TCP 连接。 */
|
|
112
|
+
async send(data) {
|
|
113
|
+
if (this.options.debug) {
|
|
114
|
+
this.log.info("\n>>>>>>>>>>>>>>>>>>>>");
|
|
115
|
+
this.log.info(JSON.stringify(data), "\n");
|
|
116
|
+
}
|
|
117
|
+
if (!this.socket || !this.connected) {
|
|
118
|
+
throw new Error("TcpClient not connected");
|
|
119
|
+
}
|
|
120
|
+
const buffer = this.encode(data);
|
|
121
|
+
await this.writeAsync(buffer);
|
|
122
|
+
}
|
|
123
|
+
/** 处理背压:write 返回 false 时等待 drain。 */
|
|
124
|
+
writeAsync(buffer) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const socket = this.socket;
|
|
127
|
+
if (!socket || !this.connected) {
|
|
128
|
+
reject(new Error("TcpClient not connected"));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const onError = (err) => {
|
|
132
|
+
cleanup();
|
|
133
|
+
reject(err);
|
|
134
|
+
};
|
|
135
|
+
const onDrain = () => {
|
|
136
|
+
cleanup();
|
|
137
|
+
resolve();
|
|
138
|
+
};
|
|
139
|
+
const cleanup = () => {
|
|
140
|
+
socket.off("error", onError);
|
|
141
|
+
socket.off("drain", onDrain);
|
|
142
|
+
};
|
|
143
|
+
socket.on("error", onError);
|
|
144
|
+
const ok = socket.write(buffer);
|
|
145
|
+
if (ok) {
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
socket.on("drain", onDrain);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** 按百微协议格式封包:MAGIC + 长度 + JSON body。 */
|
|
154
|
+
encode(data) {
|
|
155
|
+
const body = Buffer.from(JSON.stringify(data), "utf-8");
|
|
156
|
+
const totalLength = body.length + HEADER_SIZE;
|
|
157
|
+
// Protocol length field is uint16 (4 ASCII hex chars). Prevent overflow/wrap.
|
|
158
|
+
if (totalLength > 0xffff) {
|
|
159
|
+
throw new Error(`Packet too large for protocol: totalLength=${totalLength} (max 65535)`);
|
|
160
|
+
}
|
|
161
|
+
if (totalLength > (this.options.maxPacketSize ?? 0xffff)) {
|
|
162
|
+
throw new Error(`Packet exceeds maxPacketSize: totalLength=${totalLength}, maxPacketSize=${this.options.maxPacketSize}`);
|
|
163
|
+
}
|
|
164
|
+
const lenBuf = Buffer.allocUnsafe(2);
|
|
165
|
+
lenBuf.writeUint16BE(totalLength, 0);
|
|
166
|
+
const lenHex = lenBuf.toString("hex").toUpperCase(); // 4个 hex 字符
|
|
167
|
+
const lenAscii = Buffer.from(lenHex, "ascii"); // 4个 ASCII
|
|
168
|
+
const header = Buffer.concat([Buffer.from(MAGIC), lenAscii]);
|
|
169
|
+
const buffer = Buffer.concat([header, body]);
|
|
170
|
+
return buffer;
|
|
171
|
+
}
|
|
172
|
+
/** 从字节流中持续切出完整协议帧,并恢复成 JSON 消息。 */
|
|
173
|
+
decode(chunk) {
|
|
174
|
+
const messages = [];
|
|
175
|
+
// For debug, avoid dumping huge buffers
|
|
176
|
+
if (this.options.debug) {
|
|
177
|
+
this.log.info("\n<<<<<<<<<<<<<<<<<<<<");
|
|
178
|
+
const preview = chunk.subarray(0, Math.min(chunk.length, 512));
|
|
179
|
+
this.log.info(`chunkBytes=${chunk.length} previewHex=${preview.toString("hex")}`);
|
|
180
|
+
}
|
|
181
|
+
// Append to cache
|
|
182
|
+
if (chunk && chunk.length > 0) {
|
|
183
|
+
this.cache = Buffer.concat([this.cache, chunk]);
|
|
184
|
+
}
|
|
185
|
+
const expectedMagic = Buffer.from(MAGIC, "ascii");
|
|
186
|
+
// Hard cache cap
|
|
187
|
+
if (this.cache.length > (this.options.maxCacheSize ?? 0)) {
|
|
188
|
+
this.handleProtocolError(`Cache overflow: cacheLength=${this.cache.length}, maxCacheSize=${this.options.maxCacheSize}`);
|
|
189
|
+
return messages;
|
|
190
|
+
}
|
|
191
|
+
while (true) {
|
|
192
|
+
const buffer = this.cache;
|
|
193
|
+
// Need at least header
|
|
194
|
+
if (buffer.length < HEADER_SIZE) {
|
|
195
|
+
return messages;
|
|
196
|
+
}
|
|
197
|
+
// Resync MAGIC if needed
|
|
198
|
+
const magicBuf = buffer.subarray(0, MAGIC.length);
|
|
199
|
+
if (!magicBuf.equals(expectedMagic)) {
|
|
200
|
+
const idx = buffer.indexOf(expectedMagic, 1);
|
|
201
|
+
if (idx === -1) {
|
|
202
|
+
// Keep a small tail in case MAGIC spans chunks
|
|
203
|
+
const keep = Math.max(0, MAGIC.length - 1);
|
|
204
|
+
this.cache = buffer.subarray(Math.max(0, buffer.length - keep));
|
|
205
|
+
return messages;
|
|
206
|
+
}
|
|
207
|
+
this.cache = buffer.subarray(idx);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// Parse length (ASCII hex, fixed 4 chars)
|
|
211
|
+
const lenAsciiBuf = buffer.subarray(MAGIC.length, MAGIC.length + 4);
|
|
212
|
+
const lenHex = lenAsciiBuf.toString("ascii");
|
|
213
|
+
if (!/^[0-9a-fA-F]{4}$/.test(lenHex)) {
|
|
214
|
+
this.handleProtocolError(`Invalid length field (not 4 hex chars): "${lenHex}"`);
|
|
215
|
+
return messages;
|
|
216
|
+
}
|
|
217
|
+
const totalLength = parseInt(lenHex, 16);
|
|
218
|
+
if (!Number.isFinite(totalLength) || totalLength <= 0) {
|
|
219
|
+
this.handleProtocolError(`Invalid total length parsed from header: ${totalLength}`);
|
|
220
|
+
return messages;
|
|
221
|
+
}
|
|
222
|
+
if (totalLength < HEADER_SIZE) {
|
|
223
|
+
this.handleProtocolError(`Invalid total length: ${totalLength}, smaller than HEADER_SIZE: ${HEADER_SIZE}`);
|
|
224
|
+
return messages;
|
|
225
|
+
}
|
|
226
|
+
if (totalLength > (this.options.maxPacketSize ?? 0)) {
|
|
227
|
+
this.handleProtocolError(`Packet too large: totalLength=${totalLength}, maxPacketSize=${this.options.maxPacketSize}`);
|
|
228
|
+
return messages;
|
|
229
|
+
}
|
|
230
|
+
// Wait for more bytes
|
|
231
|
+
if (buffer.length < totalLength) {
|
|
232
|
+
return messages;
|
|
233
|
+
}
|
|
234
|
+
const bodyBuf = buffer.subarray(HEADER_SIZE, totalLength);
|
|
235
|
+
if (bodyBuf.length === 0) {
|
|
236
|
+
// Consume frame, continue
|
|
237
|
+
this.cache = buffer.subarray(totalLength);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const bodyStr = bodyBuf.toString("utf-8");
|
|
241
|
+
try {
|
|
242
|
+
const json = JSON.parse(bodyStr);
|
|
243
|
+
messages.push(json);
|
|
244
|
+
// Consume frame and continue parsing next frame (if any)
|
|
245
|
+
this.cache = buffer.subarray(totalLength);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
this.handleProtocolError(`Invalid JSON body in packet: ${err.message}`);
|
|
249
|
+
return messages;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** 主动断开连接时关闭自动重连语义。 */
|
|
254
|
+
disconnect() {
|
|
255
|
+
this.manualClose = true;
|
|
256
|
+
this.connecting = false;
|
|
257
|
+
this.clearReconnectTimer();
|
|
258
|
+
if (!this.socket) {
|
|
259
|
+
this.connected = false;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.socket.destroy();
|
|
263
|
+
}
|
|
264
|
+
/** 协议解析失败时清空缓存并销毁连接,回到干净状态。 */
|
|
265
|
+
handleProtocolError(reason) {
|
|
266
|
+
if (this.options.debug) {
|
|
267
|
+
console.error(`[TcpClient] Protocol error: ${reason}`);
|
|
268
|
+
}
|
|
269
|
+
this.cache = Buffer.alloc(0);
|
|
270
|
+
// Bubble up a specialized event for callers that care
|
|
271
|
+
this.emit("protocol_error", new Error(reason));
|
|
272
|
+
// Tear down the socket to get back to a clean state
|
|
273
|
+
if (this.socket) {
|
|
274
|
+
try {
|
|
275
|
+
this.socket.destroy();
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// ignore
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** 彻底销毁客户端实例,通常只在进程退出或彻底弃用时调用。 */
|
|
283
|
+
destroy() {
|
|
284
|
+
this.destroyed = true;
|
|
285
|
+
this.manualClose = true;
|
|
286
|
+
this.connecting = false;
|
|
287
|
+
this.connected = false;
|
|
288
|
+
this.clearReconnectTimer();
|
|
289
|
+
if (this.socket) {
|
|
290
|
+
this.socket.destroy();
|
|
291
|
+
this.socket.removeAllListeners();
|
|
292
|
+
this.socket = null;
|
|
293
|
+
}
|
|
294
|
+
this.removeAllListeners();
|
|
295
|
+
}
|
|
296
|
+
/** 异常关闭后按配置延迟重连。 */
|
|
297
|
+
scheduleReconnect() {
|
|
298
|
+
if (this.destroyed)
|
|
299
|
+
return;
|
|
300
|
+
this.log.info("TCP reconnecting...");
|
|
301
|
+
if (this.reconnectTimer)
|
|
302
|
+
return;
|
|
303
|
+
this.reconnectTimer = setTimeout(() => {
|
|
304
|
+
this.reconnectTimer = undefined;
|
|
305
|
+
this.connect();
|
|
306
|
+
}, this.options.reconnectInterval);
|
|
307
|
+
}
|
|
308
|
+
clearReconnectTimer() {
|
|
309
|
+
if (this.reconnectTimer) {
|
|
310
|
+
clearTimeout(this.reconnectTimer);
|
|
311
|
+
this.reconnectTimer = undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
exports.TcpClient = TcpClient;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** 设备目录与设备基础信息定义。 */
|
|
2
|
+
export declare enum BaiweiProductType {
|
|
3
|
+
ON_OFF_LIGHT = "On/Off Light",
|
|
4
|
+
ON_OFF_SWITCH = "On/Off Switch",
|
|
5
|
+
WINDOW_COVER = "Window Covering Device",
|
|
6
|
+
IAS_ZONE = "IAS Zone",
|
|
7
|
+
AIR_BOX = "Air Box",
|
|
8
|
+
SCENE_SELECTOR = "Scene Selector",
|
|
9
|
+
AC_GATEWAY = "AC gateway",
|
|
10
|
+
NEW_WIND = "New wind controller",
|
|
11
|
+
FLOOR_HEAT = "Floor heat controller"
|
|
12
|
+
}
|
|
13
|
+
export interface DeviceQuery {
|
|
14
|
+
type_list: DeviceTypeList[];
|
|
15
|
+
}
|
|
16
|
+
export interface DeviceTypeList {
|
|
17
|
+
product_type: BaiweiProductType;
|
|
18
|
+
device_list: BaiweiDevice[];
|
|
19
|
+
}
|
|
20
|
+
export interface BaiweiDevice {
|
|
21
|
+
device_id: string;
|
|
22
|
+
product_id: number;
|
|
23
|
+
device_attr: string;
|
|
24
|
+
device_name: string;
|
|
25
|
+
room_id: number;
|
|
26
|
+
network_type: number;
|
|
27
|
+
create_time: string;
|
|
28
|
+
acoutside_id: number;
|
|
29
|
+
acgateway_id: number;
|
|
30
|
+
product_type: string;
|
|
31
|
+
product_name: string;
|
|
32
|
+
node_id: number;
|
|
33
|
+
endpoint: number;
|
|
34
|
+
address: number;
|
|
35
|
+
sn: string;
|
|
36
|
+
mac: string;
|
|
37
|
+
com: string;
|
|
38
|
+
node_type: string;
|
|
39
|
+
soft_ver: string;
|
|
40
|
+
hard_ver: string;
|
|
41
|
+
model: string;
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaiweiProductType = void 0;
|
|
4
|
+
/** 设备目录与设备基础信息定义。 */
|
|
5
|
+
var BaiweiProductType;
|
|
6
|
+
(function (BaiweiProductType) {
|
|
7
|
+
BaiweiProductType["ON_OFF_LIGHT"] = "On/Off Light";
|
|
8
|
+
BaiweiProductType["ON_OFF_SWITCH"] = "On/Off Switch";
|
|
9
|
+
BaiweiProductType["WINDOW_COVER"] = "Window Covering Device";
|
|
10
|
+
BaiweiProductType["IAS_ZONE"] = "IAS Zone";
|
|
11
|
+
BaiweiProductType["AIR_BOX"] = "Air Box";
|
|
12
|
+
BaiweiProductType["SCENE_SELECTOR"] = "Scene Selector";
|
|
13
|
+
BaiweiProductType["AC_GATEWAY"] = "AC gateway";
|
|
14
|
+
BaiweiProductType["NEW_WIND"] = "New wind controller";
|
|
15
|
+
BaiweiProductType["FLOOR_HEAT"] = "Floor heat controller";
|
|
16
|
+
})(BaiweiProductType || (exports.BaiweiProductType = BaiweiProductType = {}));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** 设备状态与各类具体状态载荷定义。 */
|
|
2
|
+
export interface DeviceStateList<T> {
|
|
3
|
+
device_list: BaiweiDeviceState<T>[];
|
|
4
|
+
}
|
|
5
|
+
export interface BaiweiDeviceState<T = any> {
|
|
6
|
+
device_id: string;
|
|
7
|
+
device_status: T;
|
|
8
|
+
}
|
|
9
|
+
export interface BaiweiDeviceStateReport<T = any> {
|
|
10
|
+
device: BaiweiDeviceState<T>;
|
|
11
|
+
}
|
|
12
|
+
export interface ACStatus {
|
|
13
|
+
state: string;
|
|
14
|
+
acoutside_id: number;
|
|
15
|
+
acgateway_id: number;
|
|
16
|
+
sys_mode: string;
|
|
17
|
+
coolpoint: number;
|
|
18
|
+
heatpoint: number;
|
|
19
|
+
wind_level: string;
|
|
20
|
+
curr_temp: number;
|
|
21
|
+
err_code: number;
|
|
22
|
+
}
|
|
23
|
+
export interface FloorHeatStatus {
|
|
24
|
+
state: string;
|
|
25
|
+
sys_mode: string;
|
|
26
|
+
work_mode: string;
|
|
27
|
+
lock_mode: string;
|
|
28
|
+
temp: number;
|
|
29
|
+
coolpoint: number;
|
|
30
|
+
heatpoint: number;
|
|
31
|
+
}
|
|
32
|
+
export interface IASZoneStatus {
|
|
33
|
+
state: string;
|
|
34
|
+
status: string;
|
|
35
|
+
}
|
|
36
|
+
export interface NewWindStatus {
|
|
37
|
+
state: string;
|
|
38
|
+
sys_mode: string;
|
|
39
|
+
work_mode: string;
|
|
40
|
+
lock_mode: string;
|
|
41
|
+
temp: number;
|
|
42
|
+
coolpoint: number;
|
|
43
|
+
heatpoint: number;
|
|
44
|
+
}
|
|
45
|
+
export interface LightState {
|
|
46
|
+
state: string;
|
|
47
|
+
}
|
|
48
|
+
export interface SceneStatus {
|
|
49
|
+
sceneId: number;
|
|
50
|
+
status: number;
|
|
51
|
+
state: string;
|
|
52
|
+
}
|
|
53
|
+
export interface WindowCoverStatus {
|
|
54
|
+
state: string;
|
|
55
|
+
level: number;
|
|
56
|
+
}
|