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.

Milesight VS121
VS121Sensor
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
Measurements

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.

Data into your dashboard

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.

ChirpStack v4 · decodeUplink
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.

From the field

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.

Your partner

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

Yes. Over LoRaWAN it is a standard Class A device, no Milesight cloud required. You add the codec to the device profile and provision it via OTAA. It can also report over Ethernet or Wi-Fi if you prefer a wired backhaul.
Yes. We provide a decoder framework for ChirpStack and ThingsBoard, implemented from the published Milesight byte specification. Because the payload is configuration-dependent, we enable the channels you actually report and validate it against a real uplink.
The bytes you receive depend on which features are turned on in the web GUI: people counting, up to 16 region counters, line detection, A/B/C/D flow and dwell time. The framework covers the documented channels, and we trim it to your configuration.
It is designed to be privacy-safe: the AI ToF processing happens on the device and only anonymous counts are transmitted, no images leave the sensor. That makes it suitable for offices and public spaces where camera-based counting would be a problem.
Total occupancy, per-region occupied/free state and head counts for up to 16 regions, cumulative people in/out, directional line crossings, a periodic maximum and per-region dwell time.
It is powered over PoE or DC and mounts on the ceiling. Coverage reaches roughly 127 square metres depending on mounting height, with up to 95 percent occupancy detection accuracy.
From the same series

Related devices

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.

Timo Wevelsiep

Your contact

Timo Wevelsiep

Founder, merkaio

15 minutes, no commitment, directly with Timo.

By submitting, you agree to our Privacy Policy.

Decoder for ChirpStack v4. merkaio is an independent integrator and is not affiliated with Milesight.