feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
@@ -0,0 +1 @@
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05dc", ATTRS{serial}=="TestClass Serial", MODE="0666"
|
||||
@@ -0,0 +1,87 @@
|
||||
use rusb::{ConfigDescriptor, Context, DeviceDescriptor, DeviceHandle, Language, UsbContext as _};
|
||||
use std::time::Duration;
|
||||
use usb_device::test_class;
|
||||
|
||||
pub const TIMEOUT: Duration = Duration::from_secs(1);
|
||||
pub const EN_US: u16 = 0x0409;
|
||||
|
||||
pub struct DeviceHandles {
|
||||
pub device_descriptor: DeviceDescriptor,
|
||||
pub config_descriptor: ConfigDescriptor,
|
||||
pub handle: DeviceHandle<Context>,
|
||||
pub en_us: Language,
|
||||
}
|
||||
|
||||
impl DeviceHandles {
|
||||
/// Indicates if this device is (true) or isn't (false) a
|
||||
/// high-speed device.
|
||||
pub fn is_high_speed(&self) -> bool {
|
||||
self.handle.device().speed() == rusb::Speed::High
|
||||
}
|
||||
/// Returns the max packet size for the `TestClass` bulk endpoint(s).
|
||||
pub fn bulk_max_packet_size(&self) -> u16 {
|
||||
self.config_descriptor
|
||||
.interfaces()
|
||||
.flat_map(|intf| intf.descriptors())
|
||||
.flat_map(|desc| {
|
||||
desc.endpoint_descriptors()
|
||||
.find(|ep| {
|
||||
// Assumes that IN and OUT endpoint MPSes are the same.
|
||||
ep.transfer_type() == rusb::TransferType::Bulk
|
||||
})
|
||||
.map(|ep| ep.max_packet_size())
|
||||
})
|
||||
.next()
|
||||
.expect("TestClass has at least one bulk endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::ops::Deref for DeviceHandles {
|
||||
type Target = DeviceHandle<Context>;
|
||||
|
||||
fn deref(&self) -> &DeviceHandle<Context> {
|
||||
&self.handle
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::ops::DerefMut for DeviceHandles {
|
||||
fn deref_mut(&mut self) -> &mut DeviceHandle<Context> {
|
||||
&mut self.handle
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_device(ctx: &Context) -> rusb::Result<DeviceHandles> {
|
||||
for device in ctx.devices()?.iter() {
|
||||
let device_descriptor = device.device_descriptor()?;
|
||||
|
||||
if !(device_descriptor.vendor_id() == test_class::VID
|
||||
&& device_descriptor.product_id() == test_class::PID)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut handle = device.open()?;
|
||||
|
||||
let langs = handle.read_languages(TIMEOUT)?;
|
||||
if langs.is_empty() || langs[0].lang_id() != EN_US {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prod = handle.read_product_string(langs[0], &device_descriptor, TIMEOUT)?;
|
||||
|
||||
if prod == test_class::PRODUCT {
|
||||
handle.reset()?;
|
||||
|
||||
let config_descriptor = device.config_descriptor(0)?;
|
||||
|
||||
return Ok(DeviceHandles {
|
||||
device_descriptor,
|
||||
config_descriptor,
|
||||
handle,
|
||||
en_us: langs[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(rusb::Error::NoDevice)
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
mod device;
|
||||
/// Runs tests against a TestClass device running on actual hardware using libusb.
|
||||
///
|
||||
/// This is implemented as an example as opposed to a test because the Rust test runner system is
|
||||
/// not well suited for running tests that must depend on outside resources such as hardware and
|
||||
/// cannot be run in parallel.
|
||||
mod tests;
|
||||
|
||||
use crate::device::open_device;
|
||||
use crate::tests::{get_tests, TestFn};
|
||||
use std::io::prelude::*;
|
||||
use std::io::stdout;
|
||||
use std::panic;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use usb_device::device::CONFIGURATION_VALUE;
|
||||
|
||||
fn main() {
|
||||
let tests = get_tests();
|
||||
run_tests(&tests[..]);
|
||||
}
|
||||
|
||||
fn run_tests(tests: &[(&str, TestFn)]) {
|
||||
const INTERFACE: u8 = 0;
|
||||
|
||||
println!("test_class_host starting");
|
||||
println!("looking for device...");
|
||||
|
||||
let ctx = rusb::Context::new().expect("create libusb context");
|
||||
|
||||
// Look for the device for about 5 seconds in case it hasn't finished enumerating yet
|
||||
let mut dev = Err(rusb::Error::NoDevice);
|
||||
for _ in 0..50 {
|
||||
dev = open_device(&ctx);
|
||||
if dev.is_ok() {
|
||||
break;
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
let mut dev = match dev {
|
||||
Ok(d) => d,
|
||||
Err(err) => {
|
||||
println!("Did not find a TestClass device. Make sure the device is correctly programmed and plugged in. Last error: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\nrunning {} tests", tests.len());
|
||||
|
||||
let mut success = 0;
|
||||
for (name, test) in tests {
|
||||
if let Err(err) = dev.reset() {
|
||||
println!("Failed to reset the device: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = dev.set_active_configuration(CONFIGURATION_VALUE) {
|
||||
println!("Failed to set active configuration: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = dev.claim_interface(INTERFACE) {
|
||||
println!("Failed to claim interface: {}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
print!("test {} ... ", name);
|
||||
let _ = stdout().flush();
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
let res = {
|
||||
let hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(|_| {}));
|
||||
let res = panic::catch_unwind(panic::AssertUnwindSafe(|| {
|
||||
test(&mut dev, &mut out);
|
||||
}));
|
||||
panic::set_hook(hook);
|
||||
|
||||
res
|
||||
};
|
||||
|
||||
dev.release_interface(INTERFACE)
|
||||
.expect("failed to release interface");
|
||||
|
||||
if let Err(err) = res {
|
||||
let err = if let Some(err) = err.downcast_ref::<&'static str>() {
|
||||
String::from(*err)
|
||||
} else if let Some(err) = err.downcast_ref::<String>() {
|
||||
err.clone()
|
||||
} else {
|
||||
String::from("???")
|
||||
};
|
||||
|
||||
println!("FAILED\nerror: {}\n", err);
|
||||
} else {
|
||||
println!("ok");
|
||||
|
||||
if !out.is_empty() {
|
||||
print!("{}", out);
|
||||
}
|
||||
|
||||
success += 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!("{} failed, {} succeeded", tests.len() - success, success);
|
||||
|
||||
if success == tests.len() {
|
||||
println!("\nALL TESTS PASSED!");
|
||||
} else {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
use crate::device::*;
|
||||
use rand::prelude::*;
|
||||
use rusb::{request_type, Direction, Recipient, RequestType, TransferType};
|
||||
use std::cmp::max;
|
||||
use std::fmt::Write;
|
||||
use std::time::{Duration, Instant};
|
||||
use usb_device::test_class;
|
||||
|
||||
pub type TestFn = fn(&mut DeviceHandles, &mut String) -> ();
|
||||
|
||||
const BENCH_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
macro_rules! tests {
|
||||
{ $(fn $name:ident($dev:ident, $out:ident) $body:expr)* } => {
|
||||
pub fn get_tests() -> Vec<(&'static str, TestFn)> {
|
||||
let mut tests: Vec<(&'static str, TestFn)> = Vec::new();
|
||||
|
||||
$(
|
||||
fn $name($dev: &mut DeviceHandles, $out: &mut String) {
|
||||
$body
|
||||
}
|
||||
|
||||
tests.push((stringify!($name), $name));
|
||||
)*
|
||||
|
||||
tests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tests! {
|
||||
|
||||
fn control_request(dev, _out) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let value: u16 = rng.gen();
|
||||
let index: u16 = rng.gen();
|
||||
let data = random_data(rng.gen_range(0..16));
|
||||
|
||||
let mut expected = [0u8; 8];
|
||||
expected[0] = 0x02_u8 << 5;
|
||||
expected[1] = test_class::REQ_STORE_REQUEST;
|
||||
expected[2..4].copy_from_slice(&value.to_le_bytes());
|
||||
expected[4..6].copy_from_slice(&index.to_le_bytes());
|
||||
expected[6..8].copy_from_slice(&(data.len() as u16).to_le_bytes());
|
||||
|
||||
assert_eq!(
|
||||
dev.write_control(
|
||||
request_type(Direction::Out, RequestType::Vendor, Recipient::Device),
|
||||
test_class::REQ_STORE_REQUEST, value, index,
|
||||
&data, TIMEOUT).expect("control write"),
|
||||
data.len());
|
||||
|
||||
let mut response = [0u8; 8];
|
||||
|
||||
assert_eq!(
|
||||
dev.read_control(
|
||||
request_type(Direction::In, RequestType::Vendor, Recipient::Device),
|
||||
test_class::REQ_READ_BUFFER, 0, 0,
|
||||
&mut response, TIMEOUT).expect("control read"),
|
||||
response.len());
|
||||
|
||||
assert_eq!(&response, &expected);
|
||||
}
|
||||
|
||||
fn control_data(dev, _out) {
|
||||
for len in &[0, 7, 8, 9, 15, 16, 17] {
|
||||
let data = random_data(*len);
|
||||
|
||||
assert_eq!(
|
||||
dev.write_control(
|
||||
request_type(Direction::Out, RequestType::Vendor, Recipient::Device),
|
||||
test_class::REQ_WRITE_BUFFER, 0, 0,
|
||||
&data, TIMEOUT).unwrap_or_else(|_| panic!("control write len {}", len)),
|
||||
data.len());
|
||||
|
||||
let mut response = vec![0u8; *len];
|
||||
|
||||
assert_eq!(
|
||||
dev.read_control(
|
||||
request_type(Direction::In, RequestType::Vendor, Recipient::Device),
|
||||
test_class::REQ_READ_BUFFER, 0, 0,
|
||||
&mut response, TIMEOUT).unwrap_or_else(|_| panic!("control read len {}", len)),
|
||||
data.len());
|
||||
|
||||
assert_eq!(&response, &data);
|
||||
}
|
||||
}
|
||||
|
||||
fn control_data_static(dev, _out) {
|
||||
let mut response = [0u8; 257];
|
||||
|
||||
assert_eq!(
|
||||
dev.read_control(
|
||||
request_type(Direction::In, RequestType::Vendor, Recipient::Device),
|
||||
test_class::REQ_READ_LONG_DATA, 0, 0,
|
||||
&mut response, TIMEOUT).expect("control read"),
|
||||
response.len());
|
||||
|
||||
assert_eq!(&response[..], test_class::LONG_DATA);
|
||||
}
|
||||
|
||||
fn control_error(dev, _out) {
|
||||
let res = dev.write_control(
|
||||
request_type(Direction::Out, RequestType::Vendor, Recipient::Device),
|
||||
test_class::REQ_UNKNOWN, 0, 0,
|
||||
&[], TIMEOUT);
|
||||
|
||||
if res.is_ok() {
|
||||
panic!("unknown control request succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
fn string_descriptors(dev, _out) {
|
||||
assert_eq!(
|
||||
dev.read_product_string(dev.en_us, &dev.device_descriptor, TIMEOUT)
|
||||
.expect("read product string"),
|
||||
test_class::PRODUCT);
|
||||
|
||||
assert_eq!(
|
||||
dev.read_manufacturer_string(dev.en_us, &dev.device_descriptor, TIMEOUT)
|
||||
.expect("read manufacturer string"),
|
||||
test_class::MANUFACTURER);
|
||||
|
||||
assert_eq!(
|
||||
dev.read_serial_number_string(dev.en_us, &dev.device_descriptor, TIMEOUT)
|
||||
.expect("read serial number string"),
|
||||
test_class::SERIAL_NUMBER);
|
||||
|
||||
assert_eq!(
|
||||
dev.read_string_descriptor(dev.en_us, 4, TIMEOUT)
|
||||
.expect("read custom string"),
|
||||
test_class::CUSTOM_STRING);
|
||||
}
|
||||
|
||||
fn interface_descriptor(dev, _out) {
|
||||
let iface = dev.config_descriptor
|
||||
.interfaces()
|
||||
.find(|i| i.number() == 0)
|
||||
.expect("interface not found");
|
||||
|
||||
let default_alt_setting = iface.descriptors()
|
||||
.find(|i| i.setting_number() == 0)
|
||||
.expect("default alt setting not found");
|
||||
|
||||
assert_eq!(default_alt_setting.description_string_index(), None);
|
||||
assert_eq!(default_alt_setting.class_code(), 0xff);
|
||||
assert_eq!(default_alt_setting.sub_class_code(), 0x00);
|
||||
|
||||
let second_alt_setting = iface.descriptors()
|
||||
.find(|i| i.setting_number() == 1)
|
||||
.expect("second alt setting not found");
|
||||
|
||||
assert_eq!(second_alt_setting.class_code(), 0xff);
|
||||
assert_eq!(second_alt_setting.sub_class_code(), 0x01);
|
||||
|
||||
let string_index = second_alt_setting.description_string_index()
|
||||
.expect("second alt setting string is undefined");
|
||||
|
||||
assert_eq!(
|
||||
dev.read_string_descriptor(dev.en_us, string_index, TIMEOUT)
|
||||
.expect("read interface string"),
|
||||
test_class::INTERFACE_STRING);
|
||||
}
|
||||
|
||||
fn iso_endpoint_descriptors(dev, _out) {
|
||||
// Tests that an isochronous endpoint descriptor is present in the first
|
||||
// alternate setting, but not in the default setting.
|
||||
let iface = dev.config_descriptor
|
||||
.interfaces()
|
||||
.find(|i| i.number() == 0)
|
||||
.expect("interface not found");
|
||||
|
||||
let mut iso_ep_count = 0;
|
||||
for iface_descriptor in iface.descriptors() {
|
||||
if iface_descriptor.setting_number() == 0 {
|
||||
// Default setting - no isochronous endpoints allowed. Per USB 2.0
|
||||
// spec rev 2.0, 5.6.3 Isochronous Transfer Packet Size Constraints:
|
||||
//
|
||||
// All device default interface settings must not include any
|
||||
// isochronous endpoints with non-zero data payload sizes (specified
|
||||
// via wMaxPacketSize in the endpoint descriptor)
|
||||
let issue = iface_descriptor
|
||||
.endpoint_descriptors()
|
||||
.find(|ep| ep.transfer_type() == TransferType::Isochronous
|
||||
&& ep.max_packet_size() != 0);
|
||||
if let Some(ep) = issue {
|
||||
panic!("Endpoint {} is isochronous and in the default setting",
|
||||
ep.number());
|
||||
}
|
||||
} else {
|
||||
iso_ep_count += iface_descriptor.endpoint_descriptors()
|
||||
.filter(|ep| ep.transfer_type() == TransferType::Isochronous)
|
||||
.count();
|
||||
}
|
||||
}
|
||||
assert!(iso_ep_count > 0, "At least one isochronous endpoint is expected");
|
||||
}
|
||||
|
||||
fn bulk_loopback(dev, _out) {
|
||||
let mut lens = vec![0, 1, 2, 32, 63, 64, 65, 127, 128, 129];
|
||||
if dev.is_high_speed() {
|
||||
lens.extend([255, 256, 257, 511, 512, 513, 1023, 1024, 1025]);
|
||||
}
|
||||
|
||||
let max_packet_size: usize = dev.bulk_max_packet_size().into();
|
||||
for len in &lens {
|
||||
let data = random_data(*len);
|
||||
|
||||
assert_eq!(
|
||||
dev.write_bulk(0x01, &data, TIMEOUT)
|
||||
.unwrap_or_else(|_| panic!("bulk write len {}", len)),
|
||||
data.len(),
|
||||
"bulk write len {}", len);
|
||||
|
||||
if *len > 0 && *len % max_packet_size == 0 {
|
||||
assert_eq!(
|
||||
dev.write_bulk(0x01, &[], TIMEOUT)
|
||||
.expect("bulk write zero-length packet"),
|
||||
0,
|
||||
"bulk write zero-length packet");
|
||||
}
|
||||
|
||||
// Prevent libusb from instantaneously reading an empty packet on Windows when
|
||||
// zero-sized buffer is passed.
|
||||
let mut response = vec![0u8; max(*len, 1)];
|
||||
|
||||
assert_eq!(
|
||||
dev.read_bulk(0x81, &mut response, TIMEOUT)
|
||||
.unwrap_or_else(|_| panic!("bulk read len {}", len)),
|
||||
data.len(),
|
||||
"bulk read len {}", len);
|
||||
|
||||
assert_eq!(&response[..*len], &data[..]);
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt_loopback(dev, _out) {
|
||||
for len in &[0, 1, 2, 15, 31] {
|
||||
let data = random_data(*len);
|
||||
|
||||
assert_eq!(
|
||||
dev.write_interrupt(0x02, &data, TIMEOUT)
|
||||
.unwrap_or_else(|_| panic!("interrupt write len {}", len)),
|
||||
data.len(),
|
||||
"interrupt write len {}", len);
|
||||
|
||||
// Prevent libusb from instantaneously reading an empty packet on Windows when
|
||||
// zero-sized buffer is passed.
|
||||
let mut response = vec![0u8; max(*len, 1)];
|
||||
|
||||
assert_eq!(
|
||||
dev.read_interrupt(0x82, &mut response, TIMEOUT)
|
||||
.unwrap_or_else(|_| panic!("interrupt read len {}", len)),
|
||||
data.len(),
|
||||
"interrupt read len {}", len);
|
||||
|
||||
assert_eq!(&response[..*len], &data[..]);
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_bulk_write(dev, out) {
|
||||
run_bench(dev, out, |data| {
|
||||
assert_eq!(
|
||||
dev.write_bulk(0x01, data, BENCH_TIMEOUT)
|
||||
.expect("bulk write"),
|
||||
data.len(),
|
||||
"bulk write");
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_bulk_read(dev, out) {
|
||||
run_bench(dev, out, |data| {
|
||||
assert_eq!(
|
||||
dev.read_bulk(0x81, data, BENCH_TIMEOUT)
|
||||
.expect("bulk read"),
|
||||
data.len(),
|
||||
"bulk read");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn run_bench(dev: &DeviceHandles, out: &mut String, f: impl Fn(&mut [u8])) {
|
||||
const TRANSFER_BYTES: usize = 64 * 1024;
|
||||
const TRANSFERS: usize = 16;
|
||||
const TOTAL_BYTES: usize = TRANSFER_BYTES * TRANSFERS;
|
||||
|
||||
dev.write_control(
|
||||
request_type(Direction::Out, RequestType::Vendor, Recipient::Device),
|
||||
test_class::REQ_SET_BENCH_ENABLED,
|
||||
1,
|
||||
0,
|
||||
&[],
|
||||
TIMEOUT,
|
||||
)
|
||||
.expect("enable bench mode");
|
||||
|
||||
let mut data = random_data(TRANSFER_BYTES);
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
for _ in 0..TRANSFERS {
|
||||
f(&mut data);
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let elapsed = elapsed.as_secs() as f64 + (elapsed.subsec_micros() as f64) * 0.000_001;
|
||||
let throughput = (TOTAL_BYTES * 8) as f64 / 1_000_000.0 / elapsed;
|
||||
|
||||
writeln!(
|
||||
out,
|
||||
" {} transfers of {} bytes in {:.3}s -> {:.3}Mbit/s",
|
||||
TRANSFERS, TRANSFER_BYTES, elapsed, throughput
|
||||
)
|
||||
.expect("write failed");
|
||||
}
|
||||
|
||||
fn random_data(len: usize) -> Vec<u8> {
|
||||
let mut data = vec![0u8; len];
|
||||
rand::thread_rng().fill(data.as_mut_slice());
|
||||
data
|
||||
}
|
||||
Reference in new issue
Block a user