miro
Cargo

Payload Format

1. Overview

1.1. Payload Types

Port Type Description

100

Welcome

Device type, firmware version, hardware ID. Sent once after reboot

101

Status

Battery level, buffer status information and sensor data

103

Location

GPS location information and time

150-200

Proprietary

Refer to application-specific documentation

212

Git

Git revision. Sent once after reboot.

220

Configuration

Device configuration using AT command downlinks

1.2. Number Encoding

The endianness of data contained in the payloads is big-endian unless noted differently. Signed numbers are encoded as 2’s complement.

2. Payload Description

2.1. Welcome Message

Contains information about the device and firmware. The welcome message is sent only once after reboot.

Byte Size Description Format

0

1

Device type (Tracker=1)

enum

1

1

Device sub-type (miro Nomad=1, miro Cargo=3)

enum

2-5

4

Firmware version hash

uint32

6

1

Reset source (1=WU, 2=PIN, 3=LPW, 4=SW, 5=POR, 6=IWDG, 7=WWDG)

enum

7-14

8

Hardware ID

uint64_t

2.2. Status Message

Contains general status information and environmental sensor data.

Byte Size Description Format

0-7

8

System time (ms since reset)

uint64_t, ms

8-11

4

UTC Date

uint32, DDMMYY

12-15

4

UTC Time

uint32, HHMMSS

16-17

2

Buffer level (STA)

uint16

18-19

2

Buffer level (GPS)

uint16

20-21

2

Buffer level (ACC)

uint16

22-23

2

Buffer level (LOG)

uint16

24-25

2

Temperature

int16, 0.1 °C

26-27

2

Pressure

uint16, 0.1 hPa

28-29

2

Orientation X

int16, mG

30-31

2

Orientation Y

int16, mG

32-33

2

Orientation Z

int16, mG

34-35

2

Battery voltage

uint16, mV

36

1

LoRaWAN battery level (1 to 254)

uint8

37

1

Last TTF (time to fix)

uint8, s

38-39

2

NMEA sentences checksum OK

uint16

40-41

2

NMEA sentences checksum fail

uint16

42-43

2

Total GPS signal to noise (0-99 for each satellite)

uint16, C/n0 [dBHz]

44

1

GPS satellite count Navstar

uint8

45

1

GPS satellite count Glonass

uint8

46

1

GPS satellite count Galileo

uint8

47

1

GPS satellite count Beidou

uint8

48-49

2

GPS dilution of precision

uint16, cm

2.3. Location Message

Contains GPS time and location information. If the payload is all zeros, the miro Cargo could not acquire a GPS fix.

Byte Size Description Format

0-3

4

UTC Date

uint32, DDMMYY

4-7

4

UTC Time

uint32, HHMMSS

8-11

4

Latitude

int32, 1/100'000 deg

12-15

4

Longitude

int32, 1/100'000 deg

16-19

4

Altitude

int32, 1/100 m

2.4. Git Revision

Contains the Git revision of the firmware build. The Git message is sent only once after reboot.

Byte Size Description Format

0-19

20

Git Revision

binary/hex

2.5. Configuration commands and responses

Configuration downlink commands and responses are sent as plain text. Note that commands need to be zero-terminated. Please refer to our Configuration documentation for more information.

Byte Size Description Format

0-(n-1)

n

AT command

ASCII

n

1

zero-termination (0x00)

char

Byte Size Description Format

0-(n-1)

n

Reply to previous AT command

ASCII

n

1

zero-termination (0x00)

char

3. JavaScript Decoder

For an easy start using miro Cargo on TTN or TTI you can make use of the following JavaScript decoder template.

