diff --git a/.github/workflows/ascii-dash.yml b/.github/workflows/ascii-dash.yml new file mode 100644 index 000000000..297f5871b --- /dev/null +++ b/.github/workflows/ascii-dash.yml @@ -0,0 +1,44 @@ +name: ascii-dash + +# Fails a PR that introduces non-ASCII dashes (Unicode General Punctuation +# U+2010-U+2015: hyphen, non-breaking hyphen, figure dash, en dash, em dash, +# horizontal bar) in comments. These should be a plain ASCII '-'. buf format +# only normalizes proto syntax, not comment text, so this is a separate gate. +# +# Runs from the base branch on every PR (including forks), so contributors +# cannot disable it within their own PR. Needs no secrets or write access. + +on: pull_request + +permissions: + contents: read + +jobs: + ascii-dash: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # No authenticated git operations follow, so don't leave the token + # in the local git config for later steps. + persist-credentials: false + + - name: Reject non-ASCII dashes + run: | + # git grep exits 0 when it finds a match (which is the failure case + # here), 1 when it finds none (the pass case), and anything else on + # tool error, which must also fail the check rather than silently + # pass. LC_ALL=C forces byte-wise matching so the \x escapes refer + # to raw UTF-8 bytes rather than being reinterpreted as codepoints. + status=0 + LC_ALL=C git grep -nP '\xe2\x80[\x90-\x95]' -- \ + '*.proto' '*.options' '*.md' '*.yml' '*.yaml' '*.kts' || status=$? + if [ "$status" -eq 0 ]; then + echo "::error::Non-ASCII dash (U+2010-U+2015) found above. Replace it with an ASCII '-'." + exit 1 + elif [ "$status" -ne 1 ]; then + echo "::error::git grep failed with exit code $status; cannot verify dashes." + exit "$status" + fi + echo "No non-ASCII dashes found." diff --git a/.github/workflows/snapshot-kmp.yml b/.github/workflows/snapshot-kmp.yml index f707cb768..8b2f78ba8 100644 --- a/.github/workflows/snapshot-kmp.yml +++ b/.github/workflows/snapshot-kmp.yml @@ -44,7 +44,7 @@ jobs: # The count goes after a DOT, not a hyphen, and that is load-bearing. In # Maven's ComparableVersion a dot-separated numeric segment outranks ANY # hyphen-nested token, so these sort ABOVE the legacy `--SNAPSHOT` - # snapshots still in the repo — whose SHA can tokenize to a huge integer + # snapshots still in the repo - whose SHA can tokenize to a huge integer # (e.g. 678281c -> 678281) that would otherwise dwarf the count and mask # every new snapshot from dependency bots. Among themselves, higher count = # newer commit sorts higher, so bots pick the newest. Still sorts ABOVE the @@ -70,7 +70,7 @@ jobs: run: | set -euo pipefail if [ -z "${ORG_GRADLE_PROJECT_mavenCentralUsername}" ] || [ -z "${ORG_GRADLE_PROJECT_mavenCentralPassword}" ]; then - echo "Missing Maven Central secrets — skipping snapshot publish." >&2 + echo "Missing Maven Central secrets - skipping snapshot publish." >&2 exit 0 fi packages/kmp/gradlew --no-daemon -p packages/kmp publishAllPublicationsToMavenCentralRepository -PVERSION_NAME=${{ env.VERSION_NAME }} diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index d120a2c6d..305079162 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -609,7 +609,7 @@ message LockdownAuth { * token at unlock time: the client-supplied boots_remaining when * non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS). * Note that boots_remaining == 0 in this message means "use firmware - * default", NOT "zero boots" — a client computing the ceiling for + * default", NOT "zero boots" - a client computing the ceiling for * display should mirror that resolution rather than multiplying the * raw request value. * @@ -619,7 +619,7 @@ message LockdownAuth { * * Uses millis() (CPU uptime), not wall-clock time, so the cap is * immune to GPS spoofing, RTC backup-battery removal, and Faraday - * cage isolation — none of those move the uptime counter. The only + * cage isolation - none of those move the uptime counter. The only * way to reset the session clock is a reboot, which costs a boot * from the on-flash, HMAC-bound counter. */ @@ -639,7 +639,7 @@ message LockdownAuth { * * NOT reversed by this operation: APPROTECT. Once the debug port * lockout has been burned (on silicon where it is effective) it is - * permanent — disabling lockdown decrypts your data and removes the + * permanent - disabling lockdown decrypts your data and removes the * access gates, but the SWD/JTAG port stays locked for the life of * the device (recoverable only via a full chip erase over a debug * probe, which destroys all data). Clients should make this diff --git a/meshtastic/atak.options b/meshtastic/atak.options index 29ec3e109..8ff4ad1fb 100644 --- a/meshtastic/atak.options +++ b/meshtastic/atak.options @@ -70,7 +70,7 @@ *RangeAndBearing.stroke_weight_x10 int_size:16 # Route pool sizing. 16 links x ~24B each ~= 384B worst case. prefix is -# ATAK's short waypoint name prefix ("CP", "RP", etc.) — 8 chars is plenty. +# ATAK's short waypoint name prefix ("CP", "RP", etc.) - 8 chars is plenty. *Route.links max_count:16 *Route.prefix max_size:8 *Route.stroke_weight_x10 int_size:16 diff --git a/meshtastic/atak.proto b/meshtastic/atak.proto index 7eaec5fe4..9d5a15d4a 100644 --- a/meshtastic/atak.proto +++ b/meshtastic/atak.proto @@ -898,7 +898,7 @@ enum CotType { // -- TAKTALK plugin shapes -- // CoT types unique to the TAKTALK ATAK plugin. Note `y-` has a literal - // trailing dash and no second atom — that's the wire format ATAK emits + // trailing dash and no second atom - that's the wire format ATAK emits // for TAKTALK room broadcasts. The CotType enum encodes the literal // string verbatim (CotType_y -> "y-") so receivers reconstruct the // original event type byte-for-byte without consulting cot_type_str. @@ -913,7 +913,7 @@ enum CotType { * y-: TAKTALK room/membership broadcast. Payload carried via the * TakTalkRoomData typed variant (sender_callsign, room_id, room_name, * participants). The CoT type literally has a trailing dash and no - * second atom — not a typo. + * second atom - not a typo. */ CotType_y = 126; } @@ -998,10 +998,10 @@ message AircraftTrack { * hundred meters of the anchor has per-vertex deltas in the ±10^4 range. * Under sint32+zigzag those encode as 2 bytes each (tag+varint), versus the * 4 bytes that sfixed32 would always require. At 32 vertices that is ~128 - * bytes of savings — the difference between fitting under the LoRa MTU or + * bytes of savings - the difference between fitting under the LoRa MTU or * not. Absolute coordinates (values ~10^9) would cost sint32 varint 5 bytes * per field, which is why TAKPacketV2's top-level latitude_i / longitude_i - * stay sfixed32 — only small values win with sint32. + * stay sfixed32 - only small values win with sint32. */ message CotGeoPoint { /* @@ -1068,7 +1068,7 @@ message DrawnShape { Kind_Bullseye = 7; /* * u-d-c-e: Ellipse with distinct major/minor axes (same storage as - * Kind_Circle — uses major_cm/minor_cm/angle_deg — but receivers + * Kind_Circle - uses major_cm/minor_cm/angle_deg - but receivers * render it as a non-circular ellipse rather than a round circle). */ Kind_Ellipse = 8; @@ -1095,7 +1095,7 @@ message DrawnShape { */ enum StyleMode { /* - * Unspecified — receiver infers from which color fields are non-zero. + * Unspecified - receiver infers from which color fields are non-zero. */ StyleMode_Unspecified = 0; /* @@ -1176,7 +1176,7 @@ message DrawnShape { * Replaces the former `repeated CotGeoPoint vertices = 12` (one length- * delimited sub-message per vertex = ~2 B framing each). Packing into two * `repeated sint32` columns pays the field framing ONCE per column instead - * of once per vertex — ~58 B saved on a 32-vertex telestration — while + * of once per vertex - ~58 B saved on a 32-vertex telestration - while * keeping the same zigzag small-delta win CotGeoPoint had. Capped at 32 by * the nanopb pool; senders MUST truncate longer inputs and set `truncated`. * @@ -1229,7 +1229,7 @@ message Marker { */ enum Kind { /* - * Unspecified — fall back to TAKPacketV2.cot_type_id + * Unspecified - fall back to TAKPacketV2.cot_type_id */ Kind_Unspecified = 0; /* @@ -1330,7 +1330,7 @@ message Marker { * * Covers CoT type u-rb-a. The anchor position is on * TAKPacketV2.latitude_i/longitude_i; the target endpoint is carried as a - * CotGeoPoint — same delta-from-anchor encoding used by DrawnShape.vertices + * CotGeoPoint - same delta-from-anchor encoding used by DrawnShape.vertices * so a self-anchored RAB (common case) encodes in zero bytes. */ message RangeAndBearing { @@ -1569,7 +1569,7 @@ message CasevacReport { */ string frequency = 15; - // --- v2.x medline extensions (tags 16–33) -------------------------------- + // --- v2.x medline extensions (tags 16-33) -------------------------------- // // Fields 16+ cost a 2-byte tag instead of 1 byte, but they're usually // sparse so the on-wire delta is modest when most stay unset. A fully @@ -1586,7 +1586,7 @@ message CasevacReport { */ string title = 16; /* - * Primary medline free-text — the single most clinically important line + * Primary medline free-text - the single most clinically important line * on a MEDLINE form (e.g. "2 urgent litter patients, smoke on approach"). * MUST be preserved under MTU pressure as long as any casevac is sent. */ @@ -1594,7 +1594,7 @@ message CasevacReport { /* * Line 3 (newer ATAK format): patient counts by precedence level. - * Coexists with the enum-style `precedence` field (tag 1) — older ATAK + * Coexists with the enum-style `precedence` field (tag 1) - older ATAK * emits a single enum, newer ATAK emits these counts, and both can be * set simultaneously. Senders populate whichever style(s) the source * XML had; receivers prefer counts when non-zero. @@ -1665,7 +1665,7 @@ message CasevacReport { /* * Per-patient clinical records. Each entry is one patient's ZMIST card - * (Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable — + * (Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable - * a mass-casualty event can carry 1-6 entries in practice, limited by * the 237 B LoRa MTU. */ @@ -1673,7 +1673,7 @@ message CasevacReport { } /* - * Per-patient clinical summary record — one entry per patient in a CASEVAC. + * Per-patient clinical summary record - one entry per patient in a CASEVAC. * Maps directly to ATAK's child element inside . * All fields are optional free-text; senders populate what they have. */ @@ -1683,7 +1683,7 @@ message ZMistEntry { */ string title = 1; /* - * Zap number — unique patient tracking ID (often a terse code like + * Zap number - unique patient tracking ID (often a terse code like * "Gunshot" or a serial). */ string z = 2; @@ -1749,7 +1749,7 @@ message EmergencyAlert { * creation time; the fields below carry structured metadata the raw-detail * fallback currently loses. * - * Fields are deliberately lean — this variant is closer to the MTU ceiling + * Fields are deliberately lean - this variant is closer to the MTU ceiling * than the others, so every string is capped in options. */ message TaskRequest { @@ -1795,7 +1795,7 @@ message TaskRequest { /* * Weather annotation from CoT detail element. * - * Attaches to any TAKPacketV2 regardless of payload_variant — an Aircraft, + * Attaches to any TAKPacketV2 regardless of payload_variant - an Aircraft, * PLI, or Marker can all carry observed conditions at the emitting station. * ATAK-CIV ships an XSD for but no dedicated handler, so the * element round-trips through the generic detail pipeline; this message @@ -1804,7 +1804,7 @@ message TaskRequest { * Target wire cost: ~6-8 bytes compressed with a fully populated instance. * * Named `TAKEnvironment` (not just `Environment`) because the bare name - * collides with `SwiftUI.Environment` — every SwiftUI view in a consuming + * collides with `SwiftUI.Environment` - every SwiftUI view in a consuming * iOS app uses the `@Environment` property wrapper, and importing the * generated proto module would make `Environment` ambiguous in every one * of those files. The `TAK` prefix matches the convention used by the @@ -1841,7 +1841,7 @@ message TAKEnvironment { * The receiving ATAK client restores those from its own defaults, same as * every other CoT carried over Meshtastic today. * - * Attaches to any TAKPacketV2 — a PLI with a sensor on the operator's head, + * Attaches to any TAKPacketV2 - a PLI with a sensor on the operator's head, * an Aircraft with a FLIR turret, a Marker dropped on a UAV. * Target wire cost: ~7-14 bytes compressed (dominated by model string). */ @@ -1870,7 +1870,7 @@ message SensorFov { uint32 azimuth_deg = 2; /* * Maximum range of the cone in meters. - * Optional — if unset, receivers should use the ATAK-CIV default of 100m. + * Optional - if unset, receivers should use the ATAK-CIV default of 100m. */ optional uint32 range_m = 3; /* @@ -1880,7 +1880,7 @@ message SensorFov { uint32 fov_horizontal_deg = 4; /* * Vertical field of view in whole degrees. ATAK-CIV default is 45°. - * Optional — a value of 0 means "not set / use horizontal FOV". + * Optional - a value of 0 means "not set / use horizontal FOV". */ uint32 fov_vertical_deg = 5; /* @@ -1890,12 +1890,12 @@ message SensorFov { sint32 elevation_deg = 6; /* * Roll (camera tilt) in whole degrees, -180 to +180. - * Optional — use 0 if the sensor doesn't track roll. + * Optional - use 0 if the sensor doesn't track roll. */ sint32 roll_deg = 7; /* * Free-form device model identifier, e.g. "FLIR-Boson-640", "SEEK". - * Optional — empty string means "unknown model" (ATAK-CIV default). + * Optional - empty string means "unknown model" (ATAK-CIV default). */ string model = 8; } @@ -1904,7 +1904,7 @@ message SensorFov { * TAKTALK chat message payload (CoT type m-t-t). * * TAKTALK is an ATAK plugin for voice + text team messaging. The voice - * audio stream goes over UDP/RTP and is NOT carried by the mesh — only + * audio stream goes over UDP/RTP and is NOT carried by the mesh - only * the text envelope (this message) is. `from_voice` marks messages sent * via push-to-talk speech-to-text so receivers can render a mic icon * next to the text. @@ -1951,7 +1951,7 @@ message TakTalkMessage { * Announces a TAKTALK chatroom's friendly name and roster so peers can * resolve room UUIDs (used in TakTalkMessage.chatroom_id and * GeoChat.room_id) to a display name and participant list. Not a chat - * message itself — these events are emitted by TAKTALK when rooms are + * message itself - these events are emitted by TAKTALK when rooms are * created or memberships change. */ message TakTalkRoomData { @@ -2005,7 +2005,7 @@ message Marti { * primary-vs-cc distinction the same way ATAK does. * * If dest_callsign is [TAKPacketV2.callsign] (self-addressed, unusual but - * legal — e.g. ATAK echoing back to its own room), the builder still emits + * legal - e.g. ATAK echoing back to its own room), the builder still emits * the element so loopback shapes round-trip cleanly. */ repeated string dest_callsign = 1; @@ -2130,7 +2130,7 @@ message TAKPacketV2 { // --- Sensor / environment annotations ---------------------------------- // // Both fields are OPTIONAL and attach to any payload_variant. They - // describe observed conditions at the emitting station — a PLI with + // describe observed conditions at the emitting station - a PLI with // environment data, an Aircraft with a sensor cone, a Marker with both. // Absent by default; presence is signaled by the message being non-null. @@ -2147,7 +2147,7 @@ message TAKPacketV2 { reserved 27, 28, 30; // Tags 27, 28 reserved for future top-level annotations. Tag 29 is consumed - // by `marti` below. Tag 30 was the former `bool pli` oneof arm — dropped + // by `marti` below. Tag 30 was the former `bool pli` oneof arm - dropped // (PLI is now the implicit payload); reserved here at the message level // because proto3 forbids `reserved` inside a oneof. @@ -2167,7 +2167,7 @@ message TAKPacketV2 { * The payload of the packet */ oneof payload_variant { - // Tag 30 was `bool pli` — a PLI carries no fields beyond the common + // Tag 30 was `bool pli` - a PLI carries no fields beyond the common // envelope (position is in latitude_i/longitude_i), so the boolean was // pure overhead (~3 wire bytes on every position beacon, the highest- // frequency packet). PLI is now the IMPLICIT payload: a packet with NO diff --git a/meshtastic/mesh.options b/meshtastic/mesh.options index 5a0f2bde8..92878bc6b 100644 --- a/meshtastic/mesh.options +++ b/meshtastic/mesh.options @@ -7,7 +7,7 @@ # Decode width stays 40 so NodeInfo/set_owner from senders built against the # old 39-byte limit still parse (nanopb halts on string overflow rather than -# truncating). Firmware clamps everything it stores or transmits to 24 bytes — +# truncating). Firmware clamps everything it stores or transmits to 24 bytes - # see NodeInfoLite.long_name in deviceonly.options. *User.long_name max_size:40 *User.short_name max_size:5 diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index 3bf6eca30..27ff8de1d 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -2480,15 +2480,15 @@ message LockdownStatus { /* * For LOCKED: machine-readable reason. Known values: - * "needs_auth" — storage already unlocked, client must auth - * "token_missing" — no boot token on flash - * "token_expired" — boot token wall-clock TTL elapsed - * "token_boots_zero" — boot token boot-count TTL exhausted - * "token_hmac_fail" — token tampered or wrong device - * "token_dek_fail" — token DEK decrypt failed - * "token_wrong_size" — token file corrupted - * "token_bad_magic" — token file corrupted - * "not_provisioned" — should generally use NEEDS_PROVISION state instead + * "needs_auth" - storage already unlocked, client must auth + * "token_missing" - no boot token on flash + * "token_expired" - boot token wall-clock TTL elapsed + * "token_boots_zero" - boot token boot-count TTL exhausted + * "token_hmac_fail" - token tampered or wrong device + * "token_dek_fail" - token DEK decrypt failed + * "token_wrong_size" - token file corrupted + * "token_bad_magic" - token file corrupted + * "not_provisioned" - should generally use NEEDS_PROVISION state instead * Other values may be added; clients should treat unknown values as * "locked, ask for passphrase". */ diff --git a/meshtastic/mesh_beacon.proto b/meshtastic/mesh_beacon.proto index c6005ab71..982bac289 100644 --- a/meshtastic/mesh_beacon.proto +++ b/meshtastic/mesh_beacon.proto @@ -15,7 +15,7 @@ option swift_prefix = ""; * Payload for MESH_BEACON_APP packets. * Periodically broadcast by nodes in beacon mode. * Listeners deliver the text message to the local inbox and cache any offered - * channel/preset for the client app to act on — the firmware never auto-applies them. + * channel/preset for the client app to act on - the firmware never auto-applies them. */ message MeshBeacon { /* diff --git a/meshtastic/module_config.proto b/meshtastic/module_config.proto index 65688697c..8134e0269 100644 --- a/meshtastic/module_config.proto +++ b/meshtastic/module_config.proto @@ -901,7 +901,7 @@ message ModuleConfig { * Single-target TX channel: channel settings (name + PSK) to send beacons on. * If unset, beacons go out on the primary channel. Used only when broadcast_targets is empty. * NOTE: the single-target path embeds the ChannelSettings inline here, whereas a - * broadcast_targets entry references a channel-table slot by channel_index instead — see + * broadcast_targets entry references a channel-table slot by channel_index instead - see * BroadcastTarget. The two paths are equal, first-class options; only this representation differs. */ ChannelSettings broadcast_on_channel = 8; @@ -956,7 +956,7 @@ message ModuleConfig { * each temporarily switching the radio to that entry's preset/region/channel. * When empty, the broadcaster uses the scalar broadcast_on_preset / broadcast_on_region / * broadcast_on_channel fields instead (the single-target path). - * Single- and multi-target are equal, first-class options — neither is preferred or + * Single- and multi-target are equal, first-class options - neither is preferred or * deprecated. They differ only in how the TX channel is named: broadcast_on_channel embeds a * ChannelSettings inline, while a target references an existing channel-table slot by * channel_index (see BroadcastTarget). diff --git a/meshtastic/telemetry.proto b/meshtastic/telemetry.proto index e3b263640..022cfb21d 100644 --- a/meshtastic/telemetry.proto +++ b/meshtastic/telemetry.proto @@ -888,7 +888,7 @@ enum TelemetrySensorType { MMC5983MA = 52; /* - * ICM-42607-P 6‑Axis IMU + * ICM-42607-P 6-Axis IMU */ ICM42607P = 53; diff --git a/packages/kmp/README.md b/packages/kmp/README.md index 4263a7999..8d522d91a 100644 --- a/packages/kmp/README.md +++ b/packages/kmp/README.md @@ -13,7 +13,7 @@ This package publishes Kotlin Multiplatform models generated from the protobuf s ## Versioning -The SDK version is derived automatically from the latest git tag — no manual version file to maintain. +The SDK version is derived automatically from the latest git tag - no manual version file to maintain. | Context | Version | Source | |---------|---------|--------| @@ -26,8 +26,8 @@ CI snapshots keep the tag and append the commit count since it (`N`) plus the SHA; local builds instead patch-bump. The count goes after a *dot* (`{tag}.{N}`), which is deliberate: in Maven's ordering a dot-separated numeric segment outranks any hyphen-nested token, so a newer commit's higher count sorts above older -snapshots — including legacy `{tag}-{sha}-SNAPSHOT` builds still in the repo whose -SHA can tokenize to a large integer — while the whole coordinate still sorts +snapshots - including legacy `{tag}-{sha}-SNAPSHOT` builds still in the repo whose +SHA can tokenize to a large integer - while the whole coordinate still sorts *above* its base release and *below* the next one. Dependency bots therefore pick the newest. A *bare* `-SNAPSHOT` sorts *below* its base, so the local fallback names the next version to stay ahead of the last release. Both land between the @@ -64,11 +64,11 @@ implementation("org.meshtastic:protobufs:2.7.23") Every push to `master` publishes a snapshot under a unique coordinate of the form `{latest-tag}.{N}-g{short-sha}-SNAPSHOT` (e.g. `2.7.23.42-g497cd88-SNAPSHOT`): -`git describe --tags --long` with the `v` stripped — the latest release tag, the +`git describe --tags --long` with the `v` stripped - the latest release tag, the commit count `N` since that tag (after a **dot**), and the 7-char commit SHA. The dot-separated numeric `N` outranks any hyphen-nested token in Maven's ordering, so -newer commits sort *above* older snapshots — including legacy `{tag}-{sha}` builds -still in the repo — letting dependency bots pick the newest and never downgrade, +newer commits sort *above* older snapshots - including legacy `{tag}-{sha}` builds +still in the repo - letting dependency bots pick the newest and never downgrade, while each snapshot still sorts *above* the release it was built from yet *below* the next release. They live in the Central Portal snapshots repository, not on the main Maven Central CDN. To consume them, add that repository @@ -83,7 +83,7 @@ repositories { } } -// build.gradle.kts — pin a specific published snapshot +// build.gradle.kts - pin a specific published snapshot implementation("org.meshtastic:protobufs:2.7.23.42-g497cd88-SNAPSHOT") ``` diff --git a/packages/kmp/build.gradle.kts b/packages/kmp/build.gradle.kts index 4b432c860..dfb9bc2f3 100644 --- a/packages/kmp/build.gradle.kts +++ b/packages/kmp/build.gradle.kts @@ -10,8 +10,8 @@ version = providers.gradleProperty("VERSION_NAME").orElse( providers.provider { // Fallback for local builds with no -PVERSION_NAME (CI always passes it): // derive a snapshot version by patch-bumping the latest git tag. Any - // failure — git missing, shallow/tagless clone, or an unexpected tag - // format — degrades to 0.0.1-SNAPSHOT instead of breaking configuration. + // failure - git missing, shallow/tagless clone, or an unexpected tag + // format - degrades to 0.0.1-SNAPSHOT instead of breaking configuration. val tag = runCatching { val process = ProcessBuilder("git", "describe", "--tags", "--abbrev=0") .directory(rootDir) @@ -102,7 +102,7 @@ wire { // Flatten oneof fields into nullable properties on the parent message // class instead of generating intermediate sealed classes. All consumers - // (Meshtastic-Android, TAKPacket-SDK) are written against this shape — + // (Meshtastic-Android, TAKPacket-SDK) are written against this shape - // e.g. `packet.decoded`, `packet.chat`, `takPacketV2.shape` are all // nullable top-level properties, not sealed-class arms. boxOneOfsMinSize = 5000