use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::Level;
pub const GPIO_OUTPUT_PATH: &str = "/run/ados/gpio-output.json";
pub const GPIO_OUTPUT_SIDECAR_VERSION: u16 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LineState {
pub chip: u32,
pub pin: u32,
pub level: Level,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GpioOutputState {
#[serde(default)]
pub version: u16,
#[serde(default)]
pub lines: Vec<LineState>,
}
impl Default for GpioOutputState {
fn default() -> Self {
Self {
version: GPIO_OUTPUT_SIDECAR_VERSION,
lines: Vec::new(),
}
}
}
impl GpioOutputState {
pub fn from_snapshot(snapshot: &[(u32, u32, Level)]) -> Self {
Self {
version: GPIO_OUTPUT_SIDECAR_VERSION,
lines: snapshot
.iter()
.map(|(chip, pin, level)| LineState {
chip: *chip,
pin: *pin,
level: *level,
})
.collect(),
}
}
pub fn load(path: &Path) -> Option<GpioOutputState> {
let text = std::fs::read_to_string(path).ok()?;
let state: GpioOutputState = serde_json::from_str(&text).ok()?;
ados_protocol::sidecar::check_sidecar_version(
"gpio-output",
state.version,
GPIO_OUTPUT_SIDECAR_VERSION,
);
Some(state)
}
pub fn save(&self, path: &Path) -> std::io::Result<()> {
ados_protocol::sidecar::write_json_atomic(path, self, 0o644)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn state_round_trips() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("gpio-output.json");
let state = GpioOutputState::from_snapshot(&[(0, 17, Level::High), (1, 4, Level::Low)]);
state.save(&path).unwrap();
let loaded = GpioOutputState::load(&path).unwrap();
assert_eq!(loaded, state);
assert_eq!(loaded.lines.len(), 2);
assert_eq!(loaded.lines[0].pin, 17);
assert_eq!(loaded.lines[0].level, Level::High);
let text = std::fs::read_to_string(&path).unwrap();
assert!(!text.contains(", "));
assert!(!text.contains(": "));
let stray = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.any(|e| e.file_name().to_string_lossy().contains(".tmp"));
assert!(!stray);
}
#[test]
fn empty_state_round_trips() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("gpio-output.json");
let state = GpioOutputState::default();
state.save(&path).unwrap();
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert!(v["lines"].as_array().unwrap().is_empty());
assert_eq!(GpioOutputState::load(&path).unwrap(), state);
}
#[test]
fn load_missing_or_malformed_is_none() {
assert!(GpioOutputState::load(Path::new("/nonexistent/gpio-output.json")).is_none());
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("gpio-output.json");
std::fs::write(&path, b"not json").unwrap();
assert!(GpioOutputState::load(&path).is_none());
}
#[test]
fn written_state_stamps_the_schema_version() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("gpio-output.json");
GpioOutputState::from_snapshot(&[(0, 17, Level::High)])
.save(&path)
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert_eq!(
v["version"].as_u64(),
Some(GPIO_OUTPUT_SIDECAR_VERSION as u64)
);
}
#[test]
fn version_matches_registry() {
assert_eq!(
GPIO_OUTPUT_SIDECAR_VERSION,
ados_protocol::contracts::sidecar_version("gpio-output").unwrap()
);
}
}