Merge named Cut 3 and H0: retire --idunn-rudp-health, muninn-contracts

Soul found no blocker; its findings (wire pins for all 15 records,
stale health text, unpinned boundary fields, inert-registration
comment) are fixed at 2742b4d. Deploy note: starfire and nightwing
need the binary and scripts installed together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 00:23:58 +02:00
commit 1d0ea0b112
19 changed files with 1202 additions and 452 deletions

15
Cargo.lock generated
View file

@ -479,6 +479,19 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "muninn-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"cultcache-rs",
"cultmesh-rs",
"cultnet-rs",
"rmp-serde",
"serde",
"serde_json",
]
[[package]]
name = "muninn-daemon"
version = "0.1.0"
@ -488,6 +501,7 @@ dependencies = [
"cultmesh-rs",
"cultnet-rs",
"libc",
"muninn-contracts",
"muninn-move-tracker",
"muninn-psmoveapi-tracker",
"odin-core",
@ -799,6 +813,7 @@ dependencies = [
"cultcache-rs",
"cultmesh-rs",
"cultnet-rs",
"muninn-contracts",
"odin-core",
"rmp-serde",
"serde_json",

View file

@ -1,5 +1,6 @@
[workspace]
members = [
"crates/muninn-contracts",
"crates/muninn-daemon",
"crates/muninn-move-tracker",
"crates/muninn-psmoveapi-tracker",

View file

@ -0,0 +1,21 @@
[package]
name = "muninn-contracts"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish.workspace = true
[lib]
name = "muninn_contracts"
path = "src/lib.rs"
[dependencies]
anyhow = "1"
cultcache-rs = { workspace = true }
cultmesh-rs = { workspace = true }
cultnet-rs = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
rmp-serde = "1.3"

View file

@ -0,0 +1,526 @@
//! Muninn's typed state records: the wire and store contract that `muninn` and
//! `sleipnir` share. Library only: no sockets, threads, devices or Odin.
mod records;
pub use records::*;
cultmesh_rs::cultmesh_documents!(MuninnRecords {
MuninnTelemetrySurfaceRecord => MUNINN_TELEMETRY_SURFACE_SCHEMA,
MuninnCaptureStreamRecord => MUNINN_CAPTURE_STREAM_SCHEMA,
MuninnCaptureStreamCommandRecord => MUNINN_CAPTURE_STREAM_COMMAND_SCHEMA,
MuninnObsStreamCatalogRecord => MUNINN_OBS_STREAM_CATALOG_SCHEMA,
MuninnMoveMarkerCandidateRecord => MUNINN_MOVE_MARKER_CANDIDATE_SCHEMA,
MuninnMoveControllerStateRecord => MUNINN_MOVE_CONTROLLER_STATE_SCHEMA,
MuninnHidControllerStateRecord => MUNINN_HID_CONTROLLER_STATE_SCHEMA,
MuninnMoveIdentityRecord => MUNINN_MOVE_IDENTITY_SCHEMA,
MuninnMoveLightCommandRecord => MUNINN_MOVE_LIGHT_COMMAND_SCHEMA,
MuninnMoveHueProgramRecord => MUNINN_MOVE_HUE_PROGRAM_SCHEMA,
MuninnMoveTrackerHealthRecord => MUNINN_MOVE_TRACKER_HEALTH_SCHEMA,
MuninnMoveEvidenceTransportHealthRecord => MUNINN_MOVE_EVIDENCE_TRANSPORT_HEALTH_SCHEMA,
MuninnQuestAccessRecord => MUNINN_QUEST_ACCESS_SCHEMA,
MuninnCommandBoundaryCompatRecord => MUNINN_COMMAND_BOUNDARY_SCHEMA,
MuninnTransportProfileCompatRecord => MUNINN_TRANSPORT_PROFILE_SCHEMA,
});
#[cfg(test)]
mod tests {
use super::*;
use cultcache_rs::{CultCache, DatabaseEntry};
use cultmesh_rs::CultMeshDocumentSet;
use cultnet_rs::CultNetDocumentRegistry;
/// Every Muninn record: its type name, its schema id, the schema constant
/// the document set binds it under, and its Rust name.
fn contract() -> Vec<(&'static str, &'static str, &'static str, &'static str)> {
macro_rules! row {
($record:ty, $constant:expr) => {
(
<$record as DatabaseEntry>::TYPE,
<$record as DatabaseEntry>::SCHEMA_NAME,
$constant,
stringify!($record),
)
};
}
vec![
row!(MuninnTelemetrySurfaceRecord, MUNINN_TELEMETRY_SURFACE_SCHEMA),
row!(MuninnCaptureStreamRecord, MUNINN_CAPTURE_STREAM_SCHEMA),
row!(MuninnCaptureStreamCommandRecord, MUNINN_CAPTURE_STREAM_COMMAND_SCHEMA),
row!(MuninnObsStreamCatalogRecord, MUNINN_OBS_STREAM_CATALOG_SCHEMA),
row!(MuninnMoveMarkerCandidateRecord, MUNINN_MOVE_MARKER_CANDIDATE_SCHEMA),
row!(MuninnMoveControllerStateRecord, MUNINN_MOVE_CONTROLLER_STATE_SCHEMA),
row!(MuninnHidControllerStateRecord, MUNINN_HID_CONTROLLER_STATE_SCHEMA),
row!(MuninnMoveIdentityRecord, MUNINN_MOVE_IDENTITY_SCHEMA),
row!(MuninnMoveLightCommandRecord, MUNINN_MOVE_LIGHT_COMMAND_SCHEMA),
row!(MuninnMoveHueProgramRecord, MUNINN_MOVE_HUE_PROGRAM_SCHEMA),
row!(MuninnMoveTrackerHealthRecord, MUNINN_MOVE_TRACKER_HEALTH_SCHEMA),
row!(
MuninnMoveEvidenceTransportHealthRecord,
MUNINN_MOVE_EVIDENCE_TRANSPORT_HEALTH_SCHEMA
),
row!(MuninnQuestAccessRecord, MUNINN_QUEST_ACCESS_SCHEMA),
row!(MuninnCommandBoundaryCompatRecord, MUNINN_COMMAND_BOUNDARY_SCHEMA),
row!(MuninnTransportProfileCompatRecord, MUNINN_TRANSPORT_PROFILE_SCHEMA),
]
}
#[test]
fn record_names_are_the_published_schema_ids() {
let expected = [
("muninn.telemetry_surface", "muninn.telemetry_surface.v1"),
("muninn.capture_stream", "muninn.capture_stream.v1"),
("muninn.capture_stream_command", "muninn.capture_stream_command.v1"),
("muninn.obs_stream_catalog", "muninn.obs_stream_catalog.v1"),
("muninn.move_marker_candidate", "muninn.move_marker_candidate.v1"),
("muninn.move_controller_state", "muninn.move_controller_state.v1"),
("muninn.hid_controller_state", "muninn.hid_controller_state.v1"),
("muninn.move_identity", "muninn.move_identity.v1"),
("muninn.move_light_command", "muninn.move_light_command.v1"),
("muninn.move_hue_program", "muninn.move_hue_program.v1"),
("muninn.move_tracker_health", "muninn.move_tracker_health.v1"),
(
"muninn.move_evidence_transport_health",
"muninn.move_evidence_transport_health.v1",
),
("muninn.quest_access", "muninn.quest_access.v1"),
("muninn.command_boundary", "muninn.command_boundary.v1"),
("muninn.transport_profile", "muninn.transport_profile.v1"),
];
let actual: Vec<_> = contract()
.into_iter()
.map(|(record_type, schema, constant, _)| {
assert_eq!(schema, constant, "schema constant and derive disagree");
(record_type, schema)
})
.collect();
assert_eq!(actual, expected);
}
#[test]
fn document_set_binds_every_record_under_its_schema() {
let mut cache = CultCache::new();
MuninnRecords.register_cache(&mut cache).unwrap();
let mut registry = CultNetDocumentRegistry::new();
MuninnRecords.register_documents(&mut registry).unwrap();
let rows = contract();
let mut registered = cache.registered_entry_types();
registered.sort();
let mut expected: Vec<String> = rows.iter().map(|row| row.0.to_string()).collect();
expected.sort();
assert_eq!(registered, expected);
for (record_type, schema, _, record) in rows {
let binding = registry
.binding_by_schema_id(schema)
.unwrap_or_else(|| panic!("{record} is not bound under {schema}"));
assert_eq!(binding.document_type, record_type, "{record}");
}
}
/// One wire pin per record. `bytes` is the record's positional MessagePack
/// array, encoded from a sample whose every field holds a distinct,
/// non-default value (Options: one `None`, one `Some`). `first_default` is
/// the key of the first defaulted tail field; records without one carry
/// `None`.
struct Pin {
record: &'static str,
bytes: Vec<u8>,
first_default: Option<usize>,
golden: &'static str,
/// Decode a payload as this record and encode it again.
reencode: fn(&[u8]) -> Result<Vec<u8>, String>,
}
fn pin<T>(
record: &'static str,
sample: T,
first_default: Option<usize>,
golden: &'static str,
) -> Pin
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
Pin {
record,
bytes: rmp_serde::to_vec(&sample).unwrap(),
first_default,
golden,
reencode: |bytes| {
let decoded: T = rmp_serde::from_slice(bytes).map_err(|error| error.to_string())?;
rmp_serde::to_vec(&decoded).map_err(|error| error.to_string())
},
}
}
fn text(value: &str) -> String {
value.to_string()
}
fn texts(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
fn pins() -> Vec<Pin> {
vec![
pin(
"MuninnTelemetrySurfaceRecord",
MuninnTelemetrySurfaceRecord {
surface_id: text("surface"),
host_id: text("host"),
state: text("live"),
available_sources: texts(&["src"]),
stream_affordances: texts(&["aff"]),
active_streams: texts(&["act"]),
activation_authority: text("auth"),
detail: text("detail"),
updated_at: text("t"),
primary_stream_id: text("psid"),
primary_stream_label: text("plabel"),
command_rudp_target: text("cmd"),
media_target_host: text("mth"),
media_port: 4001,
media_packet_bytes: 1200,
rudp_video_bitrate_kbps: 8000,
rudp_latency_budget_ms: 40,
video_source_ids: texts(&["vid"]),
video_source_labels: texts(&["vlab"]),
audio_source_ids: texts(&["aid"]),
audio_source_labels: texts(&["alab"]),
},
Some(9),
"dc0015a773757266616365a4686f7374a46c69766591a373726391a361666691a3616374a461757468a664657461696ca174a470736964a6706c6162656ca3636d64a36d7468cd0fa1cd04b0cd1f402891a376696491a4766c616291a361696491a4616c6162",
),
pin(
"MuninnCaptureStreamRecord",
MuninnCaptureStreamRecord {
stream_id: text("stream"),
host_id: text("host"),
state: text("live"),
video_source: text("vsrc"),
audio_source: text("asrc"),
transport: text("rudp"),
targets: texts(&["tgt"]),
command_witness: text("wit"),
supervisor_pid: None,
mux_pid: Some(77),
restart_count: 3,
detail: text("detail"),
updated_at: text("t"),
},
None,
"9da673747265616da4686f7374a46c697665a476737263a461737263a47275647091a3746774a3776974c04d03a664657461696ca174",
),
pin(
"MuninnCaptureStreamCommandRecord",
MuninnCaptureStreamCommandRecord {
command_id: text("cmd"),
host_id: text("host"),
stream_id: text("stream"),
state: text("live"),
action: text("start"),
target_host: text("tgt"),
port: 4001,
obs_target_host: Some(text("obs")),
obs_port: 4002,
media_transport: text("rudp"),
media_packet_bytes: 1200,
requested_by: text("who"),
detail: text("detail"),
updated_at: text("t"),
rudp_video_bitrate_kbps: 8000,
rudp_latency_budget_ms: 40,
video_source_id: text("vid"),
audio_source_id: text("aid"),
},
Some(14),
"dc0012a3636d64a4686f7374a673747265616da46c697665a57374617274a3746774cd0fa1a36f6273cd0fa2a472756470cd04b0a377686fa664657461696ca174cd1f4028a3766964a3616964",
),
pin(
"MuninnObsStreamCatalogRecord",
MuninnObsStreamCatalogRecord {
catalog_id: text("catalog"),
host_id: text("host"),
stream_ids: texts(&["sid"]),
labels: texts(&["lab"]),
urls: texts(&["url"]),
states: texts(&["st"]),
updated_at: text("t"),
command_rudp_target: text("cmd"),
media_target_host: text("mth"),
media_port: 4001,
media_packet_bytes: 1200,
rudp_video_bitrate_kbps: 8000,
rudp_latency_budget_ms: 40,
video_source_ids: texts(&["vid"]),
video_source_labels: texts(&["vlab"]),
audio_source_ids: texts(&["aid"]),
audio_source_labels: texts(&["alab"]),
},
Some(7),
"dc0011a7636174616c6f67a4686f737491a373696491a36c616291a375726c91a27374a174a3636d64a36d7468cd0fa1cd04b0cd1f402891a376696491a4766c616291a361696491a4616c6162",
),
pin(
"MuninnMoveMarkerCandidateRecord",
MuninnMoveMarkerCandidateRecord {
stream_id: text("stream"),
host_id: text("host"),
camera_id: text("cam"),
frame_sequence: 11,
source_id_hash: 12,
tile_x: 13,
tile_y: 14,
center_x_px: 1.5,
center_y_px: 2.5,
radius_px: 3.5,
area_px: 15,
mean_luma: 4.5,
peak_luma: 16,
score: 0.5,
observed_at: text("t"),
move_id: text("move"),
},
Some(15),
"dc0010a673747265616da4686f7374a363616d0b0c0d0eca3fc00000ca40200000ca406000000fca4090000010ca3f000000a174a46d6f7665",
),
pin(
"MuninnMoveControllerStateRecord",
MuninnMoveControllerStateRecord {
stream_id: text("stream"),
host_id: text("host"),
move_id: text("move"),
sequence: 21,
source_timestamp_ns: -22,
accelerometer_xyz: vec![1.0, 2.0, 3.0],
gyroscope_xyz: vec![4.0, 5.0, 6.0],
magnetometer_xyz: vec![7.0, 8.0, 9.0],
trigger_value: 0.25,
buttons: texts(&["cross"]),
battery01: 0.75,
observed_at: text("t"),
source_path: text("/dev/js0"),
},
Some(12),
"9da673747265616da4686f7374a46d6f766515ea93ca3f800000ca40000000ca4040000093ca40800000ca40a00000ca40c0000093ca40e00000ca41000000ca41100000ca3e80000091a563726f7373ca3f400000a174a82f6465762f6a7330",
),
pin(
"MuninnHidControllerStateRecord",
MuninnHidControllerStateRecord {
stream_id: text("muninn.raven.hid"),
host_id: text("raven"),
device_id: text("xbox-0"),
device_kind: text("xinput"),
sequence: 7,
source_timestamp_ns: -3,
axes: vec![0.5, -1.0],
buttons: texts(&["a", "b"]),
battery01: 1.0,
observed_at: text("unix:100"),
source_path: text("xinput://0"),
},
None,
"9bb06d756e696e6e2e726176656e2e686964a5726176656ea678626f782d30a678696e70757407fd92ca3f000000cabf80000092a161a162ca3f800000a8756e69783a313030aa78696e7075743a2f2f30",
),
pin(
"MuninnMoveIdentityRecord",
MuninnMoveIdentityRecord {
identity_id: text("identity"),
host_id: text("host"),
move_id: text("move"),
source_path: text("path"),
bluetooth_host_address: text("bt"),
state: text("live"),
detail: text("detail"),
observed_at: text("t"),
},
None,
"98a86964656e74697479a4686f7374a46d6f7665a470617468a26274a46c697665a664657461696ca174",
),
pin(
"MuninnMoveLightCommandRecord",
MuninnMoveLightCommandRecord {
command_id: text("cmd"),
host_id: text("host"),
move_id: text("move"),
hidraw_path: text("hidraw"),
colors: texts(&["red"]),
durations_ms: vec![100, 200],
repeat_count: 5,
authority: text("auth"),
state: text("live"),
detail: text("detail"),
updated_at: text("t"),
},
None,
"9ba3636d64a4686f7374a46d6f7665a668696472617791a37265649264ccc805a461757468a46c697665a664657461696ca174",
),
pin(
"MuninnMoveHueProgramRecord",
MuninnMoveHueProgramRecord {
program_id: text("prog"),
host_id: text("raven"),
mode: text("cycle"),
cycle_ms: 20,
epoch_ns: -1,
hold_at_ns: 0,
requested_by: text("mimir"),
updated_at: text("unix:1"),
order_mode: text("ring"),
transition_percent: 50,
transition_percent_explicit: true,
},
Some(8),
"9ba470726f67a5726176656ea56379636c6514ff00a56d696d6972a6756e69783a31a472696e6732c3",
),
pin(
"MuninnMoveTrackerHealthRecord",
MuninnMoveTrackerHealthRecord {
health_id: text("health"),
host_id: text("host"),
camera_id: text("cam"),
camera_index: -2,
state: text("live"),
camera_name: text("name"),
camera_api: text("api"),
width: 640,
height: 480,
exposure: 0.5,
calibrated_controller_count: 2,
update_count: 30,
observation_count: 31,
latest_observation_count: 32,
last_observation_at: text("last"),
detail: text("detail"),
updated_at: text("t"),
image_mean_rgb: vec![1, 2, 3],
image_peak_rgb: vec![4, 5, 6],
color_evidence_move_ids: texts(&["move"]),
color_evidence_pixel_counts: vec![40],
rejected_stale_count: 41,
rejected_radius_count: 42,
rejected_bounds_count: 43,
rejected_continuity_count: 44,
},
Some(17),
"dc0019a66865616c7468a4686f7374a363616dfea46c697665a46e616d65a3617069cd0280cd01e0ca3f000000021e1f20a46c617374a664657461696ca174930102039304050691a46d6f76659128292a2b2c",
),
pin(
"MuninnMoveEvidenceTransportHealthRecord",
MuninnMoveEvidenceTransportHealthRecord {
health_id: text("health"),
host_id: text("host"),
stream_id: text("stream"),
produced_frames: 50,
local_ring_admissions: 51,
remote_handoffs: 52,
remote_sends: 53,
updated_at: text("t"),
},
None,
"98a66865616c7468a4686f7374a673747265616d32333435a174",
),
pin(
"MuninnQuestAccessRecord",
MuninnQuestAccessRecord {
access_id: text("access"),
host_id: text("host"),
serial: text("serial"),
connection_state: text("conn"),
product: text("product"),
model: text("model"),
device: text("device"),
transport_id: text("tid"),
input_stream_id: text("input"),
pose_stream_id: text("pose"),
video_input_stream_id: text("video"),
video_input_transport: text("vtransport"),
state: text("live"),
detail: text("detail"),
observed_at: text("t"),
},
None,
"9fa6616363657373a4686f7374a673657269616ca4636f6e6ea770726f64756374a56d6f64656ca6646576696365a3746964a5696e707574a4706f7365a5766964656faa767472616e73706f7274a46c697665a664657461696ca174",
),
pin(
"MuninnCommandBoundaryCompatRecord",
MuninnCommandBoundaryCompatRecord {
value: serde_json::json!({ "daemon_id": "muninn" }),
},
None,
"9181a96461656d6f6e5f6964a66d756e696e6e",
),
pin(
"MuninnTransportProfileCompatRecord",
MuninnTransportProfileCompatRecord {
value: serde_json::json!({ "state": "live" }),
},
None,
"9181a57374617465a46c697665",
),
]
}
/// Every record's bytes are exactly the pinned positional array: a swapped
/// key, a changed type or a moved field changes them.
#[test]
fn every_record_encodes_to_its_pinned_positional_bytes() {
let mut wrong = Vec::new();
for pin in pins() {
let actual = hex(&pin.bytes);
if actual != pin.golden {
wrong.push(format!("{}: {actual}", pin.record));
}
let reencoded = (pin.reencode)(&pin.bytes)
.unwrap_or_else(|error| panic!("{} does not decode its own bytes: {error}", pin.record));
assert_eq!(reencoded, pin.bytes, "{} round trip", pin.record);
}
assert!(
wrong.is_empty(),
"unpinned or moved wire layouts:\n{}",
wrong.join("\n")
);
}
/// A payload written before a defaulted key existed still decodes, once
/// per defaulted key, and every key from there on reads as its zero value.
#[test]
fn every_defaulted_tail_key_decodes_from_a_payload_that_ends_before_it() {
use serde_json::Value;
let is_zero = |value: &Value| match value {
Value::Null => true,
Value::Bool(flag) => !flag,
Value::Number(number) => number.as_f64() == Some(0.0),
Value::String(text) => text.is_empty(),
Value::Array(items) => items.is_empty(),
Value::Object(_) => false,
};
for pin in pins() {
let Some(first_default) = pin.first_default else {
continue;
};
let full: Vec<Value> = rmp_serde::from_slice(&pin.bytes).unwrap();
for end in first_default..full.len() {
let truncated = rmp_serde::to_vec(&full[..end]).unwrap();
let decoded = (pin.reencode)(&truncated).unwrap_or_else(|error| {
panic!(
"{} does not decode a payload ending before key {end}: {error}",
pin.record
)
});
let decoded: Vec<Value> = rmp_serde::from_slice(&decoded).unwrap();
assert_eq!(decoded.len(), full.len(), "{} ending before key {end}", pin.record);
assert_eq!(&decoded[..end], &full[..end], "{} ending before key {end}", pin.record);
assert!(
decoded[end..].iter().all(is_zero),
"{} ending before key {end} defaulted to {:?}",
pin.record,
&decoded[end..]
);
}
}
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
}

View file

@ -0,0 +1,508 @@
use cultcache_rs::DatabaseEntry;
use serde_json::Value;
pub const MUNINN_TELEMETRY_SURFACE_SCHEMA: &str = "muninn.telemetry_surface.v1";
pub const MUNINN_CAPTURE_STREAM_SCHEMA: &str = "muninn.capture_stream.v1";
pub const MUNINN_CAPTURE_STREAM_COMMAND_SCHEMA: &str = "muninn.capture_stream_command.v1";
pub const MUNINN_OBS_STREAM_CATALOG_SCHEMA: &str = "muninn.obs_stream_catalog.v1";
pub const MUNINN_MOVE_MARKER_CANDIDATE_SCHEMA: &str = "muninn.move_marker_candidate.v1";
pub const MUNINN_MOVE_CONTROLLER_STATE_SCHEMA: &str = "muninn.move_controller_state.v1";
pub const MUNINN_HID_CONTROLLER_STATE_SCHEMA: &str = "muninn.hid_controller_state.v1";
pub const MUNINN_MOVE_IDENTITY_SCHEMA: &str = "muninn.move_identity.v1";
pub const MUNINN_MOVE_LIGHT_COMMAND_SCHEMA: &str = "muninn.move_light_command.v1";
pub const MUNINN_MOVE_HUE_PROGRAM_SCHEMA: &str = "muninn.move_hue_program.v1";
pub const MUNINN_MOVE_TRACKER_HEALTH_SCHEMA: &str = "muninn.move_tracker_health.v1";
pub const MUNINN_MOVE_EVIDENCE_TRANSPORT_HEALTH_SCHEMA: &str =
"muninn.move_evidence_transport_health.v1";
pub const MUNINN_QUEST_ACCESS_SCHEMA: &str = "muninn.quest_access.v1";
pub const MUNINN_COMMAND_BOUNDARY_SCHEMA: &str = "muninn.command_boundary.v1";
pub const MUNINN_TRANSPORT_PROFILE_SCHEMA: &str = "muninn.transport_profile.v1";
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "muninn.capture_stream", schema = "muninn.capture_stream.v1")]
pub struct MuninnCaptureStreamRecord {
#[cultcache(key = 0)]
pub stream_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub state: String,
#[cultcache(key = 3)]
pub video_source: String,
#[cultcache(key = 4)]
pub audio_source: String,
#[cultcache(key = 5)]
pub transport: String,
#[cultcache(key = 6)]
pub targets: Vec<String>,
#[cultcache(key = 7)]
pub command_witness: String,
#[cultcache(key = 8)]
pub supervisor_pid: Option<u32>,
#[cultcache(key = 9)]
pub mux_pid: Option<u32>,
#[cultcache(key = 10)]
pub restart_count: u32,
#[cultcache(key = 11)]
pub detail: String,
#[cultcache(key = 12)]
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "muninn.capture_stream_command",
schema = "muninn.capture_stream_command.v1"
)]
pub struct MuninnCaptureStreamCommandRecord {
#[cultcache(key = 0)]
pub command_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub stream_id: String,
#[cultcache(key = 3)]
pub state: String,
#[cultcache(key = 4)]
pub action: String,
#[cultcache(key = 5)]
pub target_host: String,
#[cultcache(key = 6)]
pub port: u16,
#[cultcache(key = 7)]
pub obs_target_host: Option<String>,
#[cultcache(key = 8)]
pub obs_port: u16,
#[cultcache(key = 9)]
pub media_transport: String,
#[cultcache(key = 10)]
pub media_packet_bytes: u32,
#[cultcache(key = 11)]
pub requested_by: String,
#[cultcache(key = 12)]
pub detail: String,
#[cultcache(key = 13)]
pub updated_at: String,
#[cultcache(key = 14, default)]
pub rudp_video_bitrate_kbps: u32,
#[cultcache(key = 15, default)]
pub rudp_latency_budget_ms: u32,
#[cultcache(key = 16, default)]
pub video_source_id: String,
#[cultcache(key = 17, default)]
pub audio_source_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "muninn.telemetry_surface",
schema = "muninn.telemetry_surface.v1"
)]
pub struct MuninnTelemetrySurfaceRecord {
#[cultcache(key = 0)]
pub surface_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub state: String,
#[cultcache(key = 3)]
pub available_sources: Vec<String>,
#[cultcache(key = 4)]
pub stream_affordances: Vec<String>,
#[cultcache(key = 5)]
pub active_streams: Vec<String>,
#[cultcache(key = 6)]
pub activation_authority: String,
#[cultcache(key = 7)]
pub detail: String,
#[cultcache(key = 8)]
pub updated_at: String,
#[cultcache(key = 9, default)]
pub primary_stream_id: String,
#[cultcache(key = 10, default)]
pub primary_stream_label: String,
#[cultcache(key = 11, default)]
pub command_rudp_target: String,
#[cultcache(key = 12, default)]
pub media_target_host: String,
#[cultcache(key = 13, default)]
pub media_port: u16,
#[cultcache(key = 14, default)]
pub media_packet_bytes: u32,
#[cultcache(key = 15, default)]
pub rudp_video_bitrate_kbps: u32,
#[cultcache(key = 16, default)]
pub rudp_latency_budget_ms: u32,
#[cultcache(key = 17, default)]
pub video_source_ids: Vec<String>,
#[cultcache(key = 18, default)]
pub video_source_labels: Vec<String>,
#[cultcache(key = 19, default)]
pub audio_source_ids: Vec<String>,
#[cultcache(key = 20, default)]
pub audio_source_labels: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "muninn.obs_stream_catalog",
schema = "muninn.obs_stream_catalog.v1"
)]
pub struct MuninnObsStreamCatalogRecord {
#[cultcache(key = 0)]
pub catalog_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub stream_ids: Vec<String>,
#[cultcache(key = 3)]
pub labels: Vec<String>,
#[cultcache(key = 4)]
pub urls: Vec<String>,
#[cultcache(key = 5)]
pub states: Vec<String>,
#[cultcache(key = 6)]
pub updated_at: String,
#[cultcache(key = 7, default)]
pub command_rudp_target: String,
#[cultcache(key = 8, default)]
pub media_target_host: String,
#[cultcache(key = 9, default)]
pub media_port: u16,
#[cultcache(key = 10, default)]
pub media_packet_bytes: u32,
#[cultcache(key = 11, default)]
pub rudp_video_bitrate_kbps: u32,
#[cultcache(key = 12, default)]
pub rudp_latency_budget_ms: u32,
#[cultcache(key = 13, default)]
pub video_source_ids: Vec<String>,
#[cultcache(key = 14, default)]
pub video_source_labels: Vec<String>,
#[cultcache(key = 15, default)]
pub audio_source_ids: Vec<String>,
#[cultcache(key = 16, default)]
pub audio_source_labels: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "muninn.move_marker_candidate",
schema = "muninn.move_marker_candidate.v1"
)]
pub struct MuninnMoveMarkerCandidateRecord {
#[cultcache(key = 0)]
pub stream_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub camera_id: String,
#[cultcache(key = 3)]
pub frame_sequence: u64,
#[cultcache(key = 4)]
pub source_id_hash: u64,
#[cultcache(key = 5)]
pub tile_x: u32,
#[cultcache(key = 6)]
pub tile_y: u32,
#[cultcache(key = 7)]
pub center_x_px: f32,
#[cultcache(key = 8)]
pub center_y_px: f32,
#[cultcache(key = 9)]
pub radius_px: f32,
#[cultcache(key = 10)]
pub area_px: u32,
#[cultcache(key = 11)]
pub mean_luma: f32,
#[cultcache(key = 12)]
pub peak_luma: u32,
#[cultcache(key = 13)]
pub score: f32,
#[cultcache(key = 14)]
pub observed_at: String,
#[cultcache(key = 15, default)]
pub move_id: String,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "muninn.move_controller_state",
schema = "muninn.move_controller_state.v1"
)]
pub struct MuninnMoveControllerStateRecord {
#[cultcache(key = 0)]
pub stream_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub move_id: String,
#[cultcache(key = 3)]
pub sequence: u64,
#[cultcache(key = 4)]
pub source_timestamp_ns: i64,
#[cultcache(key = 5)]
pub accelerometer_xyz: Vec<f32>,
#[cultcache(key = 6)]
pub gyroscope_xyz: Vec<f32>,
#[cultcache(key = 7)]
pub magnetometer_xyz: Vec<f32>,
#[cultcache(key = 8)]
pub trigger_value: f32,
#[cultcache(key = 9)]
pub buttons: Vec<String>,
#[cultcache(key = 10)]
pub battery01: f32,
#[cultcache(key = 11)]
pub observed_at: String,
#[cultcache(key = 12, default)]
pub source_path: String,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "muninn.hid_controller_state",
schema = "muninn.hid_controller_state.v1"
)]
pub struct MuninnHidControllerStateRecord {
#[cultcache(key = 0)]
pub stream_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub device_id: String,
#[cultcache(key = 3)]
pub device_kind: String,
#[cultcache(key = 4)]
pub sequence: u64,
#[cultcache(key = 5)]
pub source_timestamp_ns: i64,
#[cultcache(key = 6)]
pub axes: Vec<f32>,
#[cultcache(key = 7)]
pub buttons: Vec<String>,
#[cultcache(key = 8)]
pub battery01: f32,
#[cultcache(key = 9)]
pub observed_at: String,
#[cultcache(key = 10)]
pub source_path: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "muninn.move_identity", schema = "muninn.move_identity.v1")]
pub struct MuninnMoveIdentityRecord {
#[cultcache(key = 0)]
pub identity_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub move_id: String,
#[cultcache(key = 3)]
pub source_path: String,
#[cultcache(key = 4)]
pub bluetooth_host_address: String,
#[cultcache(key = 5)]
pub state: String,
#[cultcache(key = 6)]
pub detail: String,
#[cultcache(key = 7)]
pub observed_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "muninn.move_light_command",
schema = "muninn.move_light_command.v1"
)]
pub struct MuninnMoveLightCommandRecord {
#[cultcache(key = 0)]
pub command_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub move_id: String,
#[cultcache(key = 3)]
pub hidraw_path: String,
#[cultcache(key = 4)]
pub colors: Vec<String>,
#[cultcache(key = 5)]
pub durations_ms: Vec<u32>,
#[cultcache(key = 6)]
pub repeat_count: u32,
#[cultcache(key = 7)]
pub authority: String,
#[cultcache(key = 8)]
pub state: String,
#[cultcache(key = 9)]
pub detail: String,
#[cultcache(key = 10)]
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "muninn.move_hue_program",
schema = "muninn.move_hue_program.v1"
)]
pub struct MuninnMoveHueProgramRecord {
#[cultcache(key = 0)]
pub program_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub mode: String,
#[cultcache(key = 3)]
pub cycle_ms: u64,
#[cultcache(key = 4)]
pub epoch_ns: i64,
#[cultcache(key = 5)]
pub hold_at_ns: i64,
#[cultcache(key = 6)]
pub requested_by: String,
#[cultcache(key = 7)]
pub updated_at: String,
#[cultcache(key = 8, default)]
pub order_mode: String,
#[cultcache(key = 9, default)]
pub transition_percent: u8,
#[cultcache(key = 10, default)]
pub transition_percent_explicit: bool,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "muninn.move_tracker_health",
schema = "muninn.move_tracker_health.v1"
)]
pub struct MuninnMoveTrackerHealthRecord {
#[cultcache(key = 0)]
pub health_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub camera_id: String,
#[cultcache(key = 3)]
pub camera_index: i32,
#[cultcache(key = 4)]
pub state: String,
#[cultcache(key = 5)]
pub camera_name: String,
#[cultcache(key = 6)]
pub camera_api: String,
#[cultcache(key = 7)]
pub width: u32,
#[cultcache(key = 8)]
pub height: u32,
#[cultcache(key = 9)]
pub exposure: f32,
#[cultcache(key = 10)]
pub calibrated_controller_count: u32,
#[cultcache(key = 11)]
pub update_count: u64,
#[cultcache(key = 12)]
pub observation_count: u64,
#[cultcache(key = 13)]
pub latest_observation_count: u32,
#[cultcache(key = 14)]
pub last_observation_at: String,
#[cultcache(key = 15)]
pub detail: String,
#[cultcache(key = 16)]
pub updated_at: String,
#[cultcache(key = 17, default)]
pub image_mean_rgb: Vec<u32>,
#[cultcache(key = 18, default)]
pub image_peak_rgb: Vec<u32>,
#[cultcache(key = 19, default)]
pub color_evidence_move_ids: Vec<String>,
#[cultcache(key = 20, default)]
pub color_evidence_pixel_counts: Vec<u32>,
#[cultcache(key = 21, default)]
pub rejected_stale_count: u64,
#[cultcache(key = 22, default)]
pub rejected_radius_count: u64,
#[cultcache(key = 23, default)]
pub rejected_bounds_count: u64,
#[cultcache(key = 24, default)]
pub rejected_continuity_count: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "muninn.move_evidence_transport_health",
schema = "muninn.move_evidence_transport_health.v1"
)]
pub struct MuninnMoveEvidenceTransportHealthRecord {
#[cultcache(key = 0)]
pub health_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub stream_id: String,
#[cultcache(key = 3)]
pub produced_frames: u64,
#[cultcache(key = 4)]
pub local_ring_admissions: u64,
#[cultcache(key = 5)]
pub remote_handoffs: u64,
#[cultcache(key = 6)]
pub remote_sends: u64,
#[cultcache(key = 7)]
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "muninn.quest_access", schema = "muninn.quest_access.v1")]
pub struct MuninnQuestAccessRecord {
#[cultcache(key = 0)]
pub access_id: String,
#[cultcache(key = 1)]
pub host_id: String,
#[cultcache(key = 2)]
pub serial: String,
#[cultcache(key = 3)]
pub connection_state: String,
#[cultcache(key = 4)]
pub product: String,
#[cultcache(key = 5)]
pub model: String,
#[cultcache(key = 6)]
pub device: String,
#[cultcache(key = 7)]
pub transport_id: String,
#[cultcache(key = 8)]
pub input_stream_id: String,
#[cultcache(key = 9)]
pub pose_stream_id: String,
#[cultcache(key = 10)]
pub video_input_stream_id: String,
#[cultcache(key = 11)]
pub video_input_transport: String,
#[cultcache(key = 12)]
pub state: String,
#[cultcache(key = 13)]
pub detail: String,
#[cultcache(key = 14)]
pub observed_at: String,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "muninn.command_boundary",
schema = "muninn.command_boundary.v1"
)]
pub struct MuninnCommandBoundaryCompatRecord {
#[cultcache(key = 0)]
pub value: Value,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "muninn.transport_profile",
schema = "muninn.transport_profile.v1"
)]
pub struct MuninnTransportProfileCompatRecord {
#[cultcache(key = 0)]
pub value: Value,
}

