Skip to content

Commit cec0ec5

Browse files
authored
Add Milesight AM102 + AM102L devices + add --target flag to docker pull. (#14)
Adding the `--target` flag fixes pull issues on MacOS.
1 parent c5d0a93 commit cec0ec5

9 files changed

Lines changed: 807 additions & 1 deletion

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.PHONY: test nix-start nix-test
22

33
serve:
4-
docker pull chirpstack/chirpstack-device-profiles:latest
4+
docker pull --platform=linux/amd64 chirpstack/chirpstack-device-profiles:latest
55
docker run --rm -u $(id -u):$(id -g) -p 8090:8090 -v '$(shell pwd):/chirpstack-device-profiles' chirpstack/chirpstack-device-profiles:latest -p /chirpstack-device-profiles
66

77
build:
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
/**
2+
* Payload Decoder
3+
*
4+
* Copyright 2025 Milesight IoT
5+
*
6+
* @product AM102L
7+
*/
8+
var RAW_VALUE = 0x00;
9+
10+
/* eslint no-redeclare: "off" */
11+
/* eslint-disable */
12+
// Chirpstack v4
13+
function decodeUplink(input) {
14+
var decoded = milesightDeviceDecode(input.bytes);
15+
return { data: decoded };
16+
}
17+
18+
// Chirpstack v3
19+
function Decode(fPort, bytes) {
20+
return milesightDeviceDecode(bytes);
21+
}
22+
23+
// The Things Network
24+
function Decoder(bytes, port) {
25+
return milesightDeviceDecode(bytes);
26+
}
27+
/* eslint-enable */
28+
29+
function milesightDeviceDecode(bytes) {
30+
var decoded = {};
31+
32+
for (var i = 0; i < bytes.length; ) {
33+
var channel_id = bytes[i++];
34+
var channel_type = bytes[i++];
35+
36+
// IPSO VERSION
37+
if (channel_id === 0xff && channel_type === 0x01) {
38+
decoded.ipso_version = readProtocolVersion(bytes[i]);
39+
i += 1;
40+
}
41+
// HARDWARE VERSION
42+
else if (channel_id === 0xff && channel_type === 0x09) {
43+
decoded.hardware_version = readHardwareVersion(bytes.slice(i, i + 2));
44+
i += 2;
45+
}
46+
// FIRMWARE VERSION
47+
else if (channel_id === 0xff && channel_type === 0x0a) {
48+
decoded.firmware_version = readFirmwareVersion(bytes.slice(i, i + 2));
49+
i += 2;
50+
}
51+
// TSL VERSION
52+
else if (channel_id === 0xff && channel_type === 0xff) {
53+
decoded.tsl_version = readTslVersion(bytes.slice(i, i + 2));
54+
i += 2;
55+
}
56+
// SERIAL NUMBER
57+
else if (channel_id === 0xff && channel_type === 0x16) {
58+
decoded.sn = readSerialNumber(bytes.slice(i, i + 8));
59+
i += 8;
60+
}
61+
// LORAWAN CLASS TYPE
62+
else if (channel_id === 0xff && channel_type === 0x0f) {
63+
decoded.lorawan_class = readLoRaWANClass(bytes[i]);
64+
i += 1;
65+
}
66+
// RESET EVENT
67+
else if (channel_id === 0xff && channel_type === 0xfe) {
68+
decoded.reset_event = readResetEvent(1);
69+
i += 1;
70+
}
71+
// DEVICE STATUS
72+
else if (channel_id === 0xff && channel_type === 0x0b) {
73+
decoded.device_status = readDeviceStatus(1);
74+
i += 1;
75+
}
76+
77+
// BATTERY
78+
else if (channel_id === 0x01 && channel_type === 0x75) {
79+
decoded.battery = readUInt8(bytes[i]);
80+
i += 1;
81+
}
82+
// TEMPERATURE
83+
else if (channel_id === 0x03 && channel_type === 0x67) {
84+
// °C
85+
decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10;
86+
i += 2;
87+
}
88+
// HUMIDITY
89+
else if (channel_id === 0x04 && channel_type === 0x68) {
90+
decoded.humidity = readUInt8(bytes[i]) / 2;
91+
i += 1;
92+
}
93+
// HISTORY DATA
94+
else if (channel_id === 0x20 && channel_type === 0xce) {
95+
var data = {};
96+
data.timestamp = readUInt32LE(bytes.slice(i, i + 4));
97+
data.temperature = readInt16LE(bytes.slice(i + 4, i + 6)) / 10;
98+
data.humidity = readUInt8(bytes[i + 6]) / 2;
99+
i += 7;
100+
decoded.history = decoded.history || [];
101+
decoded.history.push(data);
102+
}
103+
// SENSOR ENABLE
104+
else if (channel_id === 0xff && channel_type === 0x18) {
105+
// skip 1 byte
106+
var data = readUInt8(bytes[i + 1]);
107+
var sensor_bit_offset = { temperature: 0, humidity: 1 };
108+
decoded.sensor_enable = {};
109+
for (var key in sensor_bit_offset) {
110+
decoded.sensor_enable[key] = readEnableStatus((data >> sensor_bit_offset[key]) & 0x01);
111+
}
112+
i += 2;
113+
}
114+
// DOWNLINK RESPONSE
115+
else if (channel_id === 0xfe || channel_id === 0xff) {
116+
var result = handle_downlink_response(channel_type, bytes, i);
117+
decoded = Object.assign(decoded, result.data);
118+
i = result.offset;
119+
} else {
120+
break;
121+
}
122+
}
123+
124+
return decoded;
125+
}
126+
127+
function handle_downlink_response(channel_type, bytes, offset) {
128+
var decoded = {};
129+
130+
switch (channel_type) {
131+
case 0x03:
132+
decoded.report_interval = readUInt16LE(bytes.slice(offset, offset + 2));
133+
offset += 2;
134+
break;
135+
case 0x06:
136+
decoded.temperature_alarm_config = {};
137+
var condition = readUInt8(bytes[offset]);
138+
decoded.temperature_alarm_config.condition = readMathCondition(condition & 0x07);
139+
decoded.temperature_alarm_config.threshold_min = readInt16LE(bytes.slice(offset + 1, offset + 3)) / 10;
140+
decoded.temperature_alarm_config.threshold_max = readInt16LE(bytes.slice(offset + 3, offset + 5)) / 10;
141+
// skip 4 bytes
142+
offset += 9;
143+
break;
144+
case 0x10:
145+
decoded.reboot = readYesNoStatus(1);
146+
offset += 1;
147+
break;
148+
case 0x11:
149+
decoded.timestamp = readUInt32LE(bytes.slice(offset, offset + 4));
150+
offset += 4;
151+
break;
152+
case 0x17:
153+
decoded.time_zone = readTimeZone(readInt16LE(bytes.slice(offset, offset + 2)));
154+
offset += 2;
155+
break;
156+
case 0x27:
157+
decoded.clear_history = readYesNoStatus(1);
158+
offset += 1;
159+
break;
160+
case 0x2f:
161+
decoded.led_indicator_mode = readLedIndicatorStatus(bytes[offset]);
162+
offset += 1;
163+
break;
164+
case 0x3a:
165+
var num = readUInt8(bytes[offset]);
166+
offset += 1;
167+
for (var i = 0; i < num; i++) {
168+
var report_schedule_config = {};
169+
report_schedule_config.start_time = readUInt8(bytes[offset]) / 10;
170+
report_schedule_config.end_time = readUInt8(bytes[offset + 1]) / 10;
171+
report_schedule_config.report_interval = readUInt16LE(bytes.slice(offset + 2, offset + 4));
172+
// skip 1 byte
173+
report_schedule_config.collection_interval = readUInt8(bytes[offset + 5]);
174+
offset += 6;
175+
decoded.report_schedule_config = decoded.report_schedule_config || [];
176+
decoded.report_schedule_config.push(report_schedule_config);
177+
}
178+
break;
179+
case 0x3b:
180+
decoded.time_sync_enable = readEnableStatus(bytes[offset]);
181+
offset += 1;
182+
break;
183+
case 0x57:
184+
decoded.clear_report_schedule = readYesNoStatus(1);
185+
offset += 1;
186+
break;
187+
case 0x59:
188+
decoded.reset_battery = readYesNoStatus(1);
189+
offset += 1;
190+
break;
191+
case 0x68:
192+
decoded.history_enable = readEnableStatus(bytes[offset]);
193+
offset += 1;
194+
break;
195+
case 0x69:
196+
decoded.retransmit_enable = readEnableStatus(bytes[offset]);
197+
offset += 1;
198+
break;
199+
case 0x6a:
200+
var interval_type = readUInt8(bytes[offset]);
201+
if (interval_type === 0) {
202+
decoded.retransmit_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3));
203+
} else if (interval_type === 1) {
204+
decoded.resend_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3));
205+
}
206+
offset += 3;
207+
break;
208+
default:
209+
throw new Error("unknown downlink response");
210+
}
211+
212+
return { data: decoded, offset: offset };
213+
}
214+
215+
function readProtocolVersion(bytes) {
216+
var major = (bytes & 0xf0) >> 4;
217+
var minor = bytes & 0x0f;
218+
return "v" + major + "." + minor;
219+
}
220+
221+
function readHardwareVersion(bytes) {
222+
var major = (bytes[0] & 0xff).toString(16);
223+
var minor = (bytes[1] & 0xff) >> 4;
224+
return "v" + major + "." + minor;
225+
}
226+
227+
function readFirmwareVersion(bytes) {
228+
var major = (bytes[0] & 0xff).toString(16);
229+
var minor = (bytes[1] & 0xff).toString(16);
230+
return "v" + major + "." + minor;
231+
}
232+
233+
function readTslVersion(bytes) {
234+
var major = bytes[0] & 0xff;
235+
var minor = bytes[1] & 0xff;
236+
return "v" + major + "." + minor;
237+
}
238+
239+
function readSerialNumber(bytes) {
240+
var temp = [];
241+
for (var idx = 0; idx < bytes.length; idx++) {
242+
temp.push(("0" + (bytes[idx] & 0xff).toString(16)).slice(-2));
243+
}
244+
return temp.join("");
245+
}
246+
247+
function readLoRaWANClass(type) {
248+
var class_map = {
249+
0: "Class A",
250+
1: "Class B",
251+
2: "Class C",
252+
3: "Class CtoB",
253+
};
254+
return getValue(class_map, type);
255+
}
256+
257+
function readResetEvent(status) {
258+
var status_map = { 0: "normal", 1: "reset" };
259+
return getValue(status_map, status);
260+
}
261+
262+
function readDeviceStatus(status) {
263+
var status_map = { 0: "off", 1: "on" };
264+
return getValue(status_map, status);
265+
}
266+
267+
function readYesNoStatus(status) {
268+
var status_map = { 0: "no", 1: "yes" };
269+
return getValue(status_map, status);
270+
}
271+
272+
function readEnableStatus(status) {
273+
var status_map = { 0: "disable", 1: "enable" };
274+
return getValue(status_map, status);
275+
}
276+
277+
function readTimeZone(time_zone) {
278+
var timezone_map = { "-120": "UTC-12", "-110": "UTC-11", "-100": "UTC-10", "-95": "UTC-9:30", "-90": "UTC-9", "-80": "UTC-8", "-70": "UTC-7", "-60": "UTC-6", "-50": "UTC-5", "-40": "UTC-4", "-35": "UTC-3:30", "-30": "UTC-3", "-20": "UTC-2", "-10": "UTC-1", 0: "UTC", 10: "UTC+1", 20: "UTC+2", 30: "UTC+3", 35: "UTC+3:30", 40: "UTC+4", 45: "UTC+4:30", 50: "UTC+5", 55: "UTC+5:30", 57: "UTC+5:45", 60: "UTC+6", 65: "UTC+6:30", 70: "UTC+7", 80: "UTC+8", 90: "UTC+9", 95: "UTC+9:30", 100: "UTC+10", 105: "UTC+10:30", 110: "UTC+11", 120: "UTC+12", 127: "UTC+12:45", 130: "UTC+13", 140: "UTC+14" };
279+
return getValue(timezone_map, time_zone);
280+
}
281+
282+
function readLedIndicatorStatus(status) {
283+
var status_map = { 0: "off", 2: "blink" };
284+
return getValue(status_map, status);
285+
}
286+
287+
function readMathCondition(type) {
288+
var condition_map = { 0: "disable", 1: "below", 2: "above", 3: "between", 4: "outside" };
289+
return getValue(condition_map, type);
290+
}
291+
292+
/* eslint-disable */
293+
function readUInt8(bytes) {
294+
return bytes & 0xff;
295+
}
296+
297+
function readInt8(bytes) {
298+
var ref = readUInt8(bytes);
299+
return ref > 0x7f ? ref - 0x100 : ref;
300+
}
301+
302+
function readUInt16LE(bytes) {
303+
var value = (bytes[1] << 8) + bytes[0];
304+
return value & 0xffff;
305+
}
306+
307+
function readInt16LE(bytes) {
308+
var ref = readUInt16LE(bytes);
309+
return ref > 0x7fff ? ref - 0x10000 : ref;
310+
}
311+
312+
function readUInt32LE(bytes) {
313+
var value = (bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0];
314+
return (value & 0xffffffff) >>> 0;
315+
}
316+
317+
function readInt32LE(bytes) {
318+
var ref = readUInt32LE(bytes);
319+
return ref > 0x7fffffff ? ref - 0x100000000 : ref;
320+
}
321+
322+
function getValue(map, key) {
323+
if (RAW_VALUE) return key;
324+
325+
var value = map[key];
326+
if (!value) value = "unknown";
327+
return value;
328+
}
329+
330+
//if (!Object.assign) {
331+
Object.defineProperty(Object, "assign", {
332+
enumerable: false,
333+
configurable: true,
334+
writable: true,
335+
value: function (target) {
336+
"use strict";
337+
if (target == null) {
338+
throw new TypeError("Cannot convert first argument to object");
339+
}
340+
341+
var to = Object(target);
342+
for (var i = 1; i < arguments.length; i++) {
343+
var nextSource = arguments[i];
344+
if (nextSource == null) {
345+
continue;
346+
}
347+
nextSource = Object(nextSource);
348+
349+
var keysArray = Object.keys(Object(nextSource));
350+
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
351+
var nextKey = keysArray[nextIndex];
352+
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
353+
if (desc !== undefined && desc.enumerable) {
354+
// concat array
355+
if (Array.isArray(to[nextKey]) && Array.isArray(nextSource[nextKey])) {
356+
to[nextKey] = to[nextKey].concat(nextSource[nextKey]);
357+
} else {
358+
to[nextKey] = nextSource[nextKey];
359+
}
360+
}
361+
}
362+
}
363+
return to;
364+
},
365+
});
366+
//}

0 commit comments

Comments
 (0)