Initial boilerplate scaffold for continuum-proxy
This commit is contained in:
+130
@@ -0,0 +1,130 @@
|
||||
mod adapters;
|
||||
mod cache;
|
||||
mod config;
|
||||
mod discovery;
|
||||
mod go2rtc;
|
||||
mod plate_changer;
|
||||
mod uplink;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{info, warn};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::cache::EdgeCache;
|
||||
use crate::config::Config;
|
||||
use crate::uplink::ClientMessage;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
|
||||
.json()
|
||||
.init();
|
||||
|
||||
dotenvy_load();
|
||||
|
||||
let config = Config::from_env()?;
|
||||
info!(gateway_id = %config.gateway_id, farm_id = %config.farm_id, "starting continuum-proxy");
|
||||
|
||||
let cache = EdgeCache::open(&config.sqlite_path)?;
|
||||
|
||||
// Channels wiring the printer adapters -> uplink, and uplink -> task
|
||||
// handlers for cloud-issued commands.
|
||||
let (telemetry_tx, telemetry_rx) = mpsc::channel::<ClientMessage>(256);
|
||||
let (task_tx, mut task_rx) = mpsc::channel::<uplink::ServerMessage>(64);
|
||||
let (discovered_tx, mut discovered_rx) = mpsc::channel::<discovery::DiscoveredPrinter>(32);
|
||||
|
||||
let uplink_handle = tokio::spawn(uplink::run(config.clone(), telemetry_rx, task_tx));
|
||||
|
||||
let go2rtc_handle = tokio::spawn(go2rtc::watchdog(config.go2rtc_bin.clone(), config.go2rtc_config.clone()));
|
||||
|
||||
let discovery_handle = tokio::spawn(discovery::run(
|
||||
Duration::from_secs(config.discovery_interval_secs),
|
||||
discovered_tx,
|
||||
));
|
||||
|
||||
// Cloud-issued task dispatcher: pause/resume/plate-change/etc. commands
|
||||
// arriving over the uplink get routed to the right adapter here.
|
||||
let task_dispatcher = tokio::spawn(async move {
|
||||
while let Some(msg) = task_rx.recv().await {
|
||||
match msg {
|
||||
uplink::ServerMessage::Task { task_id, kind, payload } => {
|
||||
info!(task_id, kind, ?payload, "received task from cloud");
|
||||
// TODO: route to adapters::bambu / prusalink / moonraker or plate_changer
|
||||
// based on `kind`, then send a ClientMessage::TaskResult back upstream.
|
||||
}
|
||||
uplink::ServerMessage::HelloAck { session_id } => {
|
||||
info!(session_id, "uplink session established");
|
||||
}
|
||||
uplink::ServerMessage::HeartbeatAck { seq } => {
|
||||
tracing::debug!(seq, "heartbeat acked");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Newly-discovered printers get their telemetry adapters spawned on the fly.
|
||||
let telemetry_tx_for_discovery = telemetry_tx.clone();
|
||||
let discovery_dispatcher = tokio::spawn(async move {
|
||||
while let Some(printer) = discovered_rx.recv().await {
|
||||
info!(host = %printer.host, "spawning adapter for discovered printer");
|
||||
let _ = &telemetry_tx_for_discovery;
|
||||
// TODO: match printer.vendor and spawn adapters::bambu::run_telemetry / moonraker::run / prusalink::poll_loop
|
||||
}
|
||||
});
|
||||
|
||||
// Periodically flush anything the SQLite cache buffered while the uplink was down.
|
||||
let cache_flusher = tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(Duration::from_secs(10));
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
match cache.drain_telemetry(100) {
|
||||
Ok(rows) if !rows.is_empty() => {
|
||||
let ids: Vec<i64> = rows.iter().map(|(id, _, _)| *id).collect();
|
||||
for (_, printer_id, payload) in &rows {
|
||||
let payload: serde_json::Value = serde_json::from_str(payload).unwrap_or_default();
|
||||
let _ = telemetry_tx
|
||||
.send(ClientMessage::Telemetry { printer_id: printer_id.clone(), payload })
|
||||
.await;
|
||||
}
|
||||
if let Err(err) = cache.ack_telemetry(&ids) {
|
||||
warn!(?err, "failed to ack drained telemetry rows");
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => warn!(?err, "failed to drain edge cache"),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::select! {
|
||||
res = uplink_handle => warn!(?res, "uplink task exited"),
|
||||
res = go2rtc_handle => warn!(?res, "go2rtc watchdog exited"),
|
||||
res = discovery_handle => warn!(?res, "discovery task exited"),
|
||||
res = task_dispatcher => warn!(?res, "task dispatcher exited"),
|
||||
res = discovery_dispatcher => warn!(?res, "discovery dispatcher exited"),
|
||||
res = cache_flusher => warn!(?res, "cache flusher exited"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads a `.env` file if present, without pulling in a heavyweight config
|
||||
/// crate. No-op (and safe to ignore errors) when the file doesn't exist.
|
||||
fn dotenvy_load() {
|
||||
if let Ok(contents) = std::fs::read_to_string(".env") {
|
||||
for line in contents.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
if std::env::var(key).is_err() {
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user