diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8ee45c1..1d49aac 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -10,4 +10,5 @@ - [Using Simulated Devices](./tutorial/100-simulator.md) - [Starting Simulated Devices](./tutorial/101-start.md) - [The Poststation User Interface](./tutorial/102-tui.md) + - [The `poststation-cli` tool](./tutorial/103-cli.md) - [Using an RP2040](./tutorial/200-rp2040.md) diff --git a/book/src/tutorial/102-endpoints.png b/book/src/tutorial/102-endpoints.png index 009fe72..8b35690 100644 Binary files a/book/src/tutorial/102-endpoints.png and b/book/src/tutorial/102-endpoints.png differ diff --git a/book/src/tutorial/102-topics.png b/book/src/tutorial/102-topics.png index 7b4b5d5..c5b71c1 100644 Binary files a/book/src/tutorial/102-topics.png and b/book/src/tutorial/102-topics.png differ diff --git a/book/src/tutorial/102-tui.md b/book/src/tutorial/102-tui.md index 479a860..a1fbe76 100644 --- a/book/src/tutorial/102-tui.md +++ b/book/src/tutorial/102-tui.md @@ -74,7 +74,7 @@ The Endpoints page is broken into two parts, the **Types** section, and the #### The "Endpoints (by path)" section -The current page shows that the simulator device supports five endpoints. +The current page shows that the simulator device supports six endpoints. The first endpoint, `postcard-rpc/ping` is an endpoint built in to all postcard-rpc devices. This endpoint takes a `u32`, or 32-bit unsigned @@ -104,6 +104,10 @@ that has an attached color LED. We can see that this endpoint does not return any data, though we will still get an "empty" response from the device as a confirmation. +The sixth endpoint, `simulator/status_led/get`, takes no data, but returns +the currently set value. In the simulator, this starts with all zero values, +and will retain the last value set by the `simulator/status_led/set` endpoint. + We can see some more information about the types mentioned in these endpoints in the section above. diff --git a/book/src/tutorial/103-cli.md b/book/src/tutorial/103-cli.md new file mode 100644 index 0000000..09e3e27 --- /dev/null +++ b/book/src/tutorial/103-cli.md @@ -0,0 +1,212 @@ +# The poststation-cli tool + +For this next section, we'll use the Command Line Interface tool, +`poststation-cli`. Make sure that: + +1. You've [installed](../installation-utils.md) the CLI tool +2. Your poststation server **is still running** with a simulator device + +In a new window, we can verify that we can connect to the server by running +the "list devices" command: + +```sh +$ poststation-cli ls + +# Devices + +| serial | name | interface | connected | +| :--------------- | ---------: | :-------- | :-------- | +| 563BF78B6A56DF04 | QUIRKY-344 | usb | yes | + +``` + +This prints out a table of the devices currently known by `poststation`. + +We can see the same simulator device from the previous chapter, and it is also +showing as currently connected. + +## Top level commands + +You can print help with `poststation-cli --help`: + +```sh +$ poststation-cli --help +A CLI tool for poststation + +Usage: poststation-cli [OPTIONS] + +Commands: + ls List devices + folder Show paths for configuration, database storage, and the CA certificate for external usage + device Interact with a specific device + help Print this message or the help of the given subcommand(s) + +Options: + -s, --server A path to the server. Defaults to `127.0.0.1:51837` + --insecure When set, a plaintext connection will be made with the server + --timings Print timing information + -h, --help Print help + -V, --version Print version +``` + +We've seen the output of `ls` already, we'll now focus on the `device` command, which +is the primary way of interacting with connected devices. + +## The `device` command + +We can see all of the `device` subcommands with `--help`: + +```sh +$ poststation-cli device --help +Interact with a specific device + +Usage: poststation-cli device [SERIAL] + +Commands: + types View all types used for communicating with a given device + endpoints View all endpoints available for communicating with a given device + topics-out View all topics published by a given device + topics-in View all topics handled by a given device + logs View the most recent logs from a given device + logs-range View the selected range of logs from a given device + proxy Proxy message to device endpoint + publish Publish a topic message to a device + listen Listen to a given "topic-out" path from a device + help Print this message or the help of the given subcommand(s) + +Arguments: + [SERIAL] Device Serial Number or Name. Can be set via POSTSTATION_SERIAL env var + +Options: + -h, --help Print help +``` + +Here we can fully interact with our device: + +* `types` will print the types known to the device on all endpoints/topics +* `endpoints` will print information about the device's endpoints +* `topics-out` will print all topics sent TO the client FROM the server +* `topics-in` will print all topics sent TO the server FROM the client +* `logs` will print the most recent logs, and `logs-range` can be used to print + a specific range of logs +* `proxy` is used to send an endpoint request and get the response from the device +* `publish` is used to send a `topics-in` message to the device +* `listen` is used to receive `topics-out` messages from the device + +### Common patterns + +These device commands each take a common "kinds" of arguments. + +#### Serial Number + +We will need to specify what device we want to interact with. We can use the +full serial number of our device, in hex format: + +```sh +$ poststation-cli device 563BF78B6A56DF04 endpoints + +Endpoints offered by device 563BF78B6A56DF04 + +* 'postcard-rpc/ping' => async fn(u32) -> u32 +* 'postcard-rpc/schemas/get' => async fn(()) -> SchemaTotals +* 'poststation/unique_id/get' => async fn(()) -> u64 +* 'simulator/picoboot/reset' => async fn(()) +* 'simulator/status_led/set' => async fn(Rgb8) +* 'simulator/status_led/get' => async fn(()) -> Rgb8 +``` + +However for convenience, the CLI also supports "fuzzy" matching, on part of +the serial number, or on part of the short name. + +For example, we can also use the last four digits of the serial number: + +```sh +$ poststation-cli device DF04 types + +Types used by device 563BF78B6A56DF04 + +* struct Key([u8; 8]) +* struct Rgb8 { r: u8, g: u8, b: u8 } +* enum OwnedSchemaData { Type(Schema), Endpoint{ path: String, request_key: Key, response_key: Key}, Topic{ path: String, key: Key, direction: TopicDirection} } +* [u8; 8] +* struct Temperature { temp: f64 } +* enum TopicDirection { ToServer, ToClient } +* struct SchemaTotals { types_sent: u32, endpoints_sent: u32, topics_in_sent: u32, topics_out_sent: u32, errors: u32 } +``` + +Or we can use part of the short name, "QUIRKY-344": + +```sh +$ poststation-cli device quirky topics-out + +Topics offered by device 563BF78B6A56DF04 + +* 'postcard-rpc/schema/data' => Channel +* 'postcard-rpc/logging' => Channel +* 'simulator/temperature' => Channel +``` + +#### Path + +When we need to specify the path, we can also use "fuzzy" matching. For example, +instead of using `simulator/status_led/get`, we can just say `led/get`: + +```sh +$ poststation-cli device quirky proxy led/get '{}' +Response: '{"b":30,"g":20,"r":10}' +``` + +However if we aren't specific enough, then we will get an error instead: + +```sh +$ poststation-cli device quirky proxy led +Given 'led', found: + +* 'simulator/status_led/set' => async fn(Rgb8) +* 'simulator/status_led/get' => async fn(()) -> Rgb8 + +Error: Too many matches, be more specific! +``` + +#### Values + +Since `postcard` is a binary format, the CLI will automatically translate all +messages to and from JSON, to make it possible to type on the command line. + +As we saw above with `device quirky proxy led/get`, the CLI printed out: + +```json +{"b":30,"g":20,"r":10} +``` + +If we want to send a command, we will also need to provide JSON. You may +want to use single quotes on your shell, to avoid needing to escape `"` +double quotes. + +```sh +$ poststation-cli device quirky proxy led/set '{"r": 20, "g": 30, "b": 40}' +Response: 'null' +``` + +If an endpoint or topic-in takes `()` as a value, we can also omit the value +entirely. For example, these two commands do the same thing: + +```sh +$ poststation-cli device quirky proxy led/get '{}' +Response: '{"b":30,"g":20,"r":10}' + +$ poststation-cli device quirky proxy led/get +Response: '{"b":30,"g":20,"r":10}' +``` + +Don't forget that JSON requires keys to be strings! If you forget this, you'll +get an error when poststation trys to convert this: + +```sh +$ poststation-cli device quirky proxy led/set '{r: 20, g: 30, b: 40}' +Error: 'Dynamic("provided JSON does not match the expected schema for this endpoint")' +``` + +## Next up + +In the next section, we'll explore using the SDK crate, `poststation-sdk`. diff --git a/tools/Cargo.lock b/tools/Cargo.lock index 2ba94fe..c4ad22b 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -817,6 +817,7 @@ dependencies = [ "postcard-rpc", "poststation-api-icd", "poststation-sdk", + "rand", "serde_json", "tokio", "uuid", @@ -841,6 +842,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -859,6 +869,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -1672,6 +1712,27 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "zeroize" version = "1.8.1" diff --git a/tools/poststation-cli/Cargo.toml b/tools/poststation-cli/Cargo.toml index 2ccbe86..220f018 100644 --- a/tools/poststation-cli/Cargo.toml +++ b/tools/poststation-cli/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT OR Apache-2.0" anyhow = "1.0.89" clap = { version = "4.5.19", features = ["derive"] } directories = "5.0.1" +rand = "0.8.5" serde_json = "1.0.128" [dependencies.postcard-rpc] diff --git a/tools/poststation-cli/src/device/mod.rs b/tools/poststation-cli/src/device/mod.rs new file mode 100644 index 0000000..9c99a0c --- /dev/null +++ b/tools/poststation-cli/src/device/mod.rs @@ -0,0 +1,394 @@ +use crate::{guess_serial, print_endpoint, print_topic}; +use anyhow::bail; +use clap::{Args, Subcommand}; +use postcard_rpc::host_client::{EndpointReport, SchemaReport, TopicReport}; +use poststation_api_icd::postsock::Direction; +use poststation_sdk::{schema::schema::owned::OwnedDataModelType, PoststationClient}; +use rand::{thread_rng, Rng}; +use serde_json::json; +use uuid::Uuid; + +#[derive(Args)] +pub struct Device { + /// Device Serial Number or Name. Can be set via POSTSTATION_SERIAL env var + pub serial: Option, + #[command(subcommand)] + pub command: DeviceCommands, +} + +#[derive(Subcommand)] +pub enum DeviceCommands { + /// View all types used for communicating with a given device + Types, + /// View all endpoints available for communicating with a given device + Endpoints, + /// View all topics published by a given device + TopicsOut, + /// View all topics handled by a given device + TopicsIn, + /// View the most recent logs from a given device + Logs { count: Option }, + /// View the selected range of logs from a given device + LogsRange { + /// Number of logs to print + count: Option, + /// The UUID of the log to start from + start: String, + /// Direction to print from ('before' or 'after') + direction: String, + }, + /// Proxy message to device endpoint + Proxy { + path: String, + message: Option, + }, + /// Publish a topic message to a device + Publish { + path: String, + message: Option, + }, + /// Listen to a given "topic-out" path from a device + Listen { + #[arg(value_name = "PATH")] + path: String, + }, +} + +pub async fn device_cmds(client: PoststationClient, device: &Device) -> anyhow::Result<()> { + let serial = guess_serial(device.serial.as_deref(), &client).await?; + let schema = client + .get_device_schemas(serial) + .await + .expect("expected to get schemas for device") + .expect("expected device to have known schemas"); + match &device.command { + DeviceCommands::Types => device_types(serial, &schema).await, + DeviceCommands::Endpoints => device_endpoints(serial, &schema).await, + DeviceCommands::TopicsOut => device_topics_out(serial, &schema).await, + DeviceCommands::TopicsIn => device_topics_in(serial, &schema).await, + DeviceCommands::Logs { count } => device_logs(client, serial, count).await, + DeviceCommands::LogsRange { + count, + start, + direction, + } => device_logs_range(client, serial, count, start, direction).await, + DeviceCommands::Proxy { path, message } => { + device_smart_proxy(client, serial, &schema, path, message.as_deref()).await + } + DeviceCommands::Publish { path, message } => { + device_smart_publish(client, &schema, serial, path, message.as_deref()).await + } + DeviceCommands::Listen { path } => device_smart_listen(client, &schema, serial, path).await, + } +} + +async fn device_smart_listen( + client: PoststationClient, + schema: &SchemaReport, + serial: u64, + path: &str, +) -> anyhow::Result<()> { + let path = &fuzzy_topic_out_match(path, schema)?.path; + + let mut sub = match client.stream_topic_json(serial, path).await { + Ok(s) => s, + Err(e) => bail!("{e}"), + }; + + while let Some(m) = sub.recv().await { + println!("{serial:016X}:'{path}':{m}"); + } + println!("Closed"); + Ok(()) +} + +async fn device_topics_in(serial: u64, schema: &SchemaReport) -> anyhow::Result<()> { + println!(); + println!("Topics handled by device {:016X}", serial); + println!(); + + for tp in &schema.topics_in { + println!("* '{}' => Channel<{}>", tp.path, tp.ty.name); + } + println!(); + Ok(()) +} + +async fn device_topics_out(serial: u64, schema: &SchemaReport) -> anyhow::Result<()> { + println!(); + println!("Topics offered by device {:016X}", serial); + println!(); + + for tp in &schema.topics_out { + println!("* '{}' => Channel<{}>", tp.path, tp.ty.name); + } + println!(); + Ok(()) +} + +async fn device_endpoints(serial: u64, schema: &SchemaReport) -> anyhow::Result<()> { + println!(); + println!("Endpoints offered by device {:016X}", serial); + println!(); + + for ep in &schema.endpoints { + print_endpoint(ep); + } + println!(); + Ok(()) +} + +async fn device_types(serial: u64, schema: &SchemaReport) -> anyhow::Result<()> { + println!(); + println!("Types used by device {:016X}", serial); + println!(); + + let base = SchemaReport::default(); + let uniq_tys = schema.types.difference(&base.types); + + for ty in uniq_tys { + println!("* {ty}"); + } + println!(); + Ok(()) +} + +async fn device_logs( + client: PoststationClient, + serial: u64, + count: &Option, +) -> anyhow::Result<()> { + let count = count.unwrap_or(8); + let logs = client + .get_device_logs(serial, count) + .await + .expect("expected to be able to get logs for device") + .expect("expected device to have known logs"); + + println!(); + println!("Logs (last {} messages):", count.min(logs.len() as u32)); + println!(); + for log in logs { + // println!("* {} => {}", log.uuidv7.id_to_time().time(), log.msg); + let time = log.uuidv7.id_to_time().time(); + println!( + "* {} ({}) => {}", + uuid::Uuid::from(log.uuidv7), + time, + log.msg + ); + } + println!(); + Ok(()) +} + +async fn device_logs_range( + client: PoststationClient, + serial: u64, + count: &Option, + start: &str, + direction: &str, +) -> anyhow::Result<()> { + let count = count.unwrap_or(8); + let start = start.parse::()?; + let dir = match direction.to_lowercase().as_str() { + "after" => Direction::After, + "before" => Direction::Before, + _ => bail!("Should provide 'after' or 'before' for direction"), + }; + + let logs = client + .get_device_logs_range( + serial, + count, + dir, + poststation_api_icd::postsock::Anchor::Uuid(start.into()), + ) + .await + .expect("expected to be able to get log range for device") + .expect("expected device to have known logs"); + + println!(); + println!("Logs (last {} messages):", count.min(logs.len() as u32)); + println!(); + for log in logs { + // println!("* {} => {}", log.uuidv7.id_to_time().time(), log.msg); + let time = log.uuidv7.id_to_time().time(); + println!( + "* {} ({}) => {}", + uuid::Uuid::from(log.uuidv7), + time, + log.msg + ); + } + println!(); + Ok(()) +} + +fn fuzzy_endpoint_match<'a>( + path: &str, + schema: &'a SchemaReport, +) -> anyhow::Result<&'a EndpointReport> { + let matches = schema + .endpoints + .iter() + .filter(|e| e.path.contains(path)) + .collect::>(); + + match matches.as_slice() { + [] => { + bail!("No endpoint found matching '{path}'"); + } + [ep] => Ok(ep), + more @ [..] => { + println!("Given '{path}', found:"); + println!(); + for matched_endpoint in more { + print_endpoint(matched_endpoint); + } + println!(); + bail!("Too many matches, be more specific!"); + } + } +} + +fn fuzzy_topic_out_match<'a>( + path: &str, + schema: &'a SchemaReport, +) -> anyhow::Result<&'a TopicReport> { + let matches = schema + .topics_out + .iter() + .filter(|to| to.path.contains(path)) + .collect::>(); + + match matches.as_slice() { + [] => { + bail!("No topic-out found matching '{path}'"); + } + [tp] => Ok(tp), + more @ [..] => { + println!("Given '{path}', found:"); + println!(); + for matched_topic in more { + print_topic(matched_topic); + } + println!(); + bail!("Too many matches, be more specific!"); + } + } +} + +fn fuzzy_topic_in_match<'a>( + path: &str, + schema: &'a SchemaReport, +) -> anyhow::Result<&'a TopicReport> { + let matches = schema + .topics_in + .iter() + .filter(|to| to.path.contains(path)) + .collect::>(); + + match matches.as_slice() { + [] => { + bail!("No topic-in found matching '{path}'"); + } + [tp] => Ok(tp), + more @ [..] => { + println!("Given '{path}', found:"); + println!(); + for matched_topic in more { + print_topic(matched_topic); + } + println!(); + bail!("Too many matches, be more specific!"); + } + } +} + +async fn device_smart_proxy( + client: PoststationClient, + serial: u64, + schema: &SchemaReport, + command: &str, + message: Option<&str>, +) -> anyhow::Result<()> { + let ep = fuzzy_endpoint_match(command, schema)?; + + match (&ep.req_ty.ty, message) { + (OwnedDataModelType::Unit, None) => { + device_proxy(client, serial, ep.path.clone(), "".to_string()).await?; + } + (_, None) => { + bail!( + "Endpoint '{}' requires a message to be sent of the type: async fn({}) -> {}", + ep.path, + ep.req_ty.name, + ep.resp_ty.name + ); + } + (_, Some(message)) => { + device_proxy(client, serial, ep.path.clone(), message.to_string()).await?; + } + } + + Ok(()) +} + +async fn device_smart_publish( + client: PoststationClient, + schema: &SchemaReport, + serial: u64, + path: &str, + message: Option<&str>, +) -> anyhow::Result<()> { + let topic_in = fuzzy_topic_in_match(path, schema)?; + + let msg = match (&topic_in.ty.ty, message) { + (OwnedDataModelType::Unit, None) => serde_json::Value::Null, + (_, None) => { + bail!( + "Topic '{}' requires a message to be sent of the type: {}", + topic_in.path, + topic_in.ty.name, + ); + } + (_, Some(message)) => message.parse()?, + }; + + let seq = thread_rng().gen(); + let res = client + .publish_topic_json(serial, &topic_in.path, seq, msg) + .await; + + match res { + Ok(()) => println!("Published."), + Err(e) => println!("Error: '{e}'"), + } + + Ok(()) +} + +async fn device_proxy( + client: PoststationClient, + serial: u64, + path: String, + message: String, +) -> anyhow::Result<()> { + let msg = match message.parse() { + Ok(m) => m, + Err(_) => { + //Attempting to just parse value as a string if it fails + json!(message) + } + }; + let seq = thread_rng().gen(); + let res = client.proxy_endpoint_json(serial, &path, seq, msg).await; + + match res { + Ok(v) => println!("Response: '{v}'"), + Err(e) => println!("Error: '{e}'"), + } + + Ok(()) +} diff --git a/tools/poststation-cli/src/main.rs b/tools/poststation-cli/src/main.rs index 3005b6d..cffe2ac 100644 --- a/tools/poststation-cli/src/main.rs +++ b/tools/poststation-cli/src/main.rs @@ -1,20 +1,15 @@ -use std::{collections::HashSet, net::SocketAddr, path::PathBuf, time::Instant}; +use std::{net::SocketAddr, path::PathBuf, time::Instant}; use anyhow::bail; -use clap::{command, Args, Parser, Subcommand}; +use clap::{command, Parser, Subcommand}; +use device::{device_cmds, Device}; use directories::ProjectDirs; -use postcard_rpc::host_client::{EndpointReport, SchemaReport}; -use poststation_api_icd::postsock::Direction; +use postcard_rpc::host_client::{EndpointReport, TopicReport}; use poststation_sdk::{ - connect, connect_insecure, - schema::schema::{ - fmt::{discover_tys, is_prim}, - owned::{OwnedDataModelType, OwnedNamedType}, - }, - PoststationClient, + connect, connect_insecure, schema::schema::owned::OwnedDataModelType, PoststationClient, }; -use serde_json::json; -use uuid::Uuid; + +mod device; /// The Poststation CLI #[derive(Parser)] @@ -29,8 +24,9 @@ struct Cli { insecure: bool, #[command(subcommand)] - command: Option, + command: Commands, + /// Print timing information #[arg(long)] timings: bool, } @@ -40,71 +36,12 @@ enum Commands { /// List devices Ls, - /// Endpoints of a given device - Endpoints { serial: Option }, - - /// Show the folder used for configuration, database storage, and + /// Show paths for configuration, database storage, and /// the CA certificate for external usage Folder, - /// Get information about a device + /// Interact with a specific device Device(Device), - /// Proxy an endpoint request/response through the server - Proxy { - #[arg(short, long, value_name = "SERIAL")] - serial: Option, - #[arg(short, long, value_name = "PATH")] - path: String, - #[arg(short, long, value_name = "MSG_JSON")] - message: String, - }, - Publish { - #[arg(short, long, value_name = "SERIAL")] - serial: String, - #[arg(short, long, value_name = "PATH")] - path: String, - #[arg(short, long, value_name = "MSG_JSON")] - message: String, - }, - /// Listen to a given "topic-out" path from a device - Listen { - #[arg(short, long, value_name = "SERIAL")] - serial: String, - #[arg(short, long, value_name = "PATH")] - path: String, - }, -} - -#[derive(Args)] -struct Device { - serial: Option, - #[command(subcommand)] - command: DeviceCommands, -} - -#[derive(Subcommand)] -enum DeviceCommands { - /// View all types used for communicating with a given device - Types, - /// View all endpoints available for communicating with a given device - Endpoints, - /// View all topics published by a given device - TopicsOut, - /// View all topics handled by a given device - TopicsIn, - /// View the most recent logs from a given device - Logs { count: Option }, - /// View the most recent logs from a given device - LogsRange { - count: Option, - start: String, - direction: String, - }, - /// Takes a guess at which endpoint you want to proxy and sends a message to it if you provide one - SmartProxy { - command: String, - message: Option, - }, } #[tokio::main] @@ -124,9 +61,7 @@ async fn inner_main(cli: Cli) -> anyhow::Result<()> { .server .unwrap_or_else(|| "127.0.0.1:51837".parse().unwrap()); - let Some(command) = cli.command else { - return Ok(()); - }; + let command = cli.command; let client = if cli.insecure { connect_insecure(server.port()).await } else { @@ -154,106 +89,9 @@ async fn inner_main(cli: Cli) -> anyhow::Result<()> { Ok(()) } Commands::Device(d) => device_cmds(client, &d).await, - Commands::Proxy { - serial, - message, - path, - } => { - let serial = guess_serial(serial.as_deref(), &client).await?; - device_proxy(client, serial, path, message).await - } - Commands::Publish { - serial, - message, - path, - } => device_publish(client, serial, path, message).await, - Commands::Endpoints { serial } => { - let serial_num = guess_serial(serial.as_deref(), &client).await?; - - println!("{serial_num:016X}"); - let schema = client - .get_device_schemas(serial_num) - .await - .expect("expected to be able to get schemas for device") - .expect("expected device to have schemas known by the server"); - - println!(); - println!("# Endpoints for {serial_num:016X}"); - println!(); - println!("## By path"); - println!(); - - let longest_ep = schema.endpoints.iter().map(|e| e.path.len()).max().unwrap(); - let longest_req = schema - .endpoints - .iter() - .map(|e| e.req_ty.name.len()) - .max() - .unwrap_or(0) - .max("Request Type".len()); - let longest_resp = schema - .endpoints - .iter() - .map(|e| e.resp_ty.name.len()) - .max() - .unwrap_or(0) - .max("Response Type".len()); - - println!( - "| {:longest_ep$} | {:longest_req$} | {:longest_resp$} |", - "path", "Request Type", "Response Type" - ); - println!( - "| {:- = used_tys - .into_iter() - .filter(|ont| !is_prim(&ont.ty)) - .collect(); - tys.sort_by_key(|o| o.name.clone()); - - for ty in tys { - println!(); - println!("### `{}`", ty.name); - println!(); - println!("{}", ty.to_pseudocode()); - } - println!(); - - Ok(()) - } - Commands::Listen { serial, path } => { - let serial_num = guess_serial(Some(&serial), &client).await?; - let mut sub = match client.stream_topic_json(serial_num, &path).await { - Ok(s) => s, - Err(e) => bail!("{e}"), - }; - - while let Some(m) = sub.recv().await { - println!("{serial_num:016X}:'{path}':{m}"); - } - println!("Closed"); - Ok(()) - } Commands::Folder => { - let Some(dirs) = ProjectDirs::from("com.onevariable", "onevariable", "poststation") else { + let Some(dirs) = ProjectDirs::from("com.onevariable", "onevariable", "poststation") + else { bail!("Failed to get working directory!"); }; let data_dir = dirs.data_dir(); @@ -270,201 +108,6 @@ async fn inner_main(cli: Cli) -> anyhow::Result<()> { println!("Configuration: {cfg_path:?}"); println!(); Ok(()) - }, - } -} - -async fn device_proxy( - client: PoststationClient, - serial: u64, - path: String, - message: String, -) -> anyhow::Result<()> { - let msg = match message.parse() { - Ok(m) => m, - Err(_) => { - //Attempting to just parse value as a string if it fails - json!(message) - } - }; - - let res = client.proxy_endpoint_json(serial, &path, 0, msg).await; - - match res { - Ok(v) => println!("Response: '{v}'"), - Err(e) => println!("Error: '{e}'"), - } - - Ok(()) -} - -async fn device_publish( - client: PoststationClient, - serial: String, - path: String, - message: String, -) -> anyhow::Result<()> { - let serial = u64::from_str_radix(&serial, 16)?; - let msg = message.parse()?; - - let res = client.publish_topic_json(serial, &path, 0, msg).await; - - match res { - Ok(()) => println!("Published."), - Err(e) => println!("Error: '{e}'"), - } - - Ok(()) -} - -async fn device_cmds(client: PoststationClient, device: &Device) -> anyhow::Result<()> { - let serial = guess_serial(device.serial.as_deref(), &client).await?; - let schema = client - .get_device_schemas(serial) - .await - .expect("expected to get schemas for device") - .expect("expected device to have known schemas"); - match &device.command { - DeviceCommands::Types => { - println!(); - println!("Types used by device {}", serial); - println!(); - - let base = SchemaReport::default(); - let uniq_tys = schema.types.difference(&base.types); - - for ty in uniq_tys { - println!("* {ty}"); - } - println!(); - Ok(()) - } - DeviceCommands::Endpoints => { - println!(); - println!("Endpoints offered by device {}", serial); - println!(); - - for ep in schema.endpoints { - print_endpoint(&ep); - } - println!(); - Ok(()) - } - DeviceCommands::TopicsOut => { - println!(); - println!("Topics offered by device {}", serial); - println!(); - - for tp in schema.topics_out { - println!("* '{}' => Channel<{}>", tp.path, tp.ty.name); - } - println!(); - Ok(()) - } - DeviceCommands::TopicsIn => { - println!(); - println!("Topics handled by device {}", serial); - println!(); - - for tp in schema.topics_in { - println!("* '{}' => Channel<{}>", tp.path, tp.ty.name); - } - println!(); - Ok(()) - } - DeviceCommands::Logs { count } => { - let count = count.unwrap_or(8); - let logs = client - .get_device_logs(serial, count) - .await - .expect("expected to be able to get logs for device") - .expect("expected device to have known logs"); - - println!(); - println!("Logs (last {} messages):", count.min(logs.len() as u32)); - println!(); - for log in logs { - // println!("* {} => {}", log.uuidv7.id_to_time().time(), log.msg); - let time = log.uuidv7.id_to_time().time(); - println!( - "* {} ({}) => {}", - uuid::Uuid::from(log.uuidv7), - time, - log.msg - ); - } - println!(); - Ok(()) - } - DeviceCommands::LogsRange { - count, - start, - direction, - } => { - let count = count.unwrap_or(8); - let start = start.parse::()?; - let dir = match direction.to_lowercase().as_str() { - "after" => Direction::After, - "before" => Direction::Before, - _ => bail!("Should provide 'after' or 'before' for direction"), - }; - - let logs = client - .get_device_logs_range( - serial, - count, - dir, - poststation_api_icd::postsock::Anchor::Uuid(start.into()), - ) - .await - .expect("expected to be able to get log range for device") - .expect("expected device to have known logs"); - - println!(); - println!("Logs (last {} messages):", count.min(logs.len() as u32)); - println!(); - for log in logs { - // println!("* {} => {}", log.uuidv7.id_to_time().time(), log.msg); - let time = log.uuidv7.id_to_time().time(); - println!( - "* {} ({}) => {}", - uuid::Uuid::from(log.uuidv7), - time, - log.msg - ); - } - println!(); - Ok(()) - } - DeviceCommands::SmartProxy { command, message } => { - let matches = schema - .endpoints - .iter() - .filter(|e| e.path.contains(command)) - .collect::>(); - if matches.is_empty() { - bail!("No endpoint found matching '{command}'"); - } else if matches.len() > 1 { - println!("Given '{command}', found:"); - println!(); - for matched_endpoint in matches { - print_endpoint(matched_endpoint); - } - println!(); - bail!("Too many matches, be more specific!"); - } else { - let ep = matches[0]; - if ep.req_ty.ty == OwnedDataModelType::Unit { - device_proxy(client, serial, ep.path.clone(), "".to_string()).await?; - return Ok(()); - } - if let Some(message) = message { - device_proxy(client, serial, ep.path.clone(), message.to_owned()).await?; - } else { - bail!("Endpoint '{}' requires a message to be sent of the type: async fn({}) -> {}", ep.path, ep.req_ty.name, ep.resp_ty.name); - } - } - Ok(()) } } } @@ -477,7 +120,9 @@ async fn guess_serial(serial: Option<&str>, client: &PoststationClient) -> anyho std::env::var("POSTSTATION_SERIAL"); match serial_from_env { Ok(serial) => serial.to_uppercase(), - Err(_) => bail!("No serial provided and no POSTSTATION_SERIAL env var found"), + Err(_) => bail!( + "No serial provided and no POSTSTATION_SERIAL env var found.\nHELP: Try `poststation-cli device SERIAL COMMAND`" + ), } } }; @@ -531,6 +176,10 @@ async fn guess_serial(serial: Option<&str>, client: &PoststationClient) -> anyho Ok(serial_num) } +fn print_topic(tp: &TopicReport) { + println!("* '{}' => Channel<{}>", tp.path, tp.ty.name); +} + fn print_endpoint(ep: &EndpointReport) { if ep.resp_ty.ty == OwnedDataModelType::Unit { println!("* '{}' => async fn({})", ep.path, ep.req_ty.name);