Files
ruvnet--RuView/v2/crates/cog-person-count/src/main.rs
T
rUv 5c914e63c7 feat(cog-person-count): wire run subcommand — v0.0.1 fully functional (#697)
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)
2026-05-21 19:10:15 -04:00

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,
))
}