mirror of
https://github.com/ruvnet/RuView
synced 2026-08-04 19:31:42 +00:00
5c914e63c7
Phase 4 of ADR-103. Adds the long-running polling loop so the cog's
fourth verb (`run`) does real work, completing the ADR-100 runtime
contract end-to-end:
cog-person-count version → "person-count 0.3.0"
cog-person-count manifest → JSON skeleton
cog-person-count health → loads weights + 1-shot infer + emit
cog-person-count run --config → long-running per-frame emit ← THIS
What ships:
* src/runtime.rs (new) — `run_loop` polls sensing_url every poll_ms,
slides a [56, 20] CSI window, runs InferenceEngine::infer, emits
publisher::person_count events. Same shape as
cog-pose-estimation::runtime — fetch_frame extracts amplitudes
from `snapshot.nodes[0].amplitude[]`, fails open on connect errors
with a WARN log rather than crashing.
* src/lib.rs — registers the runtime module.
* src/main.rs — cmd_run now loads RunConfig from a JSON file, builds
the InferenceEngine (with weights if cfg.model_path is set,
otherwise auto-discover), emits a run.started event, and hands off
to the Tokio multi-thread runtime's block_on(run_loop). Single-node
fusion is a no-op for N=1 today; v0.2.0 will append predictions
from sibling nodes and call fusion::fuse_confidence_weighted before
emit.
Verified locally:
cargo check -p cog-person-count --no-default-features → clean
cargo test -p cog-person-count → 15/15 pass (no regressions)
cargo build -p cog-person-count --release → 2.36 MB unchanged
./cog-person-count run --config bad-config.json:
line 1: {"event":"run.started","fields":{"cog":"person-count",
"sensing_url":"http://127.0.0.1:9999/...",poll_ms:100,
"model_path":"(auto-discover)"}}
line 2: WARN sensing-server fetch failed
error=Connection Failed: Connect error: actively refused
(loop alive — exits cleanly on SIGTERM, no crash, no NaN)
Also adds a "Relationship to the in-process score_to_person_count
heuristic" section to cog/README.md explaining the dual-emitter
design (sensing-server keeps emitting the PR #491 slot heuristic;
the cog runs out-of-process and emits person.count events from the
learned model). Operators choose by installing the cog or not — no
sensing-server rebuild required.
ADR-103 §"Migration" status:
1. Land ADR + scaffold ........... done (#693, #694)
2. Train count_v1 ................ done (#695)
3. Cross-compile + sign + GCS .... done (#696)
4. Server-side wiring ............ done — out-of-process design
means no rewire needed; this
cog is the wiring.
5. v0.2.0 multi-room + LoRA ...... data-bound (#645)
134 lines
3.7 KiB
Rust
134 lines
3.7 KiB
Rust
//! `cog-person-count` — Cognitum Cog binary entrypoint.
|
|
//!
|
|
//! Implements the ADR-100 runtime contract:
|
|
//! cog-person-count version
|
|
//! cog-person-count manifest
|
|
//! cog-person-count health
|
|
//! cog-person-count run --config <path>
|
|
|
|
use clap::{Parser, Subcommand};
|
|
use cog_person_count::{
|
|
inference::{InferenceEngine, SyntheticInput},
|
|
publisher,
|
|
COG_ID, COG_VERSION,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{json, Value};
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "cog-person-count", version = COG_VERSION)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Cmd,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Cmd {
|
|
Version,
|
|
Manifest,
|
|
Health,
|
|
Run {
|
|
#[arg(long, value_name = "PATH")]
|
|
config: PathBuf,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct RunConfig {
|
|
#[serde(default = "default_sensing_url")]
|
|
sensing_url: String,
|
|
model_path: Option<PathBuf>,
|
|
#[serde(default = "default_poll_ms")]
|
|
poll_ms: u64,
|
|
}
|
|
|
|
fn default_sensing_url() -> String { "http://127.0.0.1:3000/api/v1/sensing/latest".to_string() }
|
|
fn default_poll_ms() -> u64 { 40 }
|
|
|
|
fn main() -> std::process::ExitCode {
|
|
init_logging();
|
|
let cli = Cli::parse();
|
|
let result = match cli.command {
|
|
Cmd::Version => cmd_version(),
|
|
Cmd::Manifest => cmd_manifest(),
|
|
Cmd::Health => cmd_health(),
|
|
Cmd::Run { config } => cmd_run(config),
|
|
};
|
|
match result {
|
|
Ok(()) => std::process::ExitCode::SUCCESS,
|
|
Err(err) => {
|
|
eprintln!("cog-person-count: {err}");
|
|
std::process::ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
fn init_logging() {
|
|
let _ = tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
|
|
)
|
|
.with_target(false)
|
|
.try_init();
|
|
}
|
|
|
|
fn cmd_version() -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("{COG_ID} {COG_VERSION}");
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_manifest() -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("{}", serde_json::to_string_pretty(&json!({
|
|
"id": COG_ID,
|
|
"version": COG_VERSION,
|
|
"binary_url": Value::Null,
|
|
"binary_bytes": Value::Null,
|
|
"binary_sha256": Value::Null,
|
|
"binary_signature": Value::Null,
|
|
"installed_at": Value::Null,
|
|
"status": Value::Null,
|
|
}))?);
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_health() -> Result<(), Box<dyn std::error::Error>> {
|
|
let engine = InferenceEngine::new()?;
|
|
let pred = engine.infer(&SyntheticInput::default().as_window())?;
|
|
if !pred.is_finite() {
|
|
return Err("inference produced non-finite output".into());
|
|
}
|
|
publisher::health_ok(COG_ID, engine.backend(), &pred);
|
|
Ok(())
|
|
}
|
|
|
|
fn cmd_run(config_path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
|
let raw = std::fs::read_to_string(&config_path)
|
|
.map_err(|e| format!("failed to read config at {}: {}", config_path.display(), e))?;
|
|
let cfg: RunConfig = serde_json::from_str(&raw)
|
|
.map_err(|e| format!("failed to parse config at {}: {}", config_path.display(), e))?;
|
|
|
|
let engine = InferenceEngine::with_weights(cfg.model_path.as_deref())?;
|
|
publisher::run_started(
|
|
COG_ID,
|
|
&cfg.sensing_url,
|
|
cfg.poll_ms,
|
|
&cfg.model_path
|
|
.as_ref()
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|| "(auto-discover)".to_string()),
|
|
);
|
|
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()?;
|
|
rt.block_on(cog_person_count::runtime::run_loop(
|
|
cog_person_count::runtime::RunConfig {
|
|
sensing_url: cfg.sensing_url,
|
|
poll_ms: cfg.poll_ms,
|
|
},
|
|
engine,
|
|
))
|
|
}
|