Milesight VS360: LoRaWAN IR Breakbeam People Counter
Milesight VS360 LoRaWAN IR breakbeam people counter: own ChirpStack/ThingsBoard decoder, decoded example, in/out counts and entrance footfall integration.
- LoRaWAN
- Class A, OTAA
- Design
- Paired master + node (transmitter/receiver)
- Counting
- IR breakbeam, bi-directional in/out
- Accuracy
- Up to 80% (single-file passage)
- Detection width
- 1.2 to 3 m, mount height 0.7 to 1.2 m
- Battery
- 2 x ER14505 Li-SOCl2 per unit, several years
- Ingress / temperature
- IP30, -20 to +50 °C
What the VS360 measures
Total in / out
Cumulative inbound and outbound counts since the last reset (channel 04, two UINT16LE).
Period in / out
In and out counts for the current reporting period (channel 05, two UINT16LE).
Count alarms
Threshold alarms on period or cumulative counts, sent outside the regular interval.
Battery (master + node)
Each paired unit reports its own battery percentage (channels 01 and 02).
Events
Tamper, low battery and pairing events as discrete status flags.
Integration
Sensor / controller
Measures or controls in the field and sends LoRaWAN uplinks.
LoRaWAN gateway
Receives the radio packets and forwards them to the server.
ChirpStack
Network server: manages sessions and decodes the payload.
ThingsBoard / Grafana
Dashboards, alarms, rules and reports.
function decodeUplink(input) {
var bytes = input.bytes;
var data = {};
for (var i = 0; i < bytes.length; ) {
var channel = bytes[i++];
var type = bytes[i++];
// Device info on join / power-on (protocol, hw/fw, serial, class)
if (channel === 0xff) {
if (type === 0x01) { i += 1; } // IPSO version
else if (type === 0x09) { i += 2; } // hardware version
else if (type === 0x0a) { i += 2; } // firmware version
else if (type === 0x16) { i += 8; } // serial number
else if (type === 0x0f) { i += 1; } // LoRaWAN class
else if (type === 0xff) { i += 2; } // TSL version
else { break; }
} else if (channel === 0x01 && type === 0x75) { // battery master (%)
data.battery_main = bytes[i]; i += 1;
} else if (channel === 0x02 && type === 0x75) { // battery node (%)
data.battery_node = bytes[i]; i += 1;
} else if (channel === 0x03 && type === 0xf4) { // event: type + status
data.event = { type: bytes[i], status: bytes[i + 1] }; i += 2;
} else if (channel === 0x04 && type === 0xcc) { // total in / out
data.total_in = readUInt16LE(bytes, i);
data.total_out = readUInt16LE(bytes, i + 2); i += 4;
} else if (channel === 0x84 && type === 0xcc) { // total in / out + alarm
data.total_in = readUInt16LE(bytes, i);
data.total_out = readUInt16LE(bytes, i + 2);
data.total_count_alarm = bytes[i + 4]; i += 5;
} else if (channel === 0x05 && type === 0xcc) { // period in / out
data.period_in = readUInt16LE(bytes, i);
data.period_out = readUInt16LE(bytes, i + 2); i += 4;
} else if (channel === 0x85 && type === 0xcc) { // period in / out + alarm
data.period_in = readUInt16LE(bytes, i);
data.period_out = readUInt16LE(bytes, i + 2);
data.period_count_alarm = bytes[i + 4]; i += 5;
} else if (channel === 0x0a && type === 0xef) { // timestamp (UINT32LE)
data.timestamp = readUInt32LE(bytes, i); i += 4;
} else {
break;
}
}
return { data: data };
}
function readUInt16LE(b, i) {
return (b[i + 1] << 8) | b[i];
}
function readUInt32LE(b, i) {
return ((b[i + 3] << 24) | (b[i + 2] << 16) | (b[i + 1] << 8) | b[i]) >>> 0;
}
Implemented from the published Milesight byte specification (Communication Protocol / User Guide).
Channel format: 01 75 battery master (%), 02 75 battery node (%), 04 cc total in/out (two UINT16LE), 05 cc period in/out, 84/85 cc the same plus a count-alarm byte, 03 f4 event (type + status), 0a ef timestamp (UINT32LE). The decoder is implemented from the published Milesight byte specification. Historical (catch-up) uplinks on channel 20 ce and downlink-response frames are device-specific; we add them per deployment. For ThingsBoard the same channel logic goes into an uplink converter.
Uplink (hex)
01756402756304CC0C00000005CC05000200Decoded JSON
{ "battery_main": 100, "battery_node": 99, "total_in": 12, "total_out": 0, "period_in": 5, "period_out": 2 }Configuration & pitfalls
Pairing the two units
The VS360 works as a master plus a node facing each other across the doorway. Pair and align them so the IR beam is uninterrupted, then provision only the master over LoRaWAN.
Mount height and width
Keep the passage between 1.2 and 3 m wide and mount at 0.7 to 1.2 m. People walking side by side count as one, which is why accuracy is around 80% in single-file flow.
Total vs period counts
Total counts are cumulative since reset, period counts cover one interval. Build dashboard deltas from period values and use total only as a running reference.
NFC setup
Keys, reporting interval and count-alarm thresholds are set over NFC with the Milesight ToolBox app on the master before rollout.
How merkaio supports your VS360
From sourcing to day-to-day operation, all from one partner on our own European infrastructure.
Pre-staging & provisioning
We configure the VS360, set keys, intervals and alarms, and ship it ready to deploy.
Own decoder
Payload codec for ChirpStack v4 and ThingsBoard, implemented from the Milesight specification.
Dashboard integration
Data lands in your ThingsBoard or Grafana, with alarms and reports.
Operations & monitoring
We run the LoRaWAN stack and dashboards on European infrastructure, you just use the data.
Frequently asked questions
Let's discuss your infrastructure. Digital and on-site.
Whether it's IoT platform development, hardware selection, managed hosting for ChirpStack, ThingsBoard, Grafana or NetBird VPN, or migration from a self-hosted setup - we'll find the right solution for your use case. Book a free 30-minute consultation, no commitment required.
Your contact
Timo Wevelsiep
Founder, merkaio
15 minutes, no commitment, directly with Timo.
Decoder for ChirpStack v4. merkaio is an independent integrator and is not affiliated with Milesight.