Skip to content

Commit 0f4b216

Browse files
feat(vs330): support encoder
1 parent 1306052 commit 0f4b216

File tree

3 files changed

+499
-14
lines changed

3 files changed

+499
-14
lines changed

VS_Series/VS330/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
The payload decoder function is applicable to VS330.
44

5-
For more detailed information, please visit [milesight official website](https://www.milesight-iot.com).
5+
For more detailed information, please visit [Milesight Official Website](https://www.milesight.com/iot/product/lorawan-sensor/vs330).
66

77
![VS330](VS330.png)
88

99
## Payload Definition
1010

11-
| CHANNEL | ID | TYPE | LENGTH | DESCRIPTION |
12-
| :---------: | :--: | :--: | :----: | ---------------------------------------------------------------- |
13-
| Battery | 0x01 | 0x75 | 1 | battery(1B)<br/>battery, unit: % |
14-
| Distance | 0x02 | 0x82 | 2 | distance(2B)<br/>distance, unit: mm |
15-
| Occupancy | 0x03 | 0x8E | 1 | occupancy(1B)<br/>occupancy, values: (0: vacant, 1: occupied) |
16-
| Calibration | 0x04 | 0x8E | 1 | calibration(1B)<br/>calibration, values: (0: failed, 1: success) |
11+
| CHANNEL | ID | TYPE | LENGTH | DESCRIPTION |
12+
| :---------: | :--: | :--: | :----: | ------------------------------------------------------------------------------ |
13+
| Battery | 0x01 | 0x75 | 1 | battery(1B)<br/>battery, unit: % |
14+
| Distance | 0x02 | 0x82 | 2 | distance(2B)<br/>distance, unit: mm |
15+
| Occupancy | 0x03 | 0x8E | 1 | occupancy(1B)<br/>occupancy, values: (0: vacant, 1: occupied) |
16+
| Calibration | 0x04 | 0x8E | 1 | calibration_status(1B)<br/>calibration_status, values: (0: failed, 1: success) |
1717

1818
## Example
1919

@@ -24,6 +24,6 @@ For more detailed information, please visit [milesight official website](https:/
2424
"battery": 98,
2525
"distance": 15,
2626
"occupancy": "occupied",
27-
"calibration": "success"
27+
"calibration_status": "success"
2828
}
2929
```

VS_Series/VS330/VS330_Decoder.js

Lines changed: 226 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
/**
22
* Payload Decoder
33
*
4-
* Copyright 2024 Milesight IoT
4+
* Copyright 2025 Milesight IoT
55
*
66
* @product VS330
77
*/
8+
var RAW_VALUE = 0x00;
9+
810
// Chirpstack v4
911
function decodeUplink(input) {
1012
var decoded = milesightDeviceDecode(input.bytes);
@@ -24,12 +26,53 @@ function Decoder(bytes, port) {
2426
function milesightDeviceDecode(bytes) {
2527
var decoded = {};
2628

27-
for (var i = 0; i < bytes.length; ) {
29+
for (var i = 0; i < bytes.length;) {
2830
var channel_id = bytes[i++];
2931
var channel_type = bytes[i++];
3032

33+
34+
// IPSO VERSION
35+
if (channel_id === 0xff && channel_type === 0x01) {
36+
decoded.ipso_version = readProtocolVersion(bytes[i]);
37+
i += 1;
38+
}
39+
// HARDWARE VERSION
40+
else if (channel_id === 0xff && channel_type === 0x09) {
41+
decoded.hardware_version = readHardwareVersion(bytes.slice(i, i + 2));
42+
i += 2;
43+
}
44+
// FIRMWARE VERSION
45+
else if (channel_id === 0xff && channel_type === 0x0a) {
46+
decoded.firmware_version = readFirmwareVersion(bytes.slice(i, i + 2));
47+
i += 2;
48+
}
49+
// TSL VERSION
50+
else if (channel_id === 0xff && channel_type === 0xff) {
51+
decoded.tsl_version = readTslVersion(bytes.slice(i, i + 2));
52+
i += 2;
53+
}
54+
// SERIAL NUMBER
55+
else if (channel_id === 0xff && channel_type === 0x16) {
56+
decoded.sn = readSerialNumber(bytes.slice(i, i + 8));
57+
i += 8;
58+
}
59+
// LORAWAN CLASS TYPE
60+
else if (channel_id === 0xff && channel_type === 0x0f) {
61+
decoded.lorawan_class = readLoRaWANClass(bytes[i]);
62+
i += 1;
63+
}
64+
// RESET EVENT
65+
else if (channel_id === 0xff && channel_type === 0xfe) {
66+
decoded.reset_event = readResetEvent(1);
67+
i += 1;
68+
}
69+
// DEVICE STATUS
70+
else if (channel_id === 0xff && channel_type === 0x0b) {
71+
decoded.device_status = readDeviceStatus(1);
72+
i += 1;
73+
}
3174
// BATTERY
32-
if (channel_id === 0x01 && channel_type === 0x75) {
75+
else if (channel_id === 0x01 && channel_type === 0x75) {
3376
decoded.battery = bytes[i];
3477
i += 1;
3578
}
@@ -40,13 +83,18 @@ function milesightDeviceDecode(bytes) {
4083
}
4184
// OCCUPANCY
4285
else if (channel_id === 0x03 && channel_type === 0x8e) {
43-
decoded.occupancy = bytes[i] === 0 ? "vacant" : "occupied";
86+
decoded.occupancy = readOccupancyStatus(bytes[i]);
4487
i += 1;
4588
}
4689
// CALIBRATION
4790
else if (channel_id === 0x04 && channel_type === 0x8e) {
48-
decoded.calibration = bytes[i] === 0 ? "failed" : "success";
91+
decoded.calibration_status = readCalibrationStatus(bytes[i]);
4992
i += 1;
93+
} // DOWNLINK RESPONSE
94+
else if (channel_id === 0xfe || channel_id === 0xff) {
95+
result = handle_downlink_response(channel_type, bytes, i);
96+
decoded = Object.assign(decoded, result.data);
97+
i = result.offset;
5098
} else {
5199
break;
52100
}
@@ -55,8 +103,180 @@ function milesightDeviceDecode(bytes) {
55103
return decoded;
56104
}
57105

58-
// bytes to number
106+
function handle_downlink_response(channel_type, bytes, offset) {
107+
var decoded = {};
108+
109+
switch (channel_type) {
110+
case 0x02:
111+
decoded.collection_interval = readUInt16LE(bytes.slice(offset, offset + 2));
112+
offset += 2;
113+
break;
114+
case 0x03:
115+
decoded.report_interval = readUInt16LE(bytes.slice(offset, offset + 2));
116+
offset += 2;
117+
break;
118+
case 0x10:
119+
decoded.reboot = readYesNoStatus(1);
120+
offset += 1;
121+
break;
122+
case 0x70:
123+
decoded.human_exist_height = readUInt16LE(bytes.slice(offset, offset + 2));
124+
offset += 2;
125+
break;
126+
case 0x71:
127+
decoded.test_enable = readEnableStatus(bytes[offset]);
128+
offset += 1;
129+
break;
130+
case 0x72:
131+
decoded.test_duration = readUInt16LE(bytes.slice(offset, offset + 2));
132+
offset += 2;
133+
break;
134+
case 0x7a:
135+
decoded.back_test_config = {};
136+
decoded.back_test_config.enable = readEnableStatus(bytes[offset]);
137+
decoded.back_test_config.distance = readUInt16LE(bytes.slice(offset + 1, offset + 3));
138+
offset += 3;
139+
break;
140+
default:
141+
throw new Error("unknown downlink response");
142+
}
143+
144+
return { data: decoded, offset: offset };
145+
}
146+
147+
function readProtocolVersion(bytes) {
148+
var major = (bytes & 0xf0) >> 4;
149+
var minor = bytes & 0x0f;
150+
return "v" + major + "." + minor;
151+
}
152+
153+
function readHardwareVersion(bytes) {
154+
var major = bytes[0] & 0xff;
155+
var minor = (bytes[1] & 0xff) >> 4;
156+
return "v" + major + "." + minor;
157+
}
158+
159+
function readFirmwareVersion(bytes) {
160+
var major = bytes[0] & 0xff;
161+
var minor = bytes[1] & 0xff;
162+
return "v" + major + "." + minor;
163+
}
164+
165+
function readTslVersion(bytes) {
166+
var major = bytes[0] & 0xff;
167+
var minor = bytes[1] & 0xff;
168+
return "v" + major + "." + minor;
169+
}
170+
171+
function readSerialNumber(bytes) {
172+
var temp = [];
173+
for (var idx = 0; idx < bytes.length; idx++) {
174+
temp.push(("0" + (bytes[idx] & 0xff).toString(16)).slice(-2));
175+
}
176+
return temp.join("");
177+
}
178+
179+
function readLoRaWANClass(type) {
180+
var class_map = {
181+
0: "Class A",
182+
1: "Class B",
183+
2: "Class C",
184+
3: "Class CtoB",
185+
};
186+
return getValue(class_map, type);
187+
}
188+
189+
function readResetEvent(status) {
190+
var status_map = { 0: "normal", 1: "reset" };
191+
return getValue(status_map, status);
192+
}
193+
194+
function readDeviceStatus(status) {
195+
var status_map = { 0: "off", 1: "on" };
196+
return getValue(status_map, status);
197+
}
198+
199+
function readYesNoStatus(status) {
200+
var status_map = { 0: "no", 1: "yes" };
201+
return getValue(status_map, status);
202+
}
203+
204+
function readEnableStatus(status) {
205+
var status_map = { 0: "disable", 1: "enable" };
206+
return getValue(status_map, status);
207+
}
208+
209+
function readOccupancyStatus(status) {
210+
var status_map = { 0: "vacant", 1: "occupied" };
211+
return getValue(status_map, status);
212+
}
213+
214+
function readCalibrationStatus(status) {
215+
var status_map = { 0: "failed", 1: "success" };
216+
return getValue(status_map, status);
217+
}
218+
219+
function readUInt8(bytes) {
220+
return bytes & 0xff;
221+
}
222+
223+
function readInt8(bytes) {
224+
var ref = readUInt8(bytes);
225+
return ref > 0x7f ? ref - 0x100 : ref;
226+
}
227+
59228
function readUInt16LE(bytes) {
60229
var value = (bytes[1] << 8) + bytes[0];
61230
return value & 0xffff;
62231
}
232+
233+
function readInt16LE(bytes) {
234+
var ref = readUInt16LE(bytes);
235+
return ref > 0x7fff ? ref - 0x10000 : ref;
236+
}
237+
238+
function getValue(map, key) {
239+
if (RAW_VALUE) return key;
240+
241+
var value = map[key];
242+
if (!value) value = "unknown";
243+
return value;
244+
}
245+
246+
if (!Object.assign) {
247+
Object.defineProperty(Object, "assign", {
248+
enumerable: false,
249+
configurable: true,
250+
writable: true,
251+
value: function (target) {
252+
"use strict";
253+
if (target == null) {
254+
throw new TypeError("Cannot convert first argument to object");
255+
}
256+
257+
var to = Object(target);
258+
for (var i = 1; i < arguments.length; i++) {
259+
var nextSource = arguments[i];
260+
if (nextSource == null) {
261+
continue;
262+
}
263+
nextSource = Object(nextSource);
264+
265+
var keysArray = Object.keys(Object(nextSource));
266+
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
267+
var nextKey = keysArray[nextIndex];
268+
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
269+
if (desc !== undefined && desc.enumerable) {
270+
// concat array
271+
if (Array.isArray(to[nextKey]) && Array.isArray(nextSource[nextKey])) {
272+
to[nextKey] = to[nextKey].concat(nextSource[nextKey]);
273+
} else {
274+
to[nextKey] = nextSource[nextKey];
275+
}
276+
}
277+
}
278+
}
279+
return to;
280+
},
281+
});
282+
}

0 commit comments

Comments
 (0)