commit ec11802b58ca5856299b2473146e63c30c0dadce
parent 019b2184f709572d24c47b51cbd50164d2c82761
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date: Sun, 31 May 2026 21:23:16 -0500
feat(core): port kernel meter-level remap (dest->raw index)
scarlett-core/src/meter.rs faithfully reimplements
scarlett2_update_meter_level_map: build_level_map() turns the live routing
(mux[dest]=src) + the device meter_map into dest->raw-index, with source
dedup and Off->zero. Tested (4 cases). Foundation for per-channel meters;
source->meter still needs routing inversion + hardware verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat:
2 files changed, 130 insertions(+), 0 deletions(-)
diff --git a/scarlett-core/src/lib.rs b/scarlett-core/src/lib.rs
@@ -22,6 +22,7 @@
pub mod controls;
pub mod matrix;
+pub mod meter;
pub mod model;
pub mod packet;
pub mod ports;
diff --git a/scarlett-core/src/meter.rs b/scarlett-core/src/meter.rs
@@ -0,0 +1,129 @@
+//! Meter level mapping — turn the raw `GET_METER` array into per-destination
+//! levels, faithfully porting the kernel's `scarlett2_update_meter_level_map`.
+//!
+//! The device returns `meter_count` raw u32 levels in **`meter_map` order** (a
+//! list of `(start_destination, count)` spans), NOT in a friendly channel order.
+//! To read "the level at destination D" you need a map `dest -> raw_index`, and
+//! because the firmware reports a shared source only once, building that map
+//! requires the **current routing** (`mux[dest] = source`). This module builds
+//! that map so the UI can show a meter for any destination (and thus any input,
+//! by finding a destination it feeds).
+//!
+//! All values here are interoperability facts from the GPL kernel driver; the
+//! algorithm is re-implemented, not copied.
+
+/// One span of the device's meter report: `count` consecutive destinations
+/// starting at destination index `start`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct MeterSpan {
+ pub start: u16,
+ pub count: u16,
+}
+
+/// The 18i20 g3 `meter_map` (from `s18i20_gen3_info.meter_map`). Sum of counts
+/// = 65 = the GET_METER length.
+pub const METER_MAP_18I20_GEN3: &[MeterSpan] = &[
+ MeterSpan { start: 45, count: 8 },
+ MeterSpan { start: 55, count: 10 },
+ MeterSpan { start: 0, count: 20 },
+ MeterSpan { start: 53, count: 2 },
+ MeterSpan { start: 20, count: 25 },
+];
+
+/// Sentinel meaning "no live source → level is zero".
+pub const MAP_NONE: u16 = u16::MAX;
+
+/// Build `meter_level_map[dest] = raw_index` for `num_dsts` destinations.
+///
+/// `mux[dest]` is the source number currently routed to that destination (the
+/// value `0` conventionally meaning "Off"/None). Mirrors the kernel: walk the
+/// meter_map spans assigning each destination the raw index `i`; if a source is
+/// already seen at an earlier raw index, reuse it; sources that are Off map to
+/// [`MAP_NONE`].
+pub fn build_level_map(map: &[MeterSpan], mux: &[u16], num_dsts: usize) -> Vec<u16> {
+ let mut level_map = vec![MAP_NONE; num_dsts];
+
+ // seen_src[src] -> Some(raw_index) once a source has been assigned. Source 0
+ // ("Off") is pre-seen as None so it always maps to zero.
+ let max_src = mux.iter().copied().max().unwrap_or(0) as usize + 1;
+ let mut seen: Vec<Option<u16>> = vec![None; max_src.max(1)];
+ seen[0] = Some(MAP_NONE); // "Off" → zero level
+
+ let mut i: u16 = 0; // index into the raw GET_METER response
+ for span in map {
+ for j in 0..span.count {
+ let dest = (span.start + j) as usize;
+ let src = mux.get(dest).copied().unwrap_or(0) as usize;
+
+ if src < seen.len() && seen[src].is_none() {
+ seen[src] = Some(i);
+ }
+ let raw_idx = src.lt(&seen.len()).then(|| seen[src]).flatten().unwrap_or(MAP_NONE);
+ if dest < level_map.len() {
+ level_map[dest] = raw_idx;
+ }
+ i += 1;
+ }
+ }
+ level_map
+}
+
+/// Resolve the level at `dest` given a built `level_map` and the raw meter array.
+pub fn level_at(level_map: &[u16], raw: &[u32], dest: usize) -> u32 {
+ match level_map.get(dest).copied() {
+ Some(MAP_NONE) | None => 0,
+ Some(idx) => raw.get(idx as usize).copied().unwrap_or(0),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn map_sums_to_65() {
+ let total: u16 = METER_MAP_18I20_GEN3.iter().map(|s| s.count).sum();
+ assert_eq!(total, 65);
+ }
+
+ #[test]
+ fn identity_routing_assigns_increasing_raw_indices() {
+ // A simple map: one span of 4 destinations starting at 0, each fed by a
+ // distinct nonzero source.
+ let map = [MeterSpan { start: 0, count: 4 }];
+ let mux = vec![1u16, 2, 3, 4]; // dest0<-src1, dest1<-src2, ...
+ let lm = build_level_map(&map, &mux, 4);
+ // raw index increments with position
+ assert_eq!(lm, vec![0, 1, 2, 3]);
+ // and level_at pulls the right raw value
+ let raw = vec![10u32, 20, 30, 40];
+ assert_eq!(level_at(&lm, &raw, 2), 30);
+ }
+
+ #[test]
+ fn off_source_maps_to_zero() {
+ let map = [MeterSpan { start: 0, count: 3 }];
+ let mux = vec![0u16, 5, 0]; // dest0 Off, dest1<-src5, dest2 Off
+ let lm = build_level_map(&map, &mux, 3);
+ assert_eq!(lm[0], MAP_NONE);
+ assert_eq!(lm[2], MAP_NONE);
+ // raw index increments for EVERY destination (incl. Off ones), so src5
+ // at dest1 sits at raw index 1.
+ assert_eq!(lm[1], 1);
+ let raw = vec![99u32, 77, 55];
+ assert_eq!(level_at(&lm, &raw, 0), 0); // Off → 0
+ assert_eq!(level_at(&lm, &raw, 1), 77); // src5 at raw index 1
+ }
+
+ #[test]
+ fn shared_source_reuses_first_raw_index() {
+ // Same source feeds two destinations; both should read the SAME raw idx
+ // (the first time it was seen), per the kernel's dedup.
+ let map = [MeterSpan { start: 0, count: 3 }];
+ let mux = vec![7u16, 7, 8]; // dest0<-src7, dest1<-src7, dest2<-src8
+ let lm = build_level_map(&map, &mux, 3);
+ assert_eq!(lm[0], 0); // src7 first seen at raw 0
+ assert_eq!(lm[1], 0); // src7 again → reuse raw 0
+ assert_eq!(lm[2], 2); // src8 at its own position (raw idx 2)
+ }
+}