View file

@ -19,6 +19,7 @@ cultcache-rs = { workspace = true }
cultmesh-rs = { workspace = true }
cultnet-rs = { workspace = true }
libc = "0.2"
muninn-contracts = { path = "../muninn-contracts" }
muninn-move-tracker = { path = "../muninn-move-tracker" }
muninn-psmoveapi-tracker = { path = "../muninn-psmoveapi-tracker", optional = true }
odin-core = { workspace = true }

View file

@ -21,20 +21,20 @@ use cultnet_rs::{
GAMECULT_MEDIA_STREAM_REQUEST_SCHEMA, GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA,
GameCultMediaStreamAdvertisementRecord, GameCultMediaStreamRequestRecord,
GameCultRuntimePresenceHealthRecord, decode_cultnet_message_from_slice,
encode_cultnet_message_to_vec, media_stream_request_key, validate_media_stream_request,
media_stream_request_key, validate_media_stream_request,
};
#[cfg(feature = "psmoveapi-tracker")]
use odin_core::MuninnMoveTrackerHealthRecord;
use odin_core::{
EveProviderAdvertisementRecord, EveSurfaceStateRecord, IdunnDaemonHealthRecord,
use muninn_contracts::{
MUNINN_MOVE_HUE_PROGRAM_SCHEMA, MUNINN_OBS_STREAM_CATALOG_SCHEMA,
MuninnCaptureStreamCommandRecord, MuninnCaptureStreamRecord, MuninnCommandBoundaryCompatRecord,
MuninnHidControllerStateRecord, MuninnMoveControllerStateRecord,
MuninnMoveEvidenceTransportHealthRecord, MuninnMoveHueProgramRecord, MuninnMoveIdentityRecord,
MuninnMoveLightCommandRecord, MuninnMoveMarkerCandidateRecord, MuninnObsStreamCatalogRecord,
MuninnQuestAccessRecord, MuninnTelemetrySurfaceRecord, MuninnTransportProfileCompatRecord,
OdinDocuments,
MuninnQuestAccessRecord, MuninnRecords, MuninnTelemetrySurfaceRecord,
MuninnTransportProfileCompatRecord,
};
#[cfg(feature = "psmoveapi-tracker")]
use muninn_contracts::MuninnMoveTrackerHealthRecord;
use odin_core::{EveProviderAdvertisementRecord, EveSurfaceStateRecord, OdinDocuments};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::{HashMap, HashSet};
@ -70,7 +70,6 @@ use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::UI::Input::XboxController::{XINPUT_GAMEPAD, XINPUT_STATE, XInputGetState};
const CULTNET_RUDP_PROTOCOL_ID: &str = "cultnet.transport.rudp.v0";
const IDUNN_HEALTH_RUDP_CONNECTION_ID: u32 = 0x1d0d_0001;
const MUNINN_MEDIA_RUDP_CONNECTION_ID: u32 = 0x6d75_0001;
const MUNINN_HID_CONTROLLER_RUDP_CONNECTION_ID: u32 = 0x6d75_0005;
const MUNINN_COMMAND_RUDP_CONNECTION_ID: u32 = 0x61e0_0001;
@ -214,7 +213,6 @@ struct Options {
quest_input_stream_id: Option<String>,
quest_pose_stream_id: Option<String>,
quest_video_input_stream_id: Option<String>,
idunn_rudp_health: Option<IdunnRudpHealthOptions>,
odin_cultmesh_uri: Option<String>,
hid_controller_rudp_target: Option<SocketAddr>,
hid_controller_rudp_bind: Option<SocketAddr>,
@ -275,13 +273,6 @@ struct MoveStateSource {
hidraw_path: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct IdunnRudpHealthOptions {
endpoint: SocketAddr,
daemon_id: String,
health_contract: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct MuxPlan {
command_line: String,
@ -406,7 +397,6 @@ fn serve(options: Options) -> Result<()> {
let move_runtime_enabled = serve_should_manage_move_runtime(&options);
let mut active_move_states =
active_move_state_sources(serve_move_state_sources(&options, move_runtime_enabled));
start_daemon_health_worker(&options);
start_idunn_presence_worker(&options);
let mut active_move_marker_cameras =
active_move_marker_camera_sources(&options, Arc::clone(&move_hue_program));
@ -613,21 +603,6 @@ fn start_idunn_presence_worker(options: &Options) {
});
}
fn start_daemon_health_worker(options: &Options) {
if options.idunn_rudp_health.is_none() {
return;
}
let options = options.clone();
thread::spawn(move || {
loop {
if let Err(error) = run_daemon_health_publisher(&options) {
eprintln!("Muninn Idunn health publish failed: {error:#}");
}
thread::sleep(Duration::from_secs(5));
}
});
}
fn move_hue_program_key(host_id: &str) -> String {
format!("muninn:{host_id}:move-hue-program")
}
@ -2846,16 +2821,8 @@ fn publish_runtime_boundary_records(
.as_ref()
.map(|command| json!([command]))
.unwrap_or_else(|| json!([]));
let current_transport = if options.idunn_rudp_health.is_some() {
"daemon-published-rudp-health + daemon-owned-cultcache-telemetry-store"
} else {
"daemon-owned-cultcache-telemetry-store + missing-rudp-health-publication"
};
let transport_state = if options.idunn_rudp_health.is_some() {
"rudp-health-and-provider-store-live"
} else {
"cultcache-provider-store-only"
};
let current_transport = "daemon-owned-cultcache-telemetry-store";
let transport_state = "cultcache-provider-store-only";
let hid_controller_endpoint = options.hid_controller_rudp_bind.map(|bind| {
options
.hid_controller_rudp_advertise
@ -2994,18 +2961,6 @@ fn publish_runtime_boundary_records(
"state_store": store_path,
"log_root": log_root,
"lifecycle_authority": "idunn-supervisor-command",
"health_publication": options.idunn_rudp_health.as_ref().map(|idunn| json!({
"contract": idunn.health_contract,
"transport": CULTNET_RUDP_PROTOCOL_ID,
"publication_source": "daemon-published",
"endpoint": idunn.endpoint.to_string(),
"state_owner": "Muninn serve process"
})).unwrap_or_else(|| json!({
"contract": serde_json::Value::Null,
"transport": "unconfigured",
"publication_source": "missing-daemon-publication",
"state_owner": "Muninn local store"
})),
"commands": [
{
"command": "muninn.capture_stream_command",
@ -3053,7 +3008,7 @@ fn publish_runtime_boundary_records(
"target_transport": CULTNET_RUDP_PROTOCOL_ID,
"current_transport": current_transport,
"state": transport_state,
"health_contract": options.idunn_rudp_health.as_ref().map(|idunn| idunn.health_contract.clone()),
"health_contract": serde_json::Value::Null,
"provider_advertisement_schema": "gamecult.eve.provider_advertisement.v1",
"command_boundary_schema": "muninn.command_boundary.v1",
"telemetry_surface_schema": "muninn.telemetry_surface.v1",
@ -3191,9 +3146,6 @@ fn muninn_provider_id(options: &Options) -> String {
}
fn muninn_daemon_id(options: &Options) -> String {
if let Some(idunn) = options.idunn_rudp_health.as_ref() {
return idunn.daemon_id.clone();
}
match options.host_id.as_str() {
"raven" => "muninn".to_string(),
"starfire" => "starfire-muninn".to_string(),
@ -8598,31 +8550,9 @@ fn quest_access_status(options: Options) -> Result<()> {
}
fn health_check(options: &Options) -> Result<()> {
let observed_at = idunn_timestamp()?;
let result = evaluate_health(options);
let (state, detail) = match &result {
Ok(detail) => ("active", detail.clone()),
Err(error) => ("failed", error.to_string()),
};
if let Some(idunn) = options.idunn_rudp_health.as_ref() {
let publish_result = publish_idunn_rudp_health(idunn, state, &detail, &observed_at);
if let Err(publish_error) = publish_result {
return match result {
Ok(_) => Err(publish_error).context("publishing Muninn health to Idunn RUDP"),
Err(health_error) => Err(anyhow!(
"{health_error}; also failed to publish Muninn health to Idunn RUDP: {publish_error}"
)),
};
}
}
match result {
Ok(detail) => {
println!("{detail}");
Ok(())
}
Err(error) => Err(error),
}
let detail = evaluate_health(options)?;
println!("{detail}");
Ok(())
}
fn evaluate_health(options: &Options) -> Result<String> {
@ -8674,22 +8604,6 @@ fn evaluate_health_from_node(
))
}
fn publish_idunn_rudp_health(
options: &IdunnRudpHealthOptions,
state: &str,
detail: &str,
observed_at: &str,
) -> Result<()> {
let mut transport = connect_idunn_rudp_health(options)?;
transport
.send(
"schema",
idunn_health_payload(options, state, detail, observed_at)?,
)
.with_context(|| format!("sending Idunn health to {}", options.endpoint))?;
Ok(())
}
fn smootherstep(value: f64) -> f64 {
let t = value.clamp(0.0, 1.0);
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
@ -8700,104 +8614,6 @@ fn wrapped_hue_lerp(source: f64, target: f64, amount: f64) -> f64 {
(source + delta * amount).rem_euclid(1.0)
}
fn run_daemon_health_publisher(options: &Options) -> Result<()> {
let idunn = options
.idunn_rudp_health
.as_ref()
.context("Muninn daemon health publisher requires Idunn RUDP options")?;
let mut transport = connect_idunn_rudp_health(idunn)?;
let cadence = Duration::from_secs(options.interval_seconds.unwrap_or(15).max(1));
let mut next_publish_at = Instant::now();
loop {
if Instant::now() >= next_publish_at {
let observed_at = idunn_timestamp()?;
let (state, detail) = match evaluate_health(options) {
Ok(detail) => ("active", detail),
Err(error) => ("failed", error.to_string()),
};
transport
.send(
"schema",
idunn_health_payload(idunn, state, &detail, &observed_at)?,
)
.with_context(|| format!("sending Idunn health to {}", idunn.endpoint))?;
next_publish_at = Instant::now() + cadence;
}
let _ = transport.receive_once()?;
transport.poll_resends()?;
}
}
fn idunn_health_payload(
options: &IdunnRudpHealthOptions,
state: &str,
detail: &str,
observed_at: &str,
) -> Result<Vec<u8>> {
let health = IdunnDaemonHealthRecord {
daemon_id: options.daemon_id.clone(),
state: state.to_string(),
detail: detail.to_string(),
observed_at: observed_at.to_string(),
health_contract: options.health_contract.clone(),
publication_source: "daemon-published".to_string(),
transport: CULTNET_RUDP_PROTOCOL_ID.to_string(),
};
let message = CultNetMessage::DocumentPutRaw {
message_id: format!(
"muninn-health:{}:{}",
options.daemon_id,
observed_at.replace(':', "-")
),
document: CultNetRawDocumentRecord {
schema_id: "idunn.daemon_health".to_string(),
record_key: options.daemon_id.clone(),
stored_at: observed_at.to_string(),
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
payload: rmp_serde::to_vec(&health).context("encoding Idunn daemon health")?,
source_runtime_id: Some("muninn-daemon".to_string()),
source_agent_id: None,
source_role: Some("daemon-health-publisher".to_string()),
tags: Some(vec![CULTNET_RUDP_PROTOCOL_ID.to_string()]),
},
};
encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)
.context("encoding Idunn health CultNet message")
}
fn connect_idunn_rudp_health(
options: &IdunnRudpHealthOptions,
) -> Result<CultNetRudpSocketTransportConnection> {
let bind_address = if options.endpoint.is_ipv4() {
"0.0.0.0:0"
} else {
"[::]:0"
};
let socket = UdpSocket::bind(bind_address)
.with_context(|| format!("binding Muninn RUDP sender at {bind_address}"))?;
socket.set_read_timeout(Some(Duration::from_secs(1)))?;
let mut transport =
CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions::client(
"muninn-daemon",
socket,
options.endpoint,
IDUNN_HEALTH_RUDP_CONNECTION_ID,
))?;
transport.connect(Vec::new())?;
let deadline = Instant::now() + Duration::from_secs(2);
while !transport.connected() {
let _ = transport.receive_once()?;
transport.poll_resends()?;
if Instant::now() >= deadline {
return Err(anyhow!(
"timed out connecting Muninn RUDP sender to {}",
options.endpoint
));
}
}
Ok(transport)
}
fn verify_move_sources_fresh(options: &Options, node: &cultmesh_rs::CultMeshNode) -> Result<()> {
let move_state_sources =
serve_move_state_sources(options, serve_should_manage_move_runtime(options));
@ -9710,15 +9526,21 @@ fn ensure_state_dirs(options: &Options) -> Result<()> {
Ok(())
}
/// Odin's document set plus the producer-agnostic media stream contract.
/// Muninn advertises through the latter and takes requests through it; the
/// former is what the rest of its state still speaks.
/// Odin's document set, Muninn's own records, and the producer-agnostic media
/// stream contract. Today Odin's document set (odin-core) is registered first
/// and carries identical `muninn.*` type names and schema ids, so
/// `MuninnRecords` registers the same strings again and binds nothing new;
/// cultcache errors at startup if the two ever diverge. Odin's copies are to be
/// deleted, at which point `muninn-contracts` becomes the sole owner of every
/// `muninn.*` binding. The registration stays so that deletion changes nothing
/// here.
#[derive(Clone, Copy, Debug, Default)]
struct MuninnDocuments;
impl cultmesh_rs::CultMeshDocumentSet for MuninnDocuments {
fn register_cache(&self, cache: &mut cultcache_rs::CultCache) -> Result<()> {
OdinDocuments.register_cache(cache)?;
MuninnRecords.register_cache(cache)?;
cache.register_entry_type::<GameCultMediaStreamAdvertisementRecord>()?;
cache.register_entry_type::<GameCultMediaStreamRequestRecord>()?;
cache.register_entry_type::<GameCultRuntimePresenceHealthRecord>()?;
@ -9727,6 +9549,7 @@ impl cultmesh_rs::CultMeshDocumentSet for MuninnDocuments {
fn register_documents(&self, registry: &mut cultnet_rs::CultNetDocumentRegistry) -> Result<()> {
OdinDocuments.register_documents(registry)?;
MuninnRecords.register_documents(registry)?;
registry.register(
cultnet_rs::CultNetDocumentBinding::for_entry_with_schema_id::<
GameCultMediaStreamAdvertisementRecord,
@ -9837,7 +9660,6 @@ impl Options {
quest_input_stream_id: None,
quest_pose_stream_id: None,
quest_video_input_stream_id: None,
idunn_rudp_health: None,
odin_cultmesh_uri: Some("cultmesh://odin/rendezvous/provider-catalog".to_string()),
hid_controller_rudp_target: None,
hid_controller_rudp_bind: None,
@ -9848,9 +9670,6 @@ impl Options {
};
let mut args = args.peekable();
let mut idunn_rudp_health_endpoint = None;
let mut idunn_daemon_id = None;
let mut idunn_health_contract = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"serve" => options.mode = Mode::Serve,
@ -10087,19 +9906,6 @@ impl Options {
options.quest_video_input_stream_id =
Some(take_value(&mut args, "--quest-video-input-stream")?)
}
"--idunn-rudp-health" => {
idunn_rudp_health_endpoint = Some(
take_value(&mut args, "--idunn-rudp-health")?
.parse()
.context("--idunn-rudp-health must be a socket address")?,
)
}
"--idunn-daemon" => {
idunn_daemon_id = Some(take_value(&mut args, "--idunn-daemon")?)
}
"--idunn-health-contract" => {
idunn_health_contract = Some(take_value(&mut args, "--idunn-health-contract")?)
}
"--odin-cultmesh-rudp" => {
let _ = take_value(&mut args, "--odin-cultmesh-rudp")?;
return Err(anyhow!(
@ -10287,32 +10093,11 @@ impl Options {
options.quest_video_input_stream_id.as_ref(),
),
("--move-host", options.move_host_address.as_ref()),
("--idunn-daemon", idunn_daemon_id.as_ref()),
("--idunn-health-contract", idunn_health_contract.as_ref()),
] {
if value.is_some_and(|value| value.trim().is_empty()) {
return Err(anyhow!("{name} must be non-empty"));
}
}
options.idunn_rudp_health = match (
idunn_rudp_health_endpoint,
idunn_daemon_id,
idunn_health_contract,
) {
(None, None, None) => None,
(Some(endpoint), Some(daemon_id), Some(health_contract)) => {
Some(IdunnRudpHealthOptions {
endpoint,
daemon_id,
health_contract,
})
}
_ => {
return Err(anyhow!(
"--idunn-rudp-health, --idunn-daemon, and --idunn-health-contract must be provided together"
));
}
};
Ok(options)
}
}
@ -10330,14 +10115,6 @@ fn timestamp() -> Result<String> {
Ok(format!("unix-{seconds}"))
}
fn idunn_timestamp() -> Result<String> {
let seconds = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system clock is before Unix epoch")?
.as_secs();
Ok(format!("unix:{seconds}"))
}
fn timestamp_ns() -> Result<i64> {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -10406,7 +10183,7 @@ fn parse_move_marker_camera_source(value: &str) -> Result<MoveMarkerCameraSource
}
fn help_text() -> &'static str {
"Usage: muninn [serve|activate|request-stream|capture-stream-status|obs-catalog-status|request-move-light|move-light-status|move-identity-status|move-source-status|move-state-status|claim-move-host|quest-access-status] [--store <path>] [--activate-store <path>] [--stream-action <start|stop>] [--target-host <cultmesh-uri>] [--media-transport <rudp>] [--media-rudp-bind <addr>] [--media-rudp-advertise <host:port>] [--media-packet-bytes <bytes>] [--rudp-video-bitrate-kbps <kbps>] [--rudp-latency-budget-ms <ms>] [--video-source <source-id=label>] [--audio-source <source-id=label>] [--audio-source-id <source-id>] [--no-video] [--no-audio] [--loopback-script <path>] [--ffmpeg <path>] [--odin-cultmesh-uri <cultmesh-uri>] [--move-state <move-id>=<hidraw-path>] [--move-marker-camera <camera-id>=<device-path>] [--move-psmoveapi-tracker] [--move-tracker-exposure-milli <0..1000>] [--move-marker-width <px>] [--move-marker-height <px>] [--move-marker-fps <fps>] [--move-host <bt-addr>] [--move-evidence-stream <stream-id>] [--move-evidence-verse <verse-id>] [--move-evidence-ring-slots <slots>] [--move-evidence-slot-bytes <bytes>] [--move-evidence-snapshot <path>] [--quest-adb] [--quest-serial <serial>] [--quest-input-stream <stream-id>] [--quest-pose-stream <stream-id>] [--quest-video-input-stream <stream-id>] [--idunn-rudp-health <addr>] [--idunn-daemon <id>] [--idunn-health-contract <contract>] [--dry-run] [--health]\n\nMuninn is Odin's portable telemetry Verse assembler. serve publishes cheap typed telemetry affordances, optional Quest USB access surfaces, and the explicitly configured Move runtime; when serve receives --move-state, --move-marker-camera, --move-host, or --move-evidence-stream it may publish source-local Move controller state, source-local optical marker candidates, typed Move identity records, a CultMesh Move evidence stream, optionally write a latest one-copy Move proof evidence snapshot for Mimir field capture/replay, and keep USB-attached PS Moves claimed to that explicit Bluetooth host; --move-psmoveapi-tracker delegates camera exposure and optical extraction to the reference PSMoveAPI backend while Muninn retains stable-ID light actuation; serve consumes typed capture stream commands from its provider-owned activation store and owns the local ffmpeg/loopback activation child lifecycle, listens for media receivers on --media-rudp-bind and advertises that endpoint (--media-rudp-advertise when the bind address is not reachable as-is), and publishes its discovery advertisement through --odin-cultmesh-uri; activate starts an explicitly requested local CultNet RUDP stream as a daemon child that serves every receiver dialling --media-rudp-bind; request-stream discovers the provider through Odin and sends its typed command to that provider; obs-catalog-status pulls Odin-owned muninn.obs_stream_catalog discovery into the local compatibility store for OBS; capture-stream-status reads typed capture stream command receipts; use --no-video or --no-audio to request one leg over the CultNet RUDP media lane; request-move-light publishes a typed Move light command for Muninn serve to execute; move-light-status reads typed command receipts; move-identity-status reads typed Move identity records; move-source-status prints live Move source discovery; move-state-status reads typed controller-state records; claim-move-host assigns USB-attached PS Moves to a Bluetooth host; quest-access-status reads typed Quest access state. In --health mode, the Idunn RUDP flags publish the same typed daemon health document to Idunn for explicit diagnostics."
"Usage: muninn [serve|activate|request-stream|capture-stream-status|obs-catalog-status|request-move-light|move-light-status|move-identity-status|move-source-status|move-state-status|claim-move-host|quest-access-status] [--store <path>] [--activate-store <path>] [--stream-action <start|stop>] [--target-host <cultmesh-uri>] [--media-transport <rudp>] [--media-rudp-bind <addr>] [--media-rudp-advertise <host:port>] [--media-packet-bytes <bytes>] [--rudp-video-bitrate-kbps <kbps>] [--rudp-latency-budget-ms <ms>] [--video-source <source-id=label>] [--audio-source <source-id=label>] [--audio-source-id <source-id>] [--no-video] [--no-audio] [--loopback-script <path>] [--ffmpeg <path>] [--odin-cultmesh-uri <cultmesh-uri>] [--move-state <move-id>=<hidraw-path>] [--move-marker-camera <camera-id>=<device-path>] [--move-psmoveapi-tracker] [--move-tracker-exposure-milli <0..1000>] [--move-marker-width <px>] [--move-marker-height <px>] [--move-marker-fps <fps>] [--move-host <bt-addr>] [--move-evidence-stream <stream-id>] [--move-evidence-verse <verse-id>] [--move-evidence-ring-slots <slots>] [--move-evidence-slot-bytes <bytes>] [--move-evidence-snapshot <path>] [--quest-adb] [--quest-serial <serial>] [--quest-input-stream <stream-id>] [--quest-pose-stream <stream-id>] [--quest-video-input-stream <stream-id>] [--dry-run] [--health]\n\nMuninn is Odin's portable telemetry Verse assembler. serve publishes cheap typed telemetry affordances, optional Quest USB access surfaces, and the explicitly configured Move runtime; when serve receives --move-state, --move-marker-camera, --move-host, or --move-evidence-stream it may publish source-local Move controller state, source-local optical marker candidates, typed Move identity records, a CultMesh Move evidence stream, optionally write a latest one-copy Move proof evidence snapshot for Mimir field capture/replay, and keep USB-attached PS Moves claimed to that explicit Bluetooth host; --move-psmoveapi-tracker delegates camera exposure and optical extraction to the reference PSMoveAPI backend while Muninn retains stable-ID light actuation; serve consumes typed capture stream commands from its provider-owned activation store and owns the local ffmpeg/loopback activation child lifecycle, listens for media receivers on --media-rudp-bind and advertises that endpoint (--media-rudp-advertise when the bind address is not reachable as-is), and publishes its discovery advertisement through --odin-cultmesh-uri; activate starts an explicitly requested local CultNet RUDP stream as a daemon child that serves every receiver dialling --media-rudp-bind; request-stream discovers the provider through Odin and sends its typed command to that provider; obs-catalog-status pulls Odin-owned muninn.obs_stream_catalog discovery into the local compatibility store for OBS; capture-stream-status reads typed capture stream command receipts; use --no-video or --no-audio to request one leg over the CultNet RUDP media lane; request-move-light publishes a typed Move light command for Muninn serve to execute; move-light-status reads typed command receipts; move-identity-status reads typed Move identity records; move-source-status prints live Move source discovery; move-state-status reads typed controller-state records; claim-move-host assigns USB-attached PS Moves to a Bluetooth host; quest-access-status reads typed Quest access state."
}
fn parse_move_state_source(value: &str) -> Result<MoveStateSource> {
@ -10430,6 +10207,7 @@ fn parse_move_state_source(value: &str) -> Result<MoveStateSource> {
mod tests {
use super::*;
use crate::media_send::{MediaKind, QueuedMediaGroup};
use cultnet_rs::encode_cultnet_message_to_vec;
fn media_stream_request() -> GameCultMediaStreamRequestRecord {
GameCultMediaStreamRequestRecord {
@ -10875,39 +10653,20 @@ mod tests {
}
#[test]
fn health_idunn_rudp_options_are_explicit_bundle() {
let options = Options::parse(
[
"--health",
"--idunn-rudp-health",
"127.0.0.1:17870",
"--idunn-daemon",
"starfire-muninn",
"--idunn-health-contract",
"muninn.cultnet-rudp-local-telemetry-and-quest-access",
]
.into_iter()
.map(String::from),
)
.unwrap();
let idunn = options.idunn_rudp_health.unwrap();
assert_eq!(options.mode, Mode::Health);
assert_eq!(idunn.endpoint, "127.0.0.1:17870".parse().unwrap());
assert_eq!(idunn.daemon_id, "starfire-muninn");
assert_eq!(
idunn.health_contract,
"muninn.cultnet-rudp-local-telemetry-and-quest-access"
);
let error = Options::parse(
["--health", "--idunn-rudp-health", "127.0.0.1:17870"]
.into_iter()
.map(String::from),
)
.unwrap_err()
.to_string();
assert!(error.contains("must be provided together"));
fn retired_idunn_rudp_health_flags_are_unknown_arguments() {
for flag in [
"--idunn-rudp-health",
"--idunn-daemon",
"--idunn-health-contract",
] {
let error = Options::parse(["--health", flag, "127.0.0.1:17870"].into_iter().map(String::from))
.unwrap_err()
.to_string();
assert!(
error.contains(&format!("unknown Muninn argument: {flag}")),
"{flag} must be rejected, got: {error}"
);
}
}
#[test]
@ -10995,67 +10754,6 @@ mod tests {
assert!(error.contains("request-stream requires --odin-cultmesh-uri"));
}
#[test]
fn idunn_rudp_health_publisher_sends_raw_daemon_health_document() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
receiver
.set_read_timeout(Some(Duration::from_secs(1)))
.unwrap();
let receiver_addr = receiver.local_addr().unwrap();
let handle = thread::spawn(move || {
let mut transport = CultNetRudpSocketTransportConnection::new(
CultNetRudpSocketTransportOptions::server(
"idunn-test",
receiver,
IDUNN_HEALTH_RUDP_CONNECTION_ID,
),
)
.unwrap();
let deadline = Instant::now() + Duration::from_secs(2);
loop {
if let Some(frame) = transport.receive_once().unwrap() {
return cultnet_rs::decode_cultnet_message_from_slice(
&frame.payload,
CultNetWireContract::CultNetSchemaV0,
)
.unwrap();
}
transport.poll_resends().unwrap();
if Instant::now() >= deadline {
panic!("timed out waiting for Muninn RUDP health frame");
}
}
});
let options = IdunnRudpHealthOptions {
endpoint: receiver_addr,
daemon_id: "starfire-muninn".to_string(),
health_contract: "muninn.cultnet-rudp-local-telemetry-and-quest-access".to_string(),
};
publish_idunn_rudp_health(&options, "active", "Muninn healthy", "unix:100").unwrap();
let message = handle.join().unwrap();
let CultNetMessage::DocumentPutRaw { document, .. } = message else {
panic!("expected raw document put");
};
assert_eq!(document.schema_id, "idunn.daemon_health");
assert_eq!(document.record_key, "starfire-muninn");
assert_eq!(
document.payload_encoding,
CultNetRawPayloadEncoding::Messagepack
);
let health: IdunnDaemonHealthRecord = rmp_serde::from_slice(&document.payload).unwrap();
assert_eq!(health.daemon_id, "starfire-muninn");
assert_eq!(health.state, "active");
assert_eq!(
health.health_contract,
"muninn.cultnet-rudp-local-telemetry-and-quest-access"
);
assert_eq!(health.publication_source, "daemon-published");
assert_eq!(health.transport, CULTNET_RUDP_PROTOCOL_ID);
assert_eq!(health.observed_at, "unix:100");
}
#[test]
fn repeated_capture_stream_start_is_equivalent_when_media_shape_matches() {
let options = Options::parse(
@ -12970,6 +12668,64 @@ Device 00:07:04:A8:00:D0 (public)
let _ = fs::remove_file(store_path);
}
#[test]
fn help_names_no_retired_health_flag() {
assert!(!help_text().contains("--idunn"));
assert!(!help_text().contains("daemon health"));
}
#[test]
fn runtime_boundary_names_each_host_daemon_and_its_transport() {
for (host, daemon_id) in [
("raven", "muninn"),
("starfire", "starfire-muninn"),
("nightwing", "nightwing-muninn"),
] {
let store_path = std::env::temp_dir().join(format!(
"muninn-boundary-{host}-{}.cc",
timestamp_ns().unwrap()
));
let options = Options::parse(
["serve", "--host", host, "--store", store_path.to_str().unwrap()]
.into_iter()
.map(String::from),
)
.unwrap();
let mut node = open_node(&options, "muninn-boundary-host-test").unwrap();
publish_runtime_boundary_records(&mut node, &options, "idle", &[], &[]).unwrap();
let boundary = node
.get_required::<MuninnCommandBoundaryCompatRecord>(&format!(
"command-boundary:{daemon_id}"
))
.unwrap();
let transport = node
.get_required::<MuninnTransportProfileCompatRecord>(&format!(
"transport-profile:{daemon_id}"
))
.unwrap();
for record in [&boundary.value, &transport.value] {
assert_eq!(
record.get("daemon_id").and_then(|value| value.as_str()),
Some(daemon_id),
"{host}"
);
}
assert_eq!(
transport.value.get("current_transport").and_then(|value| value.as_str()),
Some("daemon-owned-cultcache-telemetry-store"),
"{host}"
);
assert_eq!(
transport.value.get("state").and_then(|value| value.as_str()),
Some("cultcache-provider-store-only"),
"{host}"
);
let _ = fs::remove_file(store_path);
}
}
#[test]
fn runtime_boundary_records_use_explicit_activation_store_path() {
let store_path =
@ -12985,12 +12741,6 @@ Device 00:07:04:A8:00:D0 (public)
"C:/Meta/Odin/state/muninn.activate.cc",
"--target-host",
"cultmesh://odin/media/muninn-raven-av",
"--idunn-rudp-health",
"198.51.100.10:17870",
"--idunn-daemon",
"muninn",
"--idunn-health-contract",
"muninn.cultnet-rudp-remote-telemetry-health",
]
.into_iter()
.map(String::from),
@ -13023,6 +12773,13 @@ Device 00:07:04:A8:00:D0 (public)
assert!(invocation.contains("request-stream"));
assert!(invocation.contains("--target-host cultmesh://odin/media/muninn-raven-av"));
assert_eq!(
boundary.value.get("daemon_id").and_then(|value| value.as_str()),
Some("muninn")
);
assert!(boundary.value.get("health_publication").is_none());
assert!(transport.value.get("health_contract").is_some_and(|value| value.is_null()));
let command_lowerings = transport
.value
.get("command_lowerings")

View file

@ -14,6 +14,7 @@ anyhow = "1"
cultmesh-rs = { workspace = true }
cultnet-rs = { workspace = true }
cultcache-rs = { workspace = true }
muninn-contracts = { path = "../muninn-contracts" }
odin-core = { workspace = true }
rmp-serde = "1.3"
serde_json = "1"

View file

@ -7,10 +7,11 @@ use cultnet_rs::{
CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions, CultNetWireContract,
encode_cultnet_message_to_vec,
};
use muninn_contracts::{MUNINN_HID_CONTROLLER_STATE_SCHEMA, MuninnHidControllerStateRecord};
use odin_core::{
EVE_PROVIDER_ADVERTISEMENT_SCHEMA, EveProviderAdvertisementRecord, EveSurfaceStateRecord,
IdunnDaemonHealthRecord, MUNINN_HID_CONTROLLER_STATE_SCHEMA, MuninnHidControllerStateRecord,
OdinDocuments, OdinEndpointQuery, SleipnirInputMappingRecord, discover_provider_endpoints,
IdunnDaemonHealthRecord, OdinDocuments, OdinEndpointQuery, SleipnirInputMappingRecord,
discover_provider_endpoints,
};
use std::collections::HashMap;
use std::env;

View file

@ -22,7 +22,7 @@ microphones, cameras, and future sensors.
```powershell
cargo build -p muninn-daemon
muninn serve --store C:\Meta\Odin\state\muninn.telemetry.cc --interval-seconds 15 --idunn-rudp-health $env:IDUNN_RUDP_HEALTH --idunn-daemon starfire-muninn --idunn-health-contract muninn.cultnet-rudp-local-telemetry-and-quest-access
muninn serve --store C:\Meta\Odin\state\muninn.telemetry.cc --interval-seconds 15
muninn --health --store C:\Meta\Odin\state\muninn.telemetry.cc
```
@ -97,10 +97,7 @@ E:\Projects\Odin\scripts\restart-starfire-muninn.cmd
E:\Projects\Odin\scripts\health-starfire-muninn.cmd
```
The restart script launches Muninn hidden with `--host starfire --quest-adb`,
`--idunn-rudp-health` from explicit `-IdunnRudpHealth` or
`IDUNN_RUDP_HEALTH`, `--idunn-daemon starfire-muninn`, and
`--idunn-health-contract muninn.cultnet-rudp-local-telemetry-and-quest-access`.
The restart script launches Muninn hidden with `--host starfire --quest-adb`.
If the CultCache store at `C:\Meta\Odin\state\starfire.muninn.telemetry.cc`
fails MessagePack decode on boot, the restart path archives the corrupt file,
clears the stale `.lock`, and relaunches the daemon instead of leaving the lane
@ -192,11 +189,7 @@ muninn move-light-status \
Idunn keeps the Muninn daemon alive. Idunn does not learn a Move-specific
watcher, and Mimir does not write HID directly except through temporary smoke
scripts used to prove hardware behavior before a Muninn daemon is available.
When `serve` is launched with `--idunn-rudp-health`, `--idunn-daemon`, and
`--idunn-health-contract`, the long-running Muninn body publishes
`idunn.daemon_health` directly to Idunn over `cultnet.transport.rudp.v0` on its
normal cadence. `--health` keeps the same publication path for manual proof and
compatibility probes, but the live owner is the daemon's `serve` process.
`--health` reads the telemetry store and reports; it publishes nothing.
Quest ADB probing is a telemetry input, not daemon liveness. If `adb` is
missing or the Quest is unavailable, Muninn publishes `muninn.quest_access` as
`unavailable` and keeps serving the local telemetry surface.
@ -235,10 +228,6 @@ actuators only, not lifecycle owners. The binary is installed at
`/home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc`, and the
restart actuator launches `serve --host nightwing --interval-seconds 15` with
`--move-state move-usb=/dev/input/by-id/usb-Sony_Computer_Entertainment_Motion_Controller-joystick`,
`--idunn-rudp-health` supplied by explicit `-IdunnRudpHealth` or
`IDUNN_RUDP_HEALTH`,
`--idunn-daemon nightwing-muninn`,
`--idunn-health-contract muninn.cultnet-rudp-remote-telemetry-and-move-hid`,
PID, and logs under
`/home/metacrat/.local/state/gamecult/muninn`.
With that Move state source attached, `serve` also publishes the
@ -248,10 +237,12 @@ Nightwing source discovery must emit one Move source per controller id. The USB
pairing collection and Bluetooth joystick can expose the same controller as
separate `/dev/input/js*` paths; `nightwing-move-state-sources.sh` prefers the
Bluetooth `HID_ID=0005:0000054C:000003D5` path so Idunn health does not demand
fresh records from two faces of the same controller. The Nightwing health
actuator requires the same configured Idunn RUDP endpoint, but that command is
fallback proof only; the live keepalive contract is now published by the
long-running Nightwing `serve` process.
fresh records from two faces of the same controller.
Starfire and Nightwing binaries and their `scripts/*-muninn.ps1` actuators
must be installed together: a binary that rejects the retired
`--idunn-rudp-health`, `--idunn-daemon` and `--idunn-health-contract` flags
fails to start under an old restart script, and a new script never passes them.
Each private PSMoveAPI Eye worker owns optical observation admission before the
parent aggregates evidence. Positions older than 50 ms, blobs below 2 px,

View file

@ -701,8 +701,7 @@ by continuity in under 10 s. The catalog on Odin named the new endpoint the
first time the restarted `serve` advertised.
Still Raven-specific and unmanaged: the two activation scripts and
`health-muninn.ps1`, which still passes the retired `--idunn-rudp-health`
flags. That flag family (`IdunnRudpHealthOptions`, `run_daemon_health_publisher`,
`idunn.daemon_health` documents) has no consumer since the previous Idunn
generation was retired and is the next cut; nightwing and starfire's restart
scripts still require it, so it goes with them.
`health-muninn.ps1`. The retired `--idunn-rudp-health` flag family
(`IdunnRudpHealthOptions`, `run_daemon_health_publisher`, `idunn.daemon_health`
documents) had no consumer since the previous Idunn generation was retired; it
was deleted from `muninn`, its five host scripts and `muninn.md` in named cut 3.

View file

@ -1,3 +1,3 @@
@echo off
echo health-muninn.cmd archived: Raven Muninn health is daemon-published over CultNet/RUDP as muninn.cultnet-rudp-remote-telemetry-health. Use health-muninn.ps1 only as an explicit witness inspection, not Idunn health truth.
echo health-muninn.cmd archived: Raven Muninn has no daemon-published health. Run health-muninn.ps1 as an explicit witness inspection (scheduled-task actions, serve process, store freshness); it is not Idunn health truth.
exit /b 2

View file

@ -5,7 +5,6 @@ param(
[string] $ActivateStorePath = "C:\Meta\Odin\state\muninn.activate.cc",
[string] $LogRoot = "C:\Meta\Odin\logs\muninn",
[int] $MaxStoreAgeSeconds = 180,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH,
[int] $ConnectTimeoutSeconds = 10,
[string] $SshUser = "madman's lullaby",
[string] $IdentityFile = ""
@ -13,10 +12,6 @@ param(
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
function Set-AsciiFile {
param(
[Parameter(Mandatory = $true)] [string] $Path,
@ -177,10 +172,7 @@ if (`$null -eq `$process) {
}
foreach (`$pattern in @(
"--host raven",
"--activate-store $ActivateStorePath",
"--idunn-rudp-health $IdunnRudpHealth",
"--idunn-daemon muninn",
"--idunn-health-contract muninn.cultnet-rudp-remote-telemetry-health"
"--activate-store $ActivateStorePath"
)) {
if (`$process.CommandLine -notlike "*`$pattern*") {
throw "Muninn serve process is missing expected command-line segment `${pattern}: `$(`$process.CommandLine)"

View file

@ -1,3 +1,3 @@
@echo off
echo health-nightwing-muninn.cmd archived: Nightwing Muninn health is daemon-published over CultNet/RUDP as muninn.cultnet-rudp-remote-telemetry-and-move-hid. Use health-nightwing-muninn.ps1 only as an explicit witness inspection, not Idunn health truth.
echo health-nightwing-muninn.cmd archived: Nightwing Muninn has no daemon-published health. Run health-nightwing-muninn.ps1 as an explicit witness inspection (serve process, store freshness, PID file); it is not Idunn health truth.
exit /b 2

View file

@ -5,16 +5,11 @@ param(
[string] $PidPath = "/home/metacrat/.local/state/gamecult/muninn/muninn.pid",
[string[]] $MoveState = @(),
[int] $IntervalSeconds = 15,
[int] $MaxStoreAgeSeconds = 180,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH
[int] $MaxStoreAgeSeconds = 180
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no WireGuard endpoint default is allowed."
}
function Set-AsciiFile {
param(
[Parameter(Mandatory = $true)] [string] $Path,
@ -76,16 +71,6 @@ if [ -z "`$process_line" ]; then
echo 'Nightwing Muninn serve command line is unavailable' >&2
exit 1
fi
for pattern in \
'--idunn-rudp-health $IdunnRudpHealth' \
'--idunn-daemon nightwing-muninn' \
'--idunn-health-contract muninn.cultnet-rudp-remote-telemetry-and-move-hid'
do
if ! printf '%s\n' "`$process_line" | grep -F -- "`$pattern" >/dev/null 2>&1; then
echo "Nightwing Muninn serve command line is missing `$pattern" >&2
exit 1
fi
done
if [ ! -f '$StorePath' ]; then
echo 'Muninn telemetry store is missing on Nightwing' >&2
exit 1

View file

@ -1,3 +1,3 @@
@echo off
echo health-starfire-muninn.cmd archived: Starfire Muninn health is daemon-published over CultNet/RUDP as muninn.cultnet-rudp-local-telemetry-and-quest-access. Use health-starfire-muninn.ps1 only as an explicit witness inspection, not Idunn health truth.
echo health-starfire-muninn.cmd archived: Starfire Muninn has no daemon-published health. Run health-starfire-muninn.ps1 as an explicit witness inspection (serve process, store freshness, PID file); it is not Idunn health truth.
exit /b 2

View file

@ -3,16 +3,11 @@ param(
[string] $StorePath = "C:\Meta\Odin\state\starfire.muninn.telemetry.cc",
[string] $LogRoot = "C:\Meta\Odin\logs\starfire-muninn",
[string[]] $MoveState = @(),
[int] $MaxStoreAgeSeconds = 180,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH
[int] $MaxStoreAgeSeconds = 180
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
if (-not (Test-Path -LiteralPath $MuninnExe)) {
throw "Muninn executable not found at $MuninnExe"
}
@ -28,15 +23,6 @@ $process = Get-CimInstance Win32_Process |
if ($null -eq $process) {
throw "Starfire Muninn serve process is not running with Quest access enabled"
}
foreach ($pattern in @(
"--idunn-rudp-health $IdunnRudpHealth",
"--idunn-daemon starfire-muninn",
"--idunn-health-contract muninn.cultnet-rudp-local-telemetry-and-quest-access"
)) {
if ($process.CommandLine -notlike "*$pattern*") {
throw "Starfire Muninn serve process is missing expected command-line segment ${pattern}: $($process.CommandLine)"
}
}
if (-not (Test-Path -LiteralPath $StorePath)) {
throw "Starfire Muninn telemetry store is missing at $StorePath"
}

View file

@ -17,9 +17,6 @@ param(
"nightwing-eye-1" = 100
},
[int] $IntervalSeconds = 15,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH,
[string] $IdunnDaemon = "nightwing-muninn",
[string] $IdunnHealthContract = "muninn.cultnet-rudp-remote-telemetry-and-move-hid",
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" }),
[string] $OdinCultMeshRudpEndpoint = $(if ($env:CULTMESH_URI_ODIN_RUDP) { $env:CULTMESH_URI_ODIN_RUDP } else { "10.77.0.2:17871" }),
[string] $HidControllerRudpBind = "0.0.0.0:17888",
@ -34,12 +31,6 @@ if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daem
throw "restart-nightwing-muninn.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
if ($IdunnRudpHealth -match '^(127\.0\.0\.1|localhost):(\d+)$') {
$IdunnRudpHealth = "10.77.0.2:$($Matches[2])"
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind) -and [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
throw "HID controller RUDP advertise endpoint must be supplied by -HidControllerRudpAdvertise or MUNINN_HID_CONTROLLER_RUDP_ADVERTISE when HID RUDP bind is enabled; no Nightwing LAN default is allowed."
}
@ -162,10 +153,7 @@ set -- serve \
--host nightwing
$moveRuntimeSetBlock
set -- "`$@" \
--interval-seconds '$IntervalSeconds' \
--idunn-rudp-health '$IdunnRudpHealth' \
--idunn-daemon '$IdunnDaemon' \
--idunn-health-contract '$IdunnHealthContract'
--interval-seconds '$IntervalSeconds'
$odinCultMeshUriSetBlock
$hidControllerRudpSetBlock
$commandRudpSetBlock
@ -179,10 +167,9 @@ muninn_pid="`$(cat '$LogRoot/muninn.pid')"
kill -0 "`$muninn_pid" 2>/dev/null
muninn_cmdline="`$(tr '\0' ' ' < "/proc/`$muninn_pid/cmdline")"
if ! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--host nightwing' >/dev/null 2>&1 ||
! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--idunn-rudp-health $IdunnRudpHealth' >/dev/null 2>&1 ||
! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--move-marker-camera nightwing-eye-0=/dev/video2' >/dev/null 2>&1 ||
! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--move-marker-camera nightwing-eye-1=/dev/video3' >/dev/null 2>&1; then
echo 'Nightwing Muninn serve command line is missing Idunn health or dual-camera arguments' >&2
echo 'Nightwing Muninn serve command line is missing dual-camera arguments' >&2
exit 1
fi
"@

View file

@ -6,9 +6,6 @@ param(
[string] $MoveBluetoothHost = "",
[string[]] $MoveState = @(),
[switch] $EnableUsbMoveState,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH,
[string] $IdunnDaemon = "starfire-muninn",
[string] $IdunnHealthContract = "muninn.cultnet-rudp-local-telemetry-and-quest-access",
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" }),
[string] $OdinCultMeshRudpEndpoint = $(if ($env:CULTMESH_URI_ODIN_RUDP) { $env:CULTMESH_URI_ODIN_RUDP } else { "127.0.0.1:17871" }),
[string] $HidControllerRudpBind = "",
@ -29,9 +26,6 @@ if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daem
if (-not (Test-Path -LiteralPath $MuninnExe)) {
throw "Muninn executable not found at $MuninnExe"
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind) -and [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
throw "HID controller RUDP advertise endpoint must be supplied by -HidControllerRudpAdvertise or MUNINN_HID_CONTROLLER_RUDP_ADVERTISE when HID RUDP bind is enabled; no Starfire LAN default is allowed."
}
@ -63,10 +57,7 @@ $arguments = @(
"--host", "starfire",
"--interval-seconds", "15",
"--quest-adb",
"--quest-serial", $QuestSerial,
"--idunn-rudp-health", $IdunnRudpHealth,
"--idunn-daemon", $IdunnDaemon,
"--idunn-health-contract", $IdunnHealthContract
"--quest-serial", $QuestSerial
)
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshUri)) {
$arguments += @("--odin-cultmesh-uri", $OdinCultMeshUri)
@ -160,15 +151,3 @@ if ($null -eq $processCheck) {
if ($null -eq $processCheck) {
throw "Starfire Muninn serve process is not running after restart"
}
foreach ($pattern in @(
"--idunn-rudp-health",
$IdunnRudpHealth,
"--idunn-daemon",
$IdunnDaemon,
"--idunn-health-contract",
$IdunnHealthContract
)) {
if ($processCheck.CommandLine -notlike "*$pattern*") {
throw "Starfire Muninn serve command line is missing ${pattern}: $($processCheck.CommandLine)"
}
}