use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
use serde_json::{json, Value};
use tokio::signal::unix::{signal, SignalKind};
#[cfg(target_os = "linux")]
use tokio::sync::Mutex;
use crate::sidecar::{GpioOutputState, GPIO_OUTPUT_PATH};
use crate::{beep_schedule, parse_command, Command, GPIO_CMD_SOCK};
use ados_protocol::ipc::{bind_command_socket, serve_rpc};
const MAX_REQUEST_BYTES: usize = 64 * 1024;
fn init_logging() {
use ados_protocol::logd::layer::LogdLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
#[cfg(target_os = "linux")]
{
if let Ok(journald) = tracing_journald::layer() {
let _ = tracing_subscriber::registry()
.with(EnvFilter::new(&filter))
.with(journald)
.with(LogdLayer::new("ados-gpio"))
.try_init();
return;
}
}
let _ = tracing_subscriber::registry()
.with(EnvFilter::new(&filter))
.with(tracing_subscriber::fmt::layer())
.with(LogdLayer::new("ados-gpio"))
.try_init();
}
#[cfg(target_os = "linux")]
fn sd_ready() {
if let Err(e) = sd_notify::notify(&[sd_notify::NotifyState::Ready]) {
tracing::debug!(error = %e, "sd_notify READY failed");
}
}
#[cfg(not(target_os = "linux"))]
fn sd_ready() {}
#[cfg(target_os = "linux")]
type BeepCancels = std::collections::HashMap<(u32, u32), Arc<std::sync::atomic::AtomicBool>>;
#[derive(Clone)]
struct State {
#[cfg(target_os = "linux")]
output: Arc<Mutex<crate::GpioOutput>>,
#[cfg(target_os = "linux")]
beeps: Arc<std::sync::Mutex<BeepCancels>>,
sidecar_path: Arc<std::path::PathBuf>,
}
pub fn run() -> std::process::ExitCode {
exit_code(run_async())
}
#[tokio::main]
async fn run_async() -> Result<()> {
init_logging();
tracing::info!("ados-gpio starting");
let state = State {
#[cfg(target_os = "linux")]
output: Arc::new(Mutex::new(crate::GpioOutput::new())),
#[cfg(target_os = "linux")]
beeps: Arc::default(),
sidecar_path: Arc::new(std::path::PathBuf::from(GPIO_OUTPUT_PATH)),
};
persist_state(&state).await;
let sock_state = state.clone();
let server = tokio::spawn(async move {
if let Err(e) = serve(sock_state, Path::new(GPIO_CMD_SOCK)).await {
tracing::warn!(error = %e, "gpio command socket exited");
}
});
sd_ready();
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
tokio::select! {
_ = sigterm.recv() => tracing::info!("received SIGTERM"),
_ = sigint.recv() => tracing::info!("received SIGINT"),
}
server.abort();
#[cfg(target_os = "linux")]
{
state.output.lock().await.all_low();
}
persist_state(&state).await;
let _ = std::fs::remove_file(GPIO_CMD_SOCK);
tracing::info!("ados-gpio stopped");
Ok(())
}
async fn persist_state(state: &State) {
#[cfg(target_os = "linux")]
let snapshot = state.output.lock().await.snapshot();
#[cfg(not(target_os = "linux"))]
let snapshot: Vec<(u32, u32, crate::Level)> = Vec::new();
let blob = GpioOutputState::from_snapshot(&snapshot);
if let Err(e) = blob.save(&state.sidecar_path) {
tracing::debug!(error = %e, "gpio-output sidecar write failed");
}
}
async fn serve(state: State, sock_path: &Path) -> std::io::Result<()> {
let listener = bind_command_socket(sock_path, 0o660)?;
tracing::info!(path = %sock_path.display(), "gpio command socket listening");
serve_rpc(listener, MAX_REQUEST_BYTES, move |req: Vec<u8>| {
let state = state.clone();
async move {
let resp = dispatch(&req, &state).await;
serde_json::to_vec(&resp)
.unwrap_or_else(|_| br#"{"ok":false,"error":"E_ENCODE"}"#.to_vec())
}
})
.await;
Ok(())
}
async fn dispatch(line: &[u8], state: &State) -> Value {
match parse_command(line) {
crate::Parsed::Error(code) => json!({"ok": false, "error": code}),
crate::Parsed::Cmd(cmd) => apply(cmd, state).await,
}
}
async fn apply(cmd: Command, state: &State) -> Value {
match cmd {
Command::Set { chip, pin, level } => set_line(chip, pin, level, state).await,
Command::Beep { chip, pin, pattern } => {
let phases = beep_schedule(pattern);
let carrier = crate::carrier_timing(pattern);
let real = phases.iter().filter(|p| p.hold_ms > 0).count();
match start_beep(chip, pin, phases, carrier, state.clone()).await {
Ok(()) => json!({"ok": true, "phases": real}),
Err(code) => json!({"ok": false, "error": code}),
}
}
Command::Status => {
#[cfg(target_os = "linux")]
let snapshot = state.output.lock().await.snapshot();
#[cfg(not(target_os = "linux"))]
let snapshot: Vec<(u32, u32, crate::Level)> = Vec::new();
let lines: Vec<Value> = snapshot
.iter()
.map(|(chip, pin, level)| {
json!({"chip": chip, "pin": pin, "level": serde_json::to_value(level).unwrap_or(Value::Null)})
})
.collect();
json!({"ok": true, "lines": lines})
}
}
}
#[cfg(target_os = "linux")]
async fn set_line(chip: u32, pin: u32, level: crate::Level, state: &State) -> Value {
let result = {
let mut out = state.output.lock().await;
cancel_beep(state, chip, pin);
out.set(chip, pin, level)
};
match result {
Ok(()) => {
persist_state(state).await;
json!({"ok": true, "chip": chip, "pin": pin, "level": serde_json::to_value(level).unwrap_or(Value::Null)})
}
Err(e) => json!({"ok": false, "error": format!("E_DRIVE_FAILED: {e}")}),
}
}
#[cfg(target_os = "linux")]
fn cancel_beep(state: &State, chip: u32, pin: u32) {
let mut beeps = state
.beeps
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(flag) = beeps.remove(&(chip, pin)) {
flag.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
#[cfg(not(target_os = "linux"))]
async fn set_line(_chip: u32, _pin: u32, _level: crate::Level, _state: &State) -> Value {
json!({"ok": false, "error": "E_NO_GPIO"})
}
#[cfg(target_os = "linux")]
async fn start_beep(
chip: u32,
pin: u32,
phases: Vec<crate::BeepPhase>,
carrier: Option<(std::time::Duration, std::time::Duration)>,
state: State,
) -> Result<(), String> {
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
if phases.is_empty() {
return Ok(());
}
let cancel = Arc::new(AtomicBool::new(false));
{
let mut out = state.output.lock().await;
cancel_beep(&state, chip, pin);
out.set(chip, pin, phases[0].level)
.map_err(|e| format!("E_DRIVE_FAILED: {e}"))?;
state
.beeps
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert((chip, pin), cancel.clone());
}
persist_state(&state).await;
let rt = tokio::runtime::Handle::current();
tokio::task::spawn_blocking(move || {
let drive = |level: crate::Level| -> bool {
let mut out = state.output.blocking_lock();
if cancel.load(Ordering::SeqCst) {
return false;
}
if let Err(e) = out.set(chip, pin, level) {
tracing::warn!(chip, pin, error = %e, "beep phase drive failed");
return false;
}
true
};
let hold = |d: Duration| -> bool {
let end = Instant::now() + d;
while let Some(left) = end.checked_duration_since(Instant::now()) {
if cancel.load(Ordering::SeqCst) {
return false;
}
std::thread::sleep(left.min(Duration::from_millis(10)));
}
!cancel.load(Ordering::SeqCst)
};
'schedule: for (i, phase) in phases.iter().enumerate() {
if i > 0 && !drive(phase.level) {
break;
}
if i > 0 {
rt.block_on(persist_state(&state));
}
let span = Duration::from_millis(phase.hold_ms as u64);
match (phase.level, carrier) {
(crate::Level::High, Some((high, low))) => {
let end = Instant::now() + span;
while Instant::now() < end {
std::thread::sleep(high);
if !drive(crate::Level::Low) {
break 'schedule;
}
std::thread::sleep(low);
if Instant::now() >= end || !drive(crate::Level::High) {
break;
}
}
}
_ => {
if !hold(span) {
break;
}
}
}
}
if drive(crate::Level::Low) {
let mut beeps = state
.beeps
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if beeps
.get(&(chip, pin))
.is_some_and(|f| Arc::ptr_eq(f, &cancel))
{
beeps.remove(&(chip, pin));
}
}
rt.block_on(persist_state(&state));
});
Ok(())
}
#[cfg(not(target_os = "linux"))]
async fn start_beep(
_chip: u32,
_pin: u32,
_phases: Vec<crate::BeepPhase>,
_carrier: Option<(std::time::Duration, std::time::Duration)>,
_state: State,
) -> Result<(), String> {
Err("E_NO_GPIO".to_string())
}
#[cfg(test)]
mod tests;
fn exit_code(result: anyhow::Result<()>) -> std::process::ExitCode {
match result {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("Error: {e:?}");
std::process::ExitCode::FAILURE
}
}
}