Decode Guppy DigiMatter

I am struggling a little decoding DigiMatter Guppy messages. My temp and humidity is off quite a bit.
I am using below but both value is way off

//temp
var tempEncoded=(bytes[2]<<8)|(bytes[1]);
var tempDecoded=(tempEncoded*175.72/65536)-61.5;
obj.temp=tempDecoded.toFixed(2);

//humidity
var humEncoded=(bytes[3]);
var humDecoded=(humEncoded*125/256)-6;
obj.hum=humDecoded.toFixed(2);

Dose anybody have the code to share.
Thanks
Martin

I had never heard of ‘Guppy’ before :sunglasses:

Yeah - so far not to impressed. As far as i can tell it is based on little-endian formatting.

Beware that array indexes are zero-based. So, you’re ignoring the first byte.

Note that this yields a string value, not a number. To get a proper number, prefix with the unary plus operator.

If you expect help with the decoding, then we need at least one example payload with the expected values.

1 Like

Thanks - it was the first byte making it useless….

So… it works now ?

Yes, it works now…so far so good

payload:3E847802
Inclination:"3.00"battery:"2.40"temp:“26.00”

1 Like

Can you please post the full decoder function for future readers?

Hi, below is my decoder for temp and inclination.

//temp
var tempEncoded=(bytes[1]);
var tempDecoded=(tempEncoded*0.5)-40;
obj.temp=tempDecoded.toFixed(2);

//Inclination
var humEncoded=(bytes[3]);
var humDecoded=(humEncoded *1.5);
obj.Inclination=humDecoded.toFixed(2);

Hmmm, that doesn’t seem right, if I read https://support.digitalmatter.com/helpdesk/attachments/16024374732

3.2. Uplink

LoRaWAN uplink payloads can be as small as 11 bytes in some regions (for the longest range transmissions). The packet headers already include the device serial number, and a ‘port number’ from 1 to 223, which we will use as a message type.

3.2.1. Uplink Port 1: Status Update

Offset Description
0.0 0: Out of trip, 1: In-trip
0.1 - 0.7 Battery voltage, LSb = 14 mV, 2 V offset
1 (BYTE) Temperature, LSb = 0.5 °C, -40 °C offset
2.0 Man down (no movement for configured period), optional
2.1 - 2.7 Inclination (see configuration and usage guide), 0-120: 0-180°, optional
3 (BYTE) Azimuth (see configuration and guide), 0-239: 0-358.5°, optional

This message includes two optional bytes, which are only sent if either the Man Down or Tilt
feature is enabled. They are not enabled by default.

Above, “2.1 - 2.7” implies that you need to use bits 1 to 7 of byte 2. So: you’ll need to ignore bit 0, which is actually used to indicate “Man down”. Also, the number of the byte is different? So, I’d assume:

// Inclination: bits 1..7; ignore bit 0 by right-shifting it out of memory
var inclinationEncoded = bytes[2] >> 1;
var inclinationDecoded = inclinationEncoded * 1.5;
obj.inclination = +inclinationDecoded.toFixed(2);

Also, I’d say that toFixed is not needed here, as you’re multiplying an integer by 1.5, which will never give you more than 1 decimal. But if you’re using it, then beware that toFixed returns a string value. Prefix that string result with the unary plus operator to get a true number rather than some string, like I wrote earlier. So, you should expect to see no quotes in the value, like:

"inclination": 3.5

…rather than:

"inclination": "3.50"

All said, this one line suffices:

// Inclination: bits 1..7; ignore bit 0 by right-shifting it out of memory
obj.inclination = (bytes[2] >> 1) * 1.5;

Finally, it doesn’t seem complete, as how did you get the battery value that you posted then?

Even better, the documentation includes the following full working example for TTN:

