From fd41bb8adc06fec5ddcedb2c145ef0c4ca96c134 Mon Sep 17 00:00:00 2001 From: frectonz Date: Mon, 31 Aug 2026 13:47:52 +0300 Subject: [PATCH] feat: implement `show_json` --- src/common.rs | 12 ++- src/multi_get.rs | 141 +++++++++++++++++++++++++++++- src/proc_get.rs | 223 +++++++++++++++++++++++++++++++++++++++++++++-- src/proc_post.rs | 33 +++++++ src/types.rs | 60 +++++++++++-- 5 files changed, 449 insertions(+), 20 deletions(-) diff --git a/src/common.rs b/src/common.rs index 208b0ef..167ecc2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -8,11 +8,12 @@ pub type EyreResult = color_eyre::Result; pub trait Show { fn show(&self, format: &Format) -> EyreResult<()> { match format { - Format::Json => todo!(), + Format::Json => self.show_json(), Format::Table => self.show_table(), } } + fn show_json(&self) -> EyreResult<()>; fn show_table(&self) -> EyreResult<()>; } @@ -129,11 +130,20 @@ pub fn print_framed(text: &str) { println!("{rule}\n{text}\n{rule}"); } +pub fn print_json(value: &T) -> EyreResult<()> { + println!("{}", serde_json::to_string(value)?); + Ok(()) +} + /// Substitute a dash for fields the router reports as empty. pub const fn or_dash(value: &str) -> &str { if value.is_empty() { "—" } else { value } } +pub const fn or_none(value: &str) -> Option<&str> { + if value.is_empty() { None } else { Some(value) } +} + pub const fn yes_no(value: bool) -> &'static str { if value { "Yes" } else { "No" } } diff --git a/src/multi_get.rs b/src/multi_get.rs index 82c339f..a4fdef4 100644 --- a/src/multi_get.rs +++ b/src/multi_get.rs @@ -1,7 +1,7 @@ use crate::common::*; use crate::proc_get::WanDetails; use crate::types::*; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; /// A read from `/reqproc/proc_get` that batches several `cmd`s in one /// request via `multi_data=1`. @@ -70,6 +70,33 @@ impl ProcGetMulti for Info { } impl Show for Info { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct InfoJson<'a> { + phone_number: Option<&'a str>, + network_type: &'a str, + operator: Option<&'a str>, + plmn: &'a str, + signal_bars: u8, + internet: &'a PppStatus, + unread_sms: usize, + battery_percent: u8, + connected_devices: usize, + } + + print_json(&InfoJson { + phone_number: self.phone_number(), + network_type: &self.network_type, + operator: or_none(&self.sim_spn), + plmn: &self.sim_plmn, + signal_bars: self.signalbar, + internet: &self.ppp_status, + unread_sms: self.sms_unread_num, + battery_percent: self.battery_percentage, + connected_devices: self.sta_count, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); @@ -114,6 +141,29 @@ impl ProcGetMulti for System { } impl Show for System { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct SystemJson { + memory_total_bytes: Bytes, + memory_free_bytes: Bytes, + memory_cached_bytes: Bytes, + memory_active_bytes: Bytes, + cpu_usage_percent: Percent, + flash_used_mb: MegaBytes, + flash_total_mb: MegaBytes, + } + + print_json(&SystemJson { + memory_total_bytes: self.mem_total.into(), + memory_free_bytes: self.mem_free.into(), + memory_cached_bytes: self.mem_cached.into(), + memory_active_bytes: self.mem_active.into(), + cpu_usage_percent: self.tz_cpu_usage, + flash_used_mb: self.tz_flash_use, + flash_total_mb: self.tz_flash_total, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); @@ -174,6 +224,45 @@ pub struct InternetReport { } impl Show for InternetReport { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct InternetJson<'a> { + status: &'a PppStatus, + wan_ip: Option<&'a str>, + gateway: Option<&'a str>, + netmask: Option<&'a str>, + dns: &'a DnsServers, + lan_domain: &'a str, + imsi: &'a str, + download_bytes_per_second: BytesPerSecond, + upload_bytes_per_second: BytesPerSecond, + realtime_rx_bytes: Bytes, + realtime_tx_bytes: Bytes, + monthly_rx_bytes: Bytes, + monthly_tx_bytes: Bytes, + monthly_online_seconds: Seconds, + } + + let internet = &self.internet; + + print_json(&InternetJson { + status: &internet.ppp_status, + wan_ip: or_none(&internet.wan_ipaddr), + gateway: or_none(&self.wan.gateway), + netmask: or_none(&self.wan.netmask), + dns: &self.wan.dns, + lan_domain: &internet.lan_domain, + imsi: &internet.sim_imsi, + download_bytes_per_second: internet.realtime_rx_thrpt, + upload_bytes_per_second: internet.realtime_tx_thrpt, + realtime_rx_bytes: internet.realtime_rx_bytes, + realtime_tx_bytes: internet.realtime_tx_bytes, + monthly_rx_bytes: internet.monthly_rx_bytes, + monthly_tx_bytes: internet.monthly_tx_bytes, + monthly_online_seconds: internet.monthly_time, + }) + } + fn show_table(&self) -> EyreResult<()> { let internet = &self.internet; @@ -262,6 +351,39 @@ impl ProcGetMulti for Wifi { } impl Show for Wifi { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct WifiJson<'a> { + ssid: &'a str, + ssid_visible: bool, + mac_address: &'a str, + max_stations: u8, + channel: u8, + channel_bandwidth: ChannelBandwidth, + auth_mode: &'a str, + password: &'a str, + guest_ssid: Option<&'a str>, + guest_ssid_enabled: bool, + dhcp_start: &'a str, + dhcp_end: &'a str, + } + + print_json(&WifiJson { + ssid: &self.ssid, + ssid_visible: !self.ssid_hidden, + mac_address: &self.wifi_mac, + max_stations: self.max_station_num, + channel: self.channel, + channel_bandwidth: self.channel_bandwidth, + auth_mode: &self.auth_mode, + password: &self.password, + guest_ssid: or_none(&self.guest_ssid), + guest_ssid_enabled: self.guest_ssid_enabled, + dhcp_start: &self.dhcp_start, + dhcp_end: &self.dhcp_end, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); @@ -307,6 +429,23 @@ impl ProcGetMulti for Power { } impl Show for Power { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct PowerJson { + battery_present: bool, + battery_percent: u8, + battery_power: u8, + connected_to_power: bool, + } + + print_json(&PowerJson { + battery_present: self.battery_exist, + battery_percent: self.battery_percentage, + battery_power: self.battery_value, + connected_to_power: self.power_exist, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); diff --git a/src/proc_get.rs b/src/proc_get.rs index bae149b..6e51dd3 100644 --- a/src/proc_get.rs +++ b/src/proc_get.rs @@ -41,6 +41,32 @@ impl ProcGet for StationList { } impl Show for StationList { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct ConnectedDeviceJson<'a> { + hostname: Option<&'a str>, + ip_address: &'a str, + mac_address: &'a str, + #[serde(rename = "type")] + dev_type: &'a str, + ip_type: &'a str, + } + + let devices: BoxList = self + .station_list + .iter() + .map(|device| ConnectedDeviceJson { + hostname: or_none(&device.hostname), + ip_address: &device.ip_addr, + mac_address: &device.mac_addr, + dev_type: &device.dev_type, + ip_type: &device.ip_type, + }) + .collect(); + + print_json(&devices) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); table.set_header(["Hostname", "IP Address", "MAC Address", "Type", "IP Type"]); @@ -104,6 +130,31 @@ impl ProcGet for SmsInbox { } impl Show for SmsInbox { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct MessageJson<'a> { + id: usize, + number: &'a str, + status: &'a MessageStatus, + date: &'a Datetime, + content: &'a str, + } + + let messages: BoxList = self + .messages + .iter() + .map(|message| MessageJson { + id: message.id, + number: &message.number, + status: &message.tag, + date: &message.date, + content: message.content.trim(), + }) + .collect(); + + print_json(&messages) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); table.set_header(["ID", "Number", "Content", "Status", "Date"]); @@ -125,6 +176,25 @@ impl Show for SmsInbox { } impl Show for Message { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct MessageJson<'a> { + id: usize, + number: &'a str, + status: &'a MessageStatus, + date: &'a Datetime, + content: &'a str, + } + + print_json(&MessageJson { + id: self.id, + number: &self.number, + status: &self.tag, + date: &self.date, + content: self.content.trim(), + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); @@ -185,6 +255,28 @@ pub struct SmsSettings { } impl Show for SmsSettings { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct SmsSettingsJson<'a> { + messages_used: usize, + messages_total: usize, + smsc: Option<&'a str>, + delivery_reports: bool, + default_store: &'a SmsStore, + } + + let capacity = &self.capacity; + let parameters = &self.parameters; + + print_json(&SmsSettingsJson { + messages_used: capacity.used(), + messages_total: capacity.sms_nv_total, + smsc: or_none(¶meters.sms_para_sca), + delivery_reports: parameters.sms_para_status_report, + default_store: ¶meters.default_store, + }) + } + fn show_table(&self) -> EyreResult<()> { let capacity = &self.capacity; let parameters = &self.parameters; @@ -219,18 +311,25 @@ pub struct AirtimeBalance { } impl AirtimeBalance { + fn amount(&self) -> Option<&str> { + let after = self.airtime_balance.split_once("Br.")?.1.trim_start(); + + let end = after + .find(|c: char| !(c.is_ascii_digit() || c == ',' || c == '.')) + .unwrap_or(after.len()); + let amount = after[..end].trim_end_matches('.'); + + (!amount.is_empty()).then_some(amount) + } + /// Pull the money amount ("Br. 2,247.50") out of the USSD text. fn balance(&self) -> Option { - let after = self.airtime_balance.split_once("Br.")?.1; - - let amount: BoxStr = after - .trim_start() - .chars() - .take_while(|c| c.is_ascii_digit() || *c == ',' || *c == '.') - .collect(); - let amount = amount.trim_end_matches('.'); + self.amount() + .map(|amount| format!("Br. {amount}").into_boxed_str()) + } - (!amount.is_empty()).then(|| format!("Br. {amount}").into_boxed_str()) + fn balance_birr(&self) -> Option { + self.amount()?.replace(',', "").parse().ok() } } @@ -240,6 +339,19 @@ impl ProcGet for AirtimeBalance { } impl Show for AirtimeBalance { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct AirtimeBalanceJson<'a> { + balance_birr: Option, + text: &'a str, + } + + print_json(&AirtimeBalanceJson { + balance_birr: self.balance_birr(), + text: self.airtime_balance.trim(), + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); table.set_header(["Airtime Balance"]); @@ -352,6 +464,43 @@ pub struct SignalReport { } impl Show for SignalReport { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct SignalJson<'a> { + rsrp_dbm: Dbm, + rsrq_db: Decibels, + rssi_dbm: Dbm, + sinr_db: Decibels, + band: Option<&'a str>, + earfcn: Option<&'a str>, + tac: Option<&'a str>, + cell_id: Option<&'a str>, + enodeb_id: Option<&'a str>, + full_cell_id: Option<&'a str>, + pci: Option<&'a str>, + mcs: Option<&'a str>, + cqi: Option<&'a str>, + } + + let signal = &self.signal; + + print_json(&SignalJson { + rsrp_dbm: signal.rsrp, + rsrq_db: signal.rsrq, + rssi_dbm: signal.rssi, + sinr_db: signal.sinr, + band: or_none(&signal.band), + earfcn: or_none(&self.cell.nv_arfcn), + tac: or_none(&self.cell.lte_tac), + cell_id: or_none(&signal.cell_id), + enodeb_id: or_none(&signal.enode_id), + full_cell_id: or_none(&signal.full_cell_id), + pci: or_none(&signal.phy_cell_id), + mcs: or_none(&signal.mcs), + cqi: or_none(&signal.cqi), + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); @@ -426,6 +575,35 @@ pub struct DeviceReport { } impl Show for DeviceReport { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct DeviceJson<'a> { + firmware_version: &'a str, + build_version: &'a str, + build_time: &'a BuildTime, + platform: &'a str, + serial_number: &'a str, + imei: &'a str, + ethernet_mac: &'a str, + sim_iccid: Option<&'a str>, + uptime_seconds: Seconds, + } + + let device = &self.device; + + print_json(&DeviceJson { + firmware_version: &device.device_version, + build_version: &device.real_device_version, + build_time: &device.build_time, + platform: &device.platform_version, + serial_number: &device.sn, + imei: &device.imei, + ethernet_mac: &device.eth0_mac, + sim_iccid: or_none(&self.sim.ziccid), + uptime_seconds: device.online_time, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); @@ -508,6 +686,33 @@ pub struct ApnReport { } impl Show for ApnReport { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct ApnJson<'a> { + mode: &'a str, + profile: Option<&'a str>, + apn: Option<&'a str>, + pdp_type: Option<&'a str>, + auth_mode: Option<&'a str>, + username: Option<&'a str>, + password: Option<&'a str>, + dns_mode: Option<&'a str>, + } + + let profile = &self.profile; + + print_json(&ApnJson { + mode: &self.status.apn_mode, + profile: or_none(&profile.profile_name), + apn: or_none(&profile.apn), + pdp_type: or_none(&profile.pdp_type), + auth_mode: or_none(&profile.auth_mode), + username: or_none(&profile.username), + password: or_none(&profile.password), + dns_mode: or_none(&profile.dns_mode), + }) + } + fn show_table(&self) -> EyreResult<()> { let profile = &self.profile; diff --git a/src/proc_post.rs b/src/proc_post.rs index 20ee960..380e939 100644 --- a/src/proc_post.rs +++ b/src/proc_post.rs @@ -91,6 +91,17 @@ impl ProcPost for DeleteSms { } impl Show for DeleteSms { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct DeleteSmsJson<'a> { + result: &'a str, + } + + print_json(&DeleteSmsJson { + result: &self.result, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); table.set_header(["Delete Result"]).add_row([&self.result]); @@ -140,6 +151,17 @@ impl ProcPost for SendSms { } impl Show for SendSms { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct SendSmsJson<'a> { + result: &'a str, + } + + print_json(&SendSmsJson { + result: &self.result, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); table.set_header(["Send Result"]).add_row([&self.result]); @@ -223,6 +245,17 @@ impl ProcPost for MarkSms { } impl Show for MarkSms { + fn show_json(&self) -> EyreResult<()> { + #[derive(Serialize)] + struct MarkSmsJson<'a> { + result: &'a str, + } + + print_json(&MarkSmsJson { + result: &self.result, + }) + } + fn show_table(&self) -> EyreResult<()> { let mut table = create_table(); table.set_header(["Mark Result"]).add_row([&self.result]); diff --git a/src/types.rs b/src/types.rs index 93587c2..096ef21 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,7 +2,7 @@ //! level, so the rest of the code works with real Rust types. use crate::common::*; -use serde::{Deserialize, Deserializer, de::Error as _}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _}; use std::net::IpAddr; /// `deserialize_with` helpers for fields that don't warrant a dedicated type. @@ -74,7 +74,7 @@ where } /// A byte counter transmitted as a decimal string ("1237026476"). -#[derive(Debug, Clone, Copy, Deserialize)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct Bytes(#[serde(deserialize_with = "de::from_str")] pub u64); impl std::fmt::Display for Bytes { @@ -84,7 +84,7 @@ impl std::fmt::Display for Bytes { } /// A live throughput in bytes per second ("2339"). -#[derive(Debug, Clone, Copy, Deserialize)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct BytesPerSecond(#[serde(deserialize_with = "de::from_str")] pub u64); impl std::fmt::Display for BytesPerSecond { @@ -123,8 +123,14 @@ impl std::fmt::Display for KibiBytes { } } +impl From for Bytes { + fn from(value: KibiBytes) -> Self { + Self(value.0 * 1024) + } +} + /// A megabyte quantity transmitted as a bare decimal string ("36.15"). -#[derive(Debug, Clone, Copy, Deserialize)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct MegaBytes(#[serde(deserialize_with = "de::from_str")] pub f64); impl std::fmt::Display for MegaBytes { @@ -134,7 +140,7 @@ impl std::fmt::Display for MegaBytes { } /// A percentage like "24.78%". -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize)] pub struct Percent(pub f64); impl<'de> Deserialize<'de> for Percent { @@ -155,7 +161,7 @@ impl std::fmt::Display for Percent { } /// A duration in seconds ("10476" → "2h 54m 36s"). -#[derive(Debug, Clone, Copy, Deserialize)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct Seconds(#[serde(deserialize_with = "de::from_str")] pub u64); impl std::fmt::Display for Seconds { @@ -183,7 +189,7 @@ impl std::fmt::Display for Seconds { } /// A signal level in dBm; empty when the metric doesn't apply. -#[derive(Debug, Clone, Copy, Deserialize)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct Dbm(#[serde(deserialize_with = "optional_f64")] pub Option); impl std::fmt::Display for Dbm { @@ -196,7 +202,7 @@ impl std::fmt::Display for Dbm { } /// A signal ratio in dB; empty when the metric doesn't apply. -#[derive(Debug, Clone, Copy, Deserialize)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] pub struct Decibels(#[serde(deserialize_with = "optional_f64")] pub Option); impl std::fmt::Display for Decibels { @@ -209,7 +215,7 @@ impl std::fmt::Display for Decibels { } /// A comma-separated resolver list ("10.44.137.4,10.43.137.4"). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct DnsServers(pub BoxList); impl<'de> Deserialize<'de> for DnsServers { @@ -274,6 +280,12 @@ impl std::fmt::Display for PppStatus { } } +impl Serialize for PppStatus { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(self) + } +} + /// Wi-Fi channel bandwidth, derived from `wifi_11n_cap` the same way the /// router's own web UI does. #[derive(Debug, Clone, Copy)] @@ -304,6 +316,12 @@ impl std::fmt::Display for ChannelBandwidth { } } +impl Serialize for ChannelBandwidth { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(self) + } +} + /// SMS state (`tag` field of an inbox message). #[derive(Debug, Clone, PartialEq, Eq)] pub enum MessageStatus { @@ -336,6 +354,12 @@ impl std::fmt::Display for MessageStatus { } } +impl Serialize for MessageStatus { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(self) + } +} + /// Where the router saves new SMS (`default_store`): "nv" (device) or "sim". #[derive(Debug, Clone)] pub enum SmsStore { @@ -365,6 +389,12 @@ impl std::fmt::Display for SmsStore { } } +impl Serialize for SmsStore { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(self) + } +} + /// The firmware build stamp in "YYYY-MM-DD_HH:MM" format ("2025-02-19_13:59"). #[derive(Debug, Clone)] pub struct BuildTime(jiff::civil::DateTime); @@ -384,6 +414,12 @@ impl std::fmt::Display for BuildTime { } } +impl Serialize for BuildTime { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(&self.0) + } +} + /// A timestamp in the router's "YY,MM,DD,HH,MM,SS,+TZ" format. #[derive(Debug, Clone)] pub struct Datetime { @@ -428,3 +464,9 @@ impl std::fmt::Display for Datetime { write!(f, "{datetime}") } } + +impl Serialize for Datetime { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(&self.datetime.strftime("%FT%T%:z")) + } +} -- 2.51.2