#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(feature = "legacy-chat")]
use crate::sync::crdt::ChatCRDT;
use crate::sync::crdt::{EmergencyEvent, EventType, GCounter, Peripheral, PeripheralEvent};
use crate::NodeId;
pub const EXTENDED_MARKER: u8 = 0xAB;
pub const EMERGENCY_MARKER: u8 = 0xAC;
pub const CHAT_MARKER: u8 = 0xAD;
pub const ENCRYPTED_MARKER: u8 = 0xAE;
pub const PEER_E2EE_MARKER: u8 = 0xAF;
pub const KEY_EXCHANGE_MARKER: u8 = 0xB0;
pub const RELAY_ENVELOPE_MARKER: u8 = 0xB1;
pub const DELTA_DOCUMENT_MARKER: u8 = 0xB2;
pub const TRANSLATOR_FRAME_MARKER: u8 = 0xB6;
pub const TRANSLATOR_RESERVED_MARKER_START: u8 = 0xB7;
pub const TRANSLATOR_RESERVED_MARKER_END: u8 = 0xBF;
pub const MIN_DOCUMENT_SIZE: usize = 8;
pub const MAX_MESH_SIZE: usize = 20;
pub const TARGET_DOCUMENT_SIZE: usize = 244;
pub const MAX_DOCUMENT_SIZE: usize = 512;
#[derive(Debug, Clone)]
pub struct PeatDocument {
pub version: u32,
pub node_id: NodeId,
pub counter: GCounter,
pub peripheral: Option<Peripheral>,
pub emergency: Option<EmergencyEvent>,
#[cfg(feature = "legacy-chat")]
pub chat: Option<ChatCRDT>,
}
impl Default for PeatDocument {
fn default() -> Self {
Self {
version: 1,
node_id: NodeId::default(),
counter: GCounter::new(),
peripheral: None,
emergency: None,
#[cfg(feature = "legacy-chat")]
chat: None,
}
}
}
impl PeatDocument {
pub fn new(node_id: NodeId) -> Self {
Self {
version: 1,
node_id,
counter: GCounter::new(),
peripheral: None,
emergency: None,
#[cfg(feature = "legacy-chat")]
chat: None,
}
}
pub fn with_peripheral(mut self, peripheral: Peripheral) -> Self {
self.peripheral = Some(peripheral);
self
}
pub fn with_emergency(mut self, emergency: EmergencyEvent) -> Self {
self.emergency = Some(emergency);
self
}
#[cfg(feature = "legacy-chat")]
pub fn with_chat(mut self, chat: ChatCRDT) -> Self {
self.chat = Some(chat);
self
}
pub fn increment_version(&mut self) {
self.version = self.version.wrapping_add(1);
}
pub fn increment_counter(&mut self) {
self.counter.increment(&self.node_id, 1);
self.increment_version();
}
pub fn set_event(&mut self, event_type: EventType, timestamp: u64) {
if let Some(ref mut peripheral) = self.peripheral {
peripheral.set_event(event_type, timestamp);
self.increment_counter();
}
}
pub fn clear_event(&mut self) {
if let Some(ref mut peripheral) = self.peripheral {
peripheral.clear_event();
self.increment_version();
}
}
pub fn set_emergency(&mut self, source_node: u32, timestamp: u64, known_peers: &[u32]) {
self.emergency = Some(EmergencyEvent::new(source_node, timestamp, known_peers));
self.increment_counter();
}
pub fn ack_emergency(&mut self, node_id: u32) -> bool {
if let Some(ref mut emergency) = self.emergency {
if emergency.ack(node_id) {
self.increment_version();
return true;
}
}
false
}
pub fn clear_emergency(&mut self) {
if self.emergency.is_some() {
self.emergency = None;
self.increment_version();
}
}
pub fn get_emergency(&self) -> Option<&EmergencyEvent> {
self.emergency.as_ref()
}
pub fn has_emergency(&self) -> bool {
self.emergency.is_some()
}
#[cfg(feature = "legacy-chat")]
pub fn get_chat(&self) -> Option<&ChatCRDT> {
self.chat.as_ref()
}
#[cfg(feature = "legacy-chat")]
pub fn get_or_create_chat(&mut self) -> &mut ChatCRDT {
if self.chat.is_none() {
self.chat = Some(ChatCRDT::new());
}
self.chat.as_mut().unwrap()
}
#[cfg(feature = "legacy-chat")]
pub fn add_chat_message(
&mut self,
origin_node: u32,
timestamp: u64,
sender: &str,
text: &str,
) -> bool {
use crate::sync::crdt::ChatMessage;
let mut msg = ChatMessage::new(origin_node, timestamp, sender, text);
msg.is_broadcast = true;
let chat = self.get_or_create_chat();
if chat.add_message(msg) {
self.increment_counter();
true
} else {
false
}
}
#[cfg(feature = "legacy-chat")]
pub fn add_chat_reply(
&mut self,
origin_node: u32,
timestamp: u64,
sender: &str,
text: &str,
reply_to_node: u32,
reply_to_timestamp: u64,
) -> bool {
use crate::sync::crdt::ChatMessage;
let mut msg = ChatMessage::new(origin_node, timestamp, sender, text);
msg.is_broadcast = true;
msg.set_reply_to(reply_to_node, reply_to_timestamp);
let chat = self.get_or_create_chat();
if chat.add_message(msg) {
self.increment_counter();
true
} else {
false
}
}
#[cfg(feature = "legacy-chat")]
pub fn has_chat(&self) -> bool {
self.chat.as_ref().is_some_and(|c| !c.is_empty())
}
#[cfg(feature = "legacy-chat")]
pub fn chat_count(&self) -> usize {
self.chat.as_ref().map_or(0, |c| c.len())
}
pub fn merge(&mut self, other: &PeatDocument) -> bool {
let mut changed = false;
let old_value = self.counter.value();
self.counter.merge(&other.counter);
if self.counter.value() != old_value {
changed = true;
}
if let Some(ref other_emergency) = other.emergency {
match &mut self.emergency {
Some(ref mut our_emergency) => {
if our_emergency.merge(other_emergency) {
changed = true;
}
}
None => {
self.emergency = Some(other_emergency.clone());
changed = true;
}
}
}
#[cfg(feature = "legacy-chat")]
if let Some(ref other_chat) = other.chat {
match &mut self.chat {
Some(ref mut our_chat) => {
if our_chat.merge(other_chat) {
changed = true;
}
}
None => {
if !other_chat.is_empty() {
self.chat = Some(other_chat.clone());
changed = true;
}
}
}
}
if changed {
self.increment_version();
}
changed
}
pub fn current_event(&self) -> Option<EventType> {
self.peripheral
.as_ref()
.and_then(|p| p.last_event.as_ref())
.map(|e| e.event_type)
}
pub fn encode(&self) -> Vec<u8> {
let counter_data = self.counter.encode();
let peripheral_data = self.peripheral.as_ref().map(|p| p.encode());
let emergency_data = self.emergency.as_ref().map(|e| e.encode());
#[cfg(feature = "legacy-chat")]
let chat_data = self
.chat
.as_ref()
.filter(|c| !c.is_empty())
.map(|c| c.encode());
#[cfg(not(feature = "legacy-chat"))]
let chat_data: Option<Vec<u8>> = None;
let mut size = 8 + counter_data.len(); if let Some(ref pdata) = peripheral_data {
size += 4 + pdata.len(); }
if let Some(ref edata) = emergency_data {
size += 4 + edata.len(); }
if let Some(ref cdata) = chat_data {
size += 4 + cdata.len(); }
let mut buf = Vec::with_capacity(size);
buf.extend_from_slice(&self.version.to_le_bytes());
buf.extend_from_slice(&self.node_id.as_u32().to_le_bytes());
buf.extend_from_slice(&counter_data);
if let Some(pdata) = peripheral_data {
buf.push(EXTENDED_MARKER);
buf.push(0); buf.extend_from_slice(&(pdata.len() as u16).to_le_bytes());
buf.extend_from_slice(&pdata);
}
if let Some(edata) = emergency_data {
buf.push(EMERGENCY_MARKER);
buf.push(0); buf.extend_from_slice(&(edata.len() as u16).to_le_bytes());
buf.extend_from_slice(&edata);
}
if let Some(cdata) = chat_data {
buf.push(CHAT_MARKER);
buf.push(0); buf.extend_from_slice(&(cdata.len() as u16).to_le_bytes());
buf.extend_from_slice(&cdata);
}
buf
}
#[inline]
pub fn to_bytes(&self) -> Vec<u8> {
self.encode()
}
pub fn decode(data: &[u8]) -> Option<Self> {
if data.len() < MIN_DOCUMENT_SIZE {
return None;
}
let version = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
let node_id = NodeId::new(u32::from_le_bytes([data[4], data[5], data[6], data[7]]));
let counter = GCounter::decode(&data[8..])?;
let num_entries = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
let mut offset = 8 + 4 + num_entries * 12;
let mut peripheral = None;
let mut emergency = None;
#[cfg(feature = "legacy-chat")]
let mut chat = None;
while offset < data.len() {
let marker = data[offset];
if marker == EXTENDED_MARKER {
if data.len() < offset + 4 {
break;
}
let _reserved = data[offset + 1];
let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
let section_start = offset + 4;
if data.len() < section_start + section_len {
break;
}
peripheral = Peripheral::decode(&data[section_start..section_start + section_len]);
offset = section_start + section_len;
} else if marker == EMERGENCY_MARKER {
if data.len() < offset + 4 {
break;
}
let _reserved = data[offset + 1];
let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
let section_start = offset + 4;
if data.len() < section_start + section_len {
break;
}
emergency =
EmergencyEvent::decode(&data[section_start..section_start + section_len]);
offset = section_start + section_len;
} else if marker == CHAT_MARKER {
if data.len() < offset + 4 {
break;
}
let _reserved = data[offset + 1];
let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
let section_start = offset + 4;
if data.len() < section_start + section_len {
break;
}
#[cfg(feature = "legacy-chat")]
{
chat = ChatCRDT::decode(&data[section_start..section_start + section_len]);
}
offset = section_start + section_len;
} else {
break;
}
}
Some(Self {
version,
node_id,
counter,
peripheral,
emergency,
#[cfg(feature = "legacy-chat")]
chat,
})
}
#[inline]
pub fn from_bytes(data: &[u8]) -> Option<Self> {
Self::decode(data)
}
pub fn total_count(&self) -> u64 {
self.counter.value()
}
pub fn encoded_size(&self) -> usize {
let counter_size = 4 + self.counter.node_count_total() * 12;
let peripheral_size = self.peripheral.as_ref().map_or(0, |p| 4 + p.encode().len());
let emergency_size = self.emergency.as_ref().map_or(0, |e| 4 + e.encode().len());
#[cfg(feature = "legacy-chat")]
let chat_size = self
.chat
.as_ref()
.filter(|c| !c.is_empty())
.map_or(0, |c| 4 + c.encoded_size());
#[cfg(not(feature = "legacy-chat"))]
let chat_size = 0;
8 + counter_size + peripheral_size + emergency_size + chat_size
}
pub fn exceeds_target_size(&self) -> bool {
self.encoded_size() > TARGET_DOCUMENT_SIZE
}
pub fn exceeds_max_size(&self) -> bool {
self.encoded_size() > MAX_DOCUMENT_SIZE
}
}
#[derive(Debug, Clone)]
pub struct MergeResult {
pub source_node: NodeId,
pub event: Option<PeripheralEvent>,
pub peer_peripheral: Option<Peripheral>,
pub counter_changed: bool,
pub emergency_changed: bool,
pub chat_changed: bool,
pub total_count: u64,
}
impl MergeResult {
pub fn is_emergency(&self) -> bool {
self.event
.as_ref()
.is_some_and(|e| e.event_type == EventType::Emergency)
}
pub fn is_ack(&self) -> bool {
self.event
.as_ref()
.is_some_and(|e| e.event_type == EventType::Ack)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::crdt::PeripheralType;
const TEST_TIMESTAMP: u64 = 1705276800000;
#[test]
fn test_document_encode_decode_minimal() {
let node_id = NodeId::new(0x12345678);
let doc = PeatDocument::new(node_id);
let encoded = doc.encode();
assert_eq!(encoded.len(), 12);
let decoded = PeatDocument::decode(&encoded).unwrap();
assert_eq!(decoded.version, 1);
assert_eq!(decoded.node_id.as_u32(), 0x12345678);
assert_eq!(decoded.counter.value(), 0);
assert!(decoded.peripheral.is_none());
}
#[test]
fn test_document_encode_decode_with_counter() {
let node_id = NodeId::new(0x12345678);
let mut doc = PeatDocument::new(node_id);
doc.increment_counter();
doc.increment_counter();
let encoded = doc.encode();
assert_eq!(encoded.len(), 24);
let decoded = PeatDocument::decode(&encoded).unwrap();
assert_eq!(decoded.counter.value(), 2);
}
#[test]
fn test_document_encode_decode_with_peripheral() {
let node_id = NodeId::new(0x12345678);
let peripheral =
Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor).with_callsign("ALPHA-1");
let doc = PeatDocument::new(node_id).with_peripheral(peripheral);
let encoded = doc.encode();
let decoded = PeatDocument::decode(&encoded).unwrap();
assert!(decoded.peripheral.is_some());
let p = decoded.peripheral.unwrap();
assert_eq!(p.id, 0xAABBCCDD);
assert_eq!(p.callsign_str(), "ALPHA-1");
}
#[test]
fn test_document_encode_decode_with_event() {
let node_id = NodeId::new(0x12345678);
let mut peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
peripheral.set_event(EventType::Emergency, TEST_TIMESTAMP);
let doc = PeatDocument::new(node_id).with_peripheral(peripheral);
let encoded = doc.encode();
let decoded = PeatDocument::decode(&encoded).unwrap();
assert!(decoded.peripheral.is_some());
let p = decoded.peripheral.unwrap();
assert!(p.last_event.is_some());
let event = p.last_event.unwrap();
assert_eq!(event.event_type, EventType::Emergency);
assert_eq!(event.timestamp, TEST_TIMESTAMP);
}
#[test]
fn test_document_merge() {
let node1 = NodeId::new(0x11111111);
let node2 = NodeId::new(0x22222222);
let mut doc1 = PeatDocument::new(node1);
doc1.increment_counter();
let mut doc2 = PeatDocument::new(node2);
doc2.counter.increment(&node2, 3);
let changed = doc1.merge(&doc2);
assert!(changed);
assert_eq!(doc1.counter.value(), 4); }
#[test]
fn test_merge_result_helpers() {
let emergency_event = PeripheralEvent::new(EventType::Emergency, 123);
let result = MergeResult {
source_node: NodeId::new(0x12345678),
event: Some(emergency_event),
peer_peripheral: None,
counter_changed: true,
emergency_changed: false,
chat_changed: false,
total_count: 10,
};
assert!(result.is_emergency());
assert!(!result.is_ack());
let ack_event = PeripheralEvent::new(EventType::Ack, 456);
let result = MergeResult {
source_node: NodeId::new(0x12345678),
event: Some(ack_event),
peer_peripheral: None,
counter_changed: false,
emergency_changed: false,
chat_changed: false,
total_count: 10,
};
assert!(!result.is_emergency());
assert!(result.is_ack());
}
#[test]
fn test_document_size_calculation() {
use crate::sync::crdt::PeripheralType;
let node_id = NodeId::new(0x12345678);
let doc = PeatDocument::new(node_id);
assert_eq!(doc.encoded_size(), 12);
assert!(!doc.exceeds_target_size());
let mut doc = PeatDocument::new(node_id);
doc.increment_counter();
assert_eq!(doc.encoded_size(), 24);
let peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
let doc = PeatDocument::new(node_id).with_peripheral(peripheral);
let encoded = doc.encode();
assert_eq!(doc.encoded_size(), encoded.len());
let mut doc = PeatDocument::new(node_id);
for i in 0..10 {
doc.counter.increment(&NodeId::new(i), 1);
}
assert!(doc.encoded_size() < TARGET_DOCUMENT_SIZE);
assert!(!doc.exceeds_max_size());
}
#[cfg(feature = "legacy-chat")]
mod chat_document_tests {
use super::*;
#[test]
fn test_document_add_chat_message() {
let node_id = NodeId::new(0x12345678);
let mut doc = PeatDocument::new(node_id);
assert!(!doc.has_chat());
assert_eq!(doc.chat_count(), 0);
assert!(doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Hello mesh!"));
assert!(doc.has_chat());
assert_eq!(doc.chat_count(), 1);
assert!(!doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Hello mesh!"));
assert_eq!(doc.chat_count(), 1);
assert!(doc.add_chat_message(
0x12345678,
TEST_TIMESTAMP + 1000,
"ALPHA",
"Second message"
));
assert_eq!(doc.chat_count(), 2);
}
#[test]
fn test_document_add_chat_reply() {
let node_id = NodeId::new(0x12345678);
let mut doc = PeatDocument::new(node_id);
doc.add_chat_message(0xAABBCCDD, TEST_TIMESTAMP, "BRAVO", "Need assistance");
assert!(doc.add_chat_reply(
0x12345678,
TEST_TIMESTAMP + 1000,
"ALPHA",
"Copy that",
0xAABBCCDD, TEST_TIMESTAMP ));
assert_eq!(doc.chat_count(), 2);
let chat = doc.get_chat().unwrap();
let reply = chat.get_message(0x12345678, TEST_TIMESTAMP + 1000).unwrap();
assert!(reply.is_reply());
assert_eq!(reply.reply_to_node, 0xAABBCCDD);
assert_eq!(reply.reply_to_timestamp, TEST_TIMESTAMP);
}
#[test]
fn test_document_encode_decode_with_chat() {
let node_id = NodeId::new(0x12345678);
let mut doc = PeatDocument::new(node_id);
doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "First message");
doc.add_chat_message(0xAABBCCDD, TEST_TIMESTAMP + 1000, "BRAVO", "Second message");
let encoded = doc.encode();
let decoded = PeatDocument::decode(&encoded).unwrap();
assert!(decoded.has_chat());
assert_eq!(decoded.chat_count(), 2);
let chat = decoded.get_chat().unwrap();
let msg1 = chat.get_message(0x12345678, TEST_TIMESTAMP).unwrap();
assert_eq!(msg1.sender(), "ALPHA");
assert_eq!(msg1.text(), "First message");
let msg2 = chat.get_message(0xAABBCCDD, TEST_TIMESTAMP + 1000).unwrap();
assert_eq!(msg2.sender(), "BRAVO");
assert_eq!(msg2.text(), "Second message");
}
#[test]
fn test_document_merge_with_chat() {
let node1 = NodeId::new(0x11111111);
let node2 = NodeId::new(0x22222222);
let mut doc1 = PeatDocument::new(node1);
doc1.add_chat_message(0x11111111, TEST_TIMESTAMP, "ALPHA", "From node 1");
let mut doc2 = PeatDocument::new(node2);
doc2.add_chat_message(0x22222222, TEST_TIMESTAMP + 1000, "BRAVO", "From node 2");
let changed = doc1.merge(&doc2);
assert!(changed);
assert_eq!(doc1.chat_count(), 2);
let changed = doc1.merge(&doc2);
assert!(!changed);
let chat = doc1.get_chat().unwrap();
assert!(chat.get_message(0x11111111, TEST_TIMESTAMP).is_some());
assert!(chat
.get_message(0x22222222, TEST_TIMESTAMP + 1000)
.is_some());
}
#[test]
fn test_document_chat_encoded_size() {
let node_id = NodeId::new(0x12345678);
let mut doc = PeatDocument::new(node_id);
let base_size = doc.encoded_size();
doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Test");
let with_chat_size = doc.encoded_size();
assert!(with_chat_size > base_size);
let encoded = doc.encode();
assert_eq!(doc.encoded_size(), encoded.len());
}
} }