Newer
Older
use core::fmt::Write;
use arrayvec::{ArrayString, CapacityError};
use stm32f4xx_hal::{
flash::LockedFlash,
otg_fs::{UsbBus, USB},
};
use usbd_serial::SerialPort;
use ushell::{autocomplete::StaticAutocomplete, history::LRUHistory, Input, ShellError, UShell};
config::{BlackHoleSetting, Config, GpsSetting, Maybe, MaybeBlackHole},
UShell<SerialPort<'static, UsbBus<USB>>, StaticAutocomplete<5>, LRUHistory<256, 8>, 256>;
const SAVE_TEXT: &str =
"Saved your settings. Remove USB and press the reset button when you're ready.\r\n\
The board won't transmit when USB is attached, even if enable_transmit is true!\r\n\
";
const HELP_TEXT: &str = concat!(
"\r\n\
Mobile CATS transceiver\r\n\
Firmware version v",
env!("CARGO_PKG_VERSION"),
"\r\n\
Available commands:\r\n\
\r\n\
help Display this text\r\n\r\n\
get Show the current configuration\r\n\r\n\
set [property] [value] Update the current configuration\r\n For a list of properties, see the `get` command\r\n\r\n\
save Save the current settings to flash memory for persistence\r\n\r\n\
volts Show the current input voltage. Only reads from the screw terminals - will show ~0V if only connected to USB\r\n\
pub struct Shell {
pub ushell: ShellType,
config: Config,
}
impl Shell {
pub fn new(config: Config, serial: SerialPort<'static, UsbBus<USB>>) -> Self {
let ushell = UShell::new(
serial,
LRUHistory::default(),
);
Self { ushell, config }
}
pub fn poll<V: Mutex<T = VoltMeter>>(&mut self, flash: &mut LockedFlash, volt_meter: &mut V) {
loop {
let ushell = &mut self.ushell;
match ushell.poll() {
Ok(Some(Input::Command((cmd, args)))) => {
match cmd {
"get" => {
let Config {
callsign,
icon,
ssid,
max_hops,
comment,
transmit_period_seconds,
gps,
enable_transmit,
antenna_height,
antenna_gain,
tx_power,
let comment = if comment.is_empty() {
"<None>"
} else {
&comment
};
write!(
ushell,
"\r\n\
callsign {callsign}\r\n\
ssid {ssid}\r\n\
icon {icon}\r\n\
max_hops {max_hops}\r\n\
comment {comment}\r\n\
transmit_period {transmit_period_seconds} (seconds)\r\n\
gps {gps}\r\n\
black_hole {black_hole}\r\n\
The following settings do not change the behaviour of the transceiver.\r\n\
Instead, they only change the contents of the NodeInfo whisker.\r\n\
Info configuration:\r\n\
antenna_height {antenna_height:.1} (meters)\r\n\
antenna_gain {antenna_gain:.1} (dBi)\r\n\
tx_power {tx_power} (dBm)\r\n\
\r\n\
To change a setting, type `set <property> <value>`\r\n\
For example, `set transmit_period 300`\r\n"
)
.ok();
}
"set" => 'inner: {
let (property, value) = args.split_once(' ').unwrap_or((args, ""));
if value.trim().is_empty() {
Err("Blank callsign is invalid")
} else {
match parse_arrstring(value) {
Ok(v) => {
self.config.callsign = v;
Ok(())
}
Err(e) => Err(e),
}
}
}
"icon" => match parse_u16(value) {
Ok(v) => {
self.config.icon = v;
Ok(())
}
Err(e) => Err(e),
},
"ssid" => match parse_u8(value) {
Ok(v) => {
self.config.ssid = v;
Ok(())
}
Err(e) => Err(e),
},
"max_hops" => match parse_u8(value) {
Ok(v) => {
self.config.max_hops = v;
Ok(())
}
Err(e) => Err(e),
},
"comment" => match parse_arrstring(value) {
Ok(v) => {
self.config.comment = v;
Ok(())
}
Err(e) => Err(e),
},
"transmit_period" => match parse_u64(value) {
Ok(v) => {
self.config.transmit_period_seconds = v;
Ok(())
}
Err(e) => Err(e),
},
"gps" => match parse_gps(value) {
Ok(v) => {
self.config.gps = v;
Ok(())
}
Err(e) => Err(e),
},
"black_hole" => match parse_black_hole(value) {
Ok(v) => {
self.config.black_hole = v;
Ok(())
}
Err(e) => Err(e),
},
"min_voltage" => match parse_maybe_f32(value) {
Ok(v) => {
self.config.min_voltage = v;
Ok(())
}
Err(e) => Err(e),
},
"enable_digipeating" => match parse_bool(value) {
Ok(v) => {
self.config.enable_digipeating = v;
Ok(())
}
Err(e) => Err(e),
},
"enable_transmit" => match parse_bool(value) {
Ok(v) => {
self.config.enable_transmit = v;
Ok(())
}
Err(e) => Err(e),
},
"antenna_height" => match parse_maybe_u8(value) {
Ok(v) => {
self.config.antenna_height = v;
Ok(())
}
Err(e) => Err(e),
},
"antenna_gain" => match parse_maybe_f32(value) {
Ok(v) => {
self.config.antenna_gain = v;
Ok(())
}
Err(e) => Err(e),
},
"tx_power" => match parse_maybe_f32(value) {
Ok(v) => {
self.config.tx_power = v;
Ok(())
}
Err(e) => Err(e),
},
"" => {
write!(ushell, "\r\nUsage: set <property> <value>\r\n").ok();
_ => {
write!(ushell, "\r\nUnknown property specified. See `get` for a list of properties.\r\n").ok();
break 'inner;
}
};
match res {
Ok(()) => write!(ushell, "\r\nOK. Don't forget to save your settings when you're done with `save`!\r\n").ok(),
Err(e) => write!(ushell, "\r\nError: {}\r\n", e).ok()
};
}
"save" => {
write!(ushell, "\r\n").ok();
self.config.save_to_flash(flash);
write!(ushell, "{}", SAVE_TEXT).ok();
}
"volts" => {
let volts = volt_meter.lock(|vm| vm.voltage());
write!(ushell, "\r\n{volts:.2} V\r\n").ok();
}
"help" => {
write!(ushell, "{}", HELP_TEXT).ok();
}
"tear_my_settings_to_shreds" => {
write!(
ushell,
"\r\nResetting to factory defaults. Don't forget to save!\r\n"
)
.ok();
self.config = Config::default();
}
// Used for QA only
"quicktest" => {
self.config = Config {
callsign: args.try_into().unwrap(),
icon: 0,
ssid: 255,
max_hops: 0,
comment: ArrayString::from("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. \
Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \
Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, \
venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibu").unwrap(),
transmit_period_seconds: 3,
gps: GpsSetting::Receiver,
enable_transmit: true,
enable_digipeating: true,
black_hole: MaybeBlackHole::None,
antenna_height: Maybe::None,
antenna_gain: Maybe::None,
tx_power: Maybe::Some(30.0),
min_voltage: Maybe::Some(9.0),
};
self.config.save_to_flash(flash);
write!(ushell, "\r\nOK\r\n").ok();
}
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
"" => {
write!(ushell, "\r\n").ok();
}
_ => {
write!(ushell, "\r\nUnknown command. Type 'help' for help.\r\n").ok();
}
}
write!(ushell, "> ").ok();
}
Err(ShellError::WouldBlock) => break,
_ => {}
}
}
}
}
fn parse_arrstring<const N: usize>(val: &str) -> Result<ArrayString<N>, &'static str> {
match val.try_into() {
Ok(x) => Ok(x),
Err(CapacityError { .. }) => Err("Given value too long"),
}
}
fn parse_u8(val: &str) -> Result<u8, &'static str> {
val.parse()
.map_err(|_| "Invalid value. Expected a number between 0 and 255.")
}
fn parse_maybe_u8(val: &str) -> Result<Maybe<u8>, &'static str> {
if val.is_empty() {
return Ok(Maybe::None);
}
val.parse()
.map_err(|_| "Invalid value. Expected a number between 0 and 255.")
.map(Maybe::Some)
}
fn parse_u16(val: &str) -> Result<u16, &'static str> {
val.parse()
.map_err(|_| "Invalid value. Expected a number between 0 and 65535.")
}
fn parse_u64(val: &str) -> Result<u64, &'static str> {
val.parse()
.map_err(|_| "Invalid value. Expected a positive integer.")
}
// Only in a range of 0 - 63
fn parse_maybe_f32(val: &str) -> Result<Maybe<f32>, &'static str> {
const ERR: &str = "Invalid value. Expected a number between 0 and 63.";
if val.is_empty() {
return Ok(Maybe::None);
}
let x = val.parse().map_err(|_| ERR)?;
if !(0.0..=63.0).contains(&x) {
return Err(ERR);
}
Ok(Maybe::Some(x))
}
fn parse_gps(val: &str) -> Result<GpsSetting, &'static str> {
const GENERIC_ERR: &str = "Invalid value. Expected one of the following options:\r\n\
The literal `disabled` (set gps disable) - Do not set GPS data in transmitted packets\r\n\
The literal `receiver` (set gps receiver) - Use a connected GPS module\r\n\
A lat/lon pair (set gps 12.34 56.78) - Use the specified fixed coordinates";
match val {
"disabled" => Ok(GpsSetting::Disabled),
"receiver" => Ok(GpsSetting::Receiver),
x => {
let (lat_s, lon_s) = x.split_once(' ').ok_or(GENERIC_ERR)?;
let lat = lat_s.parse().map_err(|_| GENERIC_ERR)?;
let lon = lon_s.parse().map_err(|_| GENERIC_ERR)?;
Ok(GpsSetting::Fixed(lat, lon))
}
}
}
fn parse_black_hole(val: &str) -> Result<MaybeBlackHole, &'static str> {
const ERR: &str = "Invalid value. Expected `lat lon radius`, where radius is in meters.\r\n\
For example, `set black_hole 12.34 56.78 1000` to exclude 1000m around (12.34, 56.78).\r\n\
Alternatively, clear this setting with `set black_hole none`.";
if val.trim().eq_ignore_ascii_case("none") {
return Ok(MaybeBlackHole::None);
}
let mut iter = val.split(' ');
let latitude = iter.next().ok_or(ERR)?.parse().map_err(|_| ERR)?;
let longitude = iter.next().ok_or(ERR)?.parse().map_err(|_| ERR)?;
let radius_meters = iter.next().ok_or(ERR)?.parse().map_err(|_| ERR)?;
if iter.next().is_some() {
return Err(ERR);
}
Ok(MaybeBlackHole::Some(BlackHoleSetting {
latitude,
longitude,
radius_meters,
}))
}