3.2.5. Example JavaScript Decode
// Decode an uplink message from an array of bytes to an object of fields
function Decoder(bytes, port)
{
  var decoded = {};
  if (port === 1)
  {
    decoded.type = "status";
    decoded.inTrip = ((bytes[0] & 0x1) !== 0) ? true : false;
    decoded.batV = 2.0 + 0.014 * (bytes[0] >> 1);
    decoded.temp = -40.0 + 0.5 * bytes[1];
    if (bytes.length >= 4)
    {
      decoded.manDown = ((bytes[2] & 0x1) !== 0) ? true : false;
      decoded.inclinationDeg = (bytes[2] >> 1) * 1.5;
      decoded.azimuthDeg = bytes[3] * 1.5;

      // Extra derived angles
      decoded.xyz = {};
      {
        // The direction of 'down' in rectangular coordinates,
        // unit vector.
        d = decoded.xyz.downUnit =
          [
            Math.sin(decoded.inclinationDeg * Math.PI / 180) *
            Math.sin(decoded.azimuthDeg * Math.PI / 180),
            Math.cos(decoded.inclinationDeg * Math.PI / 180),
            Math.sin(decoded.inclinationDeg * Math.PI / 180) *
            Math.cos(decoded.azimuthDeg * Math.PI / 180),
          ];

        // The azimuthal angles about each axis, right-handed, in degrees.
        // You can set up triggers on these angles. These trigger angles
        // are not well defined if the inclination is within 7 degrees of
        // vertical, and will not trigger within that range.
        hypX = Math.sqrt(d[1]*d[1] + d[2]*d[2]);
        hypY = Math.sqrt(d[2]*d[2] + d[0]*d[0]);
        hypZ = Math.sqrt(d[0]*d[0] + d[1]*d[1]);
        decoded.xyz.azimuthDeg =
          [
            (hypX < 0.125) ? null : (Math.atan2(d[2], d[1]) * 180 / Math.PI),
            (hypY < 0.125) ? null : decoded.azimuthDeg,
            (hypZ < 0.125) ? null : (Math.atan2(d[1], d[0]) * 180 / Math.PI),
          ];
        if (decoded.xyz.azimuthDeg[0] < 0)
          decoded.xyz.azimuthDeg[0] += 360;
        if (decoded.xyz.azimuthDeg[2] < 0)
          decoded.xyz.azimuthDeg[2] += 360;

        // The angle between each axis and 'down', in degrees.
        // You can set up triggers on these angles.
        // They are always well defined.
        iX = 1 - ((d[0]-1)*(d[0]-1) + d[1]*d[1] + d[2]*d[2]) / 2;
        iZ = 1 - (d[0]*d[0] + d[1]*d[1] + (d[2]-1)*(d[2]-1)) / 2;
        iX = Math.max(iX, -1);
        iX = Math.min(iX, +1);
        iZ = Math.max(iZ, -1);
        iZ = Math.min(iZ, +1);
        decoded.xyz.inclinationDeg =
          [
            Math.acos(iX) * 180 / Math.PI,
            decoded.inclinationDeg,
            Math.acos(iZ) * 180 / Math.PI,
          ];
      }
    }
    else
    {
      decoded.manDown = null;
      decoded.inclinationDeg = null;
      decoded.azimuthDeg = null;
      decoded.xyz = null;
    }
  }
  else if (port === 2)
  {
    decoded.type = "downlink ack";
    decoded.sequence = (bytes[0] & 0x7F);
    decoded.accepted = ((bytes[0] & 0x80) !== 0) ? true : false;
    decoded.fwMaj = bytes[1];
    decoded.fwMin = bytes[2];
  }
  else if (port === 3)
  {
    decoded.type = "stats";
    decoded.initialBatV = 2.0 + 0.014 * (bytes[0] & 0x7F);
    decoded.uptimeWeeks = (bytes[0] >> 7) + bytes[1] * 2;
    decoded.txCount = 32 * (bytes[2] + bytes[3] * 256);
    decoded.tripCount = 32 * (bytes[4] + bytes[5] * 256);
    decoded.wakeupsPerTrip = bytes[6];
  }
  else if (port === 4)
  {
    decoded.type = "rtc request";
    decoded.wasSet = ((bytes[0] & 0x01) !== 0) ? true : false;
    decoded.cookie = (bytes[0] + bytes[1] * 256 + bytes[2] * 65536 +
      bytes[3] * 16777216) >>> 1;
    // seconds since 2013-01-01
    decoded.timestamp = bytes[4] + bytes[5] * 256 + bytes[6] * 65536 +
      bytes[7] * 16777216;
    // Date() takes milliseconds since 1970-01-01
    decoded.time = (new Date((decoded.timestamp + 1356998400) * 1000))
      .toUTCString();
  }
  return decoded;
}

Finally, they offer a nice online decoder, which shows the following for your payload. This shows your decoding is wrong:

{
  "type": "status",
  "inTrip": false,
  "batV": 2.43,
  "temp": 26,
  "manDown": false,
  "inclinationDeg": 90,
  "azimuthDeg": 3,
  "xyz": {
    "downUnit": [0.05234, 6.123e-17, 0.9986],
    "azimuthDeg": [90, 3, null],
    "inclinationDeg": [87, 90, 3]
  }
}

Great documentation!