SenseCAP S2120 8-in-1 payload edit

I have purchased a SenseCAP S2120 8-in-1 LoRa weather station from Seeed.

The payload decoder is in the TTN rep. and can also be found on the manufacturer site.

Everything works fine but I’d like to simplifiy the payload a lot as follows:
‘Temperature’: 21.8
‘Humidity’: 62

without the nested structure:

  let result = {
    'err': 0, 'payload': bytes, 'valid': true, messages: []

I find the code somewhat difficult to read, could maybe someone help me edit the payload decoder? Here is a sample payload:

1 Like

Hi! I’ve recently made a PR that simplifies a little the decoded payload, but the structure that Seeed is using in these devices is still annoying.

Ideally, we will just add a normalizeUplink function to achieve this (Uplink | The Things Stack for LoRaWAN), think of it as an extra/optional step that takes the output of the decodeUplink function and beautifies/standardizes it. But the normalized payload format is still young and does not yet support the UV Index and Rain Gauge fields provided by this device. Anyway, if you don’t want to touch the original decodeUplink function and don’t mind losing the unsupported fields, adding this function will be a solution:

// Be aware of corner cases
function normalizeUplink(input) {
  var data = { air: {}, wind: {} };
  for (var i = 0; i <; i++) {
    var msg =[i];
    switch (msg.measurementId) {
      case "4097":
        data.air.temperature = msg.measurementValue;
      case "4098":
        data.air.relativeHumidity = msg.measurementValue;
      case "4099":
        data.air.lightIntensity = msg.measurementValue;
      case "4101":
        data.air.pressure = msg.measurementValue / 100; // convert to hPa
      case "4104":
        data.wind.direction = msg.measurementValue;
      case "4105":
        data.wind.speed = msg.measurementValue;

  return {data};

That code will produce something like this:

  "air": {
    "temperature": 7.2,
    "relativeHumidity": 59,
    "lightIntensity": 435,
    "pressure": 89080
  "wind": {
    "speed": 1.1,
    "direction": 277

Sadly, this won’t work for you just yet, while these fields are supported in the schema, air.lightIntensity hasn’t yet been added to the validation code of The Things Stack. I have a patched version running locally and this works fine, I will try to prepare a PR as soon as possible so that everyone can use these fields in the normalized format. For now, you can just remove the lightIntensity case from the switch statement.

I will let you know with updates on using this approach.

I don’t like the hacky approach of patching the original decodeUplink function, so I’m willing to take the long route here of making a useful and easy to use format for all.

Bit scary that TTI are prepared to take PR’s from people who don’t work for the vendor - it raises all sorts of complications - if a new version of the device comes out and is registered along with a new PF, what then? Is the PF actually open-source that anyone can amend in an important publishing platform like TTS?

I fully appreciate the intent to clean up the mess that too many vendors produce after they’ve hacked an initial payload structure together that is the work of insanity. But this may well have some serious unintended consequences - and potential legal consequences should be explored.

I mencioned Seeed’s employee that submitted the original code so that they could apply that fix to their own repo and wherever they rely on the old behavior in their internal infrastructure and tools (e.g., SenseCAP Mate App). I would also have expected a response from him before the PR was merged, but I guess that this change does not break any of their other services. It just affects those relying on TTS, and I don’t know about you, but I don’t want to rely on a crazy format, I’m glad that TTI adopts fixes quickly.

If the vendor updates the decoder in TTS to a new version, and they haven’t synced the changes made in TTS with their own repo, there would be merge conflicts to solve, but that’s not the end of the world.

Regarding legal issues, I don’t see why, when submitting the decoders the vendors agree to the license of the repository and sign the CLA, just like every other contributor.

Thanks everyone! In the end, it wasn’t too difficult to edit the payload for my needs.

1 Like

Ran in to this myself. Much prefer simple, flat JSON pairs rather than the Seeed approach.

A bit of a hack but here’s what I did for a stopgap:

Create a function within the existing S2120 decoder:

function convertToPairs(inputJson) {
  const outputPairs = {};

  for (const measurements of Object.values(inputJson)) {
    for (const measurement of measurements) {
      const measurementId = measurement.measurementId;
      const measurementValue = measurement.measurementValue;
      outputPairs[measurementId] = measurementValue;

  return outputPairs;

Modify the end of the decodeUplink function:

    let newResult = convertToPairs(result.messages)
    return { data: newResult }

Now the output looks like this:

  "4097": 26.9,
  "4098": 61,
  "4099": 0,
  "4101": 99960,
  "4104": 84,
  "4105": 0,
  "4113": 0,
  "4190": 0

Weather station is inside at the moment for testing thus the 0s.

1 Like

Hi dear,
thanks for your info.
can you paste full decoder with your mod?

Thank you