piecemeal

This commit is contained in:
pszsh 2026-03-09 10:13:11 -07:00
parent 61287e5b8e
commit 10096738cf
6 changed files with 467 additions and 9 deletions

View File

@ -1,5 +1,5 @@
use futures::SinkExt; use futures::SinkExt;
use iced::widget::{button, column, container, pick_list, row, scrollable, text, text_input}; use iced::widget::{button, canvas, column, container, pick_list, row, scrollable, text, text_input};
use iced::{Element, Length, Subscription, Task, Theme}; use iced::{Element, Length, Subscription, Task, Theme};
use std::time::Duration; use std::time::Duration;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -222,7 +222,15 @@ impl App {
); );
} }
let content = column![status, controls, scrollable(data_rows)] let bode = canvas(crate::plot::BodePlot { points: &self.points })
.width(Length::FillPortion(3))
.height(300);
let nyquist = canvas(crate::plot::NyquistPlot { points: &self.points })
.width(Length::FillPortion(2))
.height(300);
let plots = row![bode, nyquist].spacing(10);
let content = column![status, controls, plots, scrollable(data_rows)]
.spacing(20) .spacing(20)
.padding(20); .padding(20);

View File

@ -1,5 +1,6 @@
mod app; mod app;
mod ble; mod ble;
mod plot;
mod protocol; mod protocol;
fn main() -> iced::Result { fn main() -> iced::Result {

445
cue/src/plot.rs Normal file
View File

@ -0,0 +1,445 @@
use iced::widget::canvas::{self, Event, Frame, Geometry, Path, Stroke, Text};
use iced::{Color, Point, Rectangle, Renderer, Theme};
use iced::mouse;
use crate::app::Message;
use crate::protocol::EisPoint;
const MARGIN_L: f32 = 55.0;
const MARGIN_R: f32 = 15.0;
const MARGIN_T: f32 = 15.0;
const MARGIN_B: f32 = 25.0;
const COL_MAG: Color = Color { r: 0.3, g: 0.85, b: 1.0, a: 1.0 };
const COL_PH: Color = Color { r: 1.0, g: 0.55, b: 0.2, a: 1.0 };
const COL_NYQ: Color = Color { r: 0.4, g: 1.0, b: 0.4, a: 1.0 };
const COL_GRID: Color = Color { r: 0.25, g: 0.25, b: 0.28, a: 1.0 };
const COL_AXIS: Color = Color { r: 0.6, g: 0.6, b: 0.6, a: 1.0 };
const COL_DIM: Color = Color { r: 0.4, g: 0.4, b: 0.4, a: 1.0 };
const ZOOM_FACTOR: f32 = 1.15;
/* ---- View range ---- */
#[derive(Clone, Copy)]
struct Vr {
lo: f32,
hi: f32,
}
impl Vr {
fn new(lo: f32, hi: f32) -> Self { Self { lo, hi } }
fn span(&self) -> f32 { self.hi - self.lo }
fn zoom_at(&mut self, factor: f32, anchor_frac: f32) {
let anchor = self.lo + anchor_frac * self.span();
let new_span = self.span() / factor;
self.lo = anchor - anchor_frac * new_span;
self.hi = anchor + (1.0 - anchor_frac) * new_span;
}
fn pan_frac(&mut self, frac: f32) {
let d = frac * self.span();
self.lo -= d;
self.hi -= d;
}
}
fn screen_frac(pos: f32, lo: f32, hi: f32) -> f32 {
if (hi - lo).abs() < 1e-6 { 0.5 } else { (pos - lo) / (hi - lo) }
}
/* ---- Drawing helpers ---- */
fn lerp(v: f32, lo: f32, hi: f32, out_lo: f32, out_hi: f32) -> f32 {
if (hi - lo).abs() < 1e-10 { return (out_lo + out_hi) / 2.0; }
out_lo + (v - lo) / (hi - lo) * (out_hi - out_lo)
}
fn nice_step(range: f32, target_ticks: usize) -> f32 {
if range.abs() < 1e-10 { return 1.0; }
let rough = range / target_ticks as f32;
let mag = 10f32.powf(rough.abs().log10().floor());
let norm = rough.abs() / mag;
let s = if norm < 1.5 { 1.0 } else if norm < 3.5 { 2.0 } else if norm < 7.5 { 5.0 } else { 10.0 };
s * mag
}
fn dl(frame: &mut Frame, a: Point, b: Point, color: Color, width: f32) {
frame.stroke(&Path::line(a, b), Stroke::default().with_color(color).with_width(width));
}
fn dt(frame: &mut Frame, pos: Point, txt: &str, color: Color, size: f32) {
frame.fill_text(Text {
content: txt.to_string(), position: pos, color, size: size.into(),
..Text::default()
});
}
fn draw_polyline(frame: &mut Frame, pts: &[Point], color: Color, width: f32) {
if pts.len() < 2 { return; }
let path = Path::new(|b| {
b.move_to(pts[0]);
for p in &pts[1..] { b.line_to(*p); }
});
frame.stroke(&path, Stroke::default().with_color(color).with_width(width));
}
fn draw_dots(frame: &mut Frame, pts: &[Point], color: Color, r: f32) {
for p in pts { frame.fill(&Path::circle(*p, r), color); }
}
/* ---- Bode state: zoom/pan on frequency axis, Y auto-scales ---- */
#[derive(Default)]
pub struct BodeState {
freq: Option<Vr>,
drag: Option<(f32, Vr)>,
}
pub struct BodePlot<'a> {
pub points: &'a [EisPoint],
}
impl BodePlot<'_> {
fn auto_freq(&self) -> Option<Vr> {
if self.points.is_empty() { return None; }
let lo = self.points.iter().map(|p| p.freq_hz.log10()).fold(f32::INFINITY, f32::min);
let hi = self.points.iter().map(|p| p.freq_hz.log10()).fold(f32::NEG_INFINITY, f32::max);
let pad = (hi - lo).max(0.1) * 0.05;
Some(Vr::new(lo - pad, hi + pad))
}
}
impl<'a> canvas::Program<Message> for BodePlot<'a> {
type State = BodeState;
fn update(
&self, state: &mut BodeState, event: Event,
bounds: Rectangle, cursor: mouse::Cursor,
) -> (canvas::event::Status, Option<Message>) {
let Some(pos) = cursor.position_in(bounds) else {
return (canvas::event::Status::Ignored, None);
};
let xl = MARGIN_L;
let xr = bounds.width - MARGIN_R;
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
let dy = match delta {
mouse::ScrollDelta::Lines { y, .. } => y,
mouse::ScrollDelta::Pixels { y, .. } => y / 40.0,
};
let factor = ZOOM_FACTOR.powf(dy);
let frac = screen_frac(pos.x, xl, xr);
let mut vr = state.freq.unwrap_or_else(|| self.auto_freq().unwrap_or(Vr::new(3.0, 5.3)));
vr.zoom_at(factor, frac);
state.freq = Some(vr);
(canvas::event::Status::Captured, None)
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let vr = state.freq.unwrap_or_else(|| self.auto_freq().unwrap_or(Vr::new(3.0, 5.3)));
state.drag = Some((pos.x, vr));
(canvas::event::Status::Captured, None)
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some((start_x, start_vr)) = state.drag {
let dx_frac = (pos.x - start_x) / (xr - xl);
let mut vr = start_vr;
vr.pan_frac(dx_frac);
state.freq = Some(vr);
return (canvas::event::Status::Captured, None);
}
(canvas::event::Status::Ignored, None)
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
state.drag = None;
(canvas::event::Status::Captured, None)
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
state.freq = None;
(canvas::event::Status::Captured, None)
}
_ => (canvas::event::Status::Ignored, None),
}
}
fn draw(
&self, state: &BodeState, renderer: &Renderer, _theme: &Theme,
bounds: Rectangle, cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let (w, h) = (bounds.width, bounds.height);
if self.points.is_empty() {
dt(&mut frame, Point::new(w / 2.0 - 25.0, h / 2.0), "No data", COL_DIM, 13.0);
return vec![frame.into_geometry()];
}
let split = (h * 0.55).floor();
let xl = MARGIN_L;
let xr = w - MARGIN_R;
let fv = state.freq.unwrap_or_else(|| self.auto_freq().unwrap());
// filter visible points for Y auto-scale
let vis: Vec<&EisPoint> = self.points.iter()
.filter(|p| { let lf = p.freq_hz.log10(); lf >= fv.lo && lf <= fv.hi })
.collect();
let all = if vis.is_empty() { self.points.iter().collect::<Vec<_>>() } else { vis };
let (m_min, m_max) = all.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| {
(lo.min(p.mag_ohms), hi.max(p.mag_ohms))
});
let m_pad = (m_max - m_min).max(1.0) * 0.12;
let (m_lo, m_hi) = (m_min - m_pad, m_max + m_pad);
let (p_min, p_max) = all.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| {
(lo.min(p.phase_deg), hi.max(p.phase_deg))
});
let p_pad = (p_max - p_min).max(0.1) * 0.25;
let (p_lo, p_hi) = (p_min - p_pad, p_max + p_pad);
// freq grid
let d0 = fv.lo.floor() as i32;
let d1 = fv.hi.ceil() as i32;
for d in d0..=d1 {
let x = lerp(d as f32, fv.lo, fv.hi, xl, xr);
if x < xl || x > xr { continue; }
dl(&mut frame, Point::new(x, MARGIN_T), Point::new(x, h - MARGIN_B), COL_GRID, 1.0);
let hz = 10f32.powi(d);
let label = if hz >= 1000.0 { format!("{}k", hz as u32 / 1000) } else { format!("{}", hz as u32) };
dt(&mut frame, Point::new(x - 8.0, split - 2.0), &label, COL_DIM, 9.0);
}
dl(&mut frame, Point::new(xl, split), Point::new(xr, split), COL_AXIS, 1.0);
// magnitude
let mag_top = MARGIN_T;
let mag_bot = split - 14.0;
let m_step = nice_step(m_hi - m_lo, 4);
if m_step > 0.0 {
let mut mg = (m_lo / m_step).ceil() * m_step;
while mg <= m_hi {
let y = lerp(mg, m_hi, m_lo, mag_top, mag_bot);
dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5);
dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.0}", mg), COL_MAG, 9.0);
mg += m_step;
}
}
dt(&mut frame, Point::new(2.0, mag_top - 2.0), "|Z|", COL_MAG, 10.0);
let mag_pts: Vec<Point> = self.points.iter().map(|p| Point::new(
lerp(p.freq_hz.log10(), fv.lo, fv.hi, xl, xr),
lerp(p.mag_ohms, m_hi, m_lo, mag_top, mag_bot),
)).collect();
draw_polyline(&mut frame, &mag_pts, COL_MAG, 2.0);
draw_dots(&mut frame, &mag_pts, COL_MAG, 2.5);
// phase
let ph_top = split + 12.0;
let ph_bot = h - MARGIN_B;
let p_step = nice_step(p_hi - p_lo, 3);
if p_step > 0.0 {
let mut pg = (p_lo / p_step).ceil() * p_step;
while pg <= p_hi {
let y = lerp(pg, p_hi, p_lo, ph_top, ph_bot);
dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5);
dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.1}", pg), COL_PH, 9.0);
pg += p_step;
}
}
dt(&mut frame, Point::new(2.0, ph_top - 2.0), "Phase", COL_PH, 10.0);
let ph_pts: Vec<Point> = self.points.iter().map(|p| Point::new(
lerp(p.freq_hz.log10(), fv.lo, fv.hi, xl, xr),
lerp(p.phase_deg, p_hi, p_lo, ph_top, ph_bot),
)).collect();
draw_polyline(&mut frame, &ph_pts, COL_PH, 2.0);
draw_dots(&mut frame, &ph_pts, COL_PH, 2.5);
// crosshair on hover
if let Some(pos) = cursor.position_in(bounds) {
if pos.x >= xl && pos.x <= xr && pos.y >= MARGIN_T && pos.y <= h - MARGIN_B {
let lf = lerp(pos.x, xl, xr, fv.lo, fv.hi);
let hz = 10f32.powf(lf);
let label = if hz >= 1000.0 { format!("{:.1}kHz", hz / 1000.0) } else { format!("{:.0}Hz", hz) };
dl(&mut frame, Point::new(pos.x, MARGIN_T), Point::new(pos.x, h - MARGIN_B),
Color { a: 0.3, ..COL_AXIS }, 1.0);
dt(&mut frame, Point::new(pos.x + 4.0, MARGIN_T + 2.0), &label, COL_AXIS, 10.0);
}
}
vec![frame.into_geometry()]
}
}
/* ---- Nyquist state: zoom/pan on both axes ---- */
#[derive(Default)]
pub struct NyquistState {
view: Option<(Vr, Vr)>,
drag: Option<(Point, Vr, Vr)>,
}
pub struct NyquistPlot<'a> {
pub points: &'a [EisPoint],
}
impl NyquistPlot<'_> {
fn auto_view(&self) -> Option<(Vr, Vr)> {
if self.points.is_empty() { return None; }
let (re_min, re_max) = self.points.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| {
(lo.min(p.z_real), hi.max(p.z_real))
});
let (ni_min, ni_max) = self.points.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| {
(lo.min(-p.z_imag), hi.max(-p.z_imag))
});
let re_span = (re_max - re_min).max(1.0);
let ni_span = (ni_max - ni_min).max(1.0);
let span = re_span.max(ni_span) * 1.3;
let re_c = (re_min + re_max) / 2.0;
let ni_c = (ni_min + ni_max) / 2.0;
Some((Vr::new(re_c - span / 2.0, re_c + span / 2.0),
Vr::new(ni_c - span / 2.0, ni_c + span / 2.0)))
}
}
impl<'a> canvas::Program<Message> for NyquistPlot<'a> {
type State = NyquistState;
fn update(
&self, state: &mut NyquistState, event: Event,
bounds: Rectangle, cursor: mouse::Cursor,
) -> (canvas::event::Status, Option<Message>) {
let Some(pos) = cursor.position_in(bounds) else {
return (canvas::event::Status::Ignored, None);
};
let xl = MARGIN_L;
let xr = bounds.width - MARGIN_R;
let yt = MARGIN_T;
let yb = bounds.height - MARGIN_B;
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
let dy = match delta {
mouse::ScrollDelta::Lines { y, .. } => y,
mouse::ScrollDelta::Pixels { y, .. } => y / 40.0,
};
let factor = ZOOM_FACTOR.powf(dy);
let fx = screen_frac(pos.x, xl, xr);
let fy = screen_frac(pos.y, yt, yb);
let (mut xv, mut yv) = state.view.unwrap_or_else(|| self.auto_view().unwrap_or(
(Vr::new(0.0, 1.0), Vr::new(0.0, 1.0))
));
xv.zoom_at(factor, fx);
yv.zoom_at(factor, 1.0 - fy);
state.view = Some((xv, yv));
(canvas::event::Status::Captured, None)
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let (xv, yv) = state.view.unwrap_or_else(|| self.auto_view().unwrap_or(
(Vr::new(0.0, 1.0), Vr::new(0.0, 1.0))
));
state.drag = Some((pos, xv, yv));
(canvas::event::Status::Captured, None)
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some((start, sx, sy)) = state.drag {
let dx = (pos.x - start.x) / (xr - xl);
let dy = (pos.y - start.y) / (yb - yt);
let mut xv = sx;
let mut yv = sy;
xv.pan_frac(dx);
yv.pan_frac(-dy);
state.view = Some((xv, yv));
return (canvas::event::Status::Captured, None);
}
(canvas::event::Status::Ignored, None)
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
state.drag = None;
(canvas::event::Status::Captured, None)
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
state.view = None;
(canvas::event::Status::Captured, None)
}
_ => (canvas::event::Status::Ignored, None),
}
}
fn draw(
&self, state: &NyquistState, renderer: &Renderer, _theme: &Theme,
bounds: Rectangle, cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let (w, h) = (bounds.width, bounds.height);
if self.points.is_empty() {
dt(&mut frame, Point::new(w / 2.0 - 25.0, h / 2.0), "No data", COL_DIM, 13.0);
return vec![frame.into_geometry()];
}
let xl = MARGIN_L;
let xr = w - MARGIN_R;
let yt = MARGIN_T;
let yb = h - MARGIN_B;
let (xv, yv) = state.view.unwrap_or_else(|| self.auto_view().unwrap());
// grid
let x_step = nice_step(xv.span(), 4);
if x_step > 0.0 {
let mut g = (xv.lo / x_step).ceil() * x_step;
while g <= xv.hi {
let x = lerp(g, xv.lo, xv.hi, xl, xr);
dl(&mut frame, Point::new(x, yt), Point::new(x, yb), COL_GRID, 0.5);
dt(&mut frame, Point::new(x - 10.0, yb + 3.0), &format!("{:.0}", g), COL_DIM, 9.0);
g += x_step;
}
}
let y_step = nice_step(yv.span(), 4);
if y_step > 0.0 {
let mut g = (yv.lo / y_step).ceil() * y_step;
while g <= yv.hi {
let y = lerp(g, yv.hi, yv.lo, yt, yb);
dl(&mut frame, Point::new(xl, y), Point::new(xr, y), COL_GRID, 0.5);
dt(&mut frame, Point::new(2.0, y - 5.0), &format!("{:.0}", g), COL_DIM, 9.0);
g += y_step;
}
}
// zero line
let zy = lerp(0.0, yv.hi, yv.lo, yt, yb);
if zy > yt && zy < yb {
dl(&mut frame, Point::new(xl, zy), Point::new(xr, zy), COL_AXIS, 1.0);
}
dt(&mut frame, Point::new(2.0, yt - 2.0), "-Z''", COL_NYQ, 10.0);
dt(&mut frame, Point::new((xl + xr) / 2.0 - 12.0, yb + 3.0), "Z'", COL_NYQ, 10.0);
let pts: Vec<Point> = self.points.iter().map(|p| Point::new(
lerp(p.z_real, xv.lo, xv.hi, xl, xr),
lerp(-p.z_imag, yv.hi, yv.lo, yt, yb),
)).collect();
draw_polyline(&mut frame, &pts, COL_NYQ, 2.0);
draw_dots(&mut frame, &pts, COL_NYQ, 3.0);
// crosshair with values on hover
if let Some(pos) = cursor.position_in(bounds) {
if pos.x >= xl && pos.x <= xr && pos.y >= yt && pos.y <= yb {
let re = lerp(pos.x, xl, xr, xv.lo, xv.hi);
let nim = lerp(pos.y, yt, yb, yv.hi, yv.lo);
dl(&mut frame, Point::new(pos.x, yt), Point::new(pos.x, yb),
Color { a: 0.3, ..COL_AXIS }, 1.0);
dl(&mut frame, Point::new(xl, pos.y), Point::new(xr, pos.y),
Color { a: 0.3, ..COL_AXIS }, 1.0);
dt(&mut frame, Point::new(pos.x + 4.0, pos.y - 14.0),
&format!("{:.1}, {:.1}", re, nim), COL_AXIS, 10.0);
}
}
vec![frame.into_geometry()]
}
}

View File

@ -478,14 +478,17 @@ static int send_sysex(const uint8_t *sysex, uint16_t len)
if (conn_hdl == BLE_HS_CONN_HANDLE_NONE || !midi_notify_en) if (conn_hdl == BLE_HS_CONN_HANDLE_NONE || !midi_notify_en)
return -1; return -1;
uint16_t pkt_len = 2 + len; /* BLE MIDI SysEx: [header, ts, F0, payload..., ts, F7] */
uint16_t pkt_len = len + 3;
uint8_t pkt[80]; uint8_t pkt[80];
if (pkt_len > sizeof(pkt)) if (pkt_len > sizeof(pkt))
return -1; return -1;
pkt[0] = 0x80; pkt[0] = 0x80;
pkt[1] = 0x80; pkt[1] = 0x80;
memcpy(&pkt[2], sysex, len); memcpy(&pkt[2], sysex, len - 1);
pkt[len + 1] = 0x80;
pkt[len + 2] = 0xF7;
struct os_mbuf *om = ble_hs_mbuf_from_flat(pkt, pkt_len); struct os_mbuf *om = ble_hs_mbuf_from_flat(pkt, pkt_len);
if (!om) return -1; if (!om) return -1;

View File

@ -2,6 +2,8 @@
#include <math.h> #include <math.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#ifndef M_PI #ifndef M_PI
#define M_PI 3.14159265358979323846 #define M_PI 3.14159265358979323846
@ -235,12 +237,11 @@ static void dft_measure(uint32_t mux_p, uint32_t mux_n, iImpCar_Type *out)
AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR, bTRUE); AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR, bTRUE);
AD5940_Delay10us(25); AD5940_Delay10us(25);
AD5940_ClrMCUIntFlag();
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE); AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE);
uint32_t timeout = 10000000; while (!AD5940_GetMCUIntFlag())
while (AD5940_INTCTestFlag(AFEINTC_1, AFEINTSRC_DFTRDY) == bFALSE) { vTaskDelay(1);
if (--timeout == 0) break;
}
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT | AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT |
AFECTRL_WG | AFECTRL_ADCPWR, bFALSE); AFECTRL_WG | AFECTRL_ADCPWR, bFALSE);

View File

@ -1754,7 +1754,7 @@ CONFIG_FATFS_DONT_TRUST_LAST_ALLOC=0
# #
# CONFIG_FREERTOS_SMP is not set # CONFIG_FREERTOS_SMP is not set
# CONFIG_FREERTOS_UNICORE is not set # CONFIG_FREERTOS_UNICORE is not set
CONFIG_FREERTOS_HZ=100 CONFIG_FREERTOS_HZ=1000
# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set # CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set
# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set # CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set
CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y