From 6cb39d593993ba760e14d40007ab8364d7c686f7 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 13 Jun 2025 14:49:41 -0400 Subject: [PATCH 1/5] refactor(core): Add placeholder files. --- twinleaf-core/src/lib.rs | 0 twinleaf-core/src/workflows/connect.rs | 0 twinleaf-core/src/workflows/firmware.rs | 0 twinleaf-core/src/workflows/log.rs | 0 twinleaf-core/src/workflows/mod.rs | 0 twinleaf-core/src/workflows/proxy.rs | 0 twinleaf-core/src/workflows/rpc.rs | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 twinleaf-core/src/lib.rs create mode 100644 twinleaf-core/src/workflows/connect.rs create mode 100644 twinleaf-core/src/workflows/firmware.rs create mode 100644 twinleaf-core/src/workflows/log.rs create mode 100644 twinleaf-core/src/workflows/mod.rs create mode 100644 twinleaf-core/src/workflows/proxy.rs create mode 100644 twinleaf-core/src/workflows/rpc.rs diff --git a/twinleaf-core/src/lib.rs b/twinleaf-core/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/twinleaf-core/src/workflows/connect.rs b/twinleaf-core/src/workflows/connect.rs new file mode 100644 index 0000000..e69de29 diff --git a/twinleaf-core/src/workflows/firmware.rs b/twinleaf-core/src/workflows/firmware.rs new file mode 100644 index 0000000..e69de29 diff --git a/twinleaf-core/src/workflows/log.rs b/twinleaf-core/src/workflows/log.rs new file mode 100644 index 0000000..e69de29 diff --git a/twinleaf-core/src/workflows/mod.rs b/twinleaf-core/src/workflows/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/twinleaf-core/src/workflows/proxy.rs b/twinleaf-core/src/workflows/proxy.rs new file mode 100644 index 0000000..e69de29 diff --git a/twinleaf-core/src/workflows/rpc.rs b/twinleaf-core/src/workflows/rpc.rs new file mode 100644 index 0000000..e69de29 From 61f0527e072f7334c5f195c38694b177d1107367 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 13 Jun 2025 14:50:51 -0400 Subject: [PATCH 2/5] docs(core): Add initial README.md --- twinleaf-core/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 twinleaf-core/README.md diff --git a/twinleaf-core/README.md b/twinleaf-core/README.md new file mode 100644 index 0000000..8c512fe --- /dev/null +++ b/twinleaf-core/README.md @@ -0,0 +1,3 @@ +# Twinleaf I/O Workflows in Rust + +This repository exposes an API for common workflows when using the low-level `twinleaf` library. It also serves as an example of how to interface with the devices in Rust. \ No newline at end of file From 43e33997322417a90e3c81fc7702b09be8770c31 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 13 Jun 2025 14:52:37 -0400 Subject: [PATCH 3/5] feat(tools): Add twinleaf-core as a dependency and error handling dependencies. --- Cargo.lock | 67 ++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + twinleaf-core/Cargo.toml | 20 ++++++++++++ twinleaf-core/src/error.rs | 0 twinleaf-tools/Cargo.toml | 2 ++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 twinleaf-core/Cargo.toml create mode 100644 twinleaf-core/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 1d56b5e..5c68755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "autocfg" version = "1.4.0" @@ -557,6 +563,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serialport" version = "4.6.1" @@ -635,7 +661,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -649,6 +684,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -678,10 +724,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "twinleaf-core" +version = "1.0.0" +dependencies = [ + "chrono", + "crossbeam", + "crossterm", + "getopts", + "serde", + "serialport", + "thiserror 2.0.12", + "toml_edit", + "twinleaf", +] + [[package]] name = "twinleaf-tools" version = "1.5.0" dependencies = [ + "anyhow", "chrono", "crossbeam", "crossterm", @@ -689,6 +751,7 @@ dependencies = [ "serialport", "toml_edit", "twinleaf", + "twinleaf-core", ] [[package]] @@ -697,7 +760,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 39ee2d3..63146fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "twinleaf", "twinleaf-tools", + "twinleaf-core", ] resolver = "2" diff --git a/twinleaf-core/Cargo.toml b/twinleaf-core/Cargo.toml new file mode 100644 index 0000000..574869b --- /dev/null +++ b/twinleaf-core/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "twinleaf-core" +version = "1.0.0" +edition = "2021" +license = "MIT" +description = "Core tasks utilizing the Twinleaf I/O protocol for reading data from Twinleaf quantum sensors." +homepage = "https://twinleaf.com" +repository = "https://github.com/twinleaf/twinleaf-rust" +readme = "README.md" + +[dependencies] +chrono = "0.4.38" +crossbeam = "0.8.4" +crossterm = {version = "0.29.0"} +getopts = "0.2.21" +serialport = "4.5.1" +serde = {version = "1.0.219", features = ["derive"]} +thiserror = "2.0.12" +toml_edit = {version = "0.22.24", features = ["parse"]} +twinleaf = { version = "1.3.1", path = "../twinleaf" } diff --git a/twinleaf-core/src/error.rs b/twinleaf-core/src/error.rs new file mode 100644 index 0000000..e69de29 diff --git a/twinleaf-tools/Cargo.toml b/twinleaf-tools/Cargo.toml index 508bb18..c4af335 100644 --- a/twinleaf-tools/Cargo.toml +++ b/twinleaf-tools/Cargo.toml @@ -14,5 +14,7 @@ crossbeam = "0.8.4" crossterm = {version = "0.29.0"} getopts = "0.2.21" serialport = "4.5.1" +anyhow = "1.0.98" toml_edit = {version = "0.22.24", features = ["parse"]} twinleaf = { version = "1.3.1", path = "../twinleaf" } +twinleaf-core = { version = "1.0.0", path = "../twinleaf-core" } \ No newline at end of file From 1dad06bb7a395343d517c40f9a4c2c6bca0b588b Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 13 Jun 2025 15:32:33 -0400 Subject: [PATCH 4/5] feat(core): Expose workflows and subfiles as public --- twinleaf-core/src/lib.rs | 1 + twinleaf-core/src/workflows/mod.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/twinleaf-core/src/lib.rs b/twinleaf-core/src/lib.rs index e69de29..1e26693 100644 --- a/twinleaf-core/src/lib.rs +++ b/twinleaf-core/src/lib.rs @@ -0,0 +1 @@ +pub mod workflows; \ No newline at end of file diff --git a/twinleaf-core/src/workflows/mod.rs b/twinleaf-core/src/workflows/mod.rs index e69de29..0fdf269 100644 --- a/twinleaf-core/src/workflows/mod.rs +++ b/twinleaf-core/src/workflows/mod.rs @@ -0,0 +1,5 @@ +pub mod connect; +pub mod firmware; +pub mod log; +pub mod proxy; +pub mod rpc; \ No newline at end of file From 0f0f6add44854be622f6cb4dbfb03a202a465398 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 13 Jun 2025 15:48:31 -0400 Subject: [PATCH 5/5] refactor(tools): Change main.rs of tio-proxy.rs to use connect.rs --- twinleaf-core/src/workflows/connect.rs | 77 +++++++++++++++++++++++ twinleaf-tools/src/bin/tio-proxy.rs | 87 +++++--------------------- 2 files changed, 94 insertions(+), 70 deletions(-) diff --git a/twinleaf-core/src/workflows/connect.rs b/twinleaf-core/src/workflows/connect.rs index e69de29..ab62a34 100644 --- a/twinleaf-core/src/workflows/connect.rs +++ b/twinleaf-core/src/workflows/connect.rs @@ -0,0 +1,77 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ConnectError { + #[error("Failed to enumerate available serial ports")] + EnumerationFailed(#[from] serialport::Error), + + #[error("Could not automatically find a sensor")] + AutoDetectionFailed, + + #[error("Found {0} multiple possible sensors; please specify one explicitly")] + MultipleSensorsFound(usize), +} + +#[derive(Debug)] +pub enum TwinleafPortInterface { + FTDI, + STM32, + Unknown(u16, u16), +} + +#[derive(Debug)] +pub struct SerialDevice { + pub url: String, + pub ifc: TwinleafPortInterface, +} + +pub fn enum_devices(all: bool) -> Result, ConnectError> { + let mut ports: Vec = Vec::new(); + + let avail_ports = serialport::available_ports()?; + + for p in avail_ports.iter() { + if let serialport::SerialPortType::UsbPort(info) = &p.port_type { + let interface = match (info.vid, info.pid) { + (0x0403, 0x6015) => TwinleafPortInterface::FTDI, + (0x0483, 0x5740) => TwinleafPortInterface::STM32, + (vid, pid) => { + if !all { + continue; + }; + TwinleafPortInterface::Unknown(vid, pid) + } + }; + #[cfg(target_os = "macos")] + if p.port_name.starts_with("/dev/tty.") && !all { + continue; + } + ports.push(SerialDevice { + url: format!("serial://{}", p.port_name), + ifc: interface, + }); + } // else ignore other types for now: bluetooth, pci, unknown + } + + Ok(ports) +} + +pub fn auto_detect_sensor() -> Result { + let devices = enum_devices(false)?; + + let valid_urls: Vec = devices + .into_iter() + .filter_map(|dev| match dev.ifc { + TwinleafPortInterface::STM32 | TwinleafPortInterface::FTDI => Some(dev.url), + _ => None, + }) + .collect(); + + if valid_urls.is_empty() { + Err(ConnectError::AutoDetectionFailed) + } else if valid_urls.len() > 1 { + Err(ConnectError::MultipleSensorsFound(valid_urls.len())) + } else { + Ok(valid_urls[0].clone()) + } +} \ No newline at end of file diff --git a/twinleaf-tools/src/bin/tio-proxy.rs b/twinleaf-tools/src/bin/tio-proxy.rs index 1376a3b..531e957 100644 --- a/twinleaf-tools/src/bin/tio-proxy.rs +++ b/twinleaf-tools/src/bin/tio-proxy.rs @@ -12,52 +12,7 @@ use std::time::Duration; use tio::{proto, proxy}; use twinleaf::tio; -// Unfortunately we cannot access USB details via the serialport module, so -// we are stuck guessing based on VID/PID. This returns a vector of possible -// serial ports. - -#[derive(Debug)] -enum TwinleafPortInterface { - FTDI, - STM32, - Unknown(u16, u16), -} - -struct SerialDevice { - url: String, - ifc: TwinleafPortInterface, -} - -fn enum_devices(all: bool) -> Vec { - let mut ports: Vec = Vec::new(); - - if let Ok(avail_ports) = serialport::available_ports() { - for p in avail_ports.iter() { - if let serialport::SerialPortType::UsbPort(info) = &p.port_type { - let interface = match (info.vid, info.pid) { - (0x0403, 0x6015) => TwinleafPortInterface::FTDI, - (0x0483, 0x5740) => TwinleafPortInterface::STM32, - (vid, pid) => { - if !all { - continue; - }; - TwinleafPortInterface::Unknown(vid, pid) - } - }; - #[cfg(target_os = "macos")] - if p.port_name.starts_with("/dev/tty.") && !all { - continue; - } - ports.push(SerialDevice { - url: format!("serial://{}", p.port_name), - ifc: interface, - }); - } // else ignore other types for now: bluetooth, pci, unknown - } - } - - ports -} +use twinleaf_core::workflows::connect; macro_rules! log{ ($tf:expr, $msg:expr)=>{ @@ -161,17 +116,22 @@ fn main() -> ExitCode { if matches.opt_present("enum") { let mut unknown_devices = vec![]; let mut found_any = false; - for dev in enum_devices(true) { - if let TwinleafPortInterface::Unknown(vid, pid) = dev.ifc { - unknown_devices.push(format!("{} (vid: {} pid:{})", dev.url, vid, pid)); - } else { - if !found_any { - println!("Possible tio ports:"); - found_any = true; + match connect::enum_devices(true) { + Ok(devices) => { + for dev in devices { + if let connect::TwinleafPortInterface::Unknown(vid, pid) = dev.ifc { + unknown_devices.push(format!("{} (vid: {} pid:{})", dev.url, vid, pid)); + } else { + if !found_any { + println!("Possible tio ports:"); + found_any = true; + } + println!(" * {}", dev.url); } - println!(" * {}", dev.url); } } + Err(e) => die!("Could not enumerate devices: {}", e), + }; if !found_any { println!("No likely ports found") } @@ -226,23 +186,10 @@ fn main() -> ExitCode { let sensor_url = if matches.free.len() == 1 { matches.free[0].clone() } else { - let devices = enum_devices(false); - let mut valid_urls = Vec::new(); - for dev in devices { - match dev.ifc { - TwinleafPortInterface::STM32 | TwinleafPortInterface::FTDI => { - valid_urls.push(dev.url.clone()); - } - _ => {} - } - } - if valid_urls.len() == 0 { - die!("Cannot find any sensor to connect to, specify URL manually") - } - if valid_urls.len() > 1 { - die!("Too many sensors detected, specify URL manually") + match connect::auto_detect_sensor() { + Ok(url) => url, + Err(e) => die!("{}", e), } - valid_urls[0].clone() }; if verbose || auto_sensor {