umshctl/command/
phy.rs

1//! `phy`: report or set the radio's enable state and LoRa parameters.
2//!
3//! The PHY must be enabled before the radio can receive, forward, or
4//! transmit — so an autonomous node or repeater needs `phy on`.
5
6use anyhow::Result;
7
8use umsh::ulcp::{FrameLink, UlcpDevice};
9use umsh::ulcp_wire::ids::prop;
10
11use super::{decode_u32, persist};
12use crate::App;
13use crate::output::field;
14
15#[derive(Debug, clap::Subcommand)]
16pub enum PhyOp {
17    /// Print the enable state and LoRa parameters.
18    Show,
19    /// Enable the radio.
20    On,
21    /// Disable the radio.
22    Off,
23    /// Set the frequency in kHz.
24    Freq {
25        #[arg(value_name = "KHZ")]
26        khz: u32,
27    },
28    /// Set the LoRa spreading factor.
29    Sf {
30        #[arg(value_parser = clap::value_parser!(u8).range(5..=12))]
31        sf: u8,
32    },
33    /// Set the LoRa bandwidth in Hz.
34    Bw {
35        #[arg(value_name = "HZ")]
36        hz: u32,
37    },
38    /// Set the LoRa coding-rate denominator (4/N).
39    Cr {
40        #[arg(value_parser = clap::value_parser!(u8).range(5..=8))]
41        cr: u8,
42    },
43    /// Set the transmit power in dBm.
44    Power {
45        #[arg(value_name = "DBM", allow_hyphen_values = true)]
46        dbm: i8,
47    },
48}
49
50pub async fn run(app: &mut App, op: Option<PhyOp>) -> Result<()> {
51    let no_save = app.no_save;
52    let device = app.device()?;
53    match op.unwrap_or(PhyOp::Show) {
54        PhyOp::Show => return report(device).await,
55        PhyOp::On => set_enabled(device, true).await?,
56        PhyOp::Off => set_enabled(device, false).await?,
57        PhyOp::Freq { khz } => {
58            let echoed = device.set_prop(prop::PHY_FREQ, &khz.to_le_bytes()).await?;
59            println!("phy freq {} kHz", decode_u32(&echoed).unwrap_or(khz));
60        }
61        PhyOp::Sf { sf } => {
62            let echoed = device.set_prop(prop::PHY_LORA_SF, &[sf]).await?;
63            println!("phy SF{}", echoed.first().copied().unwrap_or(sf));
64        }
65        PhyOp::Bw { hz } => {
66            let echoed = device
67                .set_prop(prop::PHY_LORA_BW, &hz.to_le_bytes())
68                .await?;
69            println!("phy BW {} Hz", decode_u32(&echoed).unwrap_or(hz));
70        }
71        PhyOp::Cr { cr } => {
72            let echoed = device.set_prop(prop::PHY_LORA_CR, &[cr]).await?;
73            println!("phy CR 4/{}", echoed.first().copied().unwrap_or(cr));
74        }
75        PhyOp::Power { dbm } => {
76            let echoed = device.set_prop(prop::PHY_TX_POWER, &[dbm as u8]).await?;
77            let dbm = echoed.first().copied().map_or(dbm, |byte| byte as i8);
78            println!("phy TX {dbm} dBm");
79        }
80    }
81    persist(device, no_save).await
82}
83
84async fn set_enabled<L: FrameLink>(device: &mut UlcpDevice<L>, on: bool) -> Result<()> {
85    let echoed = device.set_prop(prop::PHY_ENABLED, &[on as u8]).await?;
86    let on = echoed.first().copied().unwrap_or(on as u8) != 0;
87    println!("phy {}", if on { "enabled" } else { "disabled" });
88    Ok(())
89}
90
91/// Print the current PHY enable state and LoRa parameters on one line.
92pub async fn report<L: FrameLink>(device: &mut UlcpDevice<L>) -> Result<()> {
93    let enabled = device
94        .get_prop(prop::PHY_ENABLED)
95        .await?
96        .first()
97        .copied()
98        .unwrap_or(0)
99        != 0;
100    let mut parts = vec![if enabled { "enabled" } else { "disabled" }.to_string()];
101    if let Some(freq) = device
102        .get_prop(prop::PHY_FREQ)
103        .await
104        .ok()
105        .and_then(|value| decode_u32(&value))
106    {
107        parts.push(format!("{freq} kHz"));
108    }
109    parts.extend(lora_parts(device).await);
110    parts.extend(power_part(device).await);
111    field("phy", parts.join(", "));
112    Ok(())
113}
114
115/// The LoRa modulation parameters, each omitted when the device will not
116/// report it.
117pub async fn lora_parts<L: FrameLink>(device: &mut UlcpDevice<L>) -> Vec<String> {
118    let mut parts = Vec::new();
119    if let Some(bw) = device
120        .get_prop(prop::PHY_LORA_BW)
121        .await
122        .ok()
123        .and_then(|value| decode_u32(&value))
124    {
125        parts.push(format!("BW {bw} Hz"));
126    }
127    if let Some(sf) = device
128        .get_prop(prop::PHY_LORA_SF)
129        .await
130        .ok()
131        .and_then(|value| value.first().copied())
132    {
133        parts.push(format!("SF{sf}"));
134    }
135    if let Some(cr) = device
136        .get_prop(prop::PHY_LORA_CR)
137        .await
138        .ok()
139        .and_then(|value| value.first().copied())
140    {
141        parts.push(format!("CR 4/{cr}"));
142    }
143    if let Some(sw) = device
144        .get_prop(prop::PHY_LORA_SW)
145        .await
146        .ok()
147        .and_then(|value| <[u8; 2]>::try_from(value.as_slice()).ok())
148        .map(u16::from_le_bytes)
149    {
150        parts.push(format!("sync 0x{sw:04x}"));
151    }
152    parts
153}
154
155pub async fn power_part<L: FrameLink>(device: &mut UlcpDevice<L>) -> Option<String> {
156    device
157        .get_prop(prop::PHY_TX_POWER)
158        .await
159        .ok()
160        .and_then(|value| value.first().copied())
161        .map(|power| format!("TX {} dBm", power as i8))
162}