Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

fix(hl2): pick the radio type when connecting by IP, so a Hermes-Lite 2 works over VPN - #4528

#4528
Open
jensenpat wants to merge 6 commits into
aethersdr:mainaethersdr/AetherSDR:mainfrom
jensenpat:feat/hl2-connect-by-ipjensenpat/AetherSDR:feat/hl2-connect-by-ipCopy head branch name to clipboard
Open

fix(hl2): pick the radio type when connecting by IP, so a Hermes-Lite 2 works over VPN#4528
jensenpat wants to merge 6 commits into
aethersdr:mainaethersdr/AetherSDR:mainfrom
jensenpat:feat/hl2-connect-by-ipjensenpat/AetherSDR:feat/hl2-connect-by-ipCopy head branch name to clipboard

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

What

The Connect by IP page had no way to say what it was dialing. It probed for a Hermes-Lite 2 first (a ~600 ms HPSDR Protocol 1 discovery datagram to UDP/1024) and fell through to the FlexRadio TCP/4992 command-plane probe if nothing answered. Every Flex connect paid the HL2 timeout, and an HL2 operator got no signal about which protocol had actually been tried when a connect failed.

This adds a Radio type dropdown — FlexRadio or Hermes-Lite 2 — and probes exactly the family the operator picked. The two wire protocols are disjoint: a Hermes-Lite 2 never answers the Flex probe, and a FlexRadio never answers a Metis discovery datagram, so there is nothing to gain from trying both.

A directed (unicast) Metis discovery is the whole reason connect-by-IP matters for an HL2: Hl2Discovery's broadcast sweep never leaves the local subnet, so a radio across a VPN or a routed subnet was simply unreachable from the UI.

Also in here

  • Extracted the probe into ConnectionPanel::probeHermesLite2(), reusing MetisProtocol's discoveryRequest() / parseDiscoveryReply() / isHermesLite2() instead of the hand-rolled byte poking the inline version used, and promoted Hl2Discovery::macToSerial() so a directed probe produces byte-identical identity to a broadcast sweep. That serial IS the auto-reconnect key and the client-side nickname key — the two paths have to agree.
  • The HL2 probe now honours the Advanced VPN source-path selection. It bound to AnyIPv4 unconditionally, which on a VPN exposing several adapters can send the request out the wrong interface.
  • A successful HL2 address is remembered: rememberManualIp() + saveManualProfile(), as the Flex path already did. HL2 addresses previously never reached the recent-addresses dropdown. The chosen family persists globally (ConnectByIpRadioFamily) and per-address (routed profile identity.family), so picking a recent address restores its type. Profiles written before this carry no family and read as flex — which is what they were.
  • The HL2 result is marked isRouted, matching what the Flex manual probe records.

Bridge

connect ip <host-or-ip> [flex|hl2]. Omitting the type keeps the dialog's current selection, so existing connect ip <addr> scripts are unaffected; the response echoes "family". Anything that is not flex or hl2 is rejected rather than silently probing the wrong protocol. Documented in docs/automation-bridge.md.

Layout

Adding a second row overflowed the dialog and Qt resolved the shortfall by overlapping the labels with their own entry fields. The nested QFormLayout is replaced with explicit label+field rows, the wrapped result label reserves its height instead of reflowing the page when a message appears, and the height floor now comes from the panel's own content rather than the hardcoded 580/660 in MainWindow.

Verification

Against the real Hermes-Lite 2 at 192.168.1.21 over a VPN, where broadcast discovery cannot see it:

  • Connected by IP from both the bridge verb and the GUI button.
  • serial 00:1C:C0:A2:13:DD, gateware 74 — matching a raw EF FE 02 discovery probe exactly, which is what proves the shared macToSerial() path.
  • Live off-air RF on the panadapter (WWV at 10.000 MHz), so this is a real stream, not just a connected flag.
  • Selecting FlexRadio for that same address correctly does not connect — the dropdown genuinely gates the probe.
  • Probing a non-HL2 address as hl2 gives the HL2-specific failure message rather than falling through.
  • Radio left answering discovery 0x02 idle after a normal disconnect.
  • Full suite green: 192/192.

