TypeError: Value is not an object: undefined at apply (native) when trying to decode payload

Summary

When trying to collect air quality data via an SDS011 connected to an ESP32 Pax Counter, I get the following error:

TypeError: Value is not an object: undefined at apply (native)

I have already logged an issue in the Pax Counter git repo, however I’m posting here as well because I believe that this is an issue that the community may be able to help me with around the decoder rather than a fault with the Pax Counter itself.

Logs

No real logs as such in the console, but this is the event message:

{
  "name": "as.up.data.decode.fail",
  "time": "2024-11-30T10:25:43.662694741Z",
  "identifiers": [
    {
      "device_ids": {
        "device_id": "<STRING>",
        "application_ids": {
          "application_id": "<STRING>"
        },
        "dev_eui": "<STRING>",
        "join_eui": "<STRING>"
      }
    }
  ],
  "data": {
    "@type": "type.googleapis.com/ttn.lorawan.v3.ErrorDetails",
    "namespace": "pkg/scripting/javascript",
    "name": "script",
    "message_format": "{message}",
    "attributes": {
      "message": "TypeError: Value is not an object: undefined at apply (native)"
    },
    "correlation_id": "<STRING>",
    "code": 10
  },
  "correlation_ids": [
    "gs:uplink:<STRING>"
  ],
  "origin": "ip-10-100-15-66.eu-west-1.compute.internal",
  "context": {
    "tenant-id": "<STRING>"
  },
  "visibility": {
    "rights": [
      "RIGHT_APPLICATION_TRAFFIC_READ"
    ]
  },
  "unique_id": "<STRING>"
}

Decoder is as follows:

// Decoder for device payload encoder "PLAIN"
// copy&paste to TTN Console V3 -> Applications -> Payload formatters -> Uplink -> Javascript
// modified for The Things Stack V3 by Caspar Armster, dasdigidings e.V.

