tools: share RNode capability inspection

This commit is contained in:
DeFiDude 2026-07-25 01:28:53 -06:00
parent b1854b62aa
commit 4e1abca4ea
2 changed files with 228 additions and 489 deletions

View file

@ -12,7 +12,13 @@ use std::process::ExitCode;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use clap::{CommandFactory, Parser};
use rns_interface::{kiss, rnode, rnode_admin};
use rns_interface::{
kiss, rnode, rnode_admin,
rnode_capabilities::{
RNodeCapabilities, RNodeCapabilityParseError, RNodeKnownRadioCapabilities,
RNodeRadioCapabilities, RNodeRadioFamily, parse_rnode_capabilities, rnode_product_name,
},
};
use rns_tools::RS_RETICULUM_VERSION;
// rnodeconf support is staged: safe inspection/config modules are compiled now,
@ -528,36 +534,98 @@ fn print_responses(
}
fn eeprom_summary_lines(image: &eeprom::EepromImage) -> Vec<String> {
if !image.info_locked() {
return vec!["EEPROM: not provisioned or info lock missing".to_string()];
}
let identity = image.identity();
let product = model::product_name(identity.product).unwrap_or("Unknown product");
let model_info = model::model_info(identity.model);
let model_label = model_info
.map(|info| info.band_label)
.unwrap_or("unknown band");
let radio_label = model_info.map(|info| info.radio).unwrap_or("unknown radio");
let checksum_status = if image.checksum_valid() {
let capabilities = match parse_rnode_capabilities(image.bytes()) {
Ok(capabilities) => capabilities,
Err(RNodeCapabilityParseError::InfoNotLocked) => {
return vec!["EEPROM: not provisioned or info lock missing".to_string()];
}
Err(error) => {
let mut lines = private_eeprom_identity_lines(&identity, false);
lines.push(format!(" Radio capabilities: unavailable ({error})"));
append_startup_config_lines(image, &mut lines);
return lines;
}
};
let mut lines = private_eeprom_identity_lines(&identity, true);
append_capability_lines(capabilities, &mut lines);
append_startup_config_lines(image, &mut lines);
lines
}
fn private_eeprom_identity_lines(
identity: &eeprom::IdentityInfo,
identity_validated: bool,
) -> Vec<String> {
let checksum_status = if identity_validated {
"valid"
} else {
"invalid"
};
let product = if identity_validated {
format!(" Product code: 0x{:02x}", identity.product)
} else {
format!(" Product code: 0x{:02x} (unvalidated)", identity.product)
};
let model = if identity_validated {
format!(" Model: 0x{:02x}", identity.model)
} else {
format!(" Model code: 0x{:02x} (unvalidated)", identity.model)
};
let mut lines = vec![
vec![
"EEPROM: provisioned".to_string(),
format!(" Product: {product} (0x{:02x})", identity.product),
format!(
" Model: 0x{:02x} ({model_label}, {radio_label})",
identity.model
),
product,
model,
format!(" Hardware revision: {}", identity.hw_rev),
format!(" Serial: {}", hex::encode(identity.serial.to_be_bytes())),
format!(" Manufactured: {}", identity.made),
format!(" Identity checksum: {checksum_status}"),
];
]
}
fn append_capability_lines(capabilities: RNodeCapabilities, lines: &mut Vec<String>) {
let product = rnode_product_name(capabilities.product_code()).unwrap_or("Unknown product");
lines[1] = format!(
" Product: {product} (0x{:02x})",
capabilities.product_code()
);
lines[2] = format!(" Model: 0x{:02x}", capabilities.model_code());
match capabilities.radio() {
RNodeRadioCapabilities::Known(radio) => append_known_radio_lines(radio, lines),
_ => {
lines.push(" Radio capabilities: unavailable for this model".to_string());
}
}
}
fn append_known_radio_lines(radio: RNodeKnownRadioCapabilities, lines: &mut Vec<String>) {
lines.extend([
format!(" Radio family: {}", radio_family_name(radio.family())),
format!(
" Frequency range: {} - {} Hz",
radio.min_frequency_hz(),
radio.max_frequency_hz()
),
format!(" Maximum TX power: {} dBm", radio.max_tx_power_dbm()),
]);
}
fn radio_family_name(family: RNodeRadioFamily) -> &'static str {
match family {
RNodeRadioFamily::Sx1262 => "SX1262",
RNodeRadioFamily::Sx1268 => "SX1268",
RNodeRadioFamily::Sx1276 => "SX1276",
RNodeRadioFamily::Sx1278 => "SX1278",
RNodeRadioFamily::Sx1280 => "SX1280",
RNodeRadioFamily::Sx1262AndSx1280 => "SX1262 + SX1280",
_ => "Unrecognized",
}
}
fn append_startup_config_lines(image: &eeprom::EepromImage, lines: &mut Vec<String>) {
if let Some(radio) = image.radio_config() {
let bitrate =
rnode::calculate_bitrate(radio.spreading_factor, radio.coding_rate, radio.bandwidth);
@ -573,8 +641,6 @@ fn eeprom_summary_lines(image: &eeprom::EepromImage) -> Vec<String> {
} else {
lines.push(" Startup mode: Normal (host-controlled)".to_string());
}
lines
}
fn rnodeconf_cache_paths() -> Result<firmware::CachePaths, String> {
@ -749,6 +815,26 @@ mod tests {
);
}
fn provisioned_eeprom(model: u8, radio: Option<&eeprom::RadioConfig>) -> eeprom::EepromImage {
let identity = eeprom::IdentityInfo {
product: model::PRODUCT_RNODE,
model,
hw_rev: 2,
serial: 0x01020304,
made: 0x6553F100,
};
let mut bytes = vec![0xFF; eeprom::EEPROM_SIZE];
let mut writes = eeprom::identity_write_frames(&identity);
if let Some(radio) = radio {
writes.extend(eeprom::radio_config_write_frames(radio));
}
for frame in writes {
let address = frame.payload[0] as usize;
bytes[address] = frame.payload[1];
}
eeprom::EepromImage::new(bytes).unwrap()
}
#[test]
fn info_sequence_matches_upstream_probe_order() {
assert_eq!(
@ -999,14 +1085,7 @@ mod tests {
}
#[test]
fn eeprom_summary_reports_identity_and_radio_config() {
let info = eeprom::IdentityInfo {
product: model::PRODUCT_RNODE,
model: 0xB4,
hw_rev: 2,
serial: 0x01020304,
made: 0x6553F100,
};
fn eeprom_summary_reports_validated_identity_capabilities_and_radio_config() {
let radio = eeprom::RadioConfig {
spreading_factor: 7,
coding_rate: 5,
@ -1014,28 +1093,85 @@ mod tests {
bandwidth: 125000,
frequency: 868000000,
};
let mut bytes = vec![0xFF; eeprom::EEPROM_SIZE];
for frame in eeprom::identity_write_frames(&info)
.into_iter()
.chain(eeprom::radio_config_write_frames(&radio))
{
let address = frame.payload[0] as usize;
bytes[address] = frame.payload[1];
}
let image = eeprom::EepromImage::new(bytes).unwrap();
let image = provisioned_eeprom(0xB4, Some(&radio));
let lines = eeprom_summary_lines(&image);
assert!(lines.iter().any(|line| line == "EEPROM: provisioned"));
assert!(
lines
.iter()
.any(|line| line.contains("Product: RNode (0x03)"))
);
assert!(lines.iter().any(|line| line == " Model: 0xb4"));
assert!(lines.iter().any(|line| line == " Radio family: SX1278"));
assert!(
lines
.iter()
.any(|line| line == " Frequency range: 420000000 - 520000000 Hz")
);
assert!(
lines
.iter()
.any(|line| line == " Maximum TX power: 17 dBm")
);
assert!(lines.iter().any(|line| line.contains("Startup mode: TNC")));
assert!(
lines
.iter()
.any(|line| line.contains("Frequency: 868000000 Hz"))
);
assert!(!lines.iter().any(|line| line.contains("420 - 520 MHz")));
}
#[test]
fn eeprom_summary_quarantines_ambiguous_model_capabilities() {
let image = provisioned_eeprom(0x16, None);
let lines = eeprom_summary_lines(&image);
assert!(lines.iter().any(|line| line == " Model: 0x16"));
assert!(
lines
.iter()
.any(|line| line == " Radio capabilities: unavailable for this model")
);
assert!(!lines.iter().any(|line| line.contains("Radio family:")));
assert!(!lines.iter().any(|line| line.contains("Frequency range:")));
assert!(!lines.iter().any(|line| line.contains("Maximum TX power:")));
}
#[test]
fn eeprom_summary_does_not_trust_an_invalid_identity_checksum() {
let image = provisioned_eeprom(0xB4, None);
let mut bytes = image.bytes().to_vec();
bytes[eeprom::ADDR_CHKSUM] ^= 0x01;
let invalid = eeprom::EepromImage::new(bytes).unwrap();
let lines = eeprom_summary_lines(&invalid);
assert!(
lines
.iter()
.any(|line| line == " Product code: 0x03 (unvalidated)")
);
assert!(
lines
.iter()
.any(|line| line == " Model code: 0xb4 (unvalidated)")
);
assert!(
lines
.iter()
.any(|line| line == " Identity checksum: invalid")
);
assert!(
lines
.iter()
.any(|line| line.contains("Radio capabilities: unavailable"))
);
assert!(lines.iter().any(|line| line == " Serial: 01020304"));
assert!(!lines.iter().any(|line| line.contains("Product: RNode")));
assert!(!lines.iter().any(|line| line.contains("Radio family:")));
assert!(!lines.iter().any(|line| line.contains("Frequency range:")));
assert!(!lines.iter().any(|line| line.contains("Maximum TX power:")));
}
#[test]

View file

@ -1,4 +1,8 @@
//! RNode platform, product and firmware model tables.
//! RNode platform, product and firmware model metadata.
//!
//! Radio families and numeric operating limits belong to
//! `rns_interface::rnode_capabilities`; this tool-local table contains only
//! metadata needed to plan firmware operations.
pub const PLATFORM_AVR: u8 = 0x90;
pub const PLATFORM_ESP32: u8 = 0x80;
@ -32,439 +36,61 @@ pub const MODEL_B9_TCXO: u8 = 0x09;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModelInfo {
pub model: u8,
pub min_frequency: u32,
pub max_frequency: u32,
pub max_tx_power: u8,
pub band_label: &'static str,
pub firmware_filename: Option<&'static str>,
pub radio: &'static str,
}
pub const MODELS: &[ModelInfo] = &[
model(
0xA4,
410000000,
525000000,
14,
"410 - 525 MHz",
Some("rnode_firmware.hex"),
"SX1278",
),
model(
0xA9,
820000000,
1020000000,
17,
"820 - 1020 MHz",
Some("rnode_firmware.hex"),
"SX1276",
),
model(
0xA1,
410000000,
525000000,
22,
"410 - 525 MHz",
Some("rnode_firmware_t3s3.zip"),
"SX1268",
),
model(
0xA6,
820000000,
1020000000,
22,
"820 - 960 MHz",
Some("rnode_firmware_t3s3.zip"),
"SX1262",
),
model(
0xA5,
410000000,
525000000,
17,
"410 - 525 MHz",
Some("rnode_firmware_t3s3_sx127x.zip"),
"SX1278",
),
model(
0xAA,
820000000,
1020000000,
17,
"820 - 960 MHz",
Some("rnode_firmware_t3s3_sx127x.zip"),
"SX1276",
),
model(
0xAC,
2400000000,
2500000000,
20,
"2.4 - 2.5 GHz",
Some("rnode_firmware_t3s3_sx1280_pa.zip"),
"SX1280",
),
model(
0xA2,
410000000,
525000000,
17,
"410 - 525 MHz",
Some("rnode_firmware_ng21.zip"),
"SX1278",
),
model(
0xA7,
820000000,
1020000000,
17,
"820 - 1020 MHz",
Some("rnode_firmware_ng21.zip"),
"SX1276",
),
model(
0xA3,
410000000,
525000000,
17,
"410 - 525 MHz",
Some("rnode_firmware_ng20.zip"),
"SX1278",
),
model(
0xA8,
820000000,
1020000000,
17,
"820 - 1020 MHz",
Some("rnode_firmware_ng20.zip"),
"SX1276",
),
model(
0xB3,
420000000,
520000000,
17,
"420 - 520 MHz",
Some("rnode_firmware_lora32v20.zip"),
"SX1278",
),
model(
0xB8,
850000000,
950000000,
17,
"850 - 950 MHz",
Some("rnode_firmware_lora32v20.zip"),
"SX1276",
),
model(
0xB4,
420000000,
520000000,
17,
"420 - 520 MHz",
Some("rnode_firmware_lora32v21.zip"),
"SX1278",
),
model(
0xB9,
850000000,
950000000,
17,
"850 - 950 MHz",
Some("rnode_firmware_lora32v21.zip"),
"SX1276",
),
model(
MODEL_B4_TCXO,
420000000,
520000000,
17,
"420 - 520 MHz",
Some("rnode_firmware_lora32v21_tcxo.zip"),
"SX1278",
),
model(
MODEL_B9_TCXO,
850000000,
950000000,
17,
"850 - 950 MHz",
Some("rnode_firmware_lora32v21_tcxo.zip"),
"SX1276",
),
model(
0xBA,
420000000,
520000000,
17,
"420 - 520 MHz",
Some("rnode_firmware_lora32v10.zip"),
"SX1278",
),
model(
0xBB,
850000000,
950000000,
17,
"850 - 950 MHz",
Some("rnode_firmware_lora32v10.zip"),
"SX1276",
),
model(
0xC4,
420000000,
520000000,
17,
"420 - 520 MHz",
Some("rnode_firmware_heltec32v2.zip"),
"SX1278",
),
model(
0xC9,
850000000,
950000000,
17,
"850 - 950 MHz",
Some("rnode_firmware_heltec32v2.zip"),
"SX1276",
),
model(
0xC5,
420000000,
520000000,
22,
"420 - 520 MHz",
Some("rnode_firmware_heltec32v3.zip"),
"SX1268",
),
model(
0xCA,
850000000,
950000000,
22,
"850 - 950 MHz",
Some("rnode_firmware_heltec32v3.zip"),
"SX1262",
),
model(
0xC8,
860000000,
930000000,
28,
"850 - 950 MHz",
Some("rnode_firmware_heltec32v4pa.zip"),
"SX1262",
),
model(
0xC6,
420000000,
520000000,
22,
"420 - 520 MHz",
Some("rnode_firmware_heltec_t114.zip"),
"SX1268",
),
model(
0xC7,
850000000,
950000000,
22,
"850 - 950 MHz",
Some("rnode_firmware_heltec_t114.zip"),
"SX1262",
),
model(
0xE4,
420000000,
520000000,
17,
"420 - 520 MHz",
Some("rnode_firmware_tbeam.zip"),
"SX1278",
),
model(
0xE9,
850000000,
950000000,
17,
"850 - 950 MHz",
Some("rnode_firmware_tbeam.zip"),
"SX1276",
),
model(
0xD4,
420000000,
520000000,
22,
"420 - 520 MHz",
Some("rnode_firmware_tdeck.zip"),
"SX1268",
),
model(
0xD9,
850000000,
950000000,
22,
"850 - 950 MHz",
Some("rnode_firmware_tdeck.zip"),
"SX1262",
),
model(
0xDB,
420000000,
520000000,
22,
"420 - 520 MHz",
Some("rnode_firmware_tbeam_supreme.zip"),
"SX1268",
),
model(
0xDC,
850000000,
950000000,
22,
"850 - 950 MHz",
Some("rnode_firmware_tbeam_supreme.zip"),
"SX1262",
),
model(
0xE3,
420000000,
520000000,
22,
"420 - 520 MHz",
Some("rnode_firmware_tbeam_sx1262.zip"),
"SX1268",
),
model(
0xE8,
850000000,
950000000,
22,
"850 - 950 MHz",
Some("rnode_firmware_tbeam_sx1262.zip"),
"SX1262",
),
model(
0x11,
430000000,
510000000,
22,
"430 - 510 MHz",
Some("rnode_firmware_rak4631.zip"),
"SX1262",
),
model(
0x12,
779000000,
928000000,
22,
"779 - 928 MHz",
Some("rnode_firmware_rak4631.zip"),
"SX1262",
),
model(
0x13,
430000000,
510000000,
22,
"430 - 510 MHz",
Some("rnode_firmware_rak4631_sx1280.zip"),
"SX1262 + SX1280",
),
model(
0x14,
779000000,
928000000,
22,
"779 - 928 MHz",
Some("rnode_firmware_rak4631_sx1280.zip"),
"SX1262 + SX1280",
),
model(
0x16,
779000000,
928000000,
22,
"430 - 510 Mhz",
Some("rnode_firmware_techo.zip"),
"SX1262",
),
model(
0x17,
779000000,
928000000,
22,
"779 - 928 Mhz",
Some("rnode_firmware_techo.zip"),
"SX1262",
),
model(
0x21,
820000000,
960000000,
22,
"820 - 960 MHz",
Some("rnode_firmware_opencom_xl.zip"),
"SX1262 + SX1280",
),
model(
0xDE,
420000000,
520000000,
22,
"420 - 520 MHz",
Some("rnode_firmware_xiao_esp32s3.zip"),
"SX1262",
),
model(
0xDD,
850000000,
950000000,
22,
"850 - 950 MHz",
Some("rnode_firmware_xiao_esp32s3.zip"),
"SX1262",
),
model(
0xFE,
100000000,
1100000000,
17,
"(Band capabilities unknown)",
None,
"Unknown",
),
model(
0xFF,
100000000,
1100000000,
14,
"(Band capabilities unknown)",
None,
"Unknown",
),
model(0xA4, Some("rnode_firmware.hex")),
model(0xA9, Some("rnode_firmware.hex")),
model(0xA1, Some("rnode_firmware_t3s3.zip")),
model(0xA6, Some("rnode_firmware_t3s3.zip")),
model(0xA5, Some("rnode_firmware_t3s3_sx127x.zip")),
model(0xAA, Some("rnode_firmware_t3s3_sx127x.zip")),
model(0xAC, Some("rnode_firmware_t3s3_sx1280_pa.zip")),
model(0xA2, Some("rnode_firmware_ng21.zip")),
model(0xA7, Some("rnode_firmware_ng21.zip")),
model(0xA3, Some("rnode_firmware_ng20.zip")),
model(0xA8, Some("rnode_firmware_ng20.zip")),
model(0xB3, Some("rnode_firmware_lora32v20.zip")),
model(0xB8, Some("rnode_firmware_lora32v20.zip")),
model(0xB4, Some("rnode_firmware_lora32v21.zip")),
model(0xB9, Some("rnode_firmware_lora32v21.zip")),
model(MODEL_B4_TCXO, Some("rnode_firmware_lora32v21_tcxo.zip")),
model(MODEL_B9_TCXO, Some("rnode_firmware_lora32v21_tcxo.zip")),
model(0xBA, Some("rnode_firmware_lora32v10.zip")),
model(0xBB, Some("rnode_firmware_lora32v10.zip")),
model(0xC4, Some("rnode_firmware_heltec32v2.zip")),
model(0xC9, Some("rnode_firmware_heltec32v2.zip")),
model(0xC5, Some("rnode_firmware_heltec32v3.zip")),
model(0xCA, Some("rnode_firmware_heltec32v3.zip")),
model(0xC8, Some("rnode_firmware_heltec32v4pa.zip")),
model(0xC6, Some("rnode_firmware_heltec_t114.zip")),
model(0xC7, Some("rnode_firmware_heltec_t114.zip")),
model(0xE4, Some("rnode_firmware_tbeam.zip")),
model(0xE9, Some("rnode_firmware_tbeam.zip")),
model(0xD4, Some("rnode_firmware_tdeck.zip")),
model(0xD9, Some("rnode_firmware_tdeck.zip")),
model(0xDB, Some("rnode_firmware_tbeam_supreme.zip")),
model(0xDC, Some("rnode_firmware_tbeam_supreme.zip")),
model(0xE3, Some("rnode_firmware_tbeam_sx1262.zip")),
model(0xE8, Some("rnode_firmware_tbeam_sx1262.zip")),
model(0x11, Some("rnode_firmware_rak4631.zip")),
model(0x12, Some("rnode_firmware_rak4631.zip")),
model(0x13, Some("rnode_firmware_rak4631_sx1280.zip")),
model(0x14, Some("rnode_firmware_rak4631_sx1280.zip")),
model(0x16, Some("rnode_firmware_techo.zip")),
model(0x17, Some("rnode_firmware_techo.zip")),
model(0x21, Some("rnode_firmware_opencom_xl.zip")),
model(0xDE, Some("rnode_firmware_xiao_esp32s3.zip")),
model(0xDD, Some("rnode_firmware_xiao_esp32s3.zip")),
model(0xFE, None),
model(0xFF, None),
];
const fn model(
model: u8,
min_frequency: u32,
max_frequency: u32,
max_tx_power: u8,
band_label: &'static str,
firmware_filename: Option<&'static str>,
radio: &'static str,
) -> ModelInfo {
const fn model(model: u8, firmware_filename: Option<&'static str>) -> ModelInfo {
ModelInfo {
model,
min_frequency,
max_frequency,
max_tx_power,
band_label,
firmware_filename,
radio,
}
}
@ -480,28 +106,6 @@ pub fn normalize_eeprom_model(model: u8) -> u8 {
}
}
pub fn product_name(product: u8) -> Option<&'static str> {
Some(match product {
PRODUCT_RNODE => "RNode",
PRODUCT_HMBRW => "Hombrew RNode",
PRODUCT_TBEAM => "LilyGO T-Beam",
PRODUCT_TBEAM_S_V1 => "LilyGO T-Beam Supreme",
PRODUCT_TDECK => "LilyGO T-Deck",
PRODUCT_T32_10 => "LilyGO LoRa32 v1.0",
PRODUCT_T32_20 => "LilyGO LoRa32 v2.0",
PRODUCT_T32_21 => "LilyGO LoRa32 v2.1",
PRODUCT_H32_V2 => "Heltec LoRa32 v2",
PRODUCT_H32_V3 => "Heltec LoRa32 v3",
PRODUCT_H32_V4 => "Heltec LoRa32 v4",
PRODUCT_TECHO => "LilyGO T-Echo",
PRODUCT_RAK4631 => "RAK4631",
PRODUCT_OPENCOM_XL => "openCom XL",
PRODUCT_HELTEC_T114 => "Heltec Mesh Node T114",
PRODUCT_XIAO_S3 => "Seeed XIAO ESP32S3 Wio-SX1262",
_ => return None,
})
}
pub fn platform_name(platform: u8) -> Option<&'static str> {
Some(match platform {
PLATFORM_AVR => "AVR",
@ -526,7 +130,7 @@ mod tests {
use super::*;
#[test]
fn model_table_matches_upstream_shape() {
fn model_table_retains_only_firmware_planning_metadata() {
assert_eq!(MODELS.len(), 45);
assert_eq!(
model_info(0xA4).unwrap().firmware_filename,
@ -537,7 +141,7 @@ mod tests {
Some("rnode_firmware_t3s3_sx1280_pa.zip")
);
assert_eq!(model_info(0xFE).unwrap().firmware_filename, None);
assert_eq!(model_info(0xFF).unwrap().max_tx_power, 14);
assert_eq!(model_info(0xFF).unwrap().firmware_filename, None);
}
#[test]
@ -548,8 +152,7 @@ mod tests {
}
#[test]
fn names_match_upstream_spellings() {
assert_eq!(product_name(PRODUCT_HMBRW), Some("Hombrew RNode"));
fn platform_and_mcu_names_match_upstream_spellings() {
assert_eq!(platform_name(PLATFORM_ESP32), Some("ESP32"));
assert_eq!(mcu_name(MCU_NRF52), Some("Nordic Semiconductor nRF52840"));
}