Skip to content

Add Hermes-Lite 2 IO board (N2ADR) support - #4

Open
randal007 wants to merge 1 commit into
Zeus-SDR:mainfrom
randal007:hl2-ioboard
Open

Add Hermes-Lite 2 IO board (N2ADR) support#4
randal007 wants to merge 1 commit into
Zeus-SDR:mainfrom
randal007:hl2-ioboard

Conversation

@randal007

@randal007 randal007 commented Sep 3, 2026

Copy link
Copy Markdown

What this adds

Support for the N2ADR HL2 IO board — the Pico daughter board that mounts on the Hermes-Lite 2's filter-board header and drives amplifier, antenna, transverter, fan and Icom AH-4 tuner lines.

The board has no knowledge of the HPSDR protocol. The PC pushes the transmit frequency, the RF-input routing and the receive frequency codes into its I2C register file, and the Pico firmware makes its switching decisions from those registers. Zeus currently has no I2C path at all — I grepped ControlFrame, Protocol1Client and PacketParser for the HL2 extended opcodes (0x7A/0xFA, and 0x3D for replies) and there are no hits — so this adds the whole tunnel.

How it works

Wire layer (Zeus.Protocol1)

  • Hl2IoBoard.cs — the register map (mirroring the firmware's i2c_registers.h name-for-name so the two can be diffed by eye), the tunnelled-transaction encoding, and a scheduler that detects the board via the hard-wired PCA9536D at 0x41 and then cycles its register file. It is a pure state machine — no I/O — so it is testable without a radio, and I/O policy stays in Protocol1Client with the rest of the wire scheduling.
  • ControlFrame — a raw I2C control block may displace one rotation slot in the odd USB frame. It does not add a packet, so the EP2 cadence the TX FIFO depends on is untouched; the displaced register comes round again next turn.
  • PacketParser.TryExtractHl2I2cReply — decodes replies, which the HL2 echoes on C&C address 0x3D.
  • Protocol1Client ticks the scheduler on the EP2 loop and feeds replies back, gated on BoardKind and the operator's switch.

Hosting

  • PreferredRadioStore / RadioService persist the switch alongside Band Volts and re-push it into each fresh client on connect.
  • /api/radio/hl2-options gains ioBoard, plus a read-only ioBoardPresent so the UI can distinguish "switched on" from "switched on and answering".
  • BoardCapabilities.HasHl2IoBoard gates the frontend control (HL2 only).

Compatibility

Off by default. With it off the EP2 stream is byte-identical to before. With it on but no board fitted, the cost is one read every 2 seconds and no writes ever happen, because writes only begin after detection.

One API note worth a look: IoBoard on the Hl2OptionsSetRequest is nullable, making the PUT a partial update. A client that sends only {bandVolts} — which is what ships today — would otherwise switch the IO board off every time someone toggled Band Volts. This is the divergence the existing comment on that DTO anticipated ("kept distinct so the GET-vs-PUT request shapes can diverge in the future (e.g. PUT becoming a partial update with nullable fields)").

Two details that matter on real hardware

The TX frequency is latched per turnaround and written LSB-last. The Pico latches bytes 4..1 into a shadow register and commits the full 40-bit value only when byte 0 arrives, so a retune mid-burst would otherwise leave a torn frequency on the board.

REG_RF_INPUTS is sent as 0 and never inferred. Modes 1 and 2 switch the receiver off the HL2's antenna and onto the board's J9 input, and mode 2 additionally drives GPIO03_INTTR — the HL2's own T/R relay — on receive. They are correct only when a separate receive antenna is wired into J9, which is what deskHPSDR's if (alex_antenna != 0) gate expresses. Deriving the mode from PureSignal state looks tempting and is wrong: on a station using the board's J10 PureSignal input without a separate receive antenna it switches the receiver to an empty jack and clicks the T/R relay on receive. I know because I did it and had to be told by the operator that his receive had dropped.

N2ADR's own guidance is that the register "is 0, 1 or 2, and is a user setting", so exposing it as an explicit operator control is the right follow-up — I have deliberately left that out of this PR rather than guess at the UI.

Testing

Built clean, and in daily use on a Hermes-Lite 2 with a real IO board fitted:

  • Detection round-trips — the PCA9536D answers within 2 s of enabling, and re-detects within 2 s after a reset, confirming live bidirectional I2C rather than a latched flag.
  • The frequency, RF-input and reply paths run continuously with no effect on the RX/TX streams.
  • PureSignal via the board's J10 input works with the register at 0.
  • 38 assertions over the encoder and scheduler — byte sequences checked against deskHPSDR's, the logarithmic frequency code against N2ADR's fcode2band() curve, plus the latch-coherence property above.

The scheduler was written to be unit-testable but this repo has no test project, so those assertions live outside the tree. Happy to add them in whatever form suits you.

Operator note

There is no UI control for this — the console is not part of this repository — so the setting is reached through the engine's own API:

port=$(ps -eo args | grep -oE 'StationEngine --port [0-9]+' | grep -oE '[0-9]+$' | head -1)
curl -s -X PUT http://127.0.0.1:$port/api/radio/hl2-options \
  -H 'Content-Type: application/json' -d '{"bandVolts":false,"ioBoard":true}'

One-time: the setting persists and is re-pushed on every reconnect. ioBoardPresent then reports whether the board actually answered — true detected, false enabled but silent (suspect seating or power), null no radio connected yet. BoardCapabilities.HasHl2IoBoard is included precisely so a capability-gated control can replace that curl whenever you want one.

Since this was opened, the board has also run alongside an HL2+ AK4951 companion board with no interference — they sit on different I2C buses (IO board 0x1D on I2C-2, codec 0x12 on I2C-1), matching N2ADR's documented position that the two coexist.

Notes

I appreciate this repository is published as the complete corresponding source rather than as a development target, so this may be easier for you to port than to merge — please treat it as a patch offered in whatever form is most useful. The frontend checkbox is the remaining piece and is yours to add; HasHl2IoBoard is there to gate it.


Authorship disclosure: this patch was written by Claude (Anthropic's AI) working with the station owner, who provided the Hermes-Lite 2 and IO board hardware, ran the on-air testing, and caught the REG_RF_INPUTS bug described above. Please review it with that in mind — the reasoning and provenance comments are the AI's, and the hardware verification is the operator's.

🤖 Generated with Claude Code

The HL2 IO board (jimahlstrom/HL2IOBoard) is a Pico daughter board on the
filter-board header. It has no HPSDR knowledge: the PC pushes the transmit
frequency, RF-input routing and receive frequency codes into its I2C
register file, and Pico firmware drives amplifier, antenna, transverter,
fan and AH-4 tuner lines from them. Zeus had no I2C path at all, so this
adds one.

Wire layer (Zeus.Protocol1):

- Hl2IoBoard.cs — register map mirroring the firmware's i2c_registers.h,
  the tunnelled-transaction encoding (0x7A write / 0xFA read on I2C-2 at
  0x1D), and a pure scheduler: detect via the hard-wired PCA9536D at 0x41,
  then cycle TX frequency, RF inputs and the RX frequency codes.
- ControlFrame — a raw I2C control block may displace one rotation slot in
  the odd USB frame, so no extra EP2 packet is introduced and the TX FIFO
  cadence the transmitter depends on is untouched.
- PacketParser.TryExtractHl2I2cReply — decode replies, which the HL2
  echoes on C&C address 0x3D.
- Protocol1Client ticks the scheduler on the EP2 loop and feeds replies
  back, gated on BoardKind and the operator's switch.

Hosting:

- PreferredRadioStore / RadioService persist the switch alongside Band
  Volts and re-push it into each fresh client on connect.
- /api/radio/hl2-options gains `ioBoard` plus a read-only `ioBoardPresent`
  so the UI can tell "switched on" from "switched on and answering".
  `IoBoard` on the PUT request is nullable — a client that sends only
  BandVolts must not switch the board off.
- BoardCapabilities.HasHl2IoBoard gates the frontend control, HL2 only.

Off by default. With it off the EP2 stream is byte-identical to before.
With it on but no board fitted the cost is one read every 2 seconds, and
no writes ever happen because writes only start after detection.

Two details that matter on real hardware:

- The five TX-frequency bytes are latched per turnaround and written
  LSB-last, matching the firmware's commit-on-byte-0 behaviour, so a
  retune mid-burst cannot leave a torn frequency on the board.
- REG_RF_INPUTS is sent as 0 and is never inferred. Modes 1 and 2 switch
  the receiver onto the board's J9 input, and mode 2 also drives INTTR —
  the HL2's T/R relay — on receive. They are only correct when a separate
  receive antenna is wired to J9, and per N2ADR the register "is a user
  setting". An explicit operator control is the right way to expose it.

Wire encoding and cadence follow deskHPSDR's old_protocol.c case 11 and
the softerhardware/Hermes-Lite2 protocol documentation. Verified against a
real IO board on an HL2: detection, the frequency round-trip and the
0x3D reply path all confirmed on air.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant