Skip to content

DM-32 capabilities.json models the CPS ceiling, not the radio's real constraints #3

Description

@emuehlstein

Summary

radios/baofeng_dm32/capabilities.json currently models the CPS ceiling, not the radio:

"limits": { "max_channels": 4000, "max_zones": 250, "max_channels_per_zone": 250 }

Those are the numbers the CPS will accept. They are not the numbers that produce a
working codeplug. Every DM-32UV constraint that has actually cost us debugging time
is currently unmodeled, so a profile can validate clean and still generate a plug that
is silently broken on hardware.

This issue proposes extending the capabilities file (and the validator that reads it)
with the constraints below. All of them are field-verified or read directly out of
NeonPlug's DM-32UV codec — sources cited per item.

Proposed constraint block

Illustrative shape, not a final schema — naming/nesting is up for discussion:

{
  "id": "baofeng_dm32",
  "name": "Baofeng DM-32",
  "limits": {
    "max_channels": 4000,
    "max_zones": 250,
    "max_channels_per_zone": 250,

    "max_scanlists": 250,
    "max_scanlist_members": 15,
    "max_zone_name_chars": 16,
    "max_channel_name_chars": 16,
    "max_scanlist_name_chars": 10,
    "max_contact_name_chars": 16
  }
}

The constraints, and why each one matters

1. max_scanlist_members: 15 — highest value, hardest won

The on-radio scanlist record stores its channel list at +0x1A, and the next field
begins at +0x38. That is 30 bytes = 15 x uint16. NeonPlug's own codec agrees:
its comment reads "30 bytes, uint16 array LE, 0x0000 terminated, max 15 channels",
channelCount at +0x0B is documented 0-15, and both encoder and decoder hard-loop
i < 15.

Source: src/radios/dm32uv/structures.ts

Worth stating explicitly because it is a real point of confusion: the OEM CPS shows
16
entries. It adds a "Current Channel" entry at index 0 on top of your 15 defined
members. So a "16" in vendor-facing documentation is not wrong, it is describing the
UI. Storage is 15. A generator must target 15.

Failure mode when exceeded: not a rejected import. On a real plug, an oversized list
made every scanlist unselectable on the radio, not just the offending one.

2. The full-15 edge case (document, do not silently rely on)

NeonPlug clamps writes to Math.min(len, 15), but only emits the 0x0000 terminator
if (channelCount < 15). At exactly 15 the 30-byte field is full and no
terminator is written
, so the radio's parse can run past the end of the record into
the adjacent field.

This is the suspected mechanism behind the all-scanlists-unselectable failure above.
It is not yet verified on hardware. Recommendation: treat 15 as the hard cap for
validation, but consider a warning at exactly 15, and note in radios/README that
dropping the largest list to 14 is the first diagnostic if scan selection misbehaves.

3. channelCount must equal the exact member count — never +1

A separate, already-fixed defect in our own generator: writing channelCount = len + 1
(a "1-based" misreading) causes the radio to read one entry past a correctly sized
array. Same class of bug as #2, different cause. Any exporter writing this field needs
a test asserting channelCount == len(members).

4. Name length limits

  • Channel name: 16 chars
  • Zone name: 16 chars
  • Scanlist name: 10 chars — NeonPlug encodes it as 11 bytes null-terminated,
    name.slice(0, 10), so an 11th character is silently truncated, not rejected.
  • Contact name: 16 chars

The scanlist case matters most for a generator: truncation is silent, and two
scanlists whose names differ only after character 10 collide into the same on-radio
name. Since profiles reference things by stable ID this will not corrupt selection,
but it will produce a confusing radio UI.

Suggested validator behavior

profile.py already reads limits and raises ProfileValidationError for zones and
channels. The same pattern extends naturally:

  • Hard error: any constraint exceeded (> max_scanlist_members, name over limit).
  • Warning: scanlist at exactly 15 (see Add normalized codeplug model #2), until hardware-verified.
  • Error messages should keep the existing style, which is good — state the observed
    value, the limit, and the offending entity name.

Scope note

Profile 0.1 deliberately excludes scanlists and contacts, so items #1-#3 are not
enforceable end-to-end yet. Filing now anyway because:

  1. The name limits (Generate NeonPlug exports from ResolvedCodeplug #4) apply to 0.1 today — channels and zones are already
    generated, and max_*_name_chars is enforceable immediately.
  2. Scanlists are exactly where the DM32 pain was concentrated, so it is worth having
    these constraints written down before 0.2 designs around them, rather than
    rediscovering them on hardware.

Happy to open a PR with the capabilities block and validator changes if the shape
above looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions