Skip to content
Snippets Groups Projects
shell.rs 16.8 KiB
Newer Older
Stephen D's avatar
Stephen D committed
use core::fmt::Write;

use arrayvec::{ArrayString, CapacityError};
Stephen D's avatar
Stephen D committed
use rtic::Mutex;
Stephen D's avatar
Stephen D committed
use stm32f4xx_hal::{
    flash::LockedFlash,
    otg_fs::{UsbBus, USB},
};
use usbd_serial::SerialPort;
use ushell::{autocomplete::StaticAutocomplete, history::LRUHistory, Input, ShellError, UShell};

Stephen D's avatar
Stephen D committed
use crate::{
    config::{BlackHoleSetting, Config, GpsSetting, Maybe, MaybeBlackHole},
Stephen D's avatar
Stephen D committed
    voltage::VoltMeter,
};
Stephen D's avatar
Stephen D committed

type ShellType =
Stephen D's avatar
Stephen D committed
    UShell<SerialPort<'static, UsbBus<USB>>, StaticAutocomplete<5>, LRUHistory<256, 8>, 256>;
Stephen D's avatar
Stephen D committed

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\
	 ";

Stephen D's avatar
Stephen D committed
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\
Stephen D's avatar
Stephen D committed
	 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\
Stephen D's avatar
Stephen D committed
	 "
);
Stephen D's avatar
Stephen D committed

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,
Stephen D's avatar
Stephen D committed
            StaticAutocomplete(["get", "set", "help", "save", "volts"]),
Stephen D's avatar
Stephen D committed
            LRUHistory::default(),
        );

        Self { ushell, config }
    }

Stephen D's avatar
Stephen D committed
    pub fn poll<V: Mutex<T = VoltMeter>>(&mut self, flash: &mut LockedFlash, volt_meter: &mut V) {
Stephen D's avatar
Stephen D committed
        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,
Stephen D's avatar
Stephen D committed
                                enable_digipeating,
Stephen D's avatar
Stephen D committed
                                min_voltage,

                                antenna_height,
                                antenna_gain,
                                tx_power,
Stephen D's avatar
Stephen D committed
                            } = self.config;

Stephen D's avatar
Stephen D committed
                            let comment = if comment.is_empty() {
                                "<None>"
                            } else {
                                &comment
                            };
Stephen D's avatar
Stephen D committed

                            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\
Stephen D's avatar
Stephen D committed
							 min_voltage        {min_voltage:.2}\r\n\
Stephen D's avatar
Stephen D committed
							 enable_transmit    {enable_transmit}\r\n\
Stephen D's avatar
Stephen D committed
							 enable_digipeating {enable_digipeating}\r\n\
Stephen D's avatar
Stephen D committed
							 \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\
Stephen D's avatar
Stephen D committed
							 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, ""));
Stephen D's avatar
Stephen D committed

                            let res = match property {
                                "callsign" => {
Stephen D's avatar
Stephen D committed
                                    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) {
Stephen D's avatar
Stephen D committed
                                    Ok(v) => {
                                        self.config.icon = v;
                                        Ok(())
                                    }
                                    Err(e) => Err(e),
                                },
                                "ssid" => match parse_u8(value) {
Stephen D's avatar
Stephen D committed
                                    Ok(v) => {
                                        self.config.ssid = v;
                                        Ok(())
                                    }
                                    Err(e) => Err(e),
                                },
                                "max_hops" => match parse_u8(value) {
Stephen D's avatar
Stephen D committed
                                    Ok(v) => {
                                        self.config.max_hops = v;
                                        Ok(())
                                    }
                                    Err(e) => Err(e),
                                },
                                "comment" => match parse_arrstring(value) {
Stephen D's avatar
Stephen D committed
                                    Ok(v) => {
                                        self.config.comment = v;
                                        Ok(())
                                    }
                                    Err(e) => Err(e),
                                },
                                "transmit_period" => match parse_u64(value) {
Stephen D's avatar
Stephen D committed
                                    Ok(v) => {
                                        self.config.transmit_period_seconds = v;
                                        Ok(())
                                    }
                                    Err(e) => Err(e),
                                },
                                "gps" => match parse_gps(value) {
Stephen D's avatar
Stephen D committed
                                    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),
                                },
Stephen D's avatar
Stephen D committed
                                "min_voltage" => match parse_maybe_f32(value) {
                                    Ok(v) => {
                                        self.config.min_voltage = v;
                                        Ok(())
                                    }
                                    Err(e) => Err(e),
                                },
Stephen D's avatar
Stephen D committed
                                "enable_digipeating" => match parse_bool(value) {
                                    Ok(v) => {
                                        self.config.enable_digipeating = v;
                                        Ok(())
                                    }
                                    Err(e) => Err(e),
                                },
                                "enable_transmit" => match parse_bool(value) {
Stephen D's avatar
Stephen D committed
                                    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();
Stephen D's avatar
Stephen D committed
                                    break 'inner;
                                }
                                _ => {
                                    write!(ushell, "\r\nUnknown property specified. See `get` for a list of properties.\r\n").ok();
Stephen D's avatar
Stephen D committed
                                    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();
                        }
Stephen D's avatar
Stephen D committed
                        "volts" => {
                            let volts = volt_meter.lock(|vm| vm.voltage());

                            write!(ushell, "\r\n{volts:.2} V\r\n").ok();
                        }
Stephen D's avatar
Stephen D committed
                        "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,
Stephen D's avatar
Stephen D committed
                                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();
                        }
Stephen D's avatar
Stephen D committed
                        "" => {
                            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.")
}

Stephen D's avatar
Stephen D committed
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))
}

Stephen D's avatar
Stephen D committed
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))
        }
    }
}
Stephen D's avatar
Stephen D committed

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,
    }))
}

Stephen D's avatar
Stephen D committed
fn parse_bool(val: &str) -> Result<bool, &'static str> {
    match val {
        "true" | "y" | "Y" | "yes" => Ok(true),
        "false" | "n" | "N" | "no" => Ok(false),
        _ => Err("Invalid value. Expected either 'true' or 'false'."),
    }
}