edilkamin 1.10.2 → 1.12.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.
Files changed (52) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/bluetooth-protocol.d.ts +178 -0
  3. package/dist/cjs/src/bluetooth-protocol.js +423 -0
  4. package/dist/cjs/src/bluetooth-protocol.test.d.ts +1 -0
  5. package/dist/cjs/src/bluetooth-protocol.test.js +389 -0
  6. package/dist/cjs/src/bluetooth-utils.js +2 -6
  7. package/dist/cjs/src/bluetooth.d.ts +2 -0
  8. package/dist/cjs/src/bluetooth.js +17 -1
  9. package/dist/cjs/src/cli.js +9 -8
  10. package/dist/cjs/src/index.d.ts +4 -3
  11. package/dist/cjs/src/index.js +14 -1
  12. package/dist/cjs/src/library.d.ts +30 -0
  13. package/dist/cjs/src/library.js +97 -3
  14. package/dist/cjs/src/library.test.js +225 -4
  15. package/dist/cjs/src/mac-utils.d.ts +15 -0
  16. package/dist/cjs/src/mac-utils.js +24 -0
  17. package/dist/cjs/src/mac-utils.test.d.ts +1 -0
  18. package/dist/cjs/src/mac-utils.test.js +41 -0
  19. package/dist/cjs/src/types.d.ts +94 -2
  20. package/dist/cjs/src/types.js +95 -1
  21. package/dist/esm/package.json +1 -1
  22. package/dist/esm/src/bluetooth-protocol.d.ts +178 -0
  23. package/dist/esm/src/bluetooth-protocol.js +415 -0
  24. package/dist/esm/src/bluetooth-protocol.test.d.ts +1 -0
  25. package/dist/esm/src/bluetooth-protocol.test.js +387 -0
  26. package/dist/esm/src/bluetooth-utils.js +2 -6
  27. package/dist/esm/src/bluetooth.d.ts +2 -0
  28. package/dist/esm/src/bluetooth.js +8 -0
  29. package/dist/esm/src/cli.js +9 -8
  30. package/dist/esm/src/index.d.ts +4 -3
  31. package/dist/esm/src/index.js +3 -2
  32. package/dist/esm/src/library.d.ts +30 -0
  33. package/dist/esm/src/library.js +94 -2
  34. package/dist/esm/src/library.test.js +226 -5
  35. package/dist/esm/src/mac-utils.d.ts +15 -0
  36. package/dist/esm/src/mac-utils.js +21 -0
  37. package/dist/esm/src/mac-utils.test.d.ts +1 -0
  38. package/dist/esm/src/mac-utils.test.js +39 -0
  39. package/dist/esm/src/types.d.ts +94 -2
  40. package/dist/esm/src/types.js +89 -1
  41. package/package.json +1 -1
  42. package/src/bluetooth-protocol.test.ts +497 -0
  43. package/src/bluetooth-protocol.ts +524 -0
  44. package/src/bluetooth-utils.ts +3 -7
  45. package/src/bluetooth.ts +21 -0
  46. package/src/cli.ts +9 -8
  47. package/src/index.ts +24 -2
  48. package/src/library.test.ts +325 -4
  49. package/src/library.ts +109 -2
  50. package/src/mac-utils.test.ts +60 -0
  51. package/src/mac-utils.ts +22 -0
  52. package/src/types.ts +144 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.10.2",
3
+ "version": "1.12.0",
4
4
  "description": "",
5
5
  "main": "dist/cjs/src/index.js",
