Add Hermes-Lite 2 Plus (AK4951 companion board) support - #6
Open
randal007 wants to merge 2 commits into
Open
Conversation
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.
The HL2+ companion board adds an AK4951 stream codec to a Hermes-Lite 2:
microphone input with PTT, headphone and speaker outputs, and CW sidetone,
on gateware that fills the same Protocol-1 audio slots every other codec
board uses.
An HL2+ answers discovery exactly like a stock HL2 — no board ID, no
gateware marker, nothing to probe — so the operator declares it, and that
declaration promotes the board's capability fingerprint:
For(HermesLite2, variant, hl2PlusCodec: true)
=> caps with { HasOnboardCodec = true, HasMicBias = true }
Promoting the capability rather than special-casing each consumer is the
design. RadioSpeakerAudioSink, the Protocol-1 mic attach, ClampAudioSource
and the frontend already gate on HasOnboardCodec, so one lever lights all
of them, and almost no new audio plumbing was needed: ControlFrame already
writes the EP2 L/R slots and PacketParser already decodes the EP6 mic
slots. Every capability lookup that feeds a gate or a clamp must read the
promoted set — three separate faults during bring-up came from one that
did not, the worst being PushAudioFrontEnd, where ClampAudioSource sent
the operator's Radio Mic selection back to Host while every queryable
surface went on reporting RadioMic.
Config C3 bit 3 is the one hardware conflict. HL2+ gateware reads it as
"a codec is present"; mi0bot's HL2 fork reads the same bit as the Band
Volts PWM enable. The two are made mutually exclusive — declaring HL2+
force-disables Band Volts, arming Band Volts is refused while HL2+ is
declared, and hl2-options reports bandVoltsAvailable so the UI can
disable the control instead of watching a PUT be ignored.
CW sidetone is generated host-side and carried to the codec on the EP2
L/R slots while keyed, following deskHPSDR. The companion gateware does
not sound its own sidetone in practice, despite accepting internal_CW and
a non-zero sidetone level, so the host must supply it; the host's own
monitor tone is suppressed in that case so the operator hears one tone
from the radio rather than a second, late one from the PC. The keyer's
RF_delay now carries piHPSDR's value instead of zero — piHPSDR documents
it as working around a bug in the FPGA iambic keyer.
Also, board-agnostic and useful to any codec board: the radio-speaker
ring now primes before serving audio (its absence produced 5.3 seconds of
gaps on every start, heard as a run of pops) and corrects producer-vs-
packer rate drift a few samples at a time instead of in slices. Three
diagnostics endpoints report what the CW frames carry, the speaker ring's
latency and health, and every link in the radio-microphone chain — each
one located a fault that reading the code had not.
Off by default: a radio whose operator has not declared a companion board
keeps the stock HL2 fingerprint and byte-identical wire behaviour.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Support for the Hermes-Lite 2 Plus — an HL2 carrying the AK4951 companion board, which adds a stream codec with microphone input, headphone and speaker outputs, and CW sidetone.
Developed and tested on real hardware over a full session: headphone audio, the radio's own microphone, and the CW keyer with sidetone all work, alongside an N2ADR IO board in the same radio.
The board cannot be detected, so the operator declares it
An HL2+ answers discovery exactly like a stock HL2 — no board ID, no gateware marker, nothing to probe. So this follows the existing precedent for an operator-selected hardware fact (
OrionMkIIVariant) and lets the declaration promote the capability fingerprint:Promoting the capability rather than special-casing each consumer is the design.
RadioSpeakerAudioSink, the Protocol-1 mic attach,ClampAudioSourceand the frontend already gate onHasOnboardCodec, so one lever lights all of them, and almost no new audio plumbing was required —ControlFramealready writes the EP2 L/R slots,PacketParseralready decodes the EP6 mic slots.The corollary is worth stating for review, because it cost three separate faults during bring-up: every capability lookup that feeds a gate or a clamp must read the promoted set. The worst was
PushAudioFrontEnd, which read the unpromoted table, soClampAudioSourcesent the operator's Radio Mic selection back toHoston the way to the pipeline — while the store,/api/radio/audioand/api/radio/capabilitiesall went on reportingRadioMic, because those read the promoted set. Symptom: a mic that is correctly configured everywhere you can look and silent on the air.HasRadioLineInandHasBalancedXlrstay false — the board has neither jack, so those options stay hidden.Config C3 bit 3
The HL2+ gateware reads C3 bit 3 as "an audio codec is present". mi0bot's HL2 fork reads the same bit as the Band Volts PWM enable, which is what Zeus drives it as today. deskHPSDR sets the identical bit for the codec via its
LT2208_DITHER_ONalias. One bit, two incompatible gateware meanings, so the two features are made mutually exclusive:/api/radio/hl2-optionsreportsbandVoltsAvailable, so a UI can disable the control rather than let a PUT be silently ignored.Confirmed by the vendor's own FAQ, which describes that same Thetis "Band Volts/Dither" checkbox as the control for the companion board's speaker output.
CW sidetone
Host-generated, carried to the codec on the EP2 L/R slots while keyed, following deskHPSDR's approach.
This was not the first design. The gateware advertises its own headphone sidetone and accepts
internal_CWand a non-zerosidetone_level—/api/diagnostics/cw-wireconfirms Zeus sends both correctly — but on this hardware it never sounds. Removing the host injection silences CW entirely, which is how we established it. The host's own monitor tone is therefore suppressed while the codec carries one (HardwareSidetoneOwnsMonitorreading the promoted capability), so the operator hears a single in-time tone from the radio instead of a second, late one from the PC's speakers.The keyer's
RF_delayalso now carries piHPSDR's value rather than zero. piHPSDR documents it as "a quirk working around a bug in the FPGA iambic keyer", and parity with a client known to drive this board seemed worth more than our previous reasoning that Zeus owns key-to-RF timing host-side.Radio-speaker ring: board-agnostic fixes
These benefit any Protocol-1 codec board, not just this one:
Diagnostics
Three read-only endpoints, each of which located a fault that reading the code had not:
/api/diagnostics/cw-wire/api/diagnostics/rx-audio/api/diagnostics/radio-micThe mic bug above was found in one call by
radio-mic, which showed a store sayingRadioMicnext to a pipeline sayingHost. Three prior attempts to find it by reading the code were all wrong.Routing the HL2+ microphone
For anyone bringing one of these up, the working sequence — and the traps, each of which cost real time:
PUT /api/radio/hl2-options {"bandVolts":false,"hl2Plus":true}. Persists; re-pushed on connect.PUT /api/radio/audio {"source":"RadioMic"}. Single-select — arming the radio drops the host mic instantly, and{"source":"Host"}returns to the computer's. This is the Zeus equivalent of turning VAC1 off in Thetis, which is required there for the same reason./api/diagnostics/radio-micshould showactiveSource: RadioMic,p1MicAttached: true,handlerAttached: true, andsamplesAcceptedclimbing at ~48,000/sec.Hardware traps worth documenting, since neither is a software fault:
internal_CWbit is derived from the operating mode, not the keyer setting — so nothing CW-related works in USB.Known limitation
Faint crackle and pops remain during CW, and there is room for improvement here. They are much reduced — the startup burst is gone, and the ring measures clean in steady state (
trimmedandunderrunsflat, latency ~30 ms) — but not eliminated.The likely cause is inherent to the current sidetone design: carrying host sidetone to the codec means the codec's audio switches content on every element, band RX between dits and sidetone during them. Thetis, which is clean on identical hardware, avoids the analogous problem on its VAC path with a feedback-controlled variable-ratio resampler servoing the buffer to 50% occupancy, rather than the sample-shaving used here. That is the shape of a proper fix and is deliberately not attempted in this PR.
Compatibility
Off by default. A radio whose operator has not declared a companion board keeps the stock HL2 fingerprint and byte-identical wire behaviour.
Hl2Pluson the set request is nullable, matchingIoBoard, so a client PUTing only{bandVolts}cannot silently un-declare someone's companion board.Note for reviewers
The HL2+ gateware is not open source, which is why deskHPSDR's author declines to support the board and is a reasonable reason for this project to decline it too. Nothing here depends on that gateware being open — the wire side is ordinary Protocol-1 audio slots plus one Config bit — but the feature is only exercisable by operators running it. The ring priming, the rate correction and the diagnostics stand on their own and apply to every Protocol-1 codec board, if those are worth taking separately.
Authorship disclosure: this patch was written by Claude (Anthropic's AI) working with the station owner, who owns the HL2+ and IO board hardware, loaded the companion gateware, and did all on-air testing. The reasoning and provenance comments are the AI's; every measurement quoted above came from the operator's bench.
🤖 Generated with Claude Code