Skip to content

feat(tci): add mic_level handler (bridge existing setMicLevel to TCI)#3234

Merged
jensenpat merged 2 commits into
aethersdr:mainfrom
nigelfenton:feat/tci-mic-level-handler
May 28, 2026
Merged

feat(tci): add mic_level handler (bridge existing setMicLevel to TCI)#3234
jensenpat merged 2 commits into
aethersdr:mainfrom
nigelfenton:feat/tci-mic-level-handler

Conversation

@nigelfenton
Copy link
Copy Markdown
Contributor

@nigelfenton nigelfenton commented May 28, 2026

Summary

Adds a mic_level command to TciProtocol, bridging the existing TransmitModel::setMicLevel(int) setter to TCI clients. Until now mic_level: commands sent over TCI silently no-op'd — handleCommand's dispatcher had no entry for them.

3 files changed (+36 / -1):

File Change
src/core/TciProtocol.h Declare cmdMicLevel(const QStringList&, bool)
src/core/TciProtocol.cpp Dispatcher entry, cmdMicLevel body (cloned from cmdDrive), init-burst line
src/models/TransmitModel.cpp setMicLevel now emits micStateChanged + equality guard (see below)

The TransmitModel::setMicLevel change (behaviour change — calling it out explicitly)

setMicLevel previously updated m_micLevel and emitted commandReady toward FlexLib, but never emitted micStateChanged — the signal the mic-level slider binds to (PhoneCwApplet.cpp:667:728, also MainWindow.cpp:2420). This was invisible until now because nothing called setMicLevel from a non-UI path; the slider's own onValueChanged was the only entry. Once cmdMicLevel started calling it programmatically over TCI, the gap surfaced: the radio + internal state updated, but the UI slider froze.

Fix mirrors setSpeechProcessorLevel / setRfPower: equality guard + emit the state-changed signal before commandReady. Without it the TCI handler is functionally half-broken (no UI feedback), so it belongs in this PR.

cmdMicLevel

Near-exact clone of cmdDrive — global verb (no per-trx; mic is whole-radio), accepts spec form mic_level:N; and legacy TRX-prefixed mic_level:0,N; (trx ignored). Init burst gains a mic_level:<value>; line so clients learn the current value on connect.

Split out

The unrelated /MANIFEST:EMBED CMakeLists build fix that was previously bundled here is now #3237 (separate PR, reviewed against the Windows toolchain matrix independently). This PR is purely the mic_level TCI feature + its required UI-signal fix.

