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:

Tasha for the grace & beauty, the others for technical ability, but putting the Great back in to Great Britain is the voting public - Chris McCausland, 100% blind, wins Strictly 2024. Very proud.

100% agree - Tasha & Sarah were the best dancers IMO, but Chris definitely deserved it!

I’m still digging away at this decoder, finding time is a challenge, but a long break coming up over Christmas so hopefully I’ll get to it then, and I’ve managed to get my gateway back online too which is quite helpful! :rofl:

OK, an unexpected bout of Flu and Christmas duties out the way, and I’m back on this - I hope you managed to have a restful break!

After some playing around with the javascript in that file, I’ve got the following code that should decode this properly:

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

<p>SDS011: pm25: 1.20, pm10: 2.30</p>
<p>SDS011: pm25: 1.20, pm10: 4.20</p>

<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 = "000200082C2020342E322C2020312E32";

const fPort = 1;


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

    if (input.fPort === 1) {
        console.log("Port 1");
        var i = 0;

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

        if (input.bytes.length >= 15) {
          data.sds011 = String.fromCharCode(input.bytes[i]);
          i++;
        }
        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>

Unfortunately, when given the payload 000200082C2020342E322C2020312E32 (which is the value in the TTN console once the payload has been sent by the PaxCounter), the output is as follows:

Payload formatter tester

(The next two lines are taken from the console output of the ESP32-Paxcounter itself)
SDS011: pm25: 1.20, pm10: 2.30

SDS011: pm25: 1.20, pm10: 4.20

Input
Position:  00   01   02   03   04   05   06   07   08   09   10   11   12   13   14   15
Payload: 0x00 0x02 0x00 0x08 0x2c 0x20 0x20 0x34 0x2e 0x32 0x2c 0x20 0x20 0x31 0x2e 0x32
Port: 1

Size: 16

Result
{
    "data": {
        "wifi": 2,
        "ble": 8,
        "sds011": ",",
        "pax": 10,
        "bytes": [
            0,
            2,
            0,
            8,
            44,
            32,
            32,
            52,
            46,
            50,
            44,
            32,
            32,
            49,
            46,
            50
        ],
        "port": 1
    },
    "warnings": [],
    "errors": []
}

I think I’m extracting the correct fields now because I’ve deleted the lines from the decoder that deal with GPS (I’m not using that here), but it’s returning "," as a value, whereas I’d expect it to be one of the values from the console output.

Any help you can provide would be welcome, I’ve posted this on Github as well

You asked it to:

i gets to 4, so this line reads that byte, 0x2C, and it’s then turned in to an ASCII character, which for 0x2C is a comma.

I’ll have a dig through the Pax payload on GitHub to get up to speed.

1 Like

OK, this set me on the right path, as you say I was only reading the first character, so I added while loop in there and now it’s looking a lot better:

        if (input.bytes.length >= 15) {
            while (i < 16) {
          data.sds011 += String.fromCharCode(input.bytes[i]);
          i++;
            }
        }

returns

"sds011": "undefined,  4.2,  1.2",

which matches the second output from the device console logs.

It doesn’t seem to matter whether I start at i=4 or i=7, that just trims the whitespace between undefined and 4.2, but I can split this on the comma now and read those values into dedicated variables.

This makes the current code:

        if (input.bytes.length >= 15) {
          var sds011_tmp = "";
            while (i < 16) {
          sds011_tmp += String.fromCharCode(input.bytes[i]);
          i++;
            }
          data.pm10 = parseFloat(sds011_tmp.split(',')[1]);
          data.pm25 = parseFloat(sds011_tmp.split(',')[2]);
        }

and the output

{
    "data": {
        "wifi": 2,
        "ble": 8,
        "pm10": 4.2,
        "pm25": 1.2,
        "pax": 10,
        "bytes": [
            0,
            2,
            0,
            8,
            44,
            32,
            32,
            52,
            46,
            50,
            44,
            32,
            32,
            49,
            46,
            50
        ],
        "port": 1
    },
    "warnings": [],
    "errors": []
}

I’ll upload this to the console now and leave it running to see what it tells me!

Thanks again!

Would be more efficient and easier to process if you sent the values as numbers rather than text - but progress indeed!