Screenshots (connect dialog + live panadapter) attached in the thread.

Note

This branch stacks on and includes #4503 and #4507.

🤖 Generated with Claude Code

jensenpat and others added 6 commits July 26, 2026 21:57
…o Health

Seven quality-of-life items for the Hermes-Lite 2. Three of them share one
root cause: the GUI drove a Flex command plane this radio does not have, so
the widget moved, the setting persisted, and nothing reached the hardware.

Band switching (Band sidebar)
  The band buttons resolved a Flex band-stack key and sent
  "display pan set <pan> band=". The HL2 has no band stack and no VFO to read
  back, so that command reached nothing. MainWindow now branches on
  usesFlexCommandPlane(): for a non-Flex backend the app is authoritative and
  tunes the slice directly (mode first, then frequency, then pan centre).

  RadioCapabilities gains tuningMinHz/tuningMaxHz so the grid can be honest.
  The HL2 reports 0.1-38.4 MHz (the AD9866's first Nyquist zone) and the 6 m
  button is disabled with a tooltip rather than tuning somewhere it cannot
  hear. Zero/zero means "not reported", so Flex is unaffected.

Companion filter board (J16 open collectors)
  The HL2 has no switchable filters of its own -- it has seven open-collector
  outputs at 0x00[23:17] that the gateware forwards to I2C 0x20. Setting the
  config bits IS the whole mechanism. Selection is derived from the slice
  frequency and auto-applies on connect (from the persisted frequency) and on
  every band crossing.

  The one-hot mapping is Quisk's Hermes_BandDict verbatim -- the grouping
  (60+40 share a filter, 17+15 share one) is a property of the N2ADR board,
  not something inferable from the band plan. Ours is the frequency ranges,
  and the unit test checks them by asserting every amateur band lands on
  Quisk's answer.

  Two departures from "always engage the HPF": nothing below 1.6 MHz (it would
  remove exactly what is being listened to), and no HPF on 160 m (the HL2's
  switching supply couples spurs into those inductors -- wiki Options.md).

  Logged at INFO on a new aether.hl2 category, not debug. There is no readback
  anywhere in the protocol, so that line is the only evidence of what the
  relays were told to do, and a support log has to already contain it.

RF gain -> AD9866 LNA
  lnaGainDb was connect-time only. IRadioBackend::setPanRfGain (default no-op,
  so Flex is untouched) carries the ANT panel's slider to the register at any
  time, and panRfGainInfoChanged reports the real geometry: -12..+48 dB in
  1 dB steps, against the model's Flex-shaped -8..+32 in 8s, which had made
  two thirds of the available gain unreachable.

  Hl2DbReference moves in the same call so the trace and S-meter do not slide
  when gain changes. The persisted key is now family-scoped: a value last set
  on a Flex was being restored onto the HL2 as an LNA gain the operator never
  chose for that radio (observed live -- a Flex-era 16 was pushed at connect).
  And the restore no longer writes a DEFAULT at all: with nothing saved, the
  radio's own gain is the right answer. Seeding from the pan model does not
  fix it -- the restore can run before the backend has published, so the model
  still reads 0 and the same 0 goes to the hardware. Caught by Radio Health
  showing "LNA gain 0 dB" on a fresh profile.

Forward / reverse power (uncalibrated)
  Counts go through Quisk's power_meter_std_calibrations['HL2FilterE3'] curve.
  That is a reference curve for an N2ADR rev E3 board, NOT a calibration of
  this radio -- coupler, toroid and detector diode all vary. The meters are
  labelled "(uncalibrated)" in their own MeterDef descriptions, because the
  number itself looks exactly like a calibrated one. Raw counts still logged;
  a per-unit calibration replaces the table and nothing else.

Radio Health dialog (Help menu, before Slice Troubleshooting)
  New IRadioBackend::healthSnapshot() -- synchronous because every value is
  already cached from unprompted telemetry, so there is no round-trip to
  await. Shows firmware, ADC overload, LNA gain, TX inhibit (decoded from the
  active-low bit), PTT/keyed/tune, TX FIFO depth and under/overflow, PA
  temperature and bias, directional counts and watts, SWR, the J16 filter byte
  and its meaning, tuning/DDC frequency, IQ rate and dropped EP6 packets.
  A register the radio has not reported renders as a dash, never as zero.

Meter pacing
  WDSP handed us ~47 S-meter readings a second and every one crossed to the
  GUI thread. An EMA smooths every sample (so the published value represents
  the whole interval rather than one arbitrary instant) and a 100 ms gate
  decides how often one is published -- dropping without smoothing would
  alias. 100 ms is the cadence MetisClient already paces telemetry at, and the
  attack/decay constants are MeterModel's own Flex forward-power ballistics.

ATU button
  TxApplet greys ATU and MEM when the backend reports hasTuner=false. A live
  ATU button on a radio with no tuner is worse than a missing one: pressing it
  keys the transmitter. updateAtuAvailability() is now the single owner of
  that enabled state -- the radio-has-no-tuner and TGXL-in-Operate reasons
  arrive from different models, and whichever fired last used to win.

Verification
  192/192 tests pass, including 20 new assertions pinning the filter encoding
  and the band map against Quisk's table.

  tests/hl2_live_band_filter_probe.cpp (hardware-only, EXCLUDE_FROM_ALL)
  answers "the unit test shares our reading of the register map, so what would
  catch a convention error?". The filter byte rides the CONFIG register, so a
  misplaced bit lands in the sample rate or receiver count and the failure is
  a radio that still streams and still looks framed. Measured on a real HL2:

    - EP6 packet rate held at 381/s (within 1.002x) across all nine filter
      selections, with non-zero IQ. A bit in DATA[25:24] would have halved or
      doubled it.
    - Engaging the AM-blocking HPF at 0.6 MHz dropped the band 22-24 dB. That
      is not the radio agreeing with our register map -- that is the antenna
      path changing, and it confirms an N2ADR board is fitted. On a bare HL2
      the writes are inert and harmless.
    - A commanded 10 -> 30 dB LNA step moved the raw wire noise floor 19.8 dB,
      measured before any of our DSP or dB referencing runs.

  Driven on hardware through the automation bridge: 40 m button took the slice
  9.98 USB -> 7.200 LSB with the pan following; the filter log went
  0x48 (HPF + 30/20m) -> 0x44 (HPF + 60/40m); Radio Health populated with live
  telemetry; RF Gain read -12..+48 at 20 dB; ATU and MEM disabled with "This
  radio has no antenna tuner"; 6 m disabled as out of range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Hermes-Lite 2 that is never told to stop keeps streaming at a host that is
gone and then stops answering discovery -- alive at the network layer,
invisible to every client, and needing a physical power cycle. Reproduced
three times during aethersdr#4503: a plain `kill` on a connected AetherSDR wedged the
radio every time.

Hl2Backend's destructor already sends metis-stop, which covers a normal quit.
It does not run on a signal -- Qt tears nothing down for SIGTERM, so the
process simply vanishes mid-stream. The gateware watchdog is documented as the
anti-wedge mechanism for exactly this case (MetisProtocol.h,
kRunWatchdogDisable) and did NOT recover it in practice, so this is a belt
alongside that brace rather than a replacement for it.

NOT a self-pipe. The textbook Qt answer is to write to a pipe from the handler
and do the real work back on the event loop. That would be useless here: an
unresponsive event loop is one of the main reasons anyone reaches for kill in
the first place, so the fix must not depend on the thing that may already be
stuck. The handler does the whole job itself, calling nothing but sendto() on
an address resolved in advance -- the descriptor, the sockaddr and the 64-byte
stop datagram are all built by MetisClient::start(), on a normal thread, while
the link is coming up.

Arming happens BEFORE the first start datagram and disarming AFTER the normal
stop has gone out. That asymmetry is deliberate: armed-but-not-streaming costs
a stray 64-byte datagram to a radio that is not listening, while
streaming-but-not-armed is a radio that has to be power-cycled.

Termination signals only -- SIGTERM/SIGINT/SIGHUP/SIGQUIT. Deliberately NOT
SIGSEGV/SIGABRT/SIGBUS: a crash leaves the radio in the same state and it is
tempting to cover it, but those belong to whatever crash reporting the
platform already has (on macOS the reporter uses Mach exception ports, and
MacStartupAbortGuard owns SIGABRT during startup). SIGKILL cannot be caught,
so `kill -9` still wedges the radio; nothing in a process can change that.

An inherited SIG_IGN is left ignored. POSIX says a process should not take
over a signal its parent deliberately neutralised, and this is not academic:
nohup works by ignoring SIGHUP, so installing a handler over it turns a
neutralised signal back into a fatal one. Caught in testing -- a nohup'd run
died the moment its launching shell exited, which is the exact opposite of
what nohup is for.

Verification
  tests/hl2_signal_stop_test.py + hl2_signal_stop_child: a real MetisClient
  runs against a fake radio, the driver kills it with a real signal, and a
  metis-stop datagram must arrive. End-to-end on purpose -- calling
  fireEmergencyStop() directly would prove sendto() works, not that the
  handler is installed, that arming happened, or that the descriptor is still
  usable when the process dies.

  Covers SIGTERM, SIGINT and SIGHUP, checks the datagram is 64 bytes and
  leaves the gateware watchdog ENABLED (disabling it would tell the gateware
  to keep streaming forever at a host that is about to vanish -- the exact
  wedge this exists to prevent), and includes a NEGATIVE CONTROL: the same
  kill with the handlers not installed must send no stop, so the passes mean
  something.

  Confirmed on the real radio by the operator: pkill on a connected session,
  and the HL2 recovered and reconnected with no power cycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Saved-radio autoconnect was wired to the Flex `RadioDiscovery::radioDiscovered`
signal only. An HL2 arrives on a completely separate sweep — `hl2::Hl2Discovery`
broadcasts HPSDR Protocol 1 discovery on UDP/1024 — and that signal was
connected to the connect-list slots and nothing else. So an HL2 owner with
`AutoConnectToLastRadio` on watched their radio appear in the picker and then
had to click it by hand at every launch, with "Looking for your radio…" on
screen the whole time.

Extract the autoconnect lambda into `MainWindow::maybeAutoConnectToDiscoveredRadio()`
and connect BOTH discovery sources to it. The slot is family-agnostic on
purpose: wiring it to one discovery object is exactly what caused this, and any
future backend that grows its own sweep gets autoconnect for free.

Two behaviours the shared slot adds:

- Fail closed on a busy non-Flex radio, matching the manual-connect gate in
  ConnectionPanel (aethersdr#4448). Protocol 1 is single-client, so connecting to an HL2
  that is already streaming wedges both clients. Says so in the status line
  rather than silently doing nothing — this is the startup path and the
  operator is staring at "Looking for your radio…".
- Also listen to `Hl2Discovery::radioUpdated`, not just `radioDiscovered`. A
  radio first seen as In_Use flips to Available via `radioUpdated` when the
  other client drops; without it the saved radio stays unconnected until it
  disappears from the sweep entirely and comes back.

Flex behaviour is unchanged — same conditions, same order, same messages, now
reached through a named method instead of an inline lambda.

Verified on the HL2 at 00:1C:C0:A2:13:DD (gateware 74) with
`AutoConnectToLastRadio=True` and that MAC saved as `LastConnectedRadioSerial`:
the pre-fix build sat in "Looking for your radio…" for 45s with no autoconnect
while the radio was answering discovery the whole time; the fixed build
connects on its own at startup, confirmed over four consecutive launches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…orks over VPN

The "Connect by IP" page had no way to say what it was dialing. It probed for
a Hermes-Lite 2 first (a ~600 ms HPSDR Protocol 1 discovery datagram to
UDP/1024) and fell through to the FlexRadio TCP/4992 command-plane probe if
nothing answered. That made every Flex connect pay the HL2 timeout, and it
gave an HL2 operator no signal about which protocol had actually been tried
when the connect failed.

Add a **Radio type** dropdown — FlexRadio or Hermes-Lite 2 — and probe exactly
the family the operator picked. The two wire protocols are disjoint: a
Hermes-Lite 2 never answers the Flex probe, and a FlexRadio never answers a
Metis discovery datagram, so there is nothing to gain from trying both.

A directed (unicast) Metis discovery is the whole reason connect-by-IP matters
for an HL2 — Hl2Discovery's broadcast sweep never leaves the local subnet, so
a radio across a VPN or a routed subnet was unreachable from the UI.

Also:

- Extract the probe into ConnectionPanel::probeHermesLite2() and reuse
  MetisProtocol's discoveryRequest()/parseDiscoveryReply()/isHermesLite2()
  instead of the hand-rolled byte poking the inline version used, and promote
  Hl2Discovery::macToSerial() so a directed probe produces byte-identical
  identity to a broadcast sweep. That serial IS the auto-reconnect key and the
  client-side nickname key, so the two paths must agree.
- Honour the Advanced VPN source-path selection in the HL2 probe. It bound to
  AnyIPv4 unconditionally, which on a VPN exposing several adapters can send
  the request out the wrong interface.
- Remember a successful HL2 address: it now calls rememberManualIp() and
  saveManualProfile() like the Flex path already did, so an HL2 lands in the
  recent-addresses dropdown, and the chosen family is persisted both globally
  (ConnectByIpRadioFamily) and per-address (routed profile identity.family) so
  picking a recent address restores its type. Profiles written before this
  carry no family and are read as flex, which is what they were.
- Mark the HL2 result isRouted, matching what the Flex manual probe records.

Bridge: `connect ip <host-or-ip> [flex|hl2]`. Omitting the type keeps the
dialog's current selection, so existing scripts are unaffected; the response
echoes "family". Rejects anything that is not flex or hl2 rather than silently
probing the wrong protocol.

Layout: adding a second row overflowed the dialog, and Qt resolved the
shortfall by overlapping the labels with their own entry fields. The nested
QFormLayout is replaced with explicit label+field rows, the wrapped result
label now reserves its height instead of reflowing the page when a message
appears, and the height floor comes from the panel's own content rather than
the hardcoded 580/660 in MainWindow.

Verified against the Hermes-Lite 2 at 192.168.1.21 over a VPN (broadcast
discovery cannot see it): connected by IP from both the bridge verb and the
GUI button, serial 00:1C:C0:A2:13:DD and gateware 74 matching a raw discovery
probe exactly, live off-air RF on the panadapter (WWV at 10.000 MHz). Selecting
FlexRadio for that same address correctly does not connect, and the radio was
left answering discovery 0x02 idle. Full suite green, 192/192.

Includes aethersdr#4503 and aethersdr#4507.
@jensenpat
jensenpat marked this pull request as ready for review July 27, 2026 21:47
@jensenpat
jensenpat requested review from a team as code owners July 27, 2026 21:47
@jensenpat jensenpat changed the title feat(connect): pick the radio type when connecting by IP, so a Hermes-Lite 2 works over VPN fix(hl2): pick the radio type when connecting by IP, so a Hermes-Lite 2 works over VPN Jul 27, 2026

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change, and the reasoning in both the description and the code comments is the kind I wish more PRs had — the "the two protocols are disjoint, so stop paying the HL2 timeout on every Flex connect" framing is right, and reusing MetisProtocol's parse plus promoting macToSerial() so the directed probe and the broadcast sweep produce the same identity is exactly the correct instinct. Hardware verification over an actual VPN, including the negative case (selecting FlexRadio for that address does not connect), is convincing.

Three things I'd like fixed before merge, all in probeHermesLite2() and all in the same "the directed path should match the broadcast path" spirit that motivated macToSerial(): the nickname is not shared the way the serial now is, hostnames silently can't work on the HL2 path even though the bridge verb documents <host-or-ip>, and a bind failure is reported to the operator as "no radio answered".

Scope note: this stacks #4503 and #4507, so roughly 2,100 of the 2,548 changed lines belong to those PRs. I reviewed the feat(connect) commit as the new work here; the rest should land through its own PR first.

Would like fixed before merge

  • ConnectionPanel.cpp:1871 — connect-by-IP shows "Hermes-Lite 2" instead of the operator's saved nickname; the broadcast sweep uses effectiveNickname().
  • ConnectionPanel.cpp:1846 — a hostname can never reach an HL2 (QHostAddress(ip) is null), and the failure looks like "radio didn't answer".
  • ConnectionPanel.cpp:1840 — bind failure is collapsed into the "no HL2 answered" message; the Flex probe distinguishes it.

Polish

  • ConnectionPanel.cpp:861 — the hardcoded height floor moved rather than went away, and 700 px is tight on a 768 px-tall display.
  • Hl2Discovery.h:51 — the new doc block landed inside nicknameSettingsKey()'s comment.
  • AutomationServer.cpp:5196 — non-static QRegularExpression, unlike the rest of the file.

Non-blocking notes

  • IConnectionAutomation::automationConnectByIp() gains a defaulted parameter on a virtual. Defaults bind statically, so a future implementer that defaults it differently would silently disagree with a call through the base pointer. Only ConnectionPanel implements it today, so this is harmless now — just worth knowing it's a trap if a second implementer (aetherd?) shows up.
  • The probe still blocks the GUI thread for up to 600 ms in waitForReadyRead(), so the "Checking…" text likely never paints before the result replaces it. Pre-existing, not introduced here.
  • CodeGuard's six CG-PATH-001 hits are false positives and I've dropped them: the MainWindow.cpp lines are the FFTW-wisdom progress dialog and the macOS relaunch path derived from QCoreApplication::applicationFilePath(), and hl2_live_band_filter_probe.cpp:98 reads a host from argv[1], not a path. No untrusted path input in any of them.

🤖 aethersdr-agent · cost: $6.1861 · model: claude-opus-5

Comment on lines +1871 to +1872
info.nickname = info.model;
info.serial = hl2::Hl2Discovery::macToSerial(reply->mac);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The directed probe hard-codes the nickname to the model string, while the broadcast sweep uses the operator's saved name (Hl2Discovery.cpp:168effectiveNickname(info.serial, info.model)). So an operator who named their HL2 in Radio Setup sees that name when it's discovered locally and "Hermes-Lite 2" when they reach the same radio over the VPN. That's the same class of divergence the macToSerial() promotion was meant to close, and effectiveNickname() is already a public static on Hl2Discovery.

Needs the serial computed first, so:

Suggested change
info.nickname = info.model;
info.serial = hl2::Hl2Discovery::macToSerial(reply->mac);
info.serial = hl2::Hl2Discovery::macToSerial(reply->mac);
// Same nickname the broadcast sweep shows for this MAC: an HL2 has
// no on-radio name store, so the operator's custom name is kept
// client-side keyed by serial. The two paths have to agree.
info.nickname = hl2::Hl2Discovery::effectiveNickname(info.serial, info.model);

const auto request = hl2::discoveryRequest();
hpsdr.writeDatagram(reinterpret_cast<const char*>(request.data()),
qint64(request.size()),
QHostAddress(ip),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QHostAddress(ip) returns a null address for anything that isn't a literal IP, so a hostname produces a writeDatagram() to a null destination — no request leaves, and 600 ms later the operator gets "No Hermes-Lite 2 answered at myradio.local. Check the address, and that the radio is powered…", which points at the radio for what is actually an unsupported input.

That matters because the Flex path does accept names (connectToHost() resolves), and docs/automation-bridge.md documents the verb as connect ip <host-or-ip> [flex|hl2] — so the doc promises something this path can't do.

Either resolve first (QHostInfo::fromName() is synchronous and this path already blocks) or detect the null address up front and say so explicitly:

const QHostAddress dest(ip);
if (dest.isNull()) { /* resolve, or report "enter an IP address for a Hermes-Lite 2" */ }

Also worth checking writeDatagram()'s return value — right now a send failure of any kind is indistinguishable from a silent radio.

&& !bindSettings.bindAddress.isNull()
? hpsdr.bind(bindSettings.bindAddress, 0)
: hpsdr.bind(QHostAddress(QHostAddress::AnyIPv4), 0);
if (!bound)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failed bind returns false here, and the caller turns every false into "No Hermes-Lite 2 answered at %1. Check the address, and that the radio is powered, idle, and reachable on UDP port 1024." So an operator whose explicit VPN source path can no longer be bound is told to go check the radio.

probeFlexRadio() handles the same case properly (ConnectionPanel.cpp:1919): it sets m_manualSourceWarningLabel, reveals Advanced, and says "AetherSDR could not use that VPN source path. Try Auto or choose another path." Since this PR's whole point is that the operator should know which protocol was tried and why it failed, the HL2 path should reach the same message — e.g. set the source warning and message here before returning, or return a small enum/QString* reason so probeRadio() can pick the right text.

// restored at the old 660 px height clipped the bottom of that page, so
// hold a floor that fits it. Everything above the stack is fixed-height, so
// this is the page height plus the surrounding chrome.
setMinimumHeight(700);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number moved from MainWindow into the panel rather than being derived — MainWindow.cpp now calls minimumHeight(), which returns this constant, so it's still hand-maintained, just in one place instead of two. That's an improvement, but a hard 700 px floor is worth a second look: on a 1366×768 laptop, minus the menu bar and window chrome, a dialog that cannot be made shorter than 700 px is right at the edge of unusable, and a frameless dialog gives the user no OS-level escape.

If the reserved-height result label and the group's 240 px floor are doing the real work, layout()->minimumSize().height() (or just letting sizeHint() drive resize() with a lower setMinimumHeight) would keep the page from clipping without pinning a number the content no longer controls.

Comment on lines +51 to +56
// Canonical "AA:BB:CC:DD:EE:FF" rendering of a discovery reply's MAC, which
// IS RadioInfo::serial for this family. Shared so a directed (unicast)
// probe — ConnectionPanel's connect-by-IP path — produces byte-identical
// identity to a broadcast sweep. They must agree: the serial is the
// auto-reconnect key and the client-side nickname key.
static QString macToSerial(const std::array<std::uint8_t, 6>& mac);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block landed inside nicknameSettingsKey()'s doc comment — lines 46–50 ("AppSettings key for a user-assigned nickname… Shared with RadioSetupDialog so both sides agree on the key") now read as the opening of macToSerial()'s documentation, and nicknameSettingsKey() two lines below is left undocumented. Worth moving the macToSerial() declaration + its comment below nicknameSettingsKey() (or above the nickname block with a blank line) so each comment sits on its own function.

// The optional family picks which wire protocol to probe. Omitted keeps
// whatever the connect dialog's radio-type selector is set to, so every
// pre-existing `connect ip <addr>` script keeps working.
const QStringList ipTokens = arg.trimmed().split(QRegularExpression("\\s+"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-static QRegularExpression constructed on every connect ip — the rest of this file uses static const QRegularExpression for exactly this (e.g. line 953). Not hot enough to matter for performance, but clazy flags the pattern and it's inconsistent with the neighbours:

Suggested change
const QStringList ipTokens = arg.trimmed().split(QRegularExpression("\\s+"),
static const QRegularExpression ipTokenSep(QStringLiteral("\\s+"));
const QStringList ipTokens = arg.trimmed().split(ipTokenSep,

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (6 finding(s))

  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-4528/src/gui/MainWindow.cpp:7155
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-4528/src/gui/MainWindow.cpp:7189
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-4528/src/gui/MainWindow.cpp:7294
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-4528/src/gui/MainWindow.cpp:7295
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-4528/src/gui/MainWindow.cpp:7296
  • [MEDIUM] CG-PATH-001 — Potential path traversal in tests/hl2_live_band_filter_probe.cpp /tmp/aetherclaude/pr-4528/tests/hl2_live_band_filter_probe.cpp:98

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $6.3373 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Antares Detector — candidate vulnerable file(s)

  • src/core/backends/hl2/Hl2Backend.cpp

The hl2 backend implementation likely contains the radio selection logic for connections by IP (VPN support). Hl2Backend.cpp implements backend behavior and would be responsible for choosing a radio type when connecting via IP, making it the primary location to fix this issue.

Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting.


🤖 aethersdr-agent · cost: $6.3373 · model: claude-opus-5

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

Morty Proxy This is a proxified and sanitized view of the page, visit original site.