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 |
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;
}