Skip to content

[DRAFT] Add Hermes-Lite 2 Plus (AK4951 companion board) support - #1

Closed
randal007 wants to merge 1 commit into
hl2-ioboardfrom
hl2-plus
Closed

[DRAFT] Add Hermes-Lite 2 Plus (AK4951 companion board) support#1
randal007 wants to merge 1 commit into
hl2-ioboardfrom
hl2-plus

Conversation

@randal007

@randal007 randal007 commented Sep 3, 2026

Copy link
Copy Markdown
Owner

DRAFT — not for upstream yet. This is opened against my own fork so the
change can be read as a diff before on-air testing. The audio paths have not
been exercised on hardware; results go in the Testing section once they have.
Based on the HL2 IO board branch (Zeus-SDR#4), so the diff here
shows only the HL2+ work.

What this adds

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 played by the radio itself.

The board cannot be detected, so the operator declares it

An HL2+ answers discovery exactly like a stock HL2. There is no board ID, no gateware version, nothing to probe. So this follows the existing precedent for an operator-selected hardware fact — OrionMkIIVariant — and lets the declaration promote the capability fingerprint:

For(HermesLite2, variant, hl2PlusCodec: true)
    => caps with { HasOnboardCodec = true, HasMicBias = true }

Promoting the capability rather than special-casing each consumer is the whole design. RadioSpeakerAudioSink, the Protocol-1 mic attach in DspPipelineService, ClampAudioSource and the frontend already gate on HasOnboardCodec, so one lever lights all of them. Almost no new audio plumbing was required: ControlFrame already writes the EP2 L/R slots when a source is plumbed, and PacketParser.ExtractMicSamples already decodes the EP6 mic slots. They were simply never switched on for this board.

HasRadioLineIn and HasBalancedXlr stay false — the companion board has neither jack, so those options stay hidden.

Two blanket board == HermesLite2 exclusions in RadioSpeakerAudioSink are replaced by that capability check. They were shorthand for "HL2 has no codec", which stops being true with the companion fitted.

Config C3 bit 3 — the one real conflict

The HL2+ gateware reads Config-frame 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 currently drives it as:

else if (s.EnableHl2BandVolts) c3 |= 1 << 3;

deskHPSDR sets the identical bit for the codec via its LT2208_DITHER_ON alias. One bit, two incompatible gateware meanings, so the two features cannot both be armed:

  • declaring HL2+ force-disables Band Volts;
  • arming Band Volts is refused while HL2+ is declared (SetHl2BandVolts returns the effective value, which will be false);
  • /api/radio/hl2-options reports bandVoltsAvailable, so the UI can disable the control rather than let a PUT be silently ignored.

Without this the two settings would quietly mean whatever the loaded gateware believes, which is the worst possible failure mode: no error, just a feature that does something else.

CW sidetone

Modelled directly on deskHPSDR, whose CW sample path writes the host-generated sidetone into both EP2 audio slots alongside the CW I/Q. This is the point of the companion board for CW work — the sidetone reaches the operator from the radio instead of around the host audio loop.

Zeus already generates that sidetone: CwSidetoneSource mixes it into the RX audio bus, and the MOX fade silences the band-RX contribution while keying, so during CW transmit that bus holds sidetone and nothing else. Two things kept it off the wire:

  1. ControlFrame filled the L/R slots only while MOX was clear. It now fills them under MOX when CodecAudioWhileMox is set — written before the I/Q early-returns, so sidetone survives a null TX source or DriveLevel == 0. An operator keying with drive at zero still expects to hear themselves.
  2. RadioSpeakerAudioSink dropped every frame while keyed, on the previously correct grounds that the L/R slots carried no audio during TX. It now keeps feeding in CW only, and still drops in every other mode, where those frames are TX monitor audio that does not belong in the radio's speaker.

The ring is drained on both T/R edges. deskHPSDR drains its TX ring on the RX→TX transition "for minimum CW side tone latency"; the same drain also stops a pre-key RX tail replaying into the codec after unkey.

Scope and compatibility

Off by default. A radio whose operator has not declared a companion board keeps the stock HL2 fingerprint and byte-identical wire behaviour, so a stock HL2 is unaffected in every path touched here.

CodecAudioWhileMox is set only for the HL2+. Other codec boards keep their existing silence-while-keying behaviour — the same change would very likely suit them, but I have no ANAN hardware to confirm it on and did not want to alter transmit-time audio for boards I cannot test.

Hl2Plus on the Hl2OptionsSetRequest is nullable, matching IoBoard: a client that PUTs only {bandVolts} must not silently un-declare someone's companion board.

Testing

Built clean; the settings, exclusivity and API surface are verified against a live engine:

declare HL2+                      → hl2Plus true,  bandVoltsAvailable false
arm Band Volts while HL2+ armed   → refused, stays false
declare HL2+ while Band Volts on  → Band Volts force-disabled

On-air audio testing is pending — see the note below; I will update this section with results before this is ready to merge.

Notes for reviewers

The HL2+ gateware is not open source. That is why deskHPSDR's author declines to support the board, and it 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, so it is fair to weigh that before taking this on.

Worth knowing: the AK4951's own internal DSP (ALC, 5-band EQ, mic analog gain) is configured out-of-band by JI1UDD's ak4951_Controller, which reaches the codec over the HL2's I2C-1 bus at address 0x12 (C0=0x78, C1=0x06, C2=0x92). Zeus does not need to duplicate any of that for mic and phones to work, and this PR does not try to.


Authorship disclosure: this patch was written by Claude (Anthropic's AI) working with the station owner, who owns the HL2+ hardware, loaded the companion gateware, and performed the on-air testing. The reasoning and provenance comments are the AI's; the hardware verification is the operator's.

🤖 Generated with Claude Code

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.
@randal007

Copy link
Copy Markdown
Owner Author

Superseded — opening this upstream now that the HL2+ has been tested on air. History squashed and the body rewritten with what testing actually established.

@randal007 randal007 closed this Sep 4, 2026
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