#[cfg(all(feature = "socketcan", feature = "tui"))]
use std::time::Duration;
#[cfg(all(feature = "socketcan", feature = "tui"))]
use crossterm::event::{Event, KeyCode};
#[cfg(all(feature = "socketcan", feature = "tui"))]
use kcan::{
ActuatorConfig, ActuatorFeedback, ActuatorProtocol, ActuatorSnapshot, CanBus, CanFrame, CanId,
Error, FeedbackFilter, MotorModel, RobStrideFeedback, RobStrideFrameKind, RobStrideModel,
SocketCanBus, TuiControlIntent, TuiDashboard,
};
#[cfg(all(feature = "socketcan", feature = "tui"))]
use scrin::{Terminal, TerminalOptions};
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = DashboardOptions::from_args()?;
let snapshots = options
.targets
.iter()
.map(DashboardTarget::snapshot_without_feedback)
.collect();
let mut bus = SocketCanBus::open(&options.interface)?;
let mut dashboard = TuiDashboard::new(snapshots).with_controls_enabled(false);
let mut terminal = Terminal::init_with(TerminalOptions::new())?;
loop {
if let Some(frame) = bus.receive(Duration::from_millis(25))? {
if let Some(snapshot) = snapshot_from_frame(&options.targets, &frame)? {
dashboard.upsert_snapshot(snapshot);
}
}
dashboard.advance_tick();
terminal.draw(|frame| dashboard.render_frame(frame))?;
if let Some(event) = terminal.poll_event()? {
if let Event::Key(key) = event {
let ch = match key.code {
KeyCode::Char(ch) => Some(ch),
KeyCode::Esc => Some('q'),
_ => None,
};
if let Some(command) = ch.and_then(kcan::TuiCommand::from_key) {
if dashboard.apply_command(command) == TuiControlIntent::Quit {
break;
}
}
}
}
}
terminal.restore()?;
Ok(())
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
#[derive(Debug, Clone)]
struct DashboardOptions {
interface: String,
targets: Vec<DashboardTarget>,
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
#[derive(Debug, Clone)]
struct DashboardTarget {
config: ActuatorConfig,
protocol: DashboardProtocol,
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
#[derive(Debug, Clone)]
enum DashboardProtocol {
CubeMars {
model: MotorModel,
filter: FeedbackFilter,
},
RobStride {
status_id: CanId,
},
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
impl DashboardOptions {
fn from_args() -> kcan::Result<Self> {
let mut args = std::env::args().skip(1);
let interface = args.next().unwrap_or_else(|| "can0".to_owned());
let target_args = args.collect::<Vec<_>>();
let targets = parse_targets(&target_args)?;
Ok(Self { interface, targets })
}
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
impl DashboardTarget {
fn cubemars(model: MotorModel, motor_id: u8) -> Self {
Self {
config: ActuatorConfig {
id: motor_id,
label: Some(format!("{} 0x{:02X}", model.name(), motor_id)),
protocol: ActuatorProtocol::CubeMars(model),
},
protocol: DashboardProtocol::CubeMars {
model,
filter: FeedbackFilter::for_motor(model, motor_id),
},
}
}
fn robstride(model: RobStrideModel, motor_id: u8) -> kcan::Result<Self> {
Ok(Self {
config: ActuatorConfig {
id: motor_id,
label: Some(format!("{} 0x{:02X}", model.name(), motor_id)),
protocol: ActuatorProtocol::RobStride(model),
},
protocol: DashboardProtocol::RobStride {
status_id: RobStrideFrameKind::Status.can_id(motor_id)?,
},
})
}
fn snapshot_without_feedback(&self) -> ActuatorSnapshot {
ActuatorSnapshot {
config: self.config.clone(),
feedback: None,
}
}
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn snapshot_from_frame(
targets: &[DashboardTarget],
frame: &CanFrame,
) -> kcan::Result<Option<ActuatorSnapshot>> {
for target in targets {
match &target.protocol {
DashboardProtocol::CubeMars { model, filter } if filter.matches(frame) => {
return Ok(Some(ActuatorSnapshot {
config: target.config.clone(),
feedback: Some(ActuatorFeedback::CubeMars(
model.parse_feedback(frame.data())?,
)),
}));
}
DashboardProtocol::RobStride { status_id } if frame.id() == *status_id => {
if let Some(feedback) = RobStrideFeedback::parse(frame.data()) {
return Ok(Some(ActuatorSnapshot {
config: target.config.clone(),
feedback: Some(ActuatorFeedback::RobStride(feedback)),
}));
}
}
_ => {}
}
}
Ok(None)
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_targets(args: &[String]) -> kcan::Result<Vec<DashboardTarget>> {
let Some(first) = args.first() else {
return Ok(vec![DashboardTarget::cubemars(MotorModel::Ak60_6, 0x03)]);
};
if let Some(model) = parse_cubemars_model(first) {
let ids = parse_motor_ids_or_default(&args[1..], 0x03)?;
return Ok(ids
.into_iter()
.map(|motor_id| DashboardTarget::cubemars(model, motor_id))
.collect());
}
args.iter().map(|arg| parse_target(arg)).collect()
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_motor_ids_or_default(args: &[String], default_id: u8) -> kcan::Result<Vec<u8>> {
if args.is_empty() {
Ok(vec![default_id])
} else {
args.iter().map(|value| parse_motor_id(value)).collect()
}
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_target(value: &str) -> kcan::Result<DashboardTarget> {
let parts = value.split(':').collect::<Vec<_>>();
match parts.as_slice() {
[family, model, motor_id] if is_cubemars_family(family) => {
let model = parse_required_cubemars_model(model)?;
Ok(DashboardTarget::cubemars(model, parse_motor_id(motor_id)?))
}
[family, model, motor_id] if is_robstride_family(family) => {
let model = parse_required_robstride_model(model)?;
DashboardTarget::robstride(model, parse_motor_id(motor_id)?)
}
[model, motor_id] => {
if let Some(model) = parse_cubemars_model(model) {
return Ok(DashboardTarget::cubemars(model, parse_motor_id(motor_id)?));
}
if let Some(model) = parse_robstride_model(model) {
return DashboardTarget::robstride(model, parse_motor_id(motor_id)?);
}
Err(target_error(value))
}
_ => Err(target_error(value)),
}
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn is_cubemars_family(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"cubemars" | "cm"
)
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn is_robstride_family(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"robstride" | "rs" | "seeed"
)
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_required_cubemars_model(value: &str) -> kcan::Result<MotorModel> {
parse_cubemars_model(value).ok_or_else(|| {
Error::Transport(format!(
"unsupported CubeMars model {value:?}; run `cargo run --example cubemars_models` for names"
))
})
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_required_robstride_model(value: &str) -> kcan::Result<RobStrideModel> {
parse_robstride_model(value).ok_or_else(|| {
Error::Transport(format!(
"RobStride model must be rs00 through rs06, got {value:?}"
))
})
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_cubemars_model(value: &str) -> Option<MotorModel> {
MotorModel::from_name(value)
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_robstride_model(value: &str) -> Option<RobStrideModel> {
match value.trim().to_ascii_lowercase().as_str() {
"rs00" | "rs-00" | "rs0" => Some(RobStrideModel::Rs00),
"rs01" | "rs-01" | "rs1" => Some(RobStrideModel::Rs01),
"rs02" | "rs-02" | "rs2" => Some(RobStrideModel::Rs02),
"rs03" | "rs-03" | "rs3" => Some(RobStrideModel::Rs03),
"rs04" | "rs-04" | "rs4" => Some(RobStrideModel::Rs04),
"rs05" | "rs-05" | "rs5" => Some(RobStrideModel::Rs05),
"rs06" | "rs-06" | "rs6" => Some(RobStrideModel::Rs06),
_ => None,
}
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn parse_motor_id(value: &str) -> kcan::Result<u8> {
let value = value.trim();
let parsed = if let Some(hex) = value
.strip_prefix("0x")
.or_else(|| value.strip_prefix("0X"))
{
u8::from_str_radix(hex, 16)
} else {
value.parse::<u8>()
};
parsed.map_err(|err| {
Error::Transport(format!(
"motor id must be decimal 0..255 or hex 0x00..0xFF, got {value:?}: {err}"
))
})
}
#[cfg(all(feature = "socketcan", feature = "tui"))]
fn target_error(value: &str) -> Error {
Error::Transport(format!(
"target must look like cubemars:ak60-6:0x03 or robstride:rs01:0x01, got {value:?}"
))
}
#[cfg(all(test, feature = "socketcan", feature = "tui"))]
mod tests {
use super::*;
#[test]
fn parses_mixed_target_specs() {
let args = vec![
"cubemars:ak60-6:0x03".to_owned(),
"cubemars:ak100-33:0x04".to_owned(),
"robstride:rs01:1".to_owned(),
];
let targets = parse_targets(&args).unwrap();
assert_eq!(targets.len(), 3);
assert_eq!(
targets[0].config.protocol,
ActuatorProtocol::CubeMars(MotorModel::Ak60_6)
);
assert_eq!(targets[0].config.id, 0x03);
assert_eq!(
targets[1].config.protocol,
ActuatorProtocol::CubeMars(MotorModel::Ak100_33)
);
assert_eq!(targets[1].config.id, 0x04);
assert_eq!(
targets[2].config.protocol,
ActuatorProtocol::RobStride(RobStrideModel::Rs01)
);
assert_eq!(targets[2].config.id, 0x01);
}
#[test]
fn preserves_cubemars_shorthand() {
let args = vec!["ak80-6".to_owned(), "0x04".to_owned(), "5".to_owned()];
let targets = parse_targets(&args).unwrap();
assert_eq!(targets.len(), 2);
assert_eq!(
targets[0].config.protocol,
ActuatorProtocol::CubeMars(MotorModel::Ak80_6)
);
assert_eq!(targets[0].config.id, 0x04);
assert_eq!(targets[1].config.id, 5);
}
}
#[cfg(not(all(feature = "socketcan", feature = "tui")))]
fn main() {
eprintln!("enable the `socketcan` and `tui` features to run this example");
}