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:
commit
1d0ea0b112
19 changed files with 1202 additions and 452 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/muninn-contracts",
|
||||
"crates/muninn-daemon",
|
||||
"crates/muninn-move-tracker",
|
||||
"crates/muninn-psmoveapi-tracker",
|
||||
|
|
|
|||
21
crates/muninn-contracts/Cargo.toml
Normal file
21
crates/muninn-contracts/Cargo.toml
Normal 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"
|
||||
526
crates/muninn-contracts/src/lib.rs
Normal file
526
crates/muninn-contracts/src/lib.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
508
crates/muninn-contracts/src/records.rs
Normal file
508
crates/muninn-contracts/src/records.rs
Normal 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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"@
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue