Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,29 @@ A set of tools to plot values from the target to graph in rerun with minimal per
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;
use probe_plotter::{make_metric, make_setting};

#[entry]
fn main() -> ! {
use probe_plotter::make_metric;
let mut sawtooth = make_metric!(SAWTOOTH: i32 = 42, "(x / 10) % 100").unwrap();
let mut sine = make_metric!(SINE: i32 = 42, "100 * sin(2 * pi * x / 4000)").unwrap();
let mut sawtooth = make_metric!(SAWTOOTH: i32 = 42, "(SAWTOOTH / 10) % 100").unwrap();
let mut sine = make_metric!(SINE: i32 = 42, "100 * sin(2 * pi * SINE / 4000)").unwrap();

let mut setting_roundtrip =
make_metric!(SETTING_ROUNDTRIP: i8 = 0, "SETTING_ROUNDTRIP").unwrap();

// Allow values -1..=7, step by 2, so {-1, 1, 3, 5, 7}
let mut setting = make_setting!(SETTING: i8 = 42, -1..=7, 2).unwrap();

loop {
for i in 0..i32::MAX {
sawtooth.set(i);
sine.set(i);
cortex_m::asm::delay(100_000);

setting_roundtrip.set(setting.get());
}
}
}

```

The formulas seen in the `make_metric` macro invocation are computed by the host and will thus have zero impact on the targets performance. The `set` method on the metrics object is simply a volatile store which is quite cheap. The host will then read that value using the debug probe at regular intervals and update the graph on any changes.
Expand All @@ -50,4 +58,4 @@ cargo run ../examples/simple/target/thumbv7em-none-eabihf/debug/simple stm32g474
# Rerun will open with a graph showing all created metrics objects
```

<img width="2880" height="1920" alt="Screenshot" src="https://github.com/user-attachments/assets/5f7f20c9-009d-42c7-9613-789ae26afe54" />
<img width="2050" height="1166" alt="image" src="https://github.com/user-attachments/assets/dcdced90-9130-449f-ae0e-22a92a3dd409" />
16 changes: 13 additions & 3 deletions examples/simple/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@ use cortex_m_rt::entry;

use defmt_rtt as _;
use panic_halt as _;
use probe_plotter::{make_metric, make_setting};

#[entry]
fn main() -> ! {
use probe_plotter::make_metric;
defmt::println!("Running...");
let mut sawtooth = make_metric!(SAWTOOTH: i32 = 42, "(x / 10) % 100").unwrap();
let mut sawtooth = make_metric!(SAWTOOTH: i32 = 42, "(SAWTOOTH / 10) % 100").unwrap();
defmt::println!("foo initialized to: {}", sawtooth.get());
let mut sine = make_metric!(SINE: i32 = 42, "100 * sin(2 * pi * x / 4000)").unwrap();
let mut sine = make_metric!(SINE: i32 = 42, "100 * sin(2 * pi * SINE / 4000)").unwrap();

let mut setting_roundtrip =
make_metric!(SETTING_ROUNDTRIP: i8 = 0, "SETTING_ROUNDTRIP").unwrap();

// Allow values -1..=7, step by 2, so {-1, 1, 3, 5, 7}
let mut setting = make_setting!(SETTING: i8 = 42, -1..=7, 2).unwrap();

loop {
for i in 0..i32::MAX {
sawtooth.set(i);
sine.set(i);

setting_roundtrip.set(setting.get());

cortex_m::asm::delay(100_000);
}
}
Expand Down
2 changes: 1 addition & 1 deletion macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2024"
[dependencies]
proc-macro2 = "1.0.95"
quote = "1.0.40"
syn = "2"
syn = { version = "2", features = ["full"] }

[lib]
proc-macro = true
104 changes: 98 additions & 6 deletions macros/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
// Based on defmt

use proc_macro2::Span;
use syn::{
LitStr, Token,
LitStr, RangeLimits, Token,
parse::{self, Parse, ParseStream},
spanned::Spanned,
};

//FOO: i32 = 0, "x * 3.0"
//FOO: i32 = 0 // defaults to "x"
pub(crate) struct Args {
pub(crate) struct MetricArgs {
pub(crate) name: syn::Ident,
pub(crate) ty: syn::Ident,
pub(crate) initial_val: syn::Expr,
pub(crate) expression_string: syn::LitStr,
}

impl Parse for Args {
impl Parse for MetricArgs {
fn parse(input: ParseStream) -> parse::Result<Self> {
let name = input.parse()?;
let name: syn::Ident = input.parse()?;
let _comma: Token![:] = input.parse()?;
let ty = input.parse()?;
let _comma: Token![=] = input.parse()?;
Expand All @@ -29,7 +29,7 @@ impl Parse for Args {
let expression_string = match (comma, expression_string) {
(Ok(_), Ok(expr)) => expr,
(Ok(_), Err(e)) => return Err(e),
(Err(_), _) => LitStr::new("x", Span::mixed_site()),
(Err(_), _) => LitStr::new(&name.to_string(), name.span()),
};

Ok(Self {
Expand All @@ -40,3 +40,95 @@ impl Parse for Args {
})
}
}

// FOO: i32 = 0, 0..=10, 2
// FOO: i32 = 0, 0..=10, // Step size defaults to 1
// FOO: i32 = 0 // range defaults to the types full range
// TODO Implement the defaults
pub(crate) struct SettingArgs {
pub(crate) name: syn::Ident,
pub(crate) ty: syn::Ident,
pub(crate) initial_val: syn::Expr,
pub(crate) range_start: syn::LitFloat,
pub(crate) range_end: syn::LitFloat,
pub(crate) step_size: syn::LitFloat,
}

impl Parse for SettingArgs {
fn parse(input: ParseStream) -> parse::Result<Self> {
let name = input.parse()?;
let _colon: Token![:] = input.parse()?;
let ty = input.parse()?;
let _eq: Token![=] = input.parse()?;
let initial_val = input.parse()?;

let _comma: parse::Result<Token![,]> = input.parse();
let range: syn::Expr = input.parse()?;

let syn::Expr::Range(range) = range else {
panic!("Invalid range")
};

let range_start = range
.start
.expect("Only inclusive ranges with both a start and end are supported");
let range_end = range
.end
.expect("Only inclusive ranges with both a start and end are supported");
assert!(
matches!(range.limits, RangeLimits::Closed(_)),
"Only inclusive ranges with both a start and end are supported"
);

let _comma: parse::Result<Token![,]> = input.parse();
let step_size: syn::Lit = input.parse()?;

let step_size = match step_size {
syn::Lit::Int(i) => syn::LitFloat::new(&format!("{}.0", i.base10_digits()), i.span()),
syn::Lit::Float(f) => f,
x => return Err(syn::Error::new(x.span(), "expected float or int literal")),
};

Ok(Self {
name,
ty,
initial_val,
range_start: expr_to_float_lit(*range_start)?,
range_end: expr_to_float_lit(*range_end)?,
step_size,
})
}
}

// TODO: Clean up this mess
fn expr_to_float_lit(e: syn::Expr) -> Result<syn::LitFloat, syn::Error> {
let error_msg = "expected float or int literal";
Ok(match e {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Float(f),
..
}) => f,
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(i),
..
}) => syn::LitFloat::new(&format!("{}.0", i.base10_digits()), i.span()),
syn::Expr::Unary(syn::ExprUnary {
op: syn::UnOp::Neg(_),
expr,
..
}) => match *expr {
syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit {
// TODO: Is there a better way to handle the minus sign?
syn::Lit::Int(i) => {
syn::LitFloat::new(&format!("-{}.0", i.base10_digits()), i.span())
}
syn::Lit::Float(f) => {
syn::LitFloat::new(&format!("-{}", f.base10_digits()), f.span())
}
x => return Err(syn::Error::new(x.span(), error_msg)),
},
x => return Err(syn::Error::new(x.span(), error_msg)),
},
x => return Err(syn::Error::new(x.span(), error_msg)),
})
}
58 changes: 55 additions & 3 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::parse_macro_input;

use crate::symbol::Symbol;
use crate::symbol::{MetricsSymbol, SettingSymbol};

mod args;
mod cargo;
Expand All @@ -28,9 +28,9 @@ mod symbol;
/// ```
#[proc_macro]
pub fn make_metric(args: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as args::Args);
let args = parse_macro_input!(args as args::MetricArgs);

let sym_name = Symbol::new(
let sym_name = MetricsSymbol::new(
args.ty.to_string(),
args.name.to_string(),
args.expression_string.value(),
Expand Down Expand Up @@ -64,6 +64,58 @@ pub fn make_metric(args: TokenStream) -> TokenStream {
.into()
}

/// Create a Setting instance that will be shown as a slider in the probe-plotter utility
///
/// ```
/// make_setting!(NAME_AS_SHOWN_NEXT_TO_SLIDER: DataType = defalt_value, min_value..=max_value, step_size)
/// ```
///
/// Note that similar to `cortex_m::singleton!`, this should only be called once per setting. The macro will only return Some() the first time, then None.
///
/// ```
/// let mut setting_foo = probe_plotter::make_setting!(FOO: i32 = 0, 0..=10, 1.0).unwrap();
///
/// let value = setting_foo.get();
/// ```
#[proc_macro]
pub fn make_setting(args: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as args::SettingArgs);

let sym_name = SettingSymbol::new(
args.ty.to_string(),
args.name.to_string(),
args.range_start.base10_parse().unwrap()..=args.range_end.base10_parse().unwrap(),
args.step_size.base10_parse().unwrap(),
)
.mangle();

let name = args.name;
let ty = args.ty;
let initial_value = args.initial_val;

quote!(
cortex_m::interrupt::free(|_| {
#[unsafe(export_name = #sym_name)]
static mut #name: (#ty, bool) =
(0, false);

#[allow(unsafe_code)]
let used = unsafe { #name.1 };
if used {
None
} else {
#[allow(unsafe_code)]
unsafe {
#name.1 = true;
#name.0 = #initial_value;
Some(::probe_plotter::Setting::new(&mut #name.0))
}
}
})
)
.into()
}

pub(crate) fn crate_local_disambiguator() -> u64 {
// We want a deterministic, but unique-per-macro-invocation identifier. For that we
// hash the call site `Span`'s debug representation, which contains a counter that
Expand Down
61 changes: 58 additions & 3 deletions macros/src/symbol.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Based on defmt

use std::ops::RangeInclusive;

use crate::cargo;

pub struct Symbol {
pub struct MetricsSymbol {
/// Name of the Cargo package in which the symbol is being instantiated. Used for avoiding
/// symbol name collisions.
package: String,
Expand All @@ -23,7 +25,7 @@ pub struct Symbol {
crate_name: String,
}

impl Symbol {
impl MetricsSymbol {
pub fn new(ty: String, name: String, expr: String) -> Self {
Self {
// `CARGO_PKG_NAME` is set to the invoking package's name.
Expand All @@ -38,7 +40,7 @@ impl Symbol {

pub fn mangle(&self) -> String {
format!(
r#"{{"package":"{}","ty":"{}","name":"{}","expr": "{}","disambiguator":"{}","crate_name":"{}"}}"#,
r#"{{"type":"Metric","package":"{}","ty":"{}","name":"{}","expr":"{}","disambiguator":"{}","crate_name":"{}"}}"#,
json_escape(&self.package),
json_escape(&self.ty),
json_escape(&self.name),
Expand All @@ -49,6 +51,59 @@ impl Symbol {
}
}

pub struct SettingSymbol {
/// Name of the Cargo package in which the symbol is being instantiated. Used for avoiding
/// symbol name collisions.
package: String,

/// Unique identifier that disambiguates otherwise equivalent invocations in the same crate.
disambiguator: u64,

/// Underlaying data type
ty: String,

/// Variable name
name: String,

/// Range of valid values
range: RangeInclusive<f64>,

/// Step size
step_size: f64,

/// Crate name obtained via CARGO_CRATE_NAME (added since a Cargo package can contain many crates).
crate_name: String,
}

impl SettingSymbol {
pub fn new(ty: String, name: String, range: RangeInclusive<f64>, step_size: f64) -> Self {
Self {
// `CARGO_PKG_NAME` is set to the invoking package's name.
package: cargo::package_name(),
disambiguator: super::crate_local_disambiguator(),
ty,
name,
range,
step_size,
crate_name: cargo::crate_name(),
}
}

pub fn mangle(&self) -> String {
format!(
r#"{{"type":"Setting","package":"{}","ty":"{}","name":"{}","range":{{"start":{},"end":{}}},"step_size":{},"disambiguator":"{}","crate_name":"{}"}}"#,
json_escape(&self.package),
json_escape(&self.ty),
json_escape(&self.name),
self.range.start(),
self.range.end(),
self.step_size,
self.disambiguator,
json_escape(&self.crate_name),
)
}
}

fn json_escape(string: &str) -> String {
use std::fmt::Write;

Expand Down
Loading
Loading