function Decoder(payload, metadata) {
    // Decode an uplink message from a buffer
    // Payload - array of bytes
    // Metadata - key/value object

    /** Decoder **/

    // Decode payload to JSON
    var data = decodeToJson(payload);

    if (!("end_device_ids" in data && "uplink_message" in data)) {
        return {};
    }

    var deviceName = data.end_device_ids.dev_eui;
    var deviceType = 'miro Nomad';
    var groupName = 'Tracker Demos';

    data = data.uplink_message;

    var payload_hex = base64ToHex(data.frm_payload);
    var bytes = hexToBytes(payload_hex);
    var decoded = Decode(bytes, data.f_port);

    // Result object with device/asset attributes/telemetry data
    var result = {
        // Use deviceName and deviceType or assetName and assetType, but not both.
        deviceName: deviceName,
        deviceType: deviceType,
        groupName: groupName,
        attributes: {
            dr: data.settings.data_rate.lora.spreading_factor,
        },
        telemetry: decoded,
    };

    if (data.f_port == 100) {
        // Welcome message
        // Input example: 01010503000a04393137337e377e0d

        switch (bytes[1]) {
            case 1:
                result.deviceType = 'miro Nomad';
                break;
            case 3:
                result.deviceType = 'miro Cargo';
                break;
            default:
                break;
        }

        result.attributes.firmware = 'v' + bytes[2].toString() + '.' + bytes[3].toString() + '.' + (bytes[4] * 256 + bytes[5]).toString();
    }


    if (data.f_port == 212) {
        // GIT revision
        // Returns base64-encoded HEX string of firmware GIT revision
        // Input example: 41e44999f5678de41701cbcb22fcc55bbb4c5bfb

        result.attributes.git_revision = data.data;
    }

    // Use RSSI and SNR of the first gateway in the gateway list
    var rssiGW0 = data.rx_metadata[0].rssi;
    var snrGW0 = data.rx_metadata[0].snr;
    var freq = parseInt(data.settings.frequency);

    if (Array.isArray(result.telemetry)) {
        var obj = {
            'bat': data.bat,
            'rssi': rssiGW0,
            'freq': freq,
            'snr': snrGW0,
            'hex': payload_hex,
            'port': data.f_port,
        };
        result.telemetry.push(obj);
    } else if ("ts" in result.telemetry) {
        array = [];
        array.push(result.telemetry);
        var obj = {
            'bat': data.bat,
            'rssi': rssiGW0,
            'freq': freq,
            'snr': snrGW0,
            'hex': payload_hex,
            'port': data.f_port,
        };
        array.push(obj);
        result.telemetry = array;
    } else {
        result.telemetry.bat = data.bat;
        result.telemetry.rssi = rssiGW0;
        result.telemetry.freq = freq;
        result.telemetry.snr = snrGW0;
        result.telemetry.hex = payload_hex;
        result.telemetry.port = data.f_port;
    }

    /** Helper functions **/

    function base64ToHex(str) {
        var raw = atob(str);
        var result = '';
        for (var i = 0; i < raw.length; i++) {
            var hex = raw.charCodeAt(i).toString(16);
            result += (hex.length === 2 ? hex : '0' + hex);
        }
        return result;
    }

    function decodeToString(payload) {
        return String.fromCharCode.apply(String, payload);
    }

    function decodeToJson(payload) {
        // Convert payload to string
        var str = decodeToString(payload);

        // Parse string to JSON
        var data = JSON.parse(str);
        return data;
    }

    function Decode(bytes, port) {
        // Decode an uplink message from a buffer
        // (array) of bytes to an object of fields.

        var decoded = {};

        if (port === 100) {
            decoded.reset_source = bytes[6];
        }

        if (port === 101) {
            // Status message
            // Input example: 000000000b290481000334cb0002d57c0000000100000000010a03b2ffc0006003d00fcca624144d001a0002000000
            // Returns status information
            // Note: Not all fields are taken into account here

            var system_time_ms = (bytes[0] << 56 | bytes[1] << 48 | bytes[2] << 40 | bytes[3] << 32 | bytes[4] << 24 | bytes[5] << 16 | bytes[6] << 8 | bytes[7]) >>> 0;
            var temperature = (bytes[24] << 8 | bytes[25]) >>> 0;
            var pressure = (bytes[26] << 8 | bytes[27]) >>> 0;
            var x = (bytes[28] << 8 | bytes[29]) >>> 0;
            var y = (bytes[30] << 8 | bytes[31]) >>> 0;
            var z = (bytes[32] << 8 | bytes[33]) >>> 0;
            var battery_mv = (bytes[34] << 8 | bytes[35]) >>> 0;

            var dat = (bytes[8] << 24 | bytes[9] << 16 | bytes[10] << 8 | bytes[11]) >>> 0;
            var tim = (bytes[12] << 24 | bytes[13] << 16 | bytes[14] << 8 | bytes[15]) >>> 0;

            // Conversion to signed integer (2's complement)
            if (temperature > 0x7FFF) {
                temperature = -(0xFFFF - temperature + 1);
            }
            if (x > 0x7FFF) {
                x = -(0xFFFF - x + 1);
            }
            if (y > 0x7FFF) {
                y = -(0xFFFF - y + 1);
            }
            if (z > 0x7FFF) {
                z = -(0xFFFF - z + 1);
            }

            decoded = [];
            var ts = getTimestamp(dat, tim);
            decoded.push({
                ts: ts,
                values: {
                    battery_lorawan: bytes[36],
                    gps_ttf_s: bytes[37],
                    gps_signal: bytes[42],
                    battery_v: battery_mv / 1000.0,
                    system_time_s: system_time_ms / 1000.0,
                    temperature_deg: temperature / 10.0,
                    pressure_hpa: pressure / 1.0,
                    orientation_x_g: x / 1000.0,
                    orientation_y_g: y / 1000.0,
                    orientation_z_g: z / 1000.0,
                    sta_date_ddmmyy: dat,
                    sta_time_hhmmss: tim,
                    sta_unixtime_ms: ts,
                },
            });
        }

        if (port === 103) {
            // Location message
            // Returns date and time as DDMMYY and HHMMSS
            // Returns latitude, longitude and altitude as float
            // Input example: 0002717900024c6e00396fc4fff44f9500001d7e

            var dat = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
            var tim = (bytes[4] << 24 | bytes[5] << 16 | bytes[6] << 8 | bytes[7]) >>> 0;
            var lat = (bytes[8] << 24 | bytes[9] << 16 | bytes[10] << 8 | bytes[11]) >>> 0;
            var lon = (bytes[12] << 24 | bytes[13] << 16 | bytes[14] << 8 | bytes[15]) >>> 0;
            var alt = (bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19]) >>> 0;

            // Conversion to signed integer (2's complement)
            if (lat > 0x7FFFFFFF) {
                lat = -(0xFFFFFFFF - lat + 1);
            }
            if (lon > 0x7FFFFFFF) {
                lon = -(0xFFFFFFFF - lon + 1);
            }
            if (alt > 0x7FFFFFFF) {
                alt = -(0xFFFFFFFF - alt + 1);
            }

            if ((lat != 0) && (lon != 0)) {
                decoded = [];
                var ts = getTimestamp(dat, tim);
                decoded.push({
                    ts: ts,
                    values: {
                        gps_dat: dat,
                        gps_tim: tim,
                        gps_lat: (lat / 100000.0),
                        gps_lon: (lon / 100000.0),
                        gps_alt: (alt / 100.0),
                        unixtime_ms: ts,
                    },
                });
            }
        }

        if (port === 220) {
            // AT interface
            // Input example: 41545f4552524f52
            // Returns base64-encoded ASCII string of AT command response

            decoded.at_reply = bin2String(bytes);
        }

        return decoded;
    }

    // Convert a hex string to a byte array
    function hexToBytes(hex) {
        for (var bytes = [], c = 0; c < hex.length; c += 2)
            bytes.push(parseInt(hex.substr(c, 2), 16));
        return bytes;
    }

    function getTimestamp(ddmmyy, hhmmss) {
        day = parseInt(ddmmyy / 10000, 10);
        month = parseInt(ddmmyy / 100 - day * 100, 10);
        year = parseInt(ddmmyy - month * 100 - day * 10000 + 2000, 10);
        hour = parseInt(hhmmss / 10000, 10);
        minute = parseInt(hhmmss / 100 - hour * 100, 10);
        second = parseInt(hhmmss - minute * 100 - hour * 10000, 10);

        date = new Date(year, month - 1, day, hour, minute, second)
        return date.valueOf();
    }

    function bin2String(array) {
        var result = "";
        for (var i = 0; i < array.length; i++) {
            result += String.fromCharCode(parseInt(array[i], 2));
        }
        return result;
    }

    return result;
}