use std::collections::HashMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
#[cfg(feature = "exochain")]
use crate::chain::EVENT_KIND_MESH_SENSOR_FRAME;
#[cfg(not(feature = "exochain"))]
const EVENT_KIND_MESH_SENSOR_FRAME: &str = "mesh.sensor.v1.frame";
use crate::mesh_framing::{FrameType, MeshFrame};
use crate::process::{Pid, ProcessTable};
use crate::topic::TopicRouter;
pub const TOPIC_ENCODED: &str = "mesh.sensor.v1.encoded";
pub const TOPIC_CONSENSUS: &str = "mesh.sensor.v1.consensus";
pub const TOPIC_CONTROL: &str = "mesh.sensor.v1.control";
pub const SENSOR_TOPICS: &[&str] = &[TOPIC_ENCODED, TOPIC_CONSENSUS, TOPIC_CONTROL];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MeshSensorTopic {
Encoded,
Consensus,
Control,
}
impl MeshSensorTopic {
pub const fn as_str(self) -> &'static str {
match self {
Self::Encoded => TOPIC_ENCODED,
Self::Consensus => TOPIC_CONSENSUS,
Self::Control => TOPIC_CONTROL,
}
}
pub fn with_cluster(self, cluster: &str) -> String {
format!("{}.{}", self.as_str(), cluster)
}
pub fn parse(s: &str) -> Option<Self> {
if s == TOPIC_ENCODED || s.starts_with(&format!("{TOPIC_ENCODED}.")) {
Some(Self::Encoded)
} else if s == TOPIC_CONSENSUS || s.starts_with(&format!("{TOPIC_CONSENSUS}.")) {
Some(Self::Consensus)
} else if s == TOPIC_CONTROL || s.starts_with(&format!("{TOPIC_CONTROL}.")) {
Some(Self::Control)
} else {
None
}
}
pub const ALL: &'static [MeshSensorTopic] =
&[Self::Encoded, Self::Consensus, Self::Control];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SensorMeshEnvelope {
pub topic: MeshSensorTopic,
pub cluster: String,
pub node_id: u64,
pub timestamp_ms: u64,
pub payload: Vec<u8>,
}
impl SensorMeshEnvelope {
pub fn topic_name(&self) -> String {
self.topic.with_cluster(&self.cluster)
}
pub fn to_mesh_frame(&self) -> Result<MeshFrame, String> {
let bytes = serde_json::to_vec(self).map_err(|e| e.to_string())?;
Ok(MeshFrame {
frame_type: FrameType::SensorObservation,
payload: bytes,
})
}
pub fn from_mesh_frame(frame: &MeshFrame) -> Result<Self, String> {
if frame.frame_type != FrameType::SensorObservation {
return Err(format!(
"expected SensorObservation frame, got {:?}",
frame.frame_type
));
}
serde_json::from_slice(&frame.payload).map_err(|e| e.to_string())
}
pub fn payload_digest_hex(&self) -> String {
let mut hash: u64 = 0xcbf29ce484222325;
for &b in &self.payload {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x100000001b3);
}
format!("{hash:016x}")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SensorFrameIndexEntry {
pub seq: u64,
pub kind: String,
pub topic: String,
pub cluster: String,
pub node_id: u64,
pub timestamp_ms: u64,
pub digest: String,
pub payload_len: usize,
}
#[derive(Debug, Default, Clone)]
pub struct SensorFrameChainIndex {
pub entries: Vec<SensorFrameIndexEntry>,
next_seq: u64,
}
impl SensorFrameChainIndex {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn index(&mut self, env: &SensorMeshEnvelope) -> u64 {
let seq = self.next_seq;
self.next_seq = self.next_seq.saturating_add(1);
self.entries.push(SensorFrameIndexEntry {
seq,
kind: EVENT_KIND_MESH_SENSOR_FRAME.to_string(),
topic: env.topic_name(),
cluster: env.cluster.clone(),
node_id: env.node_id,
timestamp_ms: env.timestamp_ms,
digest: env.payload_digest_hex(),
payload_len: env.payload.len(),
});
seq
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SensorPublishResult {
pub topic: String,
pub chain_seq: u64,
pub local_deliveries: usize,
pub router_subscribers: usize,
}
pub struct SensorTopicBus {
pub router: Arc<TopicRouter>,
pub chain: SensorFrameChainIndex,
last_by_topic: HashMap<String, Vec<u8>>,
local_subs: HashMap<String, Vec<Vec<u8>>>,
}
impl SensorTopicBus {
pub fn new() -> Self {
let pt = Arc::new(ProcessTable::new(64));
Self::with_router(Arc::new(TopicRouter::new(pt)))
}
pub fn with_router(router: Arc<TopicRouter>) -> Self {
Self {
router,
chain: SensorFrameChainIndex::new(),
last_by_topic: HashMap::new(),
local_subs: HashMap::new(),
}
}
pub fn register_well_known_topics(&self) {
let sentinel: Pid = 0;
for t in SENSOR_TOPICS {
self.router.subscribe(sentinel, t);
self.router.unsubscribe(sentinel, t);
}
}
pub fn well_known_topics(&self) -> &'static [&'static str] {
SENSOR_TOPICS
}
pub fn subscribe_pid(&self, pid: Pid, topic: MeshSensorTopic, cluster: Option<&str>) {
let name = match cluster {
Some(c) if !c.is_empty() => topic.with_cluster(c),
_ => topic.as_str().to_string(),
};
self.router.subscribe(pid, &name);
}
pub fn subscribe_local(&mut self, topic: &str) {
self.local_subs.entry(topic.to_string()).or_default();
}
pub fn drain_local(&mut self, topic: &str) -> Vec<Vec<u8>> {
self.local_subs
.get_mut(topic)
.map(std::mem::take)
.unwrap_or_default()
}
pub fn publish(&mut self, env: SensorMeshEnvelope) -> Result<SensorPublishResult, String> {
let mesh_frame = env.to_mesh_frame()?;
let _wire = mesh_frame
.encode()
.map_err(|e| format!("mesh frame encode: {e}"))?;
let topic = env.topic_name();
let payload = env.payload.clone();
let mut local_deliveries = 0usize;
if let Some(q) = self.local_subs.get_mut(&topic) {
q.push(payload.clone());
local_deliveries += 1;
}
let base = env.topic.as_str();
if base != topic
&& let Some(q) = self.local_subs.get_mut(base)
{
q.push(payload.clone());
local_deliveries += 1;
}
self.last_by_topic.insert(topic.clone(), payload);
let router_subscribers = self.router.subscribers(&topic).len();
let chain_seq = self.chain.index(&env);
Ok(SensorPublishResult {
topic,
chain_seq,
local_deliveries,
router_subscribers,
})
}
pub fn last_payload(&self, topic: &str) -> Option<&[u8]> {
self.last_by_topic.get(topic).map(|v| v.as_slice())
}
}
impl Default for SensorTopicBus {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "exochain")]
#[test]
fn frame_event_kind_matches_non_exochain_copy() {
assert_eq!(super::EVENT_KIND_MESH_SENSOR_FRAME, "mesh.sensor.v1.frame");
}
use super::*;
#[test]
fn three_topics_registered_and_published() {
let mut bus = SensorTopicBus::new();
bus.register_well_known_topics();
assert_eq!(bus.well_known_topics().len(), 3);
assert_eq!(MeshSensorTopic::ALL.len(), 3);
for topic in MeshSensorTopic::ALL {
let full = topic.with_cluster("alpha");
bus.subscribe_local(&full);
}
let mut seqs = Vec::new();
for (i, topic) in MeshSensorTopic::ALL.iter().enumerate() {
let env = SensorMeshEnvelope {
topic: *topic,
cluster: "alpha".into(),
node_id: 7,
timestamp_ms: 1000 + i as u64,
payload: format!("frame-{i}").into_bytes(),
};
let r = bus.publish(env).expect("publish");
assert_eq!(r.topic, topic.with_cluster("alpha"));
assert_eq!(r.local_deliveries, 1);
seqs.push(r.chain_seq);
}
assert_eq!(bus.chain.len(), 3);
assert_eq!(seqs, vec![0, 1, 2]);
for e in &bus.chain.entries {
assert_eq!(e.kind, EVENT_KIND_MESH_SENSOR_FRAME);
assert!(e.topic.starts_with("mesh.sensor.v1."));
assert_eq!(e.cluster, "alpha");
assert_eq!(e.node_id, 7);
assert!(!e.digest.is_empty());
}
for topic in MeshSensorTopic::ALL {
let drained = bus.drain_local(&topic.with_cluster("alpha"));
assert_eq!(drained.len(), 1);
}
}
#[test]
fn mesh_frame_roundtrip_sensor_observation() {
let env = SensorMeshEnvelope {
topic: MeshSensorTopic::Encoded,
cluster: "c".into(),
node_id: 1,
timestamp_ms: 42,
payload: b"cbor-placeholder".to_vec(),
};
let frame = env.to_mesh_frame().unwrap();
assert_eq!(frame.frame_type, FrameType::SensorObservation);
let wire = frame.encode().unwrap();
let decoded = MeshFrame::decode(&wire[4..]).unwrap();
let back = SensorMeshEnvelope::from_mesh_frame(&decoded).unwrap();
assert_eq!(back, env);
}
#[test]
fn topic_parse_with_cluster() {
assert_eq!(
MeshSensorTopic::parse("mesh.sensor.v1.control.east"),
Some(MeshSensorTopic::Control)
);
assert_eq!(MeshSensorTopic::parse("other"), None);
assert_eq!(TOPIC_ENCODED, "mesh.sensor.v1.encoded");
assert_eq!(TOPIC_CONSENSUS, "mesh.sensor.v1.consensus");
assert_eq!(TOPIC_CONTROL, "mesh.sensor.v1.control");
}
}