function decodeUplink(input) {
    var data = {};

    if (input.fPort === 1) {
        var i = 0;

        if (input.bytes.length >= 2) {
            data.wifi = (input.bytes[i++] << 8) | input.bytes[i++];
        }
     
        if (input.bytes.length === 4 || input.bytes.length > 15) {
            data.ble = (input.bytes[i++] << 8) | input.bytes[i++];
        }

        if (input.bytes.length > 4) {
            data.latitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
            data.longitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
            data.sats = input.bytes[i++];
            data.hdop = (input.bytes[i++] << 8) | (input.bytes[i++]);
            data.altitude = ((input.bytes[i++] << 8) | (input.bytes[i++]));
        }
        
        // Add after the "if (bytes.length > 4)" block

        if (input.bytes.length >= 15) {
          data.sds011 = String.fromCharCode.apply(null, input.bytes[i]);
          i+=11;
        }
        data.pax = 0;
        if ('wifi' in data) {
            data.pax += data.wifi;
        }
        if ('ble' in data) {
            data.pax += data.ble;
        } 
    }

    if (input.fPort === 2) {
        var i = 0;
        data.voltage = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.uptime = ((input.bytes[i++] << 56) | (input.bytes[i++] << 48) | (input.bytes[i++] << 40) | (input.bytes[i++] << 32) | (input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.cputemp = input.bytes[i++];
        data.memory = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.reset0 = input.bytes[i++];
        data.restarts = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
    }

    if (input.fPort === 4) {
        var i = 0;
        data.latitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.longitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.sats = input.bytes[i++];
        data.hdop = (input.bytes[i++] << 8) | (input.bytes[i++]);
        data.altitude = ((input.bytes[i++] << 8) | (input.bytes[i++]));
    }

    if (input.fPort === 5) {
        var i = 0;
        data.button = input.bytes[i++];
    }

    if (input.fPort === 7) {
        var i = 0;
        data.temperature = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.pressure = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.humidity = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.air = ((input.bytes[i++] << 8) | input.bytes[i++]);
    }

    if (input.fPort === 8) {
        var i = 0;
        if (input.bytes.length >= 2) {
            data.voltage = (input.bytes[i++] << 8) | input.bytes[i++];
        }
    }

    if (input.fPort === 9) {
        // timesync request
        if (input.bytes.length === 1) {
            data.timesync_seqno = input.bytes[0];
        }
        // epoch time answer
        if (input.bytes.length === 5) {
            var i = 0;
            data.time = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
            data.timestatus = input.bytes[i++];
        }
    }

    if (data.hdop) {
        data.hdop /= 100;
        data.latitude /= 1000000;
        data.longitude /= 1000000;
    }

    data.bytes = input.bytes; // comment out if you do not want to include the original payload
    data.port = input.fPort; // comment out if you do not want to include the port

    return {
        data: data,
        warnings: [],
        errors: []
    };
}

This comment contains the details of what I’ve changed over the basic decoder

Hardware
Device is an ESP32-PaxCounter sending data to an original TTIG and a RAK 7258.

Forget ChatGPT, this is your local friendly WOPR!

The TTS JS interpreter is good but isn’t a full JS package so you may well be falling foul of some of the twiddly bits of JS that aren’t supported as being a whole heap (probably literally) of make-work for no reasonable benefit.

Why the .apply? The first argument would be the “this” that you want to use - it allows you to change the scope and eliminate the assignment but is primarily used for creating variables (that are actually constant objects) that you can then use to run that code. If you want to get in to the weeds, MDN will explain further but I’m not paying for any subsequent therapy.

You can probably just drop the .apply() altogether and go with data.sds011 = String.fromCharCode(input.bytes[i]);

In theory, but its not something I’d do even in browser code, if you already had some data in sds011, you could do String.fromCharCode.apply(data.sds011, input.bytes[i]);

If this doesn’t help, can you provide a payload for us to play with please.

Thanks!

So I removed the apply as you suggested and it’s no longer erroring, so that may well be a JS thing that isn’t supported by TTN.

The payload still isn’t quite right though:

{
  "altitude": 13824,
  "ble": 6,
  "bytes": [
    0,
    3,
    0,
    6,
    44,
    32,
    32,
    50,
    46,
    51,
    44,
    32,
    32,
    48,
    46,
    54
  ],
  "hdop": 123.34,
  "latitude": 740.302898,
  "longitude": 775.105568,
  "pax": 9,
  "port": 1,
  "sats": 32,
  "sds011": "\u0000", # <- THIS SHOULD BE TWO VALUES - one for PM2.5 and another for PM10
  "wifi": 3
}

The binary payload is 000300062C2020322E332C2020302E36, but that looks like it’s missing a bit on the end as I have to add a to that string in order for the decoder to make sense of it, and the values are all over the place - I’m definitely not 13284m above sea level! :joy:

I wondered if it was because I was looking for a payload larger than 15 bytes (which would obviously be 16 and above) but as you can tell I’m well out of my depth here as it’s been so long since I wrote any decoders for TTN!

The index in to the payload.bytes, i, is on 17 by the time you get to pulling the SDS011 data which, as you surmise is absent. It doesn’t explode but it doesn’t give you anything useful.

This is my JS PF Tester setup for your decoder. Copy n paste in to a text file ending .html and then open in your browser.

<!DOCTYPE html>
<html>
<head>
<title>Payload formatter tester</title>
</head>
<body>
<h1>Payload formatter tester</h2>

<h2>Input</h2>
<pre id="positions"></pre>
<pre id="payloadAsHex"></pre>
<p>Port: <span id="port"></span>
<p>Size: <span id="size"></span>

<h2>Result</h2>
<pre id="result"></pre>


<script>
// #### Your stuff here ####

const payloadFromConsole = "000300062C2020322E332C2020302E36";

const fPort = 1;


function decodeUplink(input) {
    var data = {};

    if (input.fPort === 1) {
        var i = 0;

        if (input.bytes.length >= 2) {
            data.wifi = (input.bytes[i++] << 8) | input.bytes[i++];
        }
     
        if (input.bytes.length === 4 || input.bytes.length > 15) {
            data.ble = (input.bytes[i++] << 8) | input.bytes[i++];
        }

        if (input.bytes.length > 4) {
            data.latitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
            data.longitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
            data.sats = input.bytes[i++];
            data.hdop = (input.bytes[i++] << 8) | (input.bytes[i++]);
            data.altitude = ((input.bytes[i++] << 8) | (input.bytes[i++]));
        }
        
        // Add after the "if (bytes.length > 4)" block

        if (input.bytes.length >= 15) {
//           data.sds011 = String.fromCharCode.apply(null, input.bytes[I]);
          data.sds011 = String.fromCharCode(input.bytes[I]);
          i+=11;
        }
        data.pax = 0;
        if ('wifi' in data) {
            data.pax += data.wifi;
        }
        if ('ble' in data) {
            data.pax += data.ble;
        } 
    }

    if (input.fPort === 2) {
        var i = 0;
        data.voltage = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.uptime = ((input.bytes[i++] << 56) | (input.bytes[i++] << 48) | (input.bytes[i++] << 40) | (input.bytes[i++] << 32) | (input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.cputemp = input.bytes[i++];
        data.memory = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.reset0 = input.bytes[i++];
        data.restarts = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
    }

    if (input.fPort === 4) {
        var i = 0;
        data.latitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.longitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
        data.sats = input.bytes[i++];
        data.hdop = (input.bytes[i++] << 8) | (input.bytes[i++]);
        data.altitude = ((input.bytes[i++] << 8) | (input.bytes[i++]));
    }

    if (input.fPort === 5) {
        var i = 0;
        data.button = input.bytes[i++];
    }

    if (input.fPort === 7) {
        var i = 0;
        data.temperature = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.pressure = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.humidity = ((input.bytes[i++] << 8) | input.bytes[i++]);
        data.air = ((input.bytes[i++] << 8) | input.bytes[i++]);
    }

    if (input.fPort === 8) {
        var i = 0;
        if (input.bytes.length >= 2) {
            data.voltage = (input.bytes[i++] << 8) | input.bytes[i++];
        }
    }

    if (input.fPort === 9) {
        // timesync request
        if (input.bytes.length === 1) {
            data.timesync_seqno = input.bytes[0];
        }
        // epoch time answer
        if (input.bytes.length === 5) {
            var i = 0;
            data.time = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
            data.timestatus = input.bytes[i++];
        }
    }

    if (data.hdop) {
        data.hdop /= 100;
        data.latitude /= 1000000;
        data.longitude /= 1000000;
    }

    data.bytes = input.bytes; // comment out if you do not want to include the original payload
    data.port = input.fPort; // comment out if you do not want to include the port

    return {
        data: data,
        warnings: [],
        errors: []
    };
}



// JS Payload support stuff - do NOT put this in to the TTS console

const toHexCount   = byteArray => 'Position:  ' + byteArray.map((x, i) => ('0' + i).slice(-2) ).join('   ');
const toHexStringX = byteArray => 'Payload: 0x' + Array.from(byteArray, byte => ('0' + byte.toString(16)).slice(-2)).join(' 0x')
function hexToBytes(hex) { let bytes = []; for (let c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)); return bytes; }
var payload = {}
payload.bytes = hexToBytes(payloadFromConsole);
payload.fPort = fPort;
result = decodeUplink(payload);
console.log("data", result.data);
document.getElementById("positions").innerHTML = toHexCount(payload.bytes);	//payload.bytes.map((x, i) => ('0' + i).slice(-2) ).join('  ');
document.getElementById("payloadAsHex").innerHTML = toHexStringX(payload.bytes);
document.getElementById("port").innerHTML = payload.fPort;
document.getElementById("size").innerHTML = payload.bytes.length;
document.getElementById("result").innerHTML = JSON.stringify(result, null, 4);

</script>

</body>
</html>

Ideally you have the browser developer tools console open (right click, inspect element will do that for you) so you can see the JS errors or how it’s being processed.

You can add console.log(); in get the Arduino Serial.println(); effect in to the decoder but best to remove them before applying them to the console. You can put any variables or messages in to the console log to see ‘stuff’

More after Strictly.

Thanks, I’m playing with this now, and totally understand about Strictly, I’ll be stopping for the same! :slight_smile: