{
  "name": "BLG20 protobuf CoAP",
  "type": "DEFAULT",
  "transportType": "COAP",
  "provisionType": "DISABLED",
  "description": "BLG20 gateway, protobuf over the hosted CoAP transport. Schema = src/nrf9151-app/src/blg20.proto verbatim.",
  "image": null,
  "defaultRuleChainId": null,
  "defaultDashboardId": null,
  "defaultQueueName": null,
  "provisionDeviceKey": null,
  "profileData": {
    "configuration": {
      "type": "DEFAULT"
    },
    "transportConfiguration": {
      "type": "COAP",
      "coapDeviceTypeConfiguration": {
        "coapDeviceType": "DEFAULT",
        "transportPayloadTypeConfiguration": {
          "transportPayloadType": "PROTOBUF",
          "deviceTelemetryProtoSchema": "// blg20.proto \u2014 the satellite-safe uplink, SPEC \u00a719.\n//\n// THE FIELD NUMBERS ARE THE CBOR KEYS. That is the whole design of \u00a707 D2: one set of numbers,\n// two wire formats, and a consumer that has decoded either one holds the same fields. encode_pb.c\n// writes these numbers as protobuf tags; encode_cbor.c writes the same numbers as CBOR map keys.\n// If a number changes here it must change in both encoders and in the host rig, and the rig will\n// say so \u2014 tools/test_encoders.py decodes the protobuf side straight out of THIS file.\n//\n// Append only. Never renumber: a gateway in the field encodes with the table it was built\n// against and there is no way to ask it which that was (record.h says the same thing about the\n// record keys, and for the same reason).\n//\n// \u2500\u2500 Three things this file is NOT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// 1. It is not generated/05-data-plane/record.proto. That file is the shape ThingsBoard's HOSTED\n//    CoAP transport demands \u2014 `optional int64 ts = 1; Values values = 2;`, one record per POST,\n//    validated by DeviceProfileDataValidator. This file is \u00a719's Uplink envelope, which that\n//    validator will refuse (its top-level message is not {ts, values}). The two are different\n//    contracts for different consumers and both are real. See F-M3-7.\n//\n// 2. It is not fed to nanopb in this build. `protoc` is absent on the authoring host, so the\n//    generator could not run; encode_pb.c is a hand-written writer against these numbers, and the\n//    host rig proves its bytes decode in a standard protobuf parser driven by this file. See\n//    F-M3-3 for the generator command and the NCS check.\n//\n// 3. It carries no options, no imports and no `required`. ThingsBoard's schema validator refuses\n//    a profile containing any of them, and a schema that cannot also be pasted into a device\n//    profile is a schema with one fewer use.\n\nsyntax = \"proto3\";\n\npackage blg20;\n\n// One alert, as \u00a709 defines it. Ten bytes is the budget; the short id (dev) is what buys it \u2014\n// two bytes instead of a six-byte address, which is the difference between eight alerts in a\n// datagram and three.\nmessage Alert {\n\t// \u2500\u2500 FIELD 1 IS RETIRED. NEVER REUSE IT. \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t//\n\t// It was `uint32 seq`, the gateway sequence repeated on every alert. It went on 13 Sep\n\t// 2026 because `Uplink.gw_seq` already stamps the datagram and an alert's position in\n\t// the repeated field already orders it inside one \u2014 so the field was three bytes an\n\t// alert to say twice what the envelope said once, and three bytes an alert is the\n\t// difference between one alert and two in a 128-byte satellite datagram (F-M3-19).\n\t//\n\t// WHY THERE IS NO `reserved 1;` HERE, WHICH IS WHAT YOU WOULD EXPECT. ThingsBoard's\n\t// DynamicProtoUtils.validateProtoSchema refuses a device-profile schema that declares a\n\t// reserved range at all \u2014 read out of the platform's source and written up in\n\t// generated/05-data-plane/record.proto. The keyword that protects the number would cost\n\t// the profile the transport needs. So the number is protected by this comment and by a\n\t// check in tools/test_encoders.py, which fails if any message ever declares a field 1\n\t// in Alert.\n\tuint32 dev    = 2;   // 16-bit short id; the device table resolves it, never the datagram\n\tuint32 rule   = 3;\n\tsint32 value  = 4;   // scaled per rule. sint32, not int32: a negative int32 varint is ten\n\t\t\t     // bytes, and the values that alert are as often below a threshold as above\n\tuint32 sev    = 5;\n\tuint32 flags  = 6;   // bit0 enter, bit1 exit, bit2 latched\n\tuint32 age_s  = 7;   // age at the moment of transmission, not an absolute time: on satellite\n\t\t\t     // the attach alone can be four minutes and the datagram is stamped at send\n}\n\n// One status per window. \u00a709: \"nothing to report\" must still be reported, cheaply, so that\n// silence means a fault.\nmessage Status {\n\tuint32 win             = 1;\n\tuint32 seen            = 2;\n\tuint32 expected        = 3;\n\tuint32 missing         = 4;\n\trepeated uint32 missing_ids = 5;   // the first few short ids, only when the datagram has room\n\tuint32 alerts_active   = 6;\n\tuint32 alerts_deferred = 7;\n\tuint32 batt_low        = 8;\n\tuint32 store_depth     = 9;\n\tuint32 dropped         = 10;\n\tuint32 quota_pct       = 11;\n\tuint32 bearer          = 12;\n\tsint32 rsrp            = 13;\n\tuint32 uptime_s        = 14;\n\tuint32 reset_cause     = 15;\n\tuint32 flags           = 16;       // bit0 time_unsynced, bit1 config_on_test\n}\n\n// One field of a digest: the per-field count / min / max / last that \u00a709 calls the digest product.\nmessage Field {\n\tuint32 id   = 1;\n\tuint32 n    = 2;\n\tsint32 min  = 3;\n\tsint32 max  = 4;\n\tsint32 last = 5;\n\tsint32 sum  = 6;\n}\n\n// One device over one window.\nmessage Digest {\n\tuint32 dev          = 1;\n\tuint32 win          = 2;\n\trepeated Field f    = 3;\n\tuint32 seen         = 4;\n\tsint32 rssi_mean    = 5;\n\tuint32 batt         = 6;\n}\n\n// The daily health record (SPEC 10, \"Diagnostics from a desk\").\n//\n// \"Per day on terrestrial: bearer transitions and their reasons, attach durations, socket errors,\n// admission drops per device, session outcomes, modem temperature (%XTEMP), watchdog and fault\n// counts, storage wear | ~200 B per day; withheld on satellite unless requested.\"\n//\n// EVERY FIELD IS A DELTA OVER THE WINDOW, not a lifetime total, with two exceptions that are\n// marked: erase_cycles and boots, which are lifetime facts about the hardware and are the numbers\n// a fleet plans replacements against. A delta is what a daily record is for \u2014 \"eleven attaches\n// yesterday, worst one 94 seconds\" is actionable and \"4,182 attaches since the unit was built\" is\n// not \u2014 and a counter that a reboot resets would otherwise send a negative day.\n//\n// WHY reason_n IS A REPEATED SCALAR AND NOT A MAP. proto3 maps are a synthetic nested message per\n// entry and ThingsBoard's schema validator refuses several of the constructs they need; a packed\n// repeated uint32 indexed by enum bearer_reason costs one tag, one length and one byte per\n// non-trivial count. The index IS the reason, which is a closed set on both chips (bearer.h), so\n// the two cannot drift without the build breaking.\nmessage Health {\n\tuint32 day             = 1;   // days since the unix epoch. The window this record covers\n\tuint32 window_s        = 2;   // how long it actually spans; a unit that rebooted sends less\n\t                              // than a day and must not be read as if it sent one\n\n\t// The bearer, which is where almost every field failure on this product shows up first.\n\tuint32 transitions     = 3;\n\trepeated uint32 reason_n = 4; // indexed by enum bearer_reason; trailing zeroes trimmed\n\tuint32 attaches        = 5;   // successful ones\n\tuint32 attach_min_s    = 6;\n\tuint32 attach_avg_s    = 7;\n\tuint32 attach_max_s    = 8;\n\tuint32 sock_errors     = 9;\n\tuint32 modem_reinits   = 10;  // the modem watchdog fired and reinitialised the library\n\tsint32 modem_temp_c    = 11;  // %XTEMP, last read\n\tsint32 modem_temp_max  = 12;\n\n\t// The BLE half's, over FRAME_HEALTH. Nothing here is computed on this chip.\n\tuint32 admission_drops = 13;\n\tuint32 sess_ok         = 14;\n\tuint32 sess_partial    = 15;\n\tuint32 sess_refused    = 16;\n\tuint32 sess_timeout    = 17;\n\n\t// Self-protection and wear.\n\tuint32 wdt_resets      = 18;\n\tuint32 faults          = 19;\n\tuint32 boots           = 20;  // LIFETIME\n\tuint32 erase_cycles    = 21;  // LIFETIME. SPEC 10: \"reported yearly in the health record\"\n\tuint32 store_dropped   = 22;  // retired by the retention policy over the window\n\tuint32 store_depth     = 23;  // records held at the moment the record was built\n\tuint32 uptime_s        = 24;  // of the cellular half\n\n\t// The evaluation cap's counters. APPENDED, never inserted, and written only by an\n\t// evaluation image: a production unit sends none of them and the message it sends is the\n\t// message it always sent. A reader that does not know these three sees unknown fields,\n\t// which is what proto3 is for.\n\tuint32 demo_win_s      = 25;  // bearer-up seconds spent in the window that has just ended\n\tuint32 demo_total_s    = 26;  // LIFETIME bearer-up seconds against the allowance\n\tuint32 demo_rearm_s    = 27;  // seconds until the window re-arms; 0 = armed\n}\n\n// One datagram's body. NOT the top-level message any more \u2014 see Telemetry at the foot of this\n// file, which is what actually goes on the wire and which is declared last for the reason this\n// comment used to give.\nmessage Uplink {\n\t// NOT WRITTEN WHEN THIS BODY IS WRAPPED IN `Telemetry` \u2014 which, since 13 Sep 2026, is\n\t// every protobuf datagram: `Telemetry.ts` carries the same value and carrying it twice\n\t// cost seven bytes of a satellite payload (F-M3-18). It is still written by an\n\t// unwrapped body, and the CBOR form (key 1) always carries it, because CBOR has no\n\t// envelope. A proto3 decoder reads the absent field back as 0, so both shapes decode.\n\tuint64 ts_ms            = 1;\n\tuint32 gw_seq           = 2;\n\n\t// SPEC \u00a719 names this field `class`. It is `cls` here for exactly the reason\n\t// link_proto.h gives for `struct cfg_dpl.cls`: this schema is read by C and C++ tooling\n\t// and by a nanopb generator that will emit the name verbatim into a struct. The NUMBER\n\t// is the contract and the number is unchanged; the CBOR side carries the same 3 under the\n\t// same name (encode.h, UPF_CLASS). See F-M3-4.\n\tuint32 cls              = 3;\n\n\trepeated Status status  = 4;\n\trepeated Alert  alert   = 5;\n\trepeated Digest digest  = 6;\n\n\t// The raw records, as the CBOR array record.c already emits \u2014 key for key, byte for byte.\n\t//\n\t// Why a blob and not `repeated Digest`: the digest engine is M4. Until it exists the thing\n\t// the gateway actually holds is a BM20 digest record, and there is exactly one encoder for\n\t// it in this product (record.c) which is already the reference for the ThingsBoard CBOR\n\t// converter and for tests/record/digest.golden.hex. Wrapping those bytes keeps ONE record\n\t// encoder rather than a second one that can drift; M4 replaces this field with `digest`\n\t// and the field number stays reserved either way.\n\tbytes raw_cbor          = 7;\n\n\t// M6. Once a day, on a terrestrial window, and NEVER on satellite unless it is asked for\n\t// (SPEC 10). It rides the same datagram as everything else rather than costing one of its\n\t// own, which is the same rule the status and the alerts follow.\n\tHealth health           = 8;\n}\n\n// \u2500\u2500 The envelope the hosted transport insists on \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// ThingsBoard's DeviceProfileDataValidator.validateTelemetryDynamicMessageFields refuses a\n// telemetry schema whose top-level message is not EXACTLY `optional int64 ts` plus a message\n// field named `values` \u2014 measured against a live tenant on 11 September 2026 and written up in\n// generated/05-data-plane/record.proto. `Uplink` is not that shape and never will be, so it is\n// wrapped rather than bent: the datagram body is unchanged, and this is the two-field lid the\n// platform's validator is looking for.\n//\n// `optional` on ts is mandatory, not stylistic \u2014 the validator names it in the refusal\n// (\"Field 'ts' should have optional keyword!\"), and in proto3 it also makes the field a member\n// of a synthetic oneof, which is what keeps an unset ts out of the JSON the platform prints.\n//\n// Set ts ONLY to a real unix epoch in milliseconds. parseWithTs takes the value verbatim, so a\n// device uptime here files the sample in January 1970. Left out, the platform stamps arrival.\n//\n// DECLARED LAST, on purpose: DynamicProtoUtils.getDynamicMessageBuilder reduces the file's\n// top-level types to the last one, so this rule cannot pick the wrong message.\nmessage Telemetry {\n\toptional int64 ts = 1;\n\tUplink values     = 2;\n}\n",
          "deviceAttributesProtoSchema": "syntax = \"proto3\";\npackage attributes;\nmessage Attributes {\n  string devTable = 1;\n  string rules = 2;\n}",
          "deviceRpcRequestProtoSchema": "syntax = \"proto3\";\npackage rpc;\nmessage RpcRequestMsg {\n  optional string method = 1;\n  optional int32 requestId = 2;\n  optional string params = 3;\n}",
          "deviceRpcResponseProtoSchema": "syntax = \"proto3\";\npackage rpc;\nmessage RpcResponseMsg {\n  optional string payload = 1;\n}"
        }
      },
      "clientSettings": {
        "edrxCycle": 0,
        "powerMode": "DRX",
        "psmActivityTimer": 0,
        "pagingTransmissionWindow": 0
      }
    },
    "provisionConfiguration": {
      "type": "DISABLED",
      "provisionDeviceSecret": null
    },
    "alarms": null
  }
}