Milesight VS121: AI Workplace Occupancy & People Counter
Milesight VS121 AI workplace occupancy sensor: GDPR-safe people counting, own ChirpStack decoder framework, region and line counts, smart-office integration.
- LoRaWAN
- Class A, OTAA
- Sensor
- AI ToF camera, anonymous (no images sent)
- Coverage
- Up to ~127 m² ceiling mount
- Regions
- Up to 16 customizable counting regions
- Accuracy
- Up to 95 % occupancy detection
- Power
- PoE or DC, with optional Wi-Fi/Ethernet
- Configuration
- Web GUI / Milesight ToolBox
What the VS121 measures
People count (total)
Current occupancy across the field of view, plus a periodic maximum.
Region occupancy
Per-region occupied/free state and per-region head counts for up to 16 zones.
In / out counting
Cumulative people in and out for entrance and corridor flow analysis.
Line crossing
Directional line-in / line-out counts when line detection is enabled.
Dwell time
Average and maximum dwell time per region for utilisation insight.
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 = { regions: {} };
for (var i = 0; i < bytes.length; ) {
var channel = bytes[i++];
var type = bytes[i++];
// Device info (join / power-on): version, serial, hw/fw
if (channel === 0xff) {
if (type === 0x01) { data.protocol_version = bytes[i]; i += 1; }
else if (type === 0x08) { i += 6; } // serial number
else if (type === 0x09) { i += 2; } // hardware version
else if (type === 0x1f) { i += 4; } // firmware version
else { break; }
continue;
}
// People counter: total + region count + 16-bit region occupancy mask (UINT16 BE)
if (channel === 0x04 && type === 0xc9) {
data.people_count_all = bytes[i];
var regionCount = bytes[i + 1];
var mask = readUInt16BE(bytes, i + 2);
for (var r = 0; r < regionCount; r++) {
data.regions["region_" + (r + 1)] = (mask >> r) & 1;
}
i += 4; continue;
}
// Cumulative people in / out (INT16 LE)
if (channel === 0x05 && type === 0xcc) {
data.people_in = readInt16LE(bytes, i);
data.people_out = readInt16LE(bytes, i + 2);
i += 4; continue;
}
// Periodic maximum people count
if (channel === 0x06 && type === 0xcd) {
data.people_count_max = bytes[i];
i += 1; continue;
}
// Total people in / out (UINT16 LE)
if (channel === 0x0d && type === 0xcc) {
data.people_total_in = readUInt16LE(bytes, i);
data.people_total_out = readUInt16LE(bytes, i + 2);
i += 4; continue;
}
// Dwell time: region (1) + avg (UINT16) + max (UINT16)
if (channel === 0x0e && type === 0xe4) {
data.dwell_region = bytes[i];
data.dwell_time_avg = readUInt16LE(bytes, i + 1);
data.dwell_time_max = readUInt16LE(bytes, i + 3);
i += 5; continue;
}
// Timestamp (UINT32 LE)
if (channel === 0x0f && type === 0x85) {
data.timestamp = readUInt32LE(bytes, i);
i += 4; continue;
}
// Directional line in / out (UINT16 LE)
if (channel === 0x10 && type === 0xf7) {
data.line_in = readUInt16LE(bytes, i);
data.line_out = readUInt16LE(bytes, i + 2);
i += 4; continue;
}
// Region counters (0x07/0x08, type 0xd5): 8 bytes, one per region
if ((channel === 0x07 || channel === 0x08) && type === 0xd5) {
var base = channel === 0x07 ? 0 : 8;
for (var k = 0; k < 8; k++) {
data.regions["region_" + (base + k + 1) + "_count"] = bytes[i + k];
}
i += 8; continue;
}
// A/B/C/D flow matrices (0x09-0x0c, type 0xda): 4 x UINT16 LE
if (channel >= 0x09 && channel <= 0x0c && type === 0xda) {
i += 8; continue;
}
// Unknown / history / downlink-response channel: stop here
break;
}
return { data: data };
}
function readUInt16LE(b, i) {
return ((b[i + 1] << 8) | b[i]) & 0xffff;
}
function readUInt16BE(b, i) {
return ((b[i] << 8) | b[i + 1]) & 0xffff;
}
function readInt16LE(b, i) {
var v = readUInt16LE(b, i);
return v > 0x7fff ? v - 0x10000 : v;
}
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).
The VS121 payload is configuration-dependent: which channels appear depends on the features you enable in the web GUI (people counting, region counting, line detection, A/B/C/D flow, dwell time, history). Core channels are 04 C9 people count plus a 16-bit region mask, 05 CC people in/out, 06 CD periodic maximum, 10 F7 directional line in/out, and 0E E4 dwell time. This is a framework implemented from the published Milesight byte specification, not a fixed drop-in: enable only the channels you report and validate the final decoder against a real uplink from your deployment. For ThingsBoard the same channel logic goes into an uplink converter.
Configuration & pitfalls
Mount height & coverage
Ceiling height directly sets the covered area and counting accuracy. Follow the height-to-coverage table in the user guide and keep the field of view clear of obstructions.
Region & line drawing
Counting regions and detection lines are drawn in the web GUI. Document each region index so dashboard labels match the physical zones in the decoded JSON.
Privacy by design
The VS121 processes the ToF image on-device and only sends counts, never images, which keeps deployments GDPR-friendly. Keep firmware current so on-device processing stays patched.
Backhaul choice
The sensor can report over LoRaWAN, Wi-Fi or Ethernet. For ChirpStack use the LoRaWAN uplink; if you also enable Ethernet/Wi-Fi reporting, avoid double-counting in your dashboard.
How merkaio supports your VS121
From sourcing to day-to-day operation, all from one partner on our own European infrastructure.
Pre-staging & provisioning
We configure the VS121, 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.