diff --git a/.cargo/config b/.cargo/config index 03a0de7d8..8a4c28771 100644 --- a/.cargo/config +++ b/.cargo/config @@ -12,3 +12,4 @@ rustflags = [ [alias] fileio = "run --bin fileio --no-default-features --features env_logger,indicatif --" +settings = "run --bin piksi-settings --no-default-features --features env_logger --" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9a897cac5..c13cd915e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -188,9 +188,9 @@ jobs: strategy: matrix: os: - - {name: ubuntu-18.04, exe_suffix: "" } - - {name: macos-10.15, exe_suffix: "" } - - {name: windows-2019, exe_suffix: ".exe" } + - {name: ubuntu-18.04, exe_suffix: "", short_name: "linux"} + - {name: macos-10.15, exe_suffix: "", short_name: "macos"} + - {name: windows-2019, exe_suffix: ".exe", short_name: "windows"} runs-on: ${{ matrix.os.name }} @@ -319,6 +319,19 @@ jobs: LIBCLANG_PATH: ${{ env.LIBCLANG_PATH_WIN }} if: matrix.os.name == 'windows-2019' + - name: Build ${{ runner.os }} piksi-settings binary. + run: | + cargo build --bin piksi-settings --features="env_logger" --release + # Building on Mac currently not working (DEVINFRA-612) + if: matrix.os.name != 'windows-2019' && matrix.os.name != 'macos-10.15' + + - name: Build ${{ runner.os }} piksi-settings binary. + run: | + cargo build --bin piksi-settings --features="env_logger" --release + env: + LIBCLANG_PATH: ${{ env.LIBCLANG_PATH_WIN }} + if: matrix.os.name == 'windows-2019' + - name: Pull Git LFS objects run: git lfs pull env: @@ -350,17 +363,12 @@ jobs: release-archive.filename - uses: actions/upload-artifact@v2 with: - name: ${{ runner.os }}-fileio - path: | - ./target/release/fileio${{ matrix.os.exe_suffix}} - + name: fileio_${{ matrix.os.short_name }} + path: ./target/release/fileio${{ matrix.os.exe_suffix }} - uses: actions/upload-artifact@v2 with: - name: ${{ runner.os }}-installer - path: | - ${{ env.INSTALLER_ARCHIVE }} - installer-archive.filename - if: github.event_name == 'push' && contains(github.ref, 'refs/tags') + name: piksi-settings_${{ matrix.os.short_name }} + path: ./target/release/piksi-settings${{ matrix.os.exe_suffix }} - uses: actions/upload-artifact@v2 with: name: ${{ runner.os }}-artifacts-debug @@ -379,6 +387,13 @@ jobs: shell: bash run: | cargo make disk-usage-bench + - uses: actions/upload-artifact@v2 + with: + name: ${{ runner.os }}-installer + path: | + ${{ env.INSTALLER_ARCHIVE }} + installer-archive.filename + if: github.event_name == 'push' && contains(github.ref, 'refs/tags') frontend_bench: name: Run Frontend Benchmarks @@ -488,7 +503,7 @@ jobs: run: | set /p executable=> $GITHUB_ENV echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - - name: Pull Windows Artifacts + - name: Pull Windows Installer uses: actions/download-artifact@v2 with: name: Windows-installer-signed - path: | - windows - - name: Pull Linux Artifacts + path: windows + - name: Pull Windows fileio + uses: actions/download-artifact@v2 + with: + name: fileio_windows + path: windows + - name: Pull Windows piksi-settings + uses: actions/download-artifact@v2 + with: + name: piksi-settings_windows + path: windows + - name: Pull Linux Installer uses: actions/download-artifact@v2 with: name: Linux-installer - path: | - linux - - name: Pull macOS Artifacts + path: linux + - name: Pull Linux fileio + uses: actions/download-artifact@v2 + with: + name: fileio_linux + path: linux + - name: Pull Linux piksi-settings + uses: actions/download-artifact@v2 + with: + name: piksi-settings_linux + path: linux + - name: Pull macOS Installer uses: actions/download-artifact@v2 with: name: macOS-installer path: | macos - - name: Store Env Vars + - name: Prepare Release shell: bash run: | + mv linux/fileio linux/fileio_${{ env.VERSION }}_linux + mv linux/piksi-settings linux/piksi-settings_${{ env.VERSION }}_linux + mv windows/fileio.exe windows/fileio_${{ env.VERSION }}_windows.exe + mv windows/piksi-settings.exe windows/piksi-settings_${{ env.VERSION }}_windows.exe echo "WINDOWS_ARCHIVE=$(cat windows/installer-archive.filename)" >>$GITHUB_ENV echo "LINUX_ARCHIVE=$(cat linux/installer-archive.filename)" >>$GITHUB_ENV echo "MACOS_ARCHIVE=$(cat macos/installer-archive.filename)" >>$GITHUB_ENV @@ -544,7 +580,11 @@ jobs: name: "${{ env.VERSION }}-${{ env.DATE }}" files: | windows/${{ env.WINDOWS_ARCHIVE }} + windows/fileio_${{ env.VERSION }}_windows.exe + windows/piksi-settings_${{ env.VERSION }}_windows.exe linux/${{ env.LINUX_ARCHIVE }} + linux/fileio_${{ env.VERSION }}_linux + linux/piksi-settings_${{ env.VERSION }}_linux macos/${{ env.MACOS_ARCHIVE }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/console_backend/Cargo.toml b/console_backend/Cargo.toml index e5a476ff0..ef431a399 100644 --- a/console_backend/Cargo.toml +++ b/console_backend/Cargo.toml @@ -72,6 +72,12 @@ bench = false name = "cpu_benches" harness = false +[[bin]] +name = "piksi-settings" +path = "src/bin/settings.rs" +bench = false +required-features = ["env_logger"] + [[bin]] name = "fileio" path = "src/bin/fileio.rs" diff --git a/console_backend/src/bin/fileio.rs b/console_backend/src/bin/fileio.rs index 6c17c06d7..d33521a28 100644 --- a/console_backend/src/bin/fileio.rs +++ b/console_backend/src/bin/fileio.rs @@ -11,16 +11,16 @@ use std::{ use anyhow::{anyhow, Context}; use clap::{ AppSettings::{ArgRequiredElseHelp, DeriveDisplayOrder}, - Args, Parser, + Parser, }; use indicatif::{ProgressBar, ProgressStyle}; use sbp::{link::LinkSource, SbpIterExt}; use console_backend::{ - cli_options::is_baudrate, - connection::{SerialConnection, TcpConnection}, + cli_options::ConnectionOpts, + connection::Connection, fileio::Fileio, - types::{FlowControl, MsgSender, Result}, + types::{MsgSender, Result}, }; fn main() -> Result<()> { @@ -95,26 +95,6 @@ struct Opts { conn: ConnectionOpts, } -#[derive(Args)] -struct ConnectionOpts { - /// The port to use when connecting via TCP - #[clap(long, default_value = "55555", conflicts_with_all = &["baudrate", "flow-control"])] - port: u16, - - /// The baudrate for processing packets when connecting via serial - #[clap( - long, - default_value = "115200", - validator(is_baudrate), - conflicts_with = "port" - )] - baudrate: u32, - - /// The flow control spec to use when connecting via serial - #[clap(long, default_value = "None", conflicts_with = "port")] - flow_control: FlowControl, -} - fn list(target: Target, conn: ConnectionOpts) -> Result<()> { let remote = target .into_remote() @@ -228,18 +208,7 @@ struct Remote { impl Remote { fn connect(&self, conn: ConnectionOpts) -> Result { - let (reader, writer) = match File::open(&self.host) { - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => return Err(e.into()), - Ok(_) => { - log::debug!("connecting via serial"); - SerialConnection::new(self.host.clone(), conn.baudrate, conn.flow_control) - .try_connect(None)? - } - Err(_) => { - log::debug!("connecting via tcp"); - TcpConnection::new(self.host.clone(), conn.port)?.try_connect(None)? - } - }; + let (reader, writer) = Connection::discover(self.host.clone(), conn)?.try_connect(None)?; let source = LinkSource::new(); let link = source.link(); std::thread::spawn(move || { diff --git a/console_backend/src/bin/settings.rs b/console_backend/src/bin/settings.rs new file mode 100644 index 000000000..b782829e5 --- /dev/null +++ b/console_backend/src/bin/settings.rs @@ -0,0 +1,220 @@ +use std::{convert::Infallible, path::PathBuf, str::FromStr, sync::Arc}; + +use clap::{AppSettings::DeriveDisplayOrder, Parser}; +use sbp::SbpIterExt; + +use console_backend::{ + cli_options::ConnectionOpts, + client_sender::TestSender, + connection::Connection, + settings_tab::SettingsTab, + shared_state::SharedState, + types::{MsgSender, Result}, +}; + +fn main() -> Result<()> { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "info"); + } + env_logger::init(); + let opts = Opts::parse(); + let settings = connect(&opts)?; + + log::info!("Loading settings..."); + settings.refresh(); + + if let Some(path) = opts.export { + settings.export(&path)?; + log::info!("Exported settings to {}", path.display()); + } else if let Some(path) = opts.import { + settings.import(&path)?; + log::info!("Imported settings from {}", path.display()); + } else if let Some(read_cmd) = opts.read { + read(read_cmd, &settings)?; + } else if let Some(write_cmds) = opts.write { + write(&write_cmds, &settings)?; + } else if opts.reset { + log::info!("Resetting settings to factory defaults"); + settings.reset(true)?; + } + + if opts.save { + log::info!("Saving settings to flash"); + settings.save()?; + } + + Ok(()) +} + +fn read(read_cmd: ReadCmd, settings: &SettingsTab) -> Result<()> { + let settings = if let Some(name) = read_cmd.name { + vec![settings.get(&read_cmd.group, &name)?] + } else { + settings.group(&read_cmd.group)? + }; + for entry in settings { + println!( + "{}.{}={}", + entry.setting.group, + entry.setting.name, + entry.value.map(|s| s.to_string()).unwrap_or_default() + ) + } + Ok(()) +} + +fn write(write_cmds: &[WriteCmd], settings: &SettingsTab) -> Result<()> { + for cmd in write_cmds { + log::debug!("{cmd:?}"); + settings.write_setting(&cmd.group, &cmd.name, &cmd.value)?; + log::info!("Wrote {}.{}={}", cmd.group, cmd.name, cmd.value); + } + Ok(()) +} + +/// Piksi settings operations. +#[derive(Parser)] +#[clap( + name = "piksi-settings", + version = include_str!("../version.txt"), + setting = DeriveDisplayOrder, + override_usage = "\ + piksi-settings [OPTIONS] + + Examples: + - Read a setting: + piksi-settings /dev/ttyUSB0 --read imu.acc_range + - Read a group of settings: + piksi-settings /dev/ttyUSB0 --read imu + - Write a setting value: + piksi-settings /dev/ttyUSB0 --write imu.acc_range=2g + - Write multiple settings and save to flash: + piksi-settings /dev/ttyUSB0 -w imu.acc_range=2g -w imu.imu_rate=100 --save + - Export a device's settings + piksi-settings /dev/ttyUSB0 --export ./config.ini + - Import a device's settings + piksi-settings /dev/ttyUSB0 --import ./config.ini + " +)] +struct Opts { + /// The serial port or TCP stream + device: String, + + /// Read a setting or a group of settings + #[clap( + long, + short, + value_name = "GROUP[.SETTING]", + conflicts_with_all = &["write", "import", "export", "reset"] + )] + read: Option, + + /// Write a setting value + #[clap( + long, + short, + value_name = "GROUP.SETTING=VALUE", + conflicts_with_all = &["read", "import", "export", "reset"] + )] + write: Option>, + + /// Export the devices settings + #[clap( + long, + value_name = "PATH", + conflicts_with_all = &["import", "read", "write", "reset"] + )] + export: Option, + + /// Import an ini file + #[clap( + long, + value_name = "PATH", + conflicts_with_all = &["read", "write", "export", "reset"] + )] + import: Option, + + /// Save settings to flash. Can be combined with --write or --import to save after writing + #[clap( + long, + conflicts_with_all = &["read", "export", "reset"] + )] + save: bool, + + /// Reset settings to factory defaults + #[clap( + long, + conflicts_with_all = &["read", "write", "export", "import", "save"] + )] + reset: bool, + + #[clap(flatten)] + conn: ConnectionOpts, +} + +fn connect(opts: &Opts) -> Result> { + let (reader, writer) = + Connection::discover(opts.device.clone(), opts.conn)?.try_connect(None)?; + let sender = MsgSender::new(writer); + let shared_state = SharedState::new(); + let settings = Arc::new(SettingsTab::new(shared_state, TestSender::boxed(), sender)); + std::thread::spawn({ + let settings = Arc::clone(&settings); + move || { + let messages = sbp::iter_messages(reader).log_errors(log::Level::Debug); + for msg in messages { + settings.handle_msg(msg); + } + } + }); + Ok(settings) +} + +struct ReadCmd { + group: String, + name: Option, +} + +impl FromStr for ReadCmd { + type Err = Infallible; + + fn from_str(s: &str) -> std::result::Result { + if let Some(idx) = s.find('.') { + let (group, name) = s.split_at(idx); + Ok(ReadCmd { + group: group.to_owned(), + name: Some(name[1..].to_owned()), + }) + } else { + Ok(ReadCmd { + group: s.to_owned(), + name: None, + }) + } + } +} + +#[derive(Debug)] +struct WriteCmd { + group: String, + name: String, + value: String, +} + +impl FromStr for WriteCmd { + type Err = &'static str; + + fn from_str(s: &str) -> std::result::Result { + const ERROR: &str = "write arguments must be of the form .="; + + let eq_idx = s.find('=').ok_or(ERROR)?; + let (setting, value) = s.split_at(eq_idx); + let dot_idx = setting.find('.').ok_or(ERROR)?; + let (group, name) = setting.split_at(dot_idx); + Ok(WriteCmd { + group: group.to_owned(), + name: name[1..].to_owned(), + value: value[1..].to_owned(), + }) + } +} diff --git a/console_backend/src/cli_options.rs b/console_backend/src/cli_options.rs index 00f5666b6..f782590b2 100644 --- a/console_backend/src/cli_options.rs +++ b/console_backend/src/cli_options.rs @@ -4,7 +4,7 @@ use std::{ str::FromStr, }; -use clap::{AppSettings::DeriveDisplayOrder, Parser}; +use clap::{AppSettings::DeriveDisplayOrder, Args, Parser}; use log::{debug, error}; use strum::VariantNames; @@ -251,6 +251,26 @@ impl Input { } } +#[derive(Clone, Copy, Args)] +pub struct ConnectionOpts { + /// The port to use when connecting via TCP + #[clap(long, default_value = "55555", conflicts_with_all = &["baudrate", "flow-control"])] + pub port: u16, + + /// The baudrate for processing packets when connecting via serial + #[clap( + long, + default_value = "115200", + validator(is_baudrate), + conflicts_with = "port" + )] + pub baudrate: u32, + + /// The flow control spec to use when connecting via serial + #[clap(long, default_value = "None", conflicts_with = "port")] + pub flow_control: FlowControl, +} + /// Validation for the refresh-rate cli option. /// /// # Parameters diff --git a/console_backend/src/connection.rs b/console_backend/src/connection.rs index c1c48a4e8..4492095b3 100644 --- a/console_backend/src/connection.rs +++ b/console_backend/src/connection.rs @@ -13,6 +13,7 @@ use std::{ use crossbeam::channel::Sender; use log::{error, info}; +use crate::cli_options::ConnectionOpts; use crate::client_sender::BoxedClientSender; use crate::constants::*; use crate::process_messages::{process_messages, Messages}; @@ -278,6 +279,21 @@ impl Connection { )) } + /// Connect via a serial port or tcp + pub fn discover(host: String, opts: ConnectionOpts) -> Result { + match fs::File::open(&host) { + Err(e) if e.kind() == io::ErrorKind::PermissionDenied => Err(e.into()), + Ok(_) => { + log::debug!("connecting via serial"); + Ok(Connection::serial(host, opts.baudrate, opts.flow_control)) + } + Err(_) => { + log::debug!("connecting via tcp"); + Connection::tcp(host, opts.port) + } + } + } + pub fn name(&self) -> String { match self { Connection::Tcp(conn) => conn.name(), diff --git a/console_backend/src/settings_tab.rs b/console_backend/src/settings_tab.rs index 3c28b412d..2f006b2b5 100644 --- a/console_backend/src/settings_tab.rs +++ b/console_backend/src/settings_tab.rs @@ -149,14 +149,28 @@ impl SettingsTab { self.shared_state.reset_settings_state(); } - fn refresh(&self) { + pub fn get(&self, group: &str, name: &str) -> Result { + self.settings.lock().get(group, name).map(Clone::clone) + } + + pub fn group(&self, group: &str) -> Result> { + let group = self + .settings + .lock() + .group(group)? + .map(Clone::clone) + .collect(); + Ok(group) + } + + pub fn refresh(&self) { (*self.settings.lock()) = Settings::new(); self.send_table_data(); self.read_all_settings(); self.send_table_data(); } - fn export(&self, path: &Path) -> Result<()> { + pub fn export(&self, path: &Path) -> Result<()> { let mut f = fs::File::create(path)?; let settings = self.settings.lock(); let groups = settings.groups(); @@ -172,7 +186,7 @@ impl SettingsTab { Ok(()) } - fn import(&self, path: &Path) -> Result<()> { + pub fn import(&self, path: &Path) -> Result<()> { let mut f = fs::File::open(path)?; let conf = Ini::read_from(&mut f)?; let old_ethernet = self.set_if_group_changes( @@ -244,7 +258,7 @@ impl SettingsTab { .send_data(serialize_capnproto_builder(builder)); } - fn reset(&self, reset_settings: bool) -> Result<()> { + pub fn reset(&self, reset_settings: bool) -> Result<()> { let flags = if reset_settings { 1 } else { 0 }; self.msg_sender.send(MsgReset { flags, @@ -252,7 +266,7 @@ impl SettingsTab { }) } - fn save(&self) -> Result<()> { + pub fn save(&self) -> Result<()> { self.msg_sender.send(MsgSettingsSave { sender_id: None }) } @@ -359,7 +373,7 @@ impl SettingsTab { .send_data(serialize_capnproto_builder(builder)); } - fn write_setting(&self, group: &str, name: &str, value: &str) -> Result<()> { + pub fn write_setting(&self, group: &str, name: &str, value: &str) -> Result<()> { { let settings = self.settings.lock(); if let Ok(e) = settings.get(group, name) { @@ -614,6 +628,7 @@ pub struct SaveRequest { pub value: String, } +#[derive(Clone)] struct Settings { inner: IndexMap>, default: SettingValue, @@ -705,8 +720,8 @@ impl Settings { } /// A reference to a particular setting and its value if it has been fetched -#[derive(Debug)] -struct SettingsEntry { - setting: Cow<'static, Setting>, - value: Option, +#[derive(Debug, Clone)] +pub struct SettingsEntry { + pub setting: Cow<'static, Setting>, + pub value: Option, }