use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NameKey {
pub manufacturer_code: u16,
pub unique_number: u32,
}
impl NameKey {
pub fn to_token(self) -> String {
format!("{}-{}", self.manufacturer_code, self.unique_number)
}
pub fn from_token(s: &str) -> Option<Self> {
let (mfr, uid) = s.split_once('-')?;
Some(Self {
manufacturer_code: mfr.parse().ok()?,
unique_number: uid.parse().ok()?,
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CachedInfo {
pub manufacturer_name: Option<String>,
pub device_function: Option<u32>,
pub device_class: Option<u32>,
pub industry_group: Option<u32>,
pub product_code: Option<u32>,
pub model_id: Option<String>,
pub software_version: Option<String>,
pub model_version: Option<String>,
pub model_serial_code: Option<String>,
pub nmea_db_version: Option<u32>,
pub certification_level: Option<u32>,
pub load_equivalency: Option<u32>,
pub installation_description_1: Option<String>,
pub installation_description_2: Option<String>,
pub manufacturer_information: Option<String>,
pub first_seen: Option<String>,
pub last_seen: Option<String>,
}
impl CachedInfo {
pub fn merge_from(&mut self, other: CachedInfo) {
fn take<T>(dst: &mut Option<T>, src: Option<T>) {
if src.is_some() {
*dst = src;
}
}
fn take_str(dst: &mut Option<String>, src: Option<String>) {
match src {
Some(s) if !s.trim().is_empty() => *dst = Some(s),
_ => {}
}
}
take_str(&mut self.manufacturer_name, other.manufacturer_name);
take(&mut self.device_function, other.device_function);
take(&mut self.device_class, other.device_class);
take(&mut self.industry_group, other.industry_group);
take(&mut self.product_code, other.product_code);
take_str(&mut self.model_id, other.model_id);
take_str(&mut self.software_version, other.software_version);
take_str(&mut self.model_version, other.model_version);
take_str(&mut self.model_serial_code, other.model_serial_code);
take(&mut self.nmea_db_version, other.nmea_db_version);
take(&mut self.certification_level, other.certification_level);
take(&mut self.load_equivalency, other.load_equivalency);
take_str(
&mut self.installation_description_1,
other.installation_description_1,
);
take_str(
&mut self.installation_description_2,
other.installation_description_2,
);
take_str(
&mut self.manufacturer_information,
other.manufacturer_information,
);
if self.first_seen.is_none() {
self.first_seen = other.first_seen.clone().or(other.last_seen.clone());
}
if let Some(ls) = other.last_seen {
self.last_seen = Some(ls);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FileEntry {
pub name: String,
#[serde(flatten)]
pub info: CachedInfo,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct FileFormat {
#[serde(default)]
pub entries: Vec<FileEntry>,
}
pub fn load_or_default(path: &PathBuf) -> HashMap<NameKey, CachedInfo> {
let Ok(body) = std::fs::read_to_string(path) else {
return HashMap::new();
};
let file: FileFormat = match serde_json::from_str(&body) {
Ok(f) => f,
Err(_) => return HashMap::new(),
};
file.entries
.into_iter()
.filter_map(|e| NameKey::from_token(&e.name).map(|k| (k, e.info)))
.collect()
}
pub fn save(cache: &HashMap<NameKey, CachedInfo>, path: &PathBuf) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating {}", parent.display()))?;
}
let mut entries: Vec<FileEntry> = cache
.iter()
.map(|(k, v)| FileEntry {
name: k.to_token(),
info: v.clone(),
})
.collect();
entries.sort_by(|a, b| a.name.cmp(&b.name));
let file = FileFormat { entries };
let body = serde_json::to_string_pretty(&file).context("serialising device cache")?;
std::fs::write(path, body).with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
pub fn default_path() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
return PathBuf::from(xdg).join("canboat-tui/device-cache.json");
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".config/canboat-tui/device-cache.json");
}
PathBuf::from("canboat-tui-device-cache.json")
}