Verified — three platforms

  • Linux (Ubuntu 24.04, Qt 6.4.2): live test — sent mic_level:0,30/60/90 over TCI, AE mic slider tracked each value. Confirmed end-to-end: parse → dispatcher → setMicLevelmicStateChanged → UI repaint.
  • Windows (MSVC 14.50, Qt 6.10.3): builds + launches (with fix(cmake): pair /MANIFESTINPUT with required /MANIFEST:EMBED #3237's CMake fix locally).
  • macOS (Apple Silicon, Qt 6.11): builds clean.

Test plan

  • Live mic_level slider-tracking verified on Linux
  • Builds on all three platforms
  • CI matrix (auto)

🤖 Generated with Claude Code

TransmitModel already has setMicLevel(int) wired into FlexLib via
commandReady (line 517-522 — same shape as setRfPower) but until now
no TCI client could reach it: TciProtocol::handleCommand has no
mic_level entry in its dispatcher, so the command silently no-ops.

This adds:
- TciProtocol::cmdMicLevel — cloned from cmdDrive (the closest sibling),
  global verb (no per-trx, mic is whole-radio), accepts spec-shorter
  `mic_level:N;` and legacy TRX-prefixed `mic_level:0,N;` for forward
  compatibility with clients that send the spec form for everything
- Dispatcher entry beside the drive/tune_drive ones
- mic_level: line in the init burst so newly-connected clients learn
  the current value immediately

## Motivation

Unblocks the [aethersdr-ulanzi-plugin](https://github.com/nigelfenton/aethersdr-ulanzi-plugin)
Mic Gain ▲▼ keys, which have been sending `mic_level:0,N;` correctly
since v0.1.0 but landing on a missing dispatcher entry.  After this
merges, the plugin's mic-gain action set works immediately with no
plugin-side change required.

## Verified

- Compilation: TciProtocol.cpp.obj rebuilt clean (369/369 .obj files)
- Live smoke test pending — local link blocked by an unrelated Windows
  SDK / mt.exe environment issue (LNK1158).  Will validate by pressing
  the Ulanzi plugin's Mic Gain keys against this build + watching AE's
  mic-level slider move, before flipping the PR from draft to ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nigelfenton nigelfenton marked this pull request as ready for review May 28, 2026 02:00
@nigelfenton nigelfenton requested a review from a team as a code owner May 28, 2026 02:00
@nigelfenton
Copy link
Copy Markdown
Contributor Author

Live smoke test — passed ✅

Verified on linux-aether (Ubuntu 24.04, Qt 6.4.2, built from feat/tci-mic-level-handler @ 6dabac51). AE launched fresh, TCI server bound 40001, no Flex attached (not required — setMicLevel updates m_micLevel in-process before commandReady goes out to FlexLib).

Test method: Python websocket client sent three discrete mic_level: values, AE's built-in TCI traffic monitor (Network Diagnostics → TCI) logged each inbound, and the MIC slider on AE's TX panel visibly moved to match.

01:54:26.310   ▶ mic_level   mic_level:0,75
01:57:42.999   ▶ mic_level   mic_level:0,30
01:58:33.444   ▶ mic_level   mic_level:0,60

(Excerpted from tci-monitor-20260528-015924.log; rx_smeter lines interleaved are unrelated AE S-meter telemetry.)

After each command, AE's MIC slider in the TX panel settled at the sent value. Result: handler works end-to-end — TCI parse → dispatcher → setMicLevel(N)m_micLevel updated → UI slider re-rendered → commandReady emitted toward FlexLib.

Both inbound formats accepted as designed:

  • Spec-shorter mic_level:N; (1 arg)
  • Legacy TRX-prefixed mic_level:0,N; (2 args; trx index ignored since mic is global)

Init burst also confirmed emitting the current mic_level:<value>; on TCI connect (newly-connected clients see the right value immediately).

Ready for review.

@nigelfenton nigelfenton marked this pull request as draft May 28, 2026 02:01
Pre-existing latent bug surfaced by PR aethersdr#3234's mic_level TCI handler:
setMicLevel updated m_micLevel and fired commandReady toward FlexLib,
but never emitted micStateChanged — the signal PhoneCwApplet's mic
slider listens to (PhoneCwApplet.cpp:667 → :728 m_micLevelSlider->
setValue(m_model->micLevel())).

Result: programmatic setMicLevel() calls (now possible via the new TCI
handler) updated the internal state and the radio, but the UI slider
stayed at its last-painted value.  The bug was previously invisible
because nothing called setMicLevel from a non-UI path — the slider's
own onValueChanged was the sole entry point and the widget refreshes
itself on its own value change.

Fix mirrors setSpeechProcessorLevel (line 539) and setRfPower (line
366) — both bound m_micLevel-style fields wrap their state mutation
in an if-different guard + emit the appropriate state-changed signal
before commandReady goes out.

Live-verified by sending mic_level:0,30 / 0,60 via TCI and watching
the PhoneCwApplet slider follow each value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nigelfenton nigelfenton marked this pull request as ready for review May 28, 2026 02:08
@nigelfenton
Copy link
Copy Markdown
Contributor Author

Updated — additional fix in 19a95f5a

Initial smoke test (commit 6dabac51) appeared to pass but was actually a near-miss: the first mic_level:0,75; happened to land on a slider that was already sitting at 75, masking the real bug. Subsequent :30; and :60; commands left the slider stubbornly at 75.

Root cause: pre-existing latent bug in TransmitModel::setMicLevel — it updates m_micLevel and emits commandReady toward FlexLib, but never emits micStateChanged, which is the signal PhoneCwApplet's mic slider listens to (PhoneCwApplet.cpp:667:728 m_micLevelSlider->setValue(m_model->micLevel())). The bug was invisible until now because nothing called setMicLevel from a non-UI path — the slider's onValueChanged was the sole entry, and the slider already knows its own value.

Once cmdMicLevel started calling setMicLevel programmatically, the gap surfaced.

Fix (19a95f5a, 3 lines):

void TransmitModel::setMicLevel(int level) {
    level = qBound(0, level, 100);
    if (m_micLevel != level) {
        m_micLevel = level;
        emit micStateChanged();   // ← added
    }
    emit commandReady(QString("transmit set miclevel=%1").arg(level));
}

Pattern matches setSpeechProcessorLevel (line 539-547) and setRfPower (line 366-374) — both bound-numeric-field setters do the same if-different guard + signal emit before commandReady.

Live re-test on linux-aether — passes ✅

Sent mic_level:0,30; after rebuild → AE's MIC slider visibly moved from 75 to 30. PR is now genuinely end-to-end verified: TCI parse → dispatcher → setMicLevel(N)micStateChanged → UI repaints → slider tracks the sent value.

Updated PR scope

  • Original: 32 lines in TciProtocol.{h,cpp} (the TCI handler itself)
  • Added: 3 lines in TransmitModel.cpp (the latent setter-doesn't-notify bug)

Both fixes are intrinsically linked — without the second, the first is functionally broken from any non-UI caller.

@nigelfenton nigelfenton requested a review from a team as a code owner May 28, 2026 02:24
@nigelfenton
Copy link
Copy Markdown
Contributor Author

Bonus fix in 2aa0347e — Windows manifest link flag

While building this PR locally on Windows (Visual Studio 18 Build Tools, toolset 14.50.35717), the link step failed with:

LINK : fatal error LNK1220: 'MANIFESTINPUT' requires '/MANIFEST:EMBED' specification

CMakeLists.txt:1062-1065 passes /MANIFESTINPUT:<path> to MSVC's linker but not the paired /MANIFEST:EMBED. Per MSVC docs the former is only valid with the latter — older toolsets accepted it with a warning, newer ones reject it. CI presumably uses an older MSVC so the issue's been latent.

One-line fix (4 lines including comment) added the missing flag. After this, the AetherSDR.exe Windows build links cleanly + the binary launches without crash (smoke-tested).

PR scope now: mic_level TCI handler + setMicLevel signal emit + Windows manifest build fix. All three are intrinsically linked to "make mic_level testable on Windows" so they belong in one PR.

@nigelfenton
Copy link
Copy Markdown
Contributor Author

Three-platform build verification — all green ✅

Platform Compiler / Qt Binary Result
Linux (Ubuntu 24.04, x86_64) GCC + Qt 6.4.2 AetherSDR ELF, 259 MB ✅ links clean, live mic_level test passed (slider tracked 30/60/90)
Windows (Win11, MSVC 14.50.35717 / VS 18.x Build Tools) MSVC + Qt 6.10.3 AetherSDR.exe, 54.9 MB ✅ links clean (CMakeLists fix required — see PR commit 2aa0347e), launches to main window
macOS (26.5, Apple Silicon) Clang + Qt 6.11.0 (Homebrew) AetherSDR.app, 45.6 MB arm64 Mach-O ✅ links clean (warnings only — duplicate library flags, pre-existing)

The CMakeLists.txt change is gated by if(MSVC) so it's a no-op on Linux/macOS — zero regression risk on the non-Windows platforms.

Ready for review.

Copy link
Copy Markdown
Contributor

@aethersdr-agent aethersdr-agent Bot left a comment

Choose a reason for hiding this comment

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

Thanks @nigelfenton — the TCI bridge itself is a clean clone of cmdDrive and the init-burst line is the right shape. Just a couple of scope/disclosure concerns since two of the four files changed aren't covered by the PR body (which says "32-line diff across 2 files"):

CMakeLists.txt — unrelated MSVC link-option fix

The /MANIFEST:EMBED + /MANIFESTINPUT change has nothing to do with mic_level and isn't mentioned in the PR description. It's a reasonable fix on its own merits (the comment about LNK1220 on newer MSVC toolsets is plausible), but it should land as a separate PR so the build-system change can be reviewed against the Windows toolchain matrix independently. It also looks like it may overlap with whatever's going on in #3225 ("Embed DFNR model and set Store identity") which just merged — worth confirming there's no conflict with the manifest handling there.

src/models/TransmitModel.cppsetMicLevel now emits micStateChanged

This is actually a behavior change, not just a refactor:

  • The new equality guard is fine and matches the pattern of setActiveMicProfile / setMicSelection etc.
  • Adding emit micStateChanged() is necessary (otherwise the AE UI mic slider wouldn't reflect TCI-initiated changes — PhoneCwApplet.cpp:667 and MainWindow.cpp:2420 both bind to that signal), so it's the right call — but please call it out explicitly in the PR description. Reviewers shouldn't have to chase down why a model setter changed in a "dispatcher entry" PR.

cmdMicLevel body — looks good

Indexing is safe (early-return on empty, then [0] if size==1 else [1]). Stashing the unbounded lvl into m_pendingNotification means a client sending mic_level:0,200; will see a notification echoing 200 even though the radio got 100, but that exactly mirrors cmdDrive/cmdTuneDrive — same pre-existing quirk, not yours to fix here.

Nit on init-burst symmetry: drive/tune_drive use txTrx in the format string even though they're global verbs (legacy form). mic_level correctly uses the spec form with no trx. That's the right choice — just noting that strict-spec clients will now see one new global verb in an init burst otherwise dominated by the legacy TRX-prefixed form. Fine.

Recommend splitting out the CMake change and updating the description to mention the TransmitModel signal addition; the TCI work itself is good to go once those are sorted.


🤖 aethersdr-agent · cost: $7.3603 · model: claude-opus-4-7

@nigelfenton
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @aethersdr-agent — both points actioned:

1. CMake fix split out → #3237. The /MANIFEST:EMBED change is now a standalone PR so it can be reviewed against the Windows toolchain matrix on its own. Force-pushed this branch to drop the CMake commit — #3234 is now purely the 3 mic_level files (TciProtocol.{h,cpp} + TransmitModel.cpp).

On the #3225 overlap you flagged: this branch was cut from 44eaef7c (the #3225 merge commit itself), so the CMake fix sits on top of the Store-identity changes — no conflict, different part of the file. Confirmed upstream main still lacks /MANIFEST:EMBED at line 1064, so the fix is still needed.

2. PR description updated to call out the TransmitModel::setMicLevel change explicitly — it now has its own section explaining why the micStateChanged emit is necessary (TCI-initiated changes wouldn't reflect in the UI slider otherwise) and that it mirrors the setSpeechProcessorLevel / setRfPower pattern.

Good catch on both — agreed the build-system change didn't belong here, and the model-setter change deserved disclosure rather than making a reviewer chase it down.

Also noted (and agreed, not changing here): the unbounded-lvl-in-notification quirk mirrors cmdDrive/cmdTuneDrive exactly, so it's pre-existing and out of scope.

@aethersdr-agent
Copy link
Copy Markdown
Contributor

Thanks @nigelfenton — both follow-ups look good. Re-reviewed against current main:

Scope split (CMake → #3237): ✅ Confirmed. The right call. As a separate PR #3237 can be reviewed against the Windows toolchain matrix on its own merits (LNK1220 wording, MSVC toolset version gates, etc.) without coupling to a TCI behaviour change. And as you noted, the branches don't conflict — different parts of CMakeLists.txt.

Description disclosure of the setMicLevel change: ✅ Reads well. The dedicated section makes the cross-file behaviour change discoverable, the "invisible until now because nothing called it from a non-UI path" framing explains why it didn't surface earlier, and the parallel to setSpeechProcessorLevel / setRfPower makes the pattern defensible.

I re-read the current TransmitModel.cpp:517-522 on main to sanity-check the claim:

void TransmitModel::setMicLevel(int level)
{
    level = qBound(0, level, 100);
    m_micLevel = level;
    emit commandReady(QString("transmit set miclevel=%1").arg(level));
}

Confirmed — no equality guard, no micStateChanged emit. The fix to the if-different-then-emit-then-command pattern is correct, and setSpeechProcessorEnable at :529-537 already establishes the exact same idiom in this file.

On the unbounded-lvl-in-notification point: agreed, out of scope. cmdDrive (TciProtocol.cpp:514) does m_pendingNotification = QStringLiteral("drive:%1;").arg(pwr); with the raw input pwr rather than the model's clamped value — pre-existing, consistent across all three cmdDrive/cmdTuneDrive/cmdMicLevel. If we want to fix it later, it's a separate cleanup PR across all three (and arguably the cleaner shape is reading back from the model after the queued setter runs, not echoing the input).

Scope now feels right: 32 lines TCI handler + 3 lines model signal fix, both intrinsically linked. LGTM from my end — handing back to a human maintainer for the merge call.

73, Jeremy KK7GWY & Claude (AI dev partner)


🤖 aethersdr-agent · cost: $2.6201 · model: claude-opus-4-7

Copy link
Copy Markdown
Collaborator

@jensenpat jensenpat left a comment

Choose a reason for hiding this comment

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

Thanks Nigel for TCI protocol support for Mic gain

@jensenpat jensenpat merged commit c80e955 into aethersdr:main May 28, 2026
11 checks passed
jensenpat pushed a commit that referenced this pull request May 28, 2026
## Summary

`CMakeLists.txt:1062` passes `/MANIFESTINPUT:<path>` to the MSVC linker
but not the required `/MANIFEST:EMBED`. Per MSVC docs the former is only
valid alongside the latter. Older toolsets accepted the lone flag with a
warning; newer ones (VS 18.x / MSVC 14.50+) fail hard:

```
LINK : fatal error LNK1220: 'MANIFESTINPUT' requires '/MANIFEST:EMBED' specification
```

One-line fix (4 lines incl. comment): add `/MANIFEST:EMBED` to the
`target_link_options` call so the manifest is embedded inline (the
intended behaviour anyway).

## Why a separate PR

Split out of #3234 (the `mic_level` TCI handler) on reviewer request —
this is an unrelated build-system change and deserves independent review
against the Windows toolchain matrix.

## Context

- Hit on a fresh Windows build with **Visual Studio 18 Build Tools,
toolset 14.50.35717**, Windows SDK 10.0.26100.0.
- This branch is cut from `44eaef7c` (the #3225 "Embed DFNR model and
set Store identity" merge), so it sits on top of the latest
Windows/manifest changes — no conflict with #3225's Store-identity
handling (different part of the file).
- CI presumably uses an older MSVC, which is why this has been latent.

## Test plan

- [x] Windows local build (MSVC 14.50) links clean with the fix; fails
with LNK1220 without it
- [x] No effect on Linux/macOS — the change is inside the existing
`if(MSVC)` block
- [ ] CI Windows matrix (auto)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nigelfenton nigelfenton deleted the feat/tci-mic-level-handler branch May 28, 2026 09:01
ten9876 pushed a commit that referenced this pull request May 28, 2026
## Problem

`TransmitModel::setMonGainSb()` updates `m_monGainSb` and emits
`commandReady` but never emits `micStateChanged()`. A UI slider bound to
monitor gain via the model signal won't repaint when the value is
changed by MIDI controller, keyboard shortcut, or radio echo — until an
unrelated event happens to trigger a redraw.

Same class of gap as #2084 (VOX setters) and the `setMicLevel` part of
#2180.

**Note:** `setMicLevel` was fixed concurrently in PR #3234 (with a
change guard). This PR addresses the remaining gap — `setMonGainSb`.

## Fix

Add `emit micStateChanged()` after updating `m_monGainSb`, consistent
with how all other mic-state setters behave.

## Setter audit (per #2180)

| Setter | Signal emitted? |
|---|---|
| `setMicSelection` | ✅ `micStateChanged()` |
| `setMicLevel` | ✅ `micStateChanged()` — fixed in #3234 |
| `setSpeechProcessorEnable` | ✅ `micStateChanged()` |
| `setSpeechProcessorLevel` | ✅ `micStateChanged()` |
| `setMonGainSb` | ❌ **missing** — fixed here |
| `setMonGainCw` | ✅ `phoneStateChanged()` |
| `setMonPanCw` | ✅ `phoneStateChanged()` |

Fixes #2180
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.

2 participants