Skip to content

PRT3: parser/EVENT_MAP appear to diverge from official ASCII spec — needs panel-side verification #600

@yozik04

Description

@yozik04

Summary

Deep review of the merged PRT3 implementation (PR #596) against the official PRT3 ASCII Programming Guide (pages 12-21) identified two categories of suspected protocol-spec divergence that need verification against real-panel captures before they can be confidently fixed.

This issue captures the findings for tracking; it does not propose code changes yet. The cheapest disambiguator (one short capture from a live PRT3) is described in the Verification section at the bottom — once that lands, we'll know which interpretation is right and can act accordingly.

File:line refs were valid at commit 04ed539 on branch pr3_followups.


Issue 1 — Area Status (RA) byte order (suspected)

File: paradox/hardware/prt3/parser.py:308-329 (_parse_area_status)

What the spec says (page 13)

After the 5-char prefix RA{nnn}:

0-indexed position spec value meaning
5 D/A/F/S/I arm state
6 M / O zone in memory
7 T / O trouble
8 N / O not ready
9 P / O in programming
10 A / O in alarm
11 S / O strobe

PRT3_Implementation_Plan.md:96 documents the same order: D/A/F/S/I, M/O, T/O, N/O, P/O, A/O, S/O.

What the implementation reads

position impl checks maps to matches spec?
5 D/A/F/S/I arm_state
6 P in_programming ✗ (spec: M)
7 T trouble
8 N not_ready
9 A alarm ✗ (spec: P)
10 S strobe ✗ (spec: A)
11 M zone_in_memory ✗ (spec: S)

Why this is hard to falsify from the test suite

The fixtures (tests/hardware/prt3/fixtures.py) and the single-flag tests (tests/hardware/prt3/test_parser.py:237-309) were authored in the same commit as the parser by the same author, citing the same spec PDF. The fixtures encode the parser's interpretation rather than independently observed panel output. No real-panel RA captures exist in the repo.

Worst-case risk if the spec is right

A real panel that emits a spec-compliant RA frame for an alarm-active partition would be read as alarm=False, strobe=False, zone_in_memory=False, in_programming=False. That is: an active alarm is invisible to PAI (and therefore to MQTT, Home Assistant, SMS / Pushbullet / Signal notifications). This is a critical-safety failure mode, but only triggers when a partition is actually in alarm — which is why it may never have been observed in normal-operation testing.


Issue 2 — EVENT_MAP misalignment

File: paradox/hardware/prt3/event.py:39-247

Cross-referenced against the spec event table (pages 17-21). The list below splits findings by confidence:

High-confidence (spec table is unambiguous)

G Spec meaning impl maps as impact
23 Zone Bypassed bypass_cancelled Inverted. First bypass event clears the flag; spec defines no separate "bypass cancelled" code, so bypassed zones never get re-set to bypassed=True in MemoryStorage.
33 Zone Tamper system.trouble (empty change) Zone-tamper events never set tamper=True on the zone object.
34 Zone Tamper Restore system.trouble_restored (empty change) Same problem, restore direction.
56 (not defined in spec) zone bypassed Phantom event — would inject bypassed=True if firmware ever emits G56.
60, 61 (spec says "Future Use") zone low_battery / zone supervision_trouble Same phantom-event risk.

Medium-confidence (spec defines, mapping looks wrong)

G Spec meaning impl maps as
18 Disarm after alarm w/ Keyswitch alarm_cancelled
20 Alarm Cancelled w/ User Code generic disarm
21 Alarm Cancelled w/ Keyswitch zone bypassed
29 Late to Disarm by User panic_alarm
31 Duress Alarm by User tamper_alarm
32 Zone Shutdown tamper_restored
59 Module Removed from Combus zone closed
62 Access Granted to User low_battery_restored
63 Access Denied to User supervision_restored

Granularity loss (collapsing subgrouped events)

G Spec impl
36 "Trouble Event" — 8 N-subnumbers (TLM / AC / Battery / Aux / Bell / Bell-absent / Clock / Global Fire) flat ac_failure
38 "Module Trouble" — 9 N-subnumbers (Combus / Tamper / ROM / TLM / FTC / Printer / AC / Battery / Aux) flat battery_trouble
30 "Special Alarm" — N0=Emergency / N1=Medical / N2=Fire panic / N3=Recent Closing / N4=Police / N5=Global Shutdown flat audible_alarm

EVENT_MAP already supports the number_overrides pattern (see G065 at event.py:229); the same mechanism would resolve G36/G38/G30.

Completely missing (spec defines, impl omits)

G004, G005, G006, G007, G008, G019, G022, G028, G035, G037, G039, G040, G041–G044, G046, G047, G049–G055, G058.

Some of these are routine (G019/G020/G021 = alarm-cancelled, G041-G044 = zone battery/supervision trouble) and would be wanted for full status reporting.


Verification (cheapest path forward)

The right next step is not to change code — it's to capture a few seconds of real PRT3 output from a panel in a known state, place those captures in tests/hardware/prt3/captures/, and then decide.

RA byte-order test (2 minutes)

  1. Arm Area 1 to Stay mode. Leave other areas disarmed. No alarm, no trouble.
  2. Send RA001\r to the PRT3 (any terminal: screen, picocom, etc.).
  3. Look at the 12-char response (after stripping \r).
    • If position 5 = S and positions 6-11 are all O → consistent with both interpretations (need step 4).
  4. Trigger an alarm (open a non-delay zone while armed). Send RA001\r again.
    • If position 10 contains Aspec is right, parser positions [6, 9, 10, 11] need to be fixed.
    • If position 9 contains A → impl is right, spec doc has a column-ordering quirk and we add a comment in parser.py citing the capture as authority.

EVENT_MAP test (5 minutes)

Enable LOGGING_DUMP_PACKETS=true and trigger the following sequence; copy the printed G... lines into a capture file:

  1. Bypass zone 1 via keypad → expect G023N001A....
  2. Un-bypass zone 1 via keypad → check whether the panel emits a second G-line (spec doesn't define a separate cancel code, so observe what actually happens).
  3. Disarm via keypad → check whether it's G013 (Master), G014 (User), or G016/G017 (after-alarm variant).
  4. Trigger keypad panic (PE on emergency keys) → check G-code; spec says G030 with sub-N for type.
  5. Force an AC failure / battery test → check whether it's G036 (with sub-N) or G038 (with sub-N).

Capture file placement

Suggested: tests/hardware/prt3/captures/ (new directory, module-scoped — analogous to how tests/hardware/prt3/fixtures.py is module-scoped). Each capture file should include a brief header comment naming the panel model, firmware version, and the action being captured.


Followups context

PR #596 had a separate three-perspective review captured in PRT3_Followups.md; this issue is orthogonal to those items. None of the C1/C2/M3/M6/R1-R5 follow-ups touch the parser byte order or EVENT_MAP table.

The simple PRT3 follow-ups (M1, M2, M4, M5, plus minor cleanups) were addressed in commit 04ed539 on branch pr3_followups.


Suggested staging (only after verification)

If captures confirm the spec interpretation, the Architect's review proposed a 5-stage rollout to keep tests green at each step:

  1. Stage 0 — land the captures (no code change).
  2. Stage 1 — extract magic numbers in parser.py into named constants (no semantic change).
  3. Stage 2 — flip ONE RA position (canary) + new spec-compliant fixture, mark old fixtures @pytest.mark.legacy_ra_order.
  4. Stage 3 — flip remaining RA positions.
  5. Stage 4 / 5 — one G-per-commit EVENT_MAP fixes, then drop legacy fixtures.

A PRT3_STRICT_SPEC_MODE config flag is not recommended — single user, six-day-old PR, no persisted state depending on event taxonomy, simpler to coordinate one minor-version bump.


cc @NaanyaBiz — would you be able to grab the captures above when you next have hands on your panel? That's the gating step before any code change here.

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