6
6
  "module": "dist/esm/src/index.js",
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Edilkamin BLE Protocol Implementation
3
+ *
4
+ * Transport-agnostic protocol layer for communicating with Edilkamin stoves via BLE.
5
+ * Handles AES-128-CBC encryption, CRC16-Modbus checksums, and Modbus packet building/parsing.
6
+ *
7
+ * The consuming application is responsible for:
8
+ * - BLE device scanning and connection
9
+ * - Writing to BLE characteristics
10
+ * - Subscribing to BLE notifications
11
+ *
12
+ * Protocol details derived from: https://github.com/netmb/Edilkamin_BT
13
+ */
14
+ /** Edilkamin GATT service UUID */
15
+ export declare const SERVICE_UUID = "0000abf0-0000-1000-8000-00805f9b34fb";
16
+ /** Write characteristic UUID (WRITE NO RESPONSE) */
17
+ export declare const WRITE_CHARACTERISTIC_UUID = "0000abf1-0000-1000-8000-00805f9b34fb";
18
+ /** Notify characteristic UUID (NOTIFY) */
19
+ export declare const NOTIFY_CHARACTERISTIC_UUID = "0000abf2-0000-1000-8000-00805f9b34fb";
20
+ /**
21
+ * Parsed Modbus response from the device.
22
+ */
23
+ export interface ModbusResponse {
24
+ /** Slave address (always 0x01 for Edilkamin) */
25
+ slaveAddress: number;
26
+ /** Function code (0x03 for read, 0x06 for write) */
27
+ functionCode: number;
28
+ /** Byte count (for read responses) */
29
+ byteCount?: number;
30
+ /** Response data */
31
+ data: Uint8Array;
32
+ /** Whether the response indicates an error */
33
+ isError: boolean;
34
+ }
35
+ /**
36
+ * Calculate CRC16-Modbus checksum.
37
+ *
38
+ * @param data - Data to calculate CRC for
39
+ * @returns 2-byte CRC in little-endian order [crcLo, crcHi]
40
+ */
41
+ export declare const crc16Modbus: (data: Uint8Array) => Uint8Array;
42
+ /**
43
+ * Encrypt data using AES-128-CBC (raw, without PKCS7 padding).
44
+ *
45
+ * This manually applies PKCS7 padding to make input a multiple of 16 bytes,
46
+ * encrypts with Web Crypto, then strips the extra padding block from output.
47
+ *
48
+ * @param plaintext - Data to encrypt (must be 32 bytes)
49
+ * @returns Encrypted data (32 bytes)
50
+ */
51
+ export declare const aesEncrypt: (plaintext: Uint8Array) => Promise<Uint8Array>;
52
+ /**
53
+ * Decrypt data using AES-128-CBC (raw, handling PKCS7 padding).
54
+ *
55
+ * @param ciphertext - Data to decrypt (must be 32 bytes)
56
+ * @returns Decrypted data (32 bytes)
57
+ */
58
+ export declare const aesDecrypt: (ciphertext: Uint8Array) => Promise<Uint8Array>;
59
+ /**
60
+ * Build and encrypt a command packet to send to the device.
61
+ *
62
+ * Packet structure (32 bytes before encryption):
63
+ * - Bytes 0-3: Unix timestamp (big-endian)
64
+ * - Bytes 4-19: Fixed key
65
+ * - Bytes 20-25: Modbus command (6 bytes)
66
+ * - Bytes 26-27: CRC16-Modbus of command
67
+ * - Bytes 28-31: Padding [0x04, 0x04, 0x04, 0x04]
68
+ *
69
+ * @param modbusCommand - 6-byte Modbus RTU command
70
+ * @returns 32-byte encrypted packet ready to send via BLE
71
+ */
72
+ export declare const createPacket: (modbusCommand: Uint8Array) => Promise<Uint8Array>;
73
+ /**
74
+ * Decrypt and parse a response packet from the device.
75
+ *
76
+ * Response structure (32 bytes before decryption):
77
+ * - Bytes 0-3: Unix timestamp
78
+ * - Bytes 4-19: Fixed key
79
+ * - Bytes 20-26: Modbus response (7 bytes)
80
+ * - Bytes 27-28: CRC16-Modbus
81
+ * - Bytes 29-31: Padding [0x03, 0x03, 0x03]
82
+ *
83
+ * @param encrypted - 32-byte encrypted response from BLE notification
84
+ * @returns Parsed Modbus response
85
+ */
86
+ export declare const parseResponse: (encrypted: Uint8Array) => Promise<ModbusResponse>;
87
+ /**
88
+ * Pre-built Modbus read commands for querying device state.
89
+ * Each command is 6 bytes: [SlaveAddr, FuncCode, RegHi, RegLo, CountHi, CountLo]
90
+ */
91
+ export declare const readCommands: {
92
+ /** Power state (0=off, 1=on) */
93
+ power: Uint8Array<ArrayBuffer>;
94
+ /** Current ambient temperature (value / 10 = °C) */
95
+ temperature: Uint8Array<ArrayBuffer>;
96
+ /** Target temperature (value / 10 = °C) */
97
+ targetTemperature: Uint8Array<ArrayBuffer>;
98
+ /** Power level (1-5) */
99
+ powerLevel: Uint8Array<ArrayBuffer>;
100
+ /** Fan 1 speed (0=auto, 1-5=speed) */
101
+ fan1Speed: Uint8Array<ArrayBuffer>;
102
+ /** Fan 2 speed (0=auto, 1-5=speed) */
103
+ fan2Speed: Uint8Array<ArrayBuffer>;
104
+ /** Device state code */
105
+ state: Uint8Array<ArrayBuffer>;
106
+ /** Alarm status code */
107
+ alarm: Uint8Array<ArrayBuffer>;
108
+ /** Pellet warning status */
109
+ pelletAlarm: Uint8Array<ArrayBuffer>;
110
+ /** Auto mode (0=manual, 1=auto) */
111
+ autoMode: Uint8Array<ArrayBuffer>;
112
+ /** Standby mode status */
113
+ standby: Uint8Array<ArrayBuffer>;
114
+ };
115
+ /**
116
+ * Builder functions for Modbus write commands.
117
+ * Each function returns a 6-byte command: [SlaveAddr, FuncCode, RegHi, RegLo, ValHi, ValLo]
118
+ */
119
+ export declare const writeCommands: {
120
+ /**
121
+ * Turn power on or off.
122
+ * @param on - true to turn on, false to turn off
123
+ */
124
+ setPower: (on: boolean) => Uint8Array;
125
+ /**
126
+ * Set target temperature.
127
+ * @param tempCelsius - Temperature in Celsius (e.g., 21.5)
128
+ */
129
+ setTemperature: (tempCelsius: number) => Uint8Array;
130
+ /**
131
+ * Set power level.
132
+ * @param level - Power level (1-5)
133
+ */
134
+ setPowerLevel: (level: number) => Uint8Array;
135
+ /**
136
+ * Set fan 1 speed.
137
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
138
+ */
139
+ setFan1Speed: (speed: number) => Uint8Array;
140
+ /**
141
+ * Set fan 2 speed.
142
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
143
+ */
144
+ setFan2Speed: (speed: number) => Uint8Array;
145
+ /**
146
+ * Enable or disable auto mode.
147
+ * @param enabled - true to enable auto mode
148
+ */
149
+ setAutoMode: (enabled: boolean) => Uint8Array;
150
+ /**
151
+ * Enable or disable standby mode.
152
+ * @param enabled - true to enable standby mode
153
+ */
154
+ setStandby: (enabled: boolean) => Uint8Array;
155
+ };
156
+ /**
157
+ * Parser functions to extract meaningful values from Modbus responses.
158
+ */
159
+ export declare const parsers: {
160
+ /**
161
+ * Parse boolean response (power state, auto mode, etc.).
162
+ * @param response - Parsed Modbus response
163
+ * @returns true if value is 0x01, false if 0x00
164
+ */
165
+ boolean: (response: ModbusResponse) => boolean;
166
+ /**
167
+ * Parse temperature response.
168
+ * @param response - Parsed Modbus response
169
+ * @returns Temperature in Celsius
170
+ */
171
+ temperature: (response: ModbusResponse) => number;
172
+ /**
173
+ * Parse numeric value (power level, fan speed, state code, etc.).
174
+ * @param response - Parsed Modbus response
175
+ * @returns Numeric value
176
+ */
177
+ number: (response: ModbusResponse) => number;
178
+ };
@@ -0,0 +1,423 @@
1
+ "use strict";
2
+ /**
3
+ * Edilkamin BLE Protocol Implementation
4
+ *
5
+ * Transport-agnostic protocol layer for communicating with Edilkamin stoves via BLE.
6
+ * Handles AES-128-CBC encryption, CRC16-Modbus checksums, and Modbus packet building/parsing.
7
+ *
8
+ * The consuming application is responsible for:
9
+ * - BLE device scanning and connection
10
+ * - Writing to BLE characteristics
11
+ * - Subscribing to BLE notifications
12
+ *
13
+ * Protocol details derived from: https://github.com/netmb/Edilkamin_BT
14
+ */
15
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
16
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
17
+ return new (P || (P = Promise))(function (resolve, reject) {
18
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
19
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
20
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
21
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
22
+ });
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.parsers = exports.writeCommands = exports.readCommands = exports.parseResponse = exports.createPacket = exports.aesDecrypt = exports.aesEncrypt = exports.crc16Modbus = exports.NOTIFY_CHARACTERISTIC_UUID = exports.WRITE_CHARACTERISTIC_UUID = exports.SERVICE_UUID = void 0;
26
+ // =============================================================================
27
+ // BLE Characteristic UUIDs (for consuming apps)
28
+ // =============================================================================
29
+ /** Edilkamin GATT service UUID */
30
+ exports.SERVICE_UUID = "0000abf0-0000-1000-8000-00805f9b34fb";
31
+ /** Write characteristic UUID (WRITE NO RESPONSE) */
32
+ exports.WRITE_CHARACTERISTIC_UUID = "0000abf1-0000-1000-8000-00805f9b34fb";
33
+ /** Notify characteristic UUID (NOTIFY) */
34
+ exports.NOTIFY_CHARACTERISTIC_UUID = "0000abf2-0000-1000-8000-00805f9b34fb";
35
+ // =============================================================================
36
+ // Constants (private)
37
+ // =============================================================================
38
+ /** AES-128-CBC key (16 bytes) */
39
+ const AES_KEY = new Uint8Array([
40
+ 0x80, 0x29, 0x47, 0x46, 0xdb, 0x35, 0x4d, 0xb7, 0x4c, 0x37, 0x01, 0xcf, 0x30,
41
+ 0xef, 0xdd, 0x65,
42
+ ]);
43
+ /** AES-128-CBC initialization vector (16 bytes) */
44
+ const AES_IV = new Uint8Array([
45
+ 0xda, 0x1a, 0x55, 0x73, 0x49, 0xf2, 0x5c, 0x64, 0x1b, 0x1a, 0x21, 0xd2, 0x6f,
46
+ 0x5b, 0x21, 0x8a,
47
+ ]);
48
+ /** Fixed key embedded in every packet (16 bytes) */
49
+ const FIXED_KEY = new Uint8Array([
50
+ 0x31, 0xdd, 0x34, 0x51, 0x26, 0x39, 0x20, 0x23, 0x9f, 0x4b, 0x68, 0x20, 0xe7,
51
+ 0x25, 0xfc, 0x75,
52
+ ]);
53
+ /** CRC16-Modbus high byte lookup table */
54
+ const CRC_HI_TABLE = new Uint8Array([
55
+ 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00,
56
+ 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1,
57
+ 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81,
58
+ 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
59
+ 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01,
60
+ 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0,
61
+ 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80,
62
+ 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
63
+ 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00,
64
+ 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0,
65
+ 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80,
66
+ 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41,
67
+ 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01,
68
+ 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1,
69
+ 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81,
70
+ 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
71
+ 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01,
72
+ 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1,
73
+ 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80,
74
+ 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
75
+ ]);
76
+ /** CRC16-Modbus low byte lookup table */
77
+ const CRC_LO_TABLE = new Uint8Array([
78
+ 0x00, 0xc0, 0xc1, 0x01, 0xc3, 0x03, 0x02, 0xc2, 0xc6, 0x06, 0x07, 0xc7, 0x05,
79
+ 0xc5, 0xc4, 0x04, 0xcc, 0x0c, 0x0d, 0xcd, 0x0f, 0xcf, 0xce, 0x0e, 0x0a, 0xca,
80
+ 0xcb, 0x0b, 0xc9, 0x09, 0x08, 0xc8, 0xd8, 0x18, 0x19, 0xd9, 0x1b, 0xdb, 0xda,
81
+ 0x1a, 0x1e, 0xde, 0xdf, 0x1f, 0xdd, 0x1d, 0x1c, 0xdc, 0x14, 0xd4, 0xd5, 0x15,
82
+ 0xd7, 0x17, 0x16, 0xd6, 0xd2, 0x12, 0x13, 0xd3, 0x11, 0xd1, 0xd0, 0x10, 0xf0,
83
+ 0x30, 0x31, 0xf1, 0x33, 0xf3, 0xf2, 0x32, 0x36, 0xf6, 0xf7, 0x37, 0xf5, 0x35,
84
+ 0x34, 0xf4, 0x3c, 0xfc, 0xfd, 0x3d, 0xff, 0x3f, 0x3e, 0xfe, 0xfa, 0x3a, 0x3b,
85
+ 0xfb, 0x39, 0xf9, 0xf8, 0x38, 0x28, 0xe8, 0xe9, 0x29, 0xeb, 0x2b, 0x2a, 0xea,
86
+ 0xee, 0x2e, 0x2f, 0xef, 0x2d, 0xed, 0xec, 0x2c, 0xe4, 0x24, 0x25, 0xe5, 0x27,
87
+ 0xe7, 0xe6, 0x26, 0x22, 0xe2, 0xe3, 0x23, 0xe1, 0x21, 0x20, 0xe0, 0xa0, 0x60,
88
+ 0x61, 0xa1, 0x63, 0xa3, 0xa2, 0x62, 0x66, 0xa6, 0xa7, 0x67, 0xa5, 0x65, 0x64,
89
+ 0xa4, 0x6c, 0xac, 0xad, 0x6d, 0xaf, 0x6f, 0x6e, 0xae, 0xaa, 0x6a, 0x6b, 0xab,
90
+ 0x69, 0xa9, 0xa8, 0x68, 0x78, 0xb8, 0xb9, 0x79, 0xbb, 0x7b, 0x7a, 0xba, 0xbe,
91
+ 0x7e, 0x7f, 0xbf, 0x7d, 0xbd, 0xbc, 0x7c, 0xb4, 0x74, 0x75, 0xb5, 0x77, 0xb7,
92
+ 0xb6, 0x76, 0x72, 0xb2, 0xb3, 0x73, 0xb1, 0x71, 0x70, 0xb0, 0x50, 0x90, 0x91,
93
+ 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54,
94
+ 0x9c, 0x5c, 0x5d, 0x9d, 0x5f, 0x9f, 0x9e, 0x5e, 0x5a, 0x9a, 0x9b, 0x5b, 0x99,
95
+ 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4b, 0x8b, 0x8a, 0x4a, 0x4e, 0x8e,
96
+ 0x8f, 0x4f, 0x8d, 0x4d, 0x4c, 0x8c, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46,
97
+ 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40,
98
+ ]);
99
+ // =============================================================================
100
+ // CRC16-Modbus
101
+ // =============================================================================
102
+ /**
103
+ * Calculate CRC16-Modbus checksum.
104
+ *
105
+ * @param data - Data to calculate CRC for
106
+ * @returns 2-byte CRC in little-endian order [crcLo, crcHi]
107
+ */
108
+ const crc16Modbus = (data) => {
109
+ let crcHi = 0xff;
110
+ let crcLo = 0xff;
111
+ for (let i = 0; i < data.length; i++) {
112
+ const index = crcLo ^ data[i];
113
+ crcLo = crcHi ^ CRC_HI_TABLE[index];
114
+ crcHi = CRC_LO_TABLE[index];
115
+ }
116
+ // Return [crcLo, crcHi] - note the order!
117
+ return new Uint8Array([crcLo, crcHi]);
118
+ };
119
+ exports.crc16Modbus = crc16Modbus;
120
+ // =============================================================================
121
+ // AES-128-CBC Encryption
122
+ // =============================================================================
123
+ /**
124
+ * Import AES key for encryption/decryption.
125
+ */
126
+ const importAesKey = (usage) => __awaiter(void 0, void 0, void 0, function* () {
127
+ return crypto.subtle.importKey("raw", AES_KEY.buffer, { name: "AES-CBC" }, false, [usage]);
128
+ });
129
+ /**
130
+ * Encrypt data using AES-128-CBC (raw, without PKCS7 padding).
131
+ *
132
+ * This manually applies PKCS7 padding to make input a multiple of 16 bytes,
133
+ * encrypts with Web Crypto, then strips the extra padding block from output.
134
+ *
135
+ * @param plaintext - Data to encrypt (must be 32 bytes)
136
+ * @returns Encrypted data (32 bytes)
137
+ */
138
+ const aesEncrypt = (plaintext) => __awaiter(void 0, void 0, void 0, function* () {
139
+ const key = yield importAesKey("encrypt");
140
+ // Clone IV since AES-CBC modifies it during operation
141
+ const iv = new Uint8Array(AES_IV);
142
+ // Add PKCS7 padding (16 bytes of 0x10 for 32-byte input)
143
+ const padded = new Uint8Array(48);
144
+ padded.set(plaintext, 0);
145
+ padded.fill(0x10, 32);
146
+ const encrypted = yield crypto.subtle.encrypt({ name: "AES-CBC", iv: iv.buffer }, key, padded.buffer);
147
+ // Return first 32 bytes (skip the padding block)
148
+ return new Uint8Array(encrypted).slice(0, 32);
149
+ });
150
+ exports.aesEncrypt = aesEncrypt;
151
+ /**
152
+ * Decrypt data using AES-128-CBC (raw, handling PKCS7 padding).
153
+ *
154
+ * @param ciphertext - Data to decrypt (must be 32 bytes)
155
+ * @returns Decrypted data (32 bytes)
156
+ */
157
+ const aesDecrypt = (ciphertext) => __awaiter(void 0, void 0, void 0, function* () {
158
+ const key = yield importAesKey("decrypt");
159
+ // Clone IV since AES-CBC modifies it during operation
160
+ const iv = new Uint8Array(AES_IV);
161
+ // To decrypt without padding validation, we need to:
162
+ // 1. Decrypt to get raw blocks
163
+ // 2. Handle padding ourselves
164
+ //
165
+ // The trick is to append a valid padding block that we encrypt separately,
166
+ // then decrypt the whole thing.
167
+ // First, encrypt a padding block using the last ciphertext block as IV
168
+ // This creates the correct padding block for decryption
169
+ const lastBlock = ciphertext.slice(16, 32);
170
+ const paddingPlain = new Uint8Array(16).fill(0x10); // Valid PKCS7 for 0 extra bytes
171
+ const paddingKey = yield importAesKey("encrypt");
172
+ const encryptedPadding = yield crypto.subtle.encrypt({ name: "AES-CBC", iv: lastBlock.buffer }, paddingKey, paddingPlain.buffer);
173
+ // Build full ciphertext with valid padding block
174
+ const fullCiphertext = new Uint8Array(48);
175
+ fullCiphertext.set(ciphertext, 0);
176
+ fullCiphertext.set(new Uint8Array(encryptedPadding).slice(0, 16), 32);
177
+ const decrypted = yield crypto.subtle.decrypt({ name: "AES-CBC", iv: iv.buffer }, key, fullCiphertext.buffer);
178
+ // Return first 32 bytes (the actual data)
179
+ return new Uint8Array(decrypted).slice(0, 32);
180
+ });
181
+ exports.aesDecrypt = aesDecrypt;
182
+ // =============================================================================
183
+ // Packet Building
184
+ // =============================================================================
185
+ /**
186
+ * Get current Unix timestamp as 4 big-endian bytes.
187
+ */
188
+ const getTimestamp = () => {
189
+ const now = Math.floor(Date.now() / 1000);
190
+ return new Uint8Array([
191
+ (now >> 24) & 0xff,
192
+ (now >> 16) & 0xff,
193
+ (now >> 8) & 0xff,
194
+ now & 0xff,
195
+ ]);
196
+ };
197
+ /**
198
+ * Build and encrypt a command packet to send to the device.
199
+ *
200
+ * Packet structure (32 bytes before encryption):
201
+ * - Bytes 0-3: Unix timestamp (big-endian)
202
+ * - Bytes 4-19: Fixed key
203
+ * - Bytes 20-25: Modbus command (6 bytes)
204
+ * - Bytes 26-27: CRC16-Modbus of command
205
+ * - Bytes 28-31: Padding [0x04, 0x04, 0x04, 0x04]
206
+ *
207
+ * @param modbusCommand - 6-byte Modbus RTU command
208
+ * @returns 32-byte encrypted packet ready to send via BLE
209
+ */
210
+ const createPacket = (modbusCommand) => __awaiter(void 0, void 0, void 0, function* () {
211
+ if (modbusCommand.length !== 6) {
212
+ throw new Error("Modbus command must be exactly 6 bytes");
213
+ }
214
+ // Build 32-byte plaintext packet
215
+ const packet = new Uint8Array(32);
216
+ // Timestamp (4 bytes, big-endian)
217
+ packet.set(getTimestamp(), 0);
218
+ // Fixed key (16 bytes)
219
+ packet.set(FIXED_KEY, 4);
220
+ // Modbus payload (6 bytes)
221
+ packet.set(modbusCommand, 20);
222
+ // CRC16-Modbus (2 bytes)
223
+ const crc = (0, exports.crc16Modbus)(modbusCommand);
224
+ packet.set(crc, 26);
225
+ // Padding (4 bytes)
226
+ packet.set([0x04, 0x04, 0x04, 0x04], 28);
227
+ // Encrypt with AES-128-CBC
228
+ return (0, exports.aesEncrypt)(packet);
229
+ });
230
+ exports.createPacket = createPacket;
231
+ // =============================================================================
232
+ // Response Parsing
233
+ // =============================================================================
234
+ /**
235
+ * Decrypt and parse a response packet from the device.
236
+ *
237
+ * Response structure (32 bytes before decryption):
238
+ * - Bytes 0-3: Unix timestamp
239
+ * - Bytes 4-19: Fixed key
240
+ * - Bytes 20-26: Modbus response (7 bytes)
241
+ * - Bytes 27-28: CRC16-Modbus
242
+ * - Bytes 29-31: Padding [0x03, 0x03, 0x03]
243
+ *
244
+ * @param encrypted - 32-byte encrypted response from BLE notification
245
+ * @returns Parsed Modbus response
246
+ */
247
+ const parseResponse = (encrypted) => __awaiter(void 0, void 0, void 0, function* () {
248
+ if (encrypted.length !== 32) {
249
+ throw new Error("Response must be exactly 32 bytes");
250
+ }
251
+ // Decrypt
252
+ const decrypted = yield (0, exports.aesDecrypt)(encrypted);
253
+ // Extract Modbus response (bytes 20-26, 7 bytes)
254
+ const modbusResponse = decrypted.slice(20, 27);
255
+ const slaveAddress = modbusResponse[0];
256
+ const functionCode = modbusResponse[1];
257
+ // Check for Modbus error (function code has high bit set)
258
+ const isError = (functionCode & 0x80) !== 0;
259
+ if (isError) {
260
+ return {
261
+ slaveAddress,
262
+ functionCode: functionCode & 0x7f,
263
+ data: new Uint8Array([modbusResponse[2]]), // Error code
264
+ isError: true,
265
+ };
266
+ }
267
+ // Read response: [slaveAddr, funcCode, byteCount, dataHi, dataLo, crcLo, crcHi]
268
+ if (functionCode === 0x03) {
269
+ const byteCount = modbusResponse[2];
270
+ return {
271
+ slaveAddress,
272
+ functionCode,
273
+ byteCount,
274
+ data: modbusResponse.slice(3, 3 + byteCount),
275
+ isError: false,
276
+ };
277
+ }
278
+ // Write response: [slaveAddr, funcCode, regHi, regLo, valHi, valLo, crcLo, crcHi]
279
+ // (echo of command - return register and value bytes)
280
+ return {
281
+ slaveAddress,
282
+ functionCode,
283
+ data: modbusResponse.slice(2, 6),
284
+ isError: false,
285
+ };
286
+ });
287
+ exports.parseResponse = parseResponse;
288
+ // =============================================================================
289
+ // Modbus Read Commands (Function Code 0x03)
290
+ // =============================================================================
291
+ /**
292
+ * Pre-built Modbus read commands for querying device state.
293
+ * Each command is 6 bytes: [SlaveAddr, FuncCode, RegHi, RegLo, CountHi, CountLo]
294
+ */
295
+ exports.readCommands = {
296
+ /** Power state (0=off, 1=on) */
297
+ power: new Uint8Array([0x01, 0x03, 0x05, 0x29, 0x00, 0x01]),
298
+ /** Current ambient temperature (value / 10 = °C) */
299
+ temperature: new Uint8Array([0x01, 0x03, 0x05, 0x25, 0x00, 0x01]),
300
+ /** Target temperature (value / 10 = °C) */
301
+ targetTemperature: new Uint8Array([0x01, 0x03, 0x05, 0x37, 0x00, 0x01]),
302
+ /** Power level (1-5) */
303
+ powerLevel: new Uint8Array([0x01, 0x03, 0x04, 0x40, 0x00, 0x01]),
304
+ /** Fan 1 speed (0=auto, 1-5=speed) */
305
+ fan1Speed: new Uint8Array([0x01, 0x03, 0x05, 0x4b, 0x00, 0x01]),
306
+ /** Fan 2 speed (0=auto, 1-5=speed) */
307
+ fan2Speed: new Uint8Array([0x01, 0x03, 0x05, 0x4d, 0x00, 0x01]),
308
+ /** Device state code */
309
+ state: new Uint8Array([0x01, 0x03, 0x05, 0x3b, 0x00, 0x01]),
310
+ /** Alarm status code */
311
+ alarm: new Uint8Array([0x01, 0x03, 0x04, 0xc7, 0x00, 0x01]),
312
+ /** Pellet warning status */
313
+ pelletAlarm: new Uint8Array([0x01, 0x03, 0x04, 0xd5, 0x00, 0x01]),
314
+ /** Auto mode (0=manual, 1=auto) */
315
+ autoMode: new Uint8Array([0x01, 0x03, 0x04, 0x43, 0x00, 0x01]),
316
+ /** Standby mode status */
317
+ standby: new Uint8Array([0x01, 0x03, 0x04, 0x44, 0x00, 0x01]),
318
+ };
319
+ // =============================================================================
320
+ // Modbus Write Commands (Function Code 0x06)
321
+ // =============================================================================
322
+ /**
323
+ * Builder functions for Modbus write commands.
324
+ * Each function returns a 6-byte command: [SlaveAddr, FuncCode, RegHi, RegLo, ValHi, ValLo]
325
+ */
326
+ exports.writeCommands = {
327
+ /**
328
+ * Turn power on or off.
329
+ * @param on - true to turn on, false to turn off
330
+ */
331
+ setPower: (on) => new Uint8Array([0x01, 0x06, 0x03, 0x1c, 0x00, on ? 0x01 : 0x00]),
332
+ /**
333
+ * Set target temperature.
334
+ * @param tempCelsius - Temperature in Celsius (e.g., 21.5)
335
+ */
336
+ setTemperature: (tempCelsius) => {
337
+ const value = Math.round(tempCelsius * 10);
338
+ return new Uint8Array([
339
+ 0x01,
340
+ 0x06,
341
+ 0x05,
342
+ 0x25,
343
+ (value >> 8) & 0xff,
344
+ value & 0xff,
345
+ ]);
346
+ },
347
+ /**
348
+ * Set power level.
349
+ * @param level - Power level (1-5)
350
+ */
351
+ setPowerLevel: (level) => {
352
+ if (level < 1 || level > 5)
353
+ throw new Error("Power level must be 1-5");
354
+ return new Uint8Array([0x01, 0x06, 0x04, 0x40, 0x00, level]);
355
+ },
356
+ /**
357
+ * Set fan 1 speed.
358
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
359
+ */
360
+ setFan1Speed: (speed) => {
361
+ if (speed < 0 || speed > 5)
362
+ throw new Error("Fan speed must be 0-5");
363
+ return new Uint8Array([0x01, 0x06, 0x05, 0x4b, 0x00, speed]);
364
+ },
365
+ /**
366
+ * Set fan 2 speed.
367
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
368
+ */
369
+ setFan2Speed: (speed) => {
370
+ if (speed < 0 || speed > 5)
371
+ throw new Error("Fan speed must be 0-5");
372
+ return new Uint8Array([0x01, 0x06, 0x05, 0x4d, 0x00, speed]);
373
+ },
374
+ /**
375
+ * Enable or disable auto mode.
376
+ * @param enabled - true to enable auto mode
377
+ */
378
+ setAutoMode: (enabled) => new Uint8Array([0x01, 0x06, 0x04, 0x43, 0x00, enabled ? 0x01 : 0x00]),
379
+ /**
380
+ * Enable or disable standby mode.
381
+ * @param enabled - true to enable standby mode
382
+ */
383
+ setStandby: (enabled) => new Uint8Array([0x01, 0x06, 0x04, 0x44, 0x00, enabled ? 0x01 : 0x00]),
384
+ };
385
+ // =============================================================================
386
+ // Response Parsers
387
+ // =============================================================================
388
+ /**
389
+ * Parser functions to extract meaningful values from Modbus responses.
390
+ */
391
+ exports.parsers = {
392
+ /**
393
+ * Parse boolean response (power state, auto mode, etc.).
394
+ * @param response - Parsed Modbus response
395
+ * @returns true if value is 0x01, false if 0x00
396
+ */
397
+ boolean: (response) => {
398
+ if (response.isError)
399
+ throw new Error(`Modbus error: ${response.data[0]}`);
400
+ return response.data[1] === 0x01;
401
+ },
402
+ /**
403
+ * Parse temperature response.
404
+ * @param response - Parsed Modbus response
405
+ * @returns Temperature in Celsius
406
+ */
407
+ temperature: (response) => {
408
+ if (response.isError)
409
+ throw new Error(`Modbus error: ${response.data[0]}`);
410
+ const value = (response.data[0] << 8) | response.data[1];
411
+ return value / 10;
412
+ },
413
+ /**
414
+ * Parse numeric value (power level, fan speed, state code, etc.).
415
+ * @param response - Parsed Modbus response
416
+ * @returns Numeric value
417
+ */
418
+ number: (response) => {
419
+ if (response.isError)
420
+ throw new Error(`Modbus error: ${response.data[0]}`);
421
+ return (response.data[0] << 8) | response.data[1];
422
+ },
423
+ };
@@ -0,0 +1 @@
1
+ export {};