Skip to content

fix(streaming-rate): a profile with an out-of-range rate makes the device refuse to record (#248) - #255

Merged
tylerkron merged 6 commits into
mainfrom
fix/issue-248-streaming-frequency-validation
Sep 6, 2026
Merged

tylerkron merged 6 commits into
mainfrom
fix/issue-248-streaming-frequency-validation

Conversation

@tylerkron

@tylerkron tylerkron commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

closes #248

Not merging — for review.

What was wrong

Apply a saved profile whose sampling frequency is out of range, press START, and the device
refuses to record. Nothing in the app tells you the rate is why.

AbstractStreamingDevice.StreamingFrequency was a bare [ObservableProperty] field, so it stored
whatever it was given — 0, -1, 1001, int.MinValue. That value is not private state: both
paths that start acquisition assign it straight onto the Core device as their first act
(InitializeStreaming, StartSdCardLogging, each doing
coreDevice.StreamingFrequency = StreamingFrequency).

The UI sliders all clamp to Minimum="1" Maximum="1000", so dragging cannot cause this. A profile
can: ProfilesPaneViewModel.ApplyProfileToDevices assigns pd.SamplingFrequency here unfiltered,
and that number is (int)d.Element("SamplingFrequency") out of an XML file on disk
(ProfileXmlStore.cs:218).

What the ticket got right, and the part it did not

Right: the field has no setter validation, and 0 / -1 / 1001 are stored and pushed to Core.

Wrong, and it changes what this is: the ticket reasons about "a silently-wrong rate reaching the
hardware". Core 1.7.0's setter is validating. It throws ArgumentOutOfRangeException outside
1..DeviceCapabilities.MaxSamplingRate. So the bad value never reaches the hardware at all — it
throws inside InitializeStreaming, LoggingFleet.Start catches it as a device refusal, and the
user gets a device that will not start, several screens from wherever the rate was set. A refusal,
not a wrong number.

I did not take that from the ticket or from prose. The five Core_* cases in the new test file (three methods)
exercise the pinned Daqifi.Core package directly and pass against unmodified main — they
are the evidence for the paragraph above, and they stay in the suite so a future Core bump that
changes the contract fails here rather than on a bench.

The ticket's third claim — that the validating property "was dropped during the port" — does not
survive checking. daqifi-desktop's AbstractStreamingDevice carries the identical unguarded
[ObservableProperty] private int _streamingFrequency = 1; and the identical two comments. The
port copied it faithfully; upstream has the same hole.

The fix

One guard, at the one choke point every real device shares:

var ceiling = Math.Max(1, Capabilities.MaxSamplingRate);
var clamped = Math.Clamp(value, 1, ceiling);

Four decisions worth arguing:

Where it dies: at the setter. That is the single place every write goes through — the shell's
SelectedStreamingFrequency, profile apply, and the mobile shell — and the class that hands the
value to Core. Guarding here means DaqifiViewModel and DevicesPaneViewModel both read the
corrected value straight back on their next get, so the RATE chip and the FREQUENCY slider
self-correct with no extra wiring.

Clamp, not throw. Throwing would move the failure to the write site, which sounds better until
you look at the write sites: ApplyProfileToDevices iterates a fleet, so a throw would abandon the
rest of the profile half-applied. Clamping also matches how Core itself handles the other
rate ceiling — EnforceStreamingFrequencyCap() lowers the live rate rather than rejecting it — and
how the other first-party Core consumer already behaves: the MCP server clamps and reports
(Requested 1001 Hz clamped to 1000 Hz). Nothing is hidden by the clamp: the stored value is what
the UI displays, what LoggingManager records as the session's SamplingFrequencyHz, and what is
logged as a warning naming the requested rate, the range and the rate used.

The range is re-established when the ceiling moves, in two places, both raised in review and
both real. This wrapper outlives the capabilities it was clamped against: SyncFromCoreDevice
re-hydrates on every ChannelsPopulated, and a reconnect builds a new Core device while keeping
the wrapper and the rate the user already chose, so a board that then describes a lower ceiling
would leave a stale out-of-range rate stored and fail at the next start — the exact failure this PR
exists to remove. ClampRateToAdvertisedCeiling therefore runs from HydrateDeviceMetadata, so the
stored rate follows a ceiling that has just moved, and immediately before each handoff to Core
(InitializeStreaming, StartSdCardLogging), which is the only moment being out of range costs the
user anything — and which closes the window between capabilities being replaced at the top of
hydration and the rate being corrected further down it.

It assigns the ceiling itself, never a value re-read from the field: hydration runs on Core's
callback thread while the UI and profile apply write from theirs, and a read-then-write would let a
rate chosen in between be overwritten by the stale one. A rate that still fits writes nothing at
all. One residual interleaving is not closable from here and is documented on the method — Core
reads its own copy of the ceiling inside its own setter — so this removes the systematic case, a
stored rate that is permanently out of range, not every possible ordering of an unsynchronized
wrapper.

The ceiling is the device's own advertised maximum, not the sliders' 1000. Hard-coding 1000
would newly reject rates a faster board reports it can sustain, and Core validates against the
advertised figure, not against 1000 — so a compiled-in constant here could only ever disagree with
the layer below it. Math.Max(1, ...) mirrors Core's own sanitization of that same property, which
is public and mutable and can be left at 0 by a malformed capability document.

Verification

Tests — 30 new cases. 28 of them compile against origin/main, and 14 of those fail there
(measured on a throwaway branch off origin/main with the final test file, not carried forward from
an earlier commit). Every one of the 14 is a stored value Core would reject: three below-1, three
above-ceiling, the lowered-ceiling re-hydration case, and the seven rows of
Whatever_the_wrapper_stores_is_a_rate_Core_will_accept, which stores through the wrapper and then
does exactly what InitializeStreaming does with the result — each row threw
ArgumentOutOfRangeException unfixed. The 14 that pass unfixed are the five Core_* contract cases
plus the four in-range rows and the default-value, PropertyChanged, rate-still-fits,
higher-advertised-ceiling and unusable-ceiling cases that pin what must not change. The remaining 2 are the handoff cases: they call ClampRateToAdvertisedCeiling, which this
PR introduces, so they cannot be run against main at all — stated rather than folded into a ratio.
All 30 pass with the fix; full suite 542/542.

There were no existing tests over this property to strengthen — StreamingFrequency had no
coverage at all, which is why a value the layer below refuses could sit here through a port.

Bench (Nq1, fw 3.7.2, USB, /dev/cu.usbmodem1101, non-destructive; rate restored to 100 Hz and
device disconnected afterwards).
Through Core against the real board: 1000 -> 1000 accepted;
1001 -> 1000 and 30000 -> 1000, both reported as clamped at "maximum 1000 Hz"; 0 refused
below 1. With no analog channels enabled the cap came back as 1000, not 0 — per Core's
SampleRateCap rule that is the "device stated no cap and published no rate model" branch, which
returns the board ceiling itself, so MaxSamplingRate on this board is 1000 and the ticket's
premise that 1001 is out of range holds here.

The 22000 comment — corrected reference, and why it is not fixed in this PR. The comment is at
AbstractStreamingDevice.cs:505-506, not :430 — the line moved as this branch grew, and the
earlier reference in this body was stale. It says the bench Nq1 "left MaxSamplingRate at the board
table's 1000 Hz while the device itself reports 22000, and no capability document at all." Checked
against the pinned Daqifi.Core 1.7.0 source, 22000 is not derivable on this board:
DeviceCapabilities defaults MaxSamplingRate to 1000 and its FromDeviceType table returns 1000
for Nyquist1, Nyquist2 and Nyquist3, and the only assignment that raises it above that table is
the capability-document merge — which the same comment says this board does not produce. The bench
run above is the matching observation. The comment is wrong, but it is left alone here: it is
pre-existing code outside this diff; no code path reads 22000 (the ceiling is always read from
Capabilities.MaxSamplingRate at runtime, so the wrong figure cannot produce a wrong ceiling); and
correcting it honestly is not a one-line edit, because the false figure is the load-bearing example
justifying an existing second HydrateDeviceMetadata call — replacing it means re-establishing what
that call actually changes on this board. Filed as #257 instead.

Build gate — desktop and iOS Release/iossimulator-arm64 both green. Warnings measured on my
own merge-base in this worktree, both sides with third_party/oxyplot-avalonia/obj cleared:
desktop 191 and iOS 192 MSBuild all-project totals on both heads, of which 171 / 172 are
first-party. Diffing the first-party warning sets gives zero added and zero removed — the only
delta is one pre-existing CS8625 in this file moving from line 1706 to 1785, which is the added
code and comment above it. No packages.lock.json changed. Android is not buildable on this Mac (workload absent);
CI covers it.

Net-additive, and what is out of scope

+359 / -4. 272 lines of that is the new test file and most of the rest is comment explaining why
the guard exists and what it deliberately does not close; the executable change is a property with
a ten-line setter replacing a two-line attributed field, a six-line private method, and three
one-line calls to it, and it deletes two now-false porting comments. No new type, no attribute, no range-config knob.

Not done, deliberately:

  • No screenshot. The visible effect is second-order — the RATE chip and FREQUENCY slider read
    back a corrected number instead of 0 Hz — and staging it needs a connected board plus a corrupt
    profile on disk, which AvaloniaCapture cannot arrange. Unverified in the UI; verified at the
    property and against Core.
  • A device-side rate correction does not refresh the RATE chip or the FREQUENCY slider. Raised
    in review and confirmed: DaqifiViewModel's selected-device handler returns early on anything but
    FriendlyName, and DevicesPaneViewModel.FrequencyHz only notifies from its own setter. This is
    not new — on main today ApplyProfileToDevices writes device.StreamingFrequency directly and
    neither display follows it — and closing it means propagating device→view-model rate changes
    through two more view models, which is a separate change. The trade here is still favourable: the
    alternative to a stale chip is a device that records nothing.
  • DaqifiViewModel.cs:1787's Math.Max(1, connectedDevice.StreamingFrequency) stays. It is now
    redundant for AbstractStreamingDevice, but it reads through IStreamingDevice, and
    BootloaderSessionStreamingDeviceAdapter implements that interface with an unguarded
    { get; set; }. Removing it would be a different change with a different argument.
  • BootloaderSessionStreamingDeviceAdapter.StreamingFrequency is left unguarded. It exists for
    a bootloader session, which never streams, so its rate never reaches Core's validating setter.

tylerkron and others added 3 commits September 5, 2026 13:48
…Frequency will store

WIP: 13 of 26 fail against origin/main. The Core_ cases pass unfixed and are
the finding: Daqifi.Core 1.7.0's StreamingFrequency setter throws
ArgumentOutOfRangeException outside 1..MaxSamplingRate, so an out-of-range
value stored by the wrapper does not reach the hardware as a wrong rate -- it
aborts InitializeStreaming / StartSdCardLogging.

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

AbstractStreamingDevice.StreamingFrequency was a bare [ObservableProperty]
field, so 0, -1 and any rate above the device's advertised maximum were stored
and then assigned onto the Core device at the top of InitializeStreaming and
StartSdCardLogging. Core 1.7.0's setter validates 1..DeviceCapabilities
.MaxSamplingRate and throws ArgumentOutOfRangeException outside it, so the
consequence was not a wrong rate at the hardware: the device refused to start
recording, with the cause several screens from where the rate was set.

Guard at the setter, clamping to the device's own advertised range (sanitized
the way Core sanitizes it) rather than a compiled-in 1000, so a board that
describes itself as faster stays drivable at its real ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…a port

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Clamp streaming rates to device-advertised limits

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Clamp every streaming-rate assignment to the device-advertised range before acquisition.
• Preserve higher-capability devices and sanitize invalid maximum-rate metadata.
• Pin wrapper behavior and Daqifi.Core’s downstream validation contract with boundary tests.
Diagram

graph TD
  A["Profile or UI"] --> B["Frequency setter"] --> C{"Within range?"}
  C -- Yes --> D["Stored rate"] --> F["Core acquisition"]
  C -- No --> E["Clamp and warn"] --> D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Validate during profile import
  • ➕ Rejects malformed persisted rates close to their source.
  • ➕ Could surface profile-specific validation feedback.
  • ➖ Leaves other callers able to store invalid rates.
  • ➖ Duplicates the downstream device-capability contract.
  • ➖ Does not protect future assignment paths.
2. Reject invalid assignments
  • ➕ Makes invalid inputs immediately visible to callers.
  • ➕ Avoids silently changing requested values.
  • ➖ Requires every caller to handle failures safely.
  • ➖ Could still prevent recording when legacy profiles contain invalid values.
  • ➖ Provides a less resilient user experience than selecting the nearest supported rate.

Recommendation: Keep the PR’s centralized setter guard. It protects every current and future assignment path, mirrors Daqifi.Core’s capability-derived range, preserves rates above the UI’s historical 1000 Hz ceiling, and prevents malformed profiles from blocking recording. Clamping with a warning is preferable to throwing because acquisition remains usable while diagnostics retain the correction.

Files changed (2) +232 / -4

Bug fix (1) +44 / -4
AbstractStreamingDevice.csClamp streaming frequency at the shared assignment point +44/-4

Clamp streaming frequency at the shared assignment point

• Replaces the generated observable property with an explicit setter that clamps rates to 1 through the device-advertised maximum, sanitized to at least 1. Out-of-range assignments emit a warning while preserving property-change notifications and support for devices exceeding 1000 Hz.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs

Tests (1) +188 / -0
StreamingFrequencyRangeTests.csCover wrapper bounds and Daqifi.Core rate validation +188/-0

Cover wrapper bounds and Daqifi.Core rate validation

• Adds boundary, advertised-capability, default-value, and property-notification tests for streaming-frequency storage. Contract tests also verify that every wrapper-stored rate is accepted by the pinned Daqifi.Core package.

Daqifi.Avalonia.Tests/Device/StreamingFrequencyRangeTests.cs

@qodo-code-review

qodo-code-review Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Capability refresh bypasses validation 🐞 Bug ≡ Correctness
Description
StreamingFrequency is validated only when assigned, while HydrateDeviceMetadata can later
replace the maximum without revalidating the stored value. After reconnecting to a device or
firmware that advertises a lower maximum, acquisition passes the stale rate to Core and still fails
with ArgumentOutOfRangeException.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[R81-82]

+            var ceiling = Math.Max(1, Capabilities.MaxSamplingRate);
+            var clamped = Math.Clamp(value, 1, ceiling);
Relevance

●●● Strong

Accepted patterns prioritize preventing stale state after reconnects or session refreshes; this
directly causes repeated acquisition failure.

PR-#226
PR-#219

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed setter clamps only during a write. Connect explicitly supports reconnecting the same
wrapper, and cleanup does not reset its metadata or frequency; final initialization then copies
newly reported metadata through HydrateDeviceMetadata without touching StreamingFrequency. Both
startup paths subsequently assign that stored value directly to Core, while the added Core test
proves a value above the refreshed maximum throws.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[81-90]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[432-472]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[686-724]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2227-2234]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1592-1617]
Daqifi.Avalonia.Tests/Device/StreamingFrequencyRangeTests.cs[57-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`StreamingFrequency` is clamped against the capabilities present when its setter runs, but reconnecting refreshes those capabilities without revalidating the stored frequency. If the refreshed maximum is lower, startup still passes an invalid rate to Core and the device refuses to record.

## Issue Context
The wrapper and its metadata survive cleanup and reconnect, while each connection creates a new Core device and copies its newly reported metadata into the existing wrapper. Both acquisition paths subsequently assign the unchanged stored frequency to Core.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[76-90]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[464-472]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2227-2234]
- Daqifi.Avalonia.Tests/Device/StreamingFrequencyRangeTests.cs[166-187]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Startup can beat re-clamp ✓ Resolved 🐞 Bug ≡ Correctness ⭐ New
Description
Capability hydration replaces Metadata before this unsynchronized re-clamp, leaving a window where
startup can read the old frequency against the new lower ceiling. Starting acquisition in that
window still sends an out-of-range value to Core and causes the recording refusal this PR is
intended to prevent.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[R2263-2266]

+        var advertisedCeiling = Math.Max(1, Capabilities.MaxSamplingRate);
+        if (_streamingFrequency > advertisedCeiling)
+        {
+            StreamingFrequency = advertisedCeiling;
Relevance

●●● Strong

Recent accepted precedents prioritize fixing cross-thread lifecycle races that can corrupt state or
user-visible behavior.

PR-#178
PR-#239

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Metadata.CopyFrom installs the refreshed capabilities before the new conditional correction runs.
ChannelsPopulated invokes this hydration from Core's callback path, while both startup paths
independently read the wrapper frequency and assign it directly to Core without synchronization, so
they can execute in the intervening window.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2231-2234]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2257-2267]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2108-2133]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1347-1353]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1592-1594]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Capability hydration installs the new device capabilities before correcting `StreamingFrequency`. Because acquisition startup is unsynchronized with hydration, it can read and submit the stale frequency during that interval, causing Core to reject the start.

## Issue Context
Both acquisition paths assign `StreamingFrequency` directly to Core. Ensure they cannot observe a lower capability ceiling before the wrapper frequency has been corrected, either by synchronizing capability hydration and startup or by validating/clamping atomically at the Core handoff.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2231-2267]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1347-1353]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1592-1594]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Capability clamp leaves stale UI ✓ Resolved 🐞 Bug ≡ Correctness
Description
When hydration lowers StreamingFrequency, neither the drawer's FrequencyHz binding nor the
shell's SelectedStreamingFrequency is refreshed. The device therefore records at the corrected
rate while the open slider and RATE chip can continue displaying the old rate.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2257]

+        StreamingFrequency = _streamingFrequency;
Relevance

●●● Strong

UI refresh omissions are accepted when asynchronous device notifications leave bound values stale.

PR-#53
PR-#4

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added assignment can change the stored rate and raise StreamingFrequency notification.
However, FrequencyHz only raises its notification from its own setter, the selected-device handler
only processes FriendlyName, and the RATE chip's backing field is only seeded when it is below
one; neither displayed value follows this asynchronous clamp.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2257]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DevicesPaneViewModel.cs[59-75]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[1225-1253]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[1775-1788]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Capability hydration can clamp the device's streaming frequency, but the view-model properties displayed by the frequency slider and RATE chip are not notified or synchronized. This leaves the UI showing a rate different from the one the device will use.

## Issue Context
`DevicesPaneViewModel.FrequencyHz` reads from the device but only raises its own notification after a UI-originated write. `DaqifiViewModel.SelectedStreamingFrequency` is likewise synchronized only by its setter or initial connection seeding.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2258]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Hydration can overwrite rate ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new read-then-set can race with a UI or profile assignment because ChannelsPopulated hydration
runs on a Core/connection worker thread. If hydration reads the old field immediately before a user
write, its setter subsequently restores that stale value and loses the user's new rate.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2257]

+        StreamingFrequency = _streamingFrequency;
Relevance

●● Moderate

Similar worker-thread race findings were rejected, but this lost-update scenario is specific and
plausibly correctness-impacting.

PR-#37
PR-#158

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Core's event handler calls SyncFromCoreDevice directly, which invokes hydration; connection itself
also runs through Task.Run. Meanwhile, both the shell setter and profile application write the
same property without shared synchronization, so line 2257's separate field read and setter write
admit a lost-update interleaving.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2102-2133]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2257]
Daqifi.Avalonia/Daqifi.Desktop/ConnectionManager.cs[478-482]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[533-553]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[388-392]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Hydration performs an unsynchronized read-then-write of `_streamingFrequency`. A concurrent user or profile update can occur between those operations and then be overwritten by the stale hydration value.

## Issue Context
`ChannelsPopulated` directly reaches hydration from Core callback threads, while frequency writes also originate from UI and profile paths. Revalidation should clamp only the value that is still current, retry when it changed, or serialize all frequency mutation through one execution context.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2258]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: ⚖️ Balanced: This is a localized but behavior-changing runtime fix affecting hydration and both acquisition handoff paths, so it carries real device-startup risk that warrants a complete review.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 8db9f90

Results up to commit c6a7a1d ⚖️ Balanced


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Capability refresh bypasses validation 🐞 Bug ≡ Correctness
Description
StreamingFrequency is validated only when assigned, while HydrateDeviceMetadata can later
replace the maximum without revalidating the stored value. After reconnecting to a device or
firmware that advertises a lower maximum, acquisition passes the stale rate to Core and still fails
with ArgumentOutOfRangeException.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[R81-82]

+            var ceiling = Math.Max(1, Capabilities.MaxSamplingRate);
+            var clamped = Math.Clamp(value, 1, ceiling);
Relevance

●●● Strong

Accepted patterns prioritize preventing stale state after reconnects or session refreshes; this
directly causes repeated acquisition failure.

PR-#226
PR-#219

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed setter clamps only during a write. Connect explicitly supports reconnecting the same
wrapper, and cleanup does not reset its metadata or frequency; final initialization then copies
newly reported metadata through HydrateDeviceMetadata without touching StreamingFrequency. Both
startup paths subsequently assign that stored value directly to Core, while the added Core test
proves a value above the refreshed maximum throws.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[81-90]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[432-472]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[686-724]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2227-2234]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[1592-1617]
Daqifi.Avalonia.Tests/Device/StreamingFrequencyRangeTests.cs[57-64]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`StreamingFrequency` is clamped against the capabilities present when its setter runs, but reconnecting refreshes those capabilities without revalidating the stored frequency. If the refreshed maximum is lower, startup still passes an invalid rate to Core and the device refuses to record.

## Issue Context
The wrapper and its metadata survive cleanup and reconnect, while each connection creates a new Core device and copies its newly reported metadata into the existing wrapper. Both acquisition paths subsequently assign the unchanged stored frequency to Core.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[76-90]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[464-472]
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2227-2234]
- Daqifi.Avalonia.Tests/Device/StreamingFrequencyRangeTests.cs[166-187]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 9d41358 ⚖️ Balanced


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Capability clamp leaves stale UI ✓ Resolved 🐞 Bug ≡ Correctness
Description
When hydration lowers StreamingFrequency, neither the drawer's FrequencyHz binding nor the
shell's SelectedStreamingFrequency is refreshed. The device therefore records at the corrected
rate while the open slider and RATE chip can continue displaying the old rate.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2257]

+        StreamingFrequency = _streamingFrequency;
Relevance

●●● Strong

UI refresh omissions are accepted when asynchronous device notifications leave bound values stale.

PR-#53
PR-#4

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added assignment can change the stored rate and raise StreamingFrequency notification.
However, FrequencyHz only raises its notification from its own setter, the selected-device handler
only processes FriendlyName, and the RATE chip's backing field is only seeded when it is below
one; neither displayed value follows this asynchronous clamp.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2257]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DevicesPaneViewModel.cs[59-75]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[1225-1253]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[1775-1788]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Capability hydration can clamp the device's streaming frequency, but the view-model properties displayed by the frequency slider and RATE chip are not notified or synchronized. This leaves the UI showing a rate different from the one the device will use.

## Issue Context
`DevicesPaneViewModel.FrequencyHz` reads from the device but only raises its own notification after a UI-originated write. `DaqifiViewModel.SelectedStreamingFrequency` is likewise synchronized only by its setter or initial connection seeding.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2258]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Hydration can overwrite rate 🐞 Bug ≡ Correctness
Description
The new read-then-set can race with a UI or profile assignment because ChannelsPopulated hydration
runs on a Core/connection worker thread. If hydration reads the old field immediately before a user
write, its setter subsequently restores that stale value and loses the user's new rate.
Code

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2257]

+        StreamingFrequency = _streamingFrequency;
Relevance

●● Moderate

Similar worker-thread race findings were rejected, but this lost-update scenario is specific and
plausibly correctness-impacting.

PR-#37
PR-#158

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Core's event handler calls SyncFromCoreDevice directly, which invokes hydration; connection itself
also runs through Task.Run. Meanwhile, both the shell setter and profile application write the
same property without shared synchronization, so line 2257's separate field read and setter write
admit a lost-update interleaving.

Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2102-2133]
Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2257]
Daqifi.Avalonia/Daqifi.Desktop/ConnectionManager.cs[478-482]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[533-553]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/ProfilesPaneViewModel.cs[388-392]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Hydration performs an unsynchronized read-then-write of `_streamingFrequency`. A concurrent user or profile update can occur between those operations and then be overwritten by the stale hydration value.

## Issue Context
`ChannelsPopulated` directly reaches hydration from Core callback threads, while frequency writes also originate from UI and profile paths. Revalidation should clamp only the value that is still current, retry when it changed, or serialize all frequency mutation through one execution context.

## Fix Focus Areas
- Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs[2249-2258]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs
…ling moves

Qodo review (High, correctness): the setter's guard held only at assignment
time, but this wrapper outlives the capabilities it was clamped against.
SyncFromCoreDevice re-hydrates on every ChannelsPopulated, and a reconnect
builds a new Core device while keeping the wrapper and the rate the user
already chose -- so a board that then describes a lower ceiling left a stale
out-of-range rate stored, and the next start handed it to Core and was
rejected there, which is the exact failure this PR removes.

Re-run the setter from HydrateDeviceMetadata. Two tests: one pins the
lowered-ceiling case (it fails with the new line disabled), one pins that a
rate which still fits is left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs Outdated
Comment thread Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9d41358

…ead rate

Qodo review round 2 (Moderate, correctness): the re-clamp read _streamingFrequency
and then wrote it back through the setter. Hydration runs on Core's callback
thread while the UI and profile apply write this property from theirs, so a rate
written between the read and the write was overwritten by the stale one -- a lost
update that discards a rate the user just chose.

Assign the ceiling itself instead, and only when the stored rate exceeds it.
Losing the race now costs at worst a rate the hardware could not have run at;
the common case (a rate that still fits) writes nothing at all. Both existing
re-hydration tests still hold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

Triage of the round-2 findings (reviewed against 9d41358; fix pushed as 3765447).

1. "Capability refresh bypasses validation" (High) — already fixed; this finding is stale.
It is the round-1 item, carried forward. Its Evidence says hydration "copies newly reported metadata
through HydrateDeviceMetadata without touching StreamingFrequency" — that was true at c6a7a1d
and is not true at 9d41358, where HydrateDeviceMetadata does re-establish the range. Finding 2
in the same review quotes the very line that does it (AbstractStreamingDevice.cs:2257). Pinned by
A_stored_rate_is_brought_inside_a_ceiling_that_moves_down, which I verified catches the defect by
disabling that line (with a touch, so MSBuild genuinely rebuilt): it fails without it and passes
with it. I have resolved the inline thread for it.

2. "Capability clamp leaves stale UI" (Medium) — the gap is real, but it predates this PR and is
a separate change.

Your reading of the wiring is correct and I confirmed it: DaqifiViewModel's selected-device
handler returns early on anything but FriendlyName, and DevicesPaneViewModel.FrequencyHz only
notifies from its own setter. So no device-side change to StreamingFrequency reaches the RATE chip
or the FREQUENCY slider.

What makes it out of scope here is that this is not new. On main today,
ProfilesPaneViewModel.ApplyProfileToDevices writes device.StreamingFrequency directly, bypassing
the shell setter, and nothing after it refreshes either display — so applying a profile already
leaves both showing the old rate. Closing this properly means propagating device→view-model rate
changes, which touches two more view models and fixes the profile path too; that is its own change
with its own argument, not a rider on a setter guard.

Worth being explicit about the trade, because it is not "correct display" versus "stale display".
Before: chip shows 5000, device stores 5000, START throws inside Core and nothing records.
After: chip shows 5000, device stores 1000, recording runs at 1000, a warning names the correction,
and the session's SamplingFrequencyHz is written as the real 1000. Strictly better, with one
display left to fix separately.

3. "Hydration can overwrite rate" (Moderate) — valid, and fixed in 3765447.
Conceded: the line did read _streamingFrequency and write it back through the setter, so a rate
written from the UI or profile-apply thread in between was lost to the stale one. That lost update
was genuinely introduced by this PR — the property had only ever been written, never
read-modify-written.

It now assigns the ceiling itself, and only when the stored rate exceeds it:

var advertisedCeiling = Math.Max(1, Capabilities.MaxSamplingRate);
if (_streamingFrequency > advertisedCeiling)
{
    StreamingFrequency = advertisedCeiling;
}

No value read from the field is ever written back, so the interleaving cannot restore a stale rate:
the loser of the race is either the user's rate or the ceiling, and the ceiling is in range. The
common case — a rate that still fits — writes nothing at all. I did not add a lock or a retry loop:
this property has never had synchronization, SelectedStreamingFrequency and
ApplyProfileToDevices already race with each other, and HydrateDeviceMetadata's other
assignments (DeviceType, NetworkConfiguration.Ssid, SecurityType, Mode) have exactly the
same shape — serializing this one int would be new machinery inconsistent with the file it sits
in.

Suite 540/540; desktop and iOS both green; first-party warning set unchanged against my own
merge-base (0 added, 0 removed).

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3765447

@tylerkron

Copy link
Copy Markdown
Contributor Author

Round 3 (3765447) triage — the review is now down to one item, and that one is stale.

"Capability clamp leaves stale UI" — Qodo has marked it ✓ Resolved. Agreed as scoped: the gap is
real but predates this PR (ApplyProfileToDevices already desynchronizes both displays on main)
and is noted as a follow-up in the PR body.

"Capability refresh bypasses validation" (High) — rebutting on evidence, for the third round. Its
Evidence paragraph is unchanged since c6a7a1d and says:

final initialization then copies newly reported metadata through HydrateDeviceMetadata without
touching StreamingFrequency

That has not been true since 9d41358. Here is HydrateDeviceMetadata as it stands at the exact
commit this round reviewed, 3765447 (the finding's own "Fix Focus Areas" points at
AbstractStreamingDevice.cs[2227-2234], the opening of this method):

var advertisedCeiling = Math.Max(1, Capabilities.MaxSamplingRate);
if (_streamingFrequency > advertisedCeiling)
{
    StreamingFrequency = advertisedCeiling;
}

Three independent confirmations that the described failure cannot occur at this head:

  1. A_stored_rate_is_brought_inside_a_ceiling_that_moves_down stores 5000 Hz under a 22000 Hz
    ceiling, re-hydrates from a device advertising 1000, and asserts both that the wrapper now holds
    1000 and that Core accepts it. It is at
    StreamingFrequencyRangeTests.cs[166-187] — the range this finding itself lists as a fix focus
    area, i.e. the test that already covers it.
  2. I verified that test genuinely exercises the guard rather than passing vacuously, by disabling
    the guard and rebuilding (with a touch, so MSBuild could not hand the runner a stale assembly):
    it fails, and passes again when restored.
  3. The finding's own Code block still quotes StreamingFrequency = _streamingFrequency; — a line
    that was replaced in 3765447 and no longer exists in the tree. The two round-2 inline threads
    anchored to it are both marked outdated by GitHub for the same reason.

If there is a different path that re-hydrates capabilities without going through
HydrateDeviceMetadata, I would take that seriously and fix it — but the three call sites the
finding cites (Connect at 432-472, cleanup/reconnect at 686-724, and 2227-2234) all route through
this method, which is why guarding it there was the fix.

State at 3765447: zero unresolved qodo review threads, the one previously-valid finding fixed and
resolved, the stale-UI finding resolved as out of scope, and this item disproven against the
reviewed commit. Suite 540/540; desktop and iOS green; first-party warning set unchanged (0 added,
0 removed) against my own merge-base.

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread Daqifi.Avalonia/Daqifi.Desktop/Device/AbstractStreamingDevice.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3765447

…ration

Qodo review round 4 (Medium, correctness): HydrateDeviceMetadata installs the
new capabilities at the top of the method and corrected the rate at the bottom,
so acquisition starting in that window read the old rate against the new lower
ceiling and Core rejected it -- the exact refusal this PR removes.

Extract the correction into ClampRateToAdvertisedCeiling and call it
immediately before each handoff to Core (InitializeStreaming,
StartSdCardLogging) as well as from hydration. The handoff call is what closes
the window; the hydration call is what keeps the stored rate, and anything
displaying it, current.

A residual interleaving remains and is documented on the method: Core reads its
own copy of the ceiling inside its own setter, so this cannot be made atomic
from here. It removes the systematic case, not every ordering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8db9f90

@tylerkron

Copy link
Copy Markdown
Contributor Author

Round 5 (8db9f90) — state of the review.

Resolved by Qodo this round: "Startup can beat re-clamp" and "Capability clamp leaves stale
UI"
. Resolved earlier: "Hydration can overwrite rate". Zero unresolved review threads.

The one item still listed, "Capability refresh bypasses validation", is the same carry-over from
c6a7a1d for a fourth round with byte-identical Evidence. It still says hydration "copies newly
reported metadata through HydrateDeviceMetadata without touching StreamingFrequency" and its
Code block still quotes the setter hunk from the first commit. At 8db9f90 — the commit this round
reviewed — the remedy its own agent prompt asks for ("either by synchronizing capability hydration
and startup or by validating/clamping atomically at the Core handoff") is implemented at all three
of the sites it names:

  • HydrateDeviceMetadata → ClampRateToAdvertisedCeiling();
  • InitializeStreaming → ClampRateToAdvertisedCeiling(); immediately before
    coreStreamingDevice.StreamingFrequency = StreamingFrequency;
  • StartSdCardLogging → the same, before coreDevice.StreamingFrequency = StreamingFrequency;

and is pinned by four tests: the two re-hydration cases and the two handoff cases. I verified the
first of them genuinely exercises the guard by disabling it and rebuilding with a touch, so
MSBuild could not hand the runner a stale assembly — it fails without the guard and passes with it.

I am treating this as disproven against the reviewed commit rather than pushing further changes for
it. If a reviewer can point at a path that refreshes capabilities without reaching
HydrateDeviceMetadata, or that hands a rate to Core without going through one of those two
acquisition methods, I will take it seriously — but the three call sites the finding itself cites
(Connect 432-472, cleanup/reconnect 686-724, hydration 2227-2234) all route through code that now
re-clamps.

Final state: 30 new tests, all passing; full suite 542/542; desktop and iOS
Release/iossimulator-arm64 both green; first-party warning set unchanged against my own
merge-base (0 added, 0 removed); no packages.lock.json drift. Bench-checked non-destructively
against the Nq1 on fw 3.7.2 over USB. Ready for a human — not merging.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Shepherd verification — independent of the author's report

Qodo, on the current head 8db9f90. Both surfaces, substring filter (startswith("qodo-code-review"),
which matches the [bot]-suffixed REST login and the unsuffixed GraphQL one alike), checked twice
~10 minutes apart (20:55Z and 21:04Z) with the head unmoved between them:

  • reviewThreads: 4 Qodo-authored threads, 0 unresolved — and 0 unresolved threads from any
    author. Author-matching leg validated against the fix(logging): a half-finished Delete All left a table-less database, and the next logging toggle killed the app #196 control, which returns 3 as expected.

  • Summary comment (5554445952, last edited 20:45:03Z): still headed 🐞 Bugs (2). Finding 2
    ("Startup can beat re-clamp") is struck/resolved. Finding 1 ("Capability refresh bypasses
    validation", High) is not struck, and it is stale — checked in code, not taken on trust:

    StreamingFrequency is validated only when assigned, while HydrateDeviceMetadata can later
    replace the maximum without revalidating the stored value.

    HydrateDeviceMetadata calls ClampRateToAdvertisedCeiling() after Metadata.CopyFrom(...)
    replaces Capabilities (AbstractStreamingDevice.cs:2288), and both handoffs to Core call it
    again immediately before assigning. The finding's premise is false on this head. Qodo edits this
    comment in place and its Bugs (N) badge does not decrement for findings it silently stops
    re-raising, so the badge is not the signal here — the threads are.

CI on that exact SHA. Run 33990869814, head 8db9f90a, all six real jobs green (Desktop
macOS, iOS, Library + Desktop + capture harness, Android, CI script self-tests, Avalonia version
lockstep). Android is covered here and not locally — the workload is absent on this Mac.

Numbers re-derived from the final diff, not carried forward. git diff --numstat origin/main...HEAD
gives +359 / -4 (272 test + 87 source added, 4 removed) — matches the body. The test file holds
20 InlineData rows across 5 [Theory] plus 10 [Fact] = 30 cases, matching "30 new". Closed
from the other end against CI's own logs: main at 4b37a22 runs 512, this head runs 542,
and 542 − 512 = 30 exactly. The 28-compile / 14-fail split is internally consistent.

Two body inaccuracies found and corrected in place (no commit, head unchanged): the 22000 comment
is at :505-506, not :430; and the list of "the 14 that pass unfixed" enumerated only 12,
omitting the higher-advertised-ceiling and unusable-ceiling cases.

The 22000 comment is wrong, and is now #257. Against the pinned Core 1.7.0 source,
DeviceCapabilities defaults MaxSamplingRate to 1000 and its board table returns 1000 for every
Nyquist type; the only assignment that raises it above that is the capability-document merge, which
the same comment says this board does not produce. So 22000 is not derivable here, independently of
the bench run. Kept out of this PR deliberately: pre-existing code outside this diff, no code path
reads the constant, and the figure is the load-bearing example for an existing re-hydration call —
so an honest correction needs bench evidence, not a one-line edit, and pushing it would have cost
this PR its CI green and its settled review for no behavioural gain.

The gap no green covered: #255 against today's main and #233

main moved six PRs today (14:02Z–14:19Z). #255's merge-base is already 4b37a22, today's main
head, so CI's green does cover it against current main. What nothing covered is the open-PR
combination — the shape that ejected #189 from the merge queue with CS0115 while showing CLEAN,
with zero file overlap. #233 touches only Loggers/, #255 only Device/: zero overlap here too.

Built locally as main 4b37a22 + #233 26335b1 + #255 8db9f90, both merging cleanly:

result
Desktop green, 191 warnings
iOS Release/iossimulator-arm64 green, 192 warnings
Suite 560/560, 0 failed
packages.lock.json no drift

The two warning totals are MSBuild all-project totals (~20 of them third_party OxyPlot), not
first-party-only, and they are identical to the totals reported for #255 alone — so #233 adds none.
The test count reconciles exactly: 512 (main) + 18 (#233) + 30 (#255) = 560, with no
interaction between the two PRs. The 18 is consistent with the 530 reported for #233 on this main.

#256 is not covered. Its head moved under me during this run (e2d49c2 at 20:51Z → 49be77c
at 21:00Z) while its builder is still working, so there was no stable tree to build against.


Qodo-clean, CI green — ready for review at 8db9f90a554d00ffa87f49fe1a201c7c7b23c074.

@tylerkron
tylerkron added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 7e7a1cd Sep 6, 2026
7 checks passed
@tylerkron
tylerkron deleted the fix/issue-248-streaming-frequency-validation branch September 6, 2026 00:42
@tylerkron

Copy link
Copy Markdown
Contributor Author

Correction to the bench section of this body — the code merged here is fine; two factual claims in
the write-up are not.

Posting this because the description is now known to be wrong on a point that already cost a ticket,
and a merged body cannot be edited away from whoever reads it next.

This body states:

With no analog channels enabled the cap came back as 1000, not 0 — per Core's SampleRateCap
rule that is the "device stated no cap and published no rate model" branch, which returns the board
ceiling itself, so MaxSamplingRate on this board is 1000

and, in the 22000 comment section:

Checked against the pinned Daqifi.Core 1.7.0 source, 22000 is not derivable on this board
[...] The comment is wrong

Both are false for this board. Re-measured on the same unit (Nq1, fw 3.7.2, USB,
/dev/cu.usbmodem1101), non-destructive:

1. The device does publish a capability document, with a rate model. Read directly with the two
queries Core itself sends:

CONFigure:CAPabilities:APIVersion?  -> 2
CONFigure:CAPabilities:JSON?        -> schema_version 2
  streaming.sample_rate_range_hz    : {"min": 1, "max": 22000}
  streaming.current_max_rate_hz     : 7746
  rate_model.absolute_max_hz        : 22000

2. MaxSamplingRate on this board is 22000, not 1000 — after InitializeAsync() has completed.
Through the pinned Daqifi.Core 1.7.0 package, snapshotting at the two points
AbstractStreamingDevice.Connect() hydrates at:

at first ChannelsPopulated : MaxSamplingRate 1000, CapabilityDocument null
after InitializeAsync()    : MaxSamplingRate 22000, CapabilityDocument present (schema 2)
ChannelsPopulated raises   : 1

1000 is the board-table bootstrap value that is correct only before the document is applied. A
reading taken at or before ChannelsPopulated sees it and is measuring the pre-hydration state, not
the device's own answer. I did not re-run the 1001 -> 1000 / 30000 -> 1000 clamping observations
above, so I am not asserting what those calls returned or why; only that the ceiling they were
compared against is not this device's ceiling once it has been initialized.

What this does and does not change. The fix merged here is unaffected and still correct: the
guard clamps to Capabilities.MaxSamplingRate rather than a hard-coded 1000, which is exactly right
precisely because the advertised ceiling can be 22000. This body argued for that choice correctly
while getting the board's number wrong.

What it does change is the verdict on the 22000 comment, which was filed from here as #257. That
ticket's premise — that 22000 was not derivable and the comment was wrong — does not hold: the
comment was right, and the one-line 22000 -> 1000 edit it proposed would have replaced a correct,
device-derived figure with the board-table default. #269 disproves #257 and closes it, and rewrites
the comment to argue from the mechanism and name Capabilities.MaxSamplingRate as the source of
truth instead of quoting any number.

Separately, current_max_rate_hz (7746) is enforced by nothing in this app, and the rate sliders
still cap at 1000 while the board advertises 22000 — filed as #270.

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.

Streaming frequency has no setter validation: 1001, 0 and -1 are stored and pushed to Core

1 participant