Skip to main content

hdds_micro/core/
writer.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright (c) 2025-2026 naskel.com
3
4//! MicroWriter - DDS DataWriter for embedded
5
6use crate::error::{Error, Result};
7use crate::rtps::submessages::Data;
8use crate::rtps::{EntityId, GuidPrefix, Locator, RtpsHeader, SequenceNumber, GUID};
9use crate::transport::Transport;
10use crate::MAX_PACKET_SIZE;
11
12/// MicroWriter - DDS DataWriter
13///
14/// Publishes data samples to a topic.
15///
16/// # Design
17///
18/// - BEST_EFFORT QoS (no retransmissions)
19/// - No history cache (fire-and-forget)
20/// - Fixed-size packets
21///
22/// # Example
23///
24/// ```ignore
25/// let writer = MicroWriter::new(
26///     participant.guid_prefix(),
27///     writer_entity_id,
28///     "Temperature",
29///     dest_locator,
30/// );
31///
32/// // Encode sample
33/// let mut buf = [0u8; 256];
34/// let mut encoder = CdrEncoder::new(&mut buf);
35/// encoder.encode_f32(23.5)?;
36/// encoder.encode_i64(123456)?;
37/// let payload = encoder.finish();
38///
39/// // Write sample
40/// writer.write(payload, participant.transport_mut())?;
41/// ```
42#[derive(Debug, PartialEq)]
43pub struct MicroWriter {
44    /// Writer GUID
45    guid: GUID,
46
47    /// Topic name
48    topic_name: [u8; 64],
49    topic_len: usize,
50
51    /// Destination locator (where to send DATA)
52    dest_locator: Locator,
53
54    /// Current sequence number
55    sequence_number: SequenceNumber,
56}
57
58impl MicroWriter {
59    /// Create a new writer
60    ///
61    /// # Arguments
62    ///
63    /// * `guid_prefix` - Participant's GUID prefix
64    /// * `entity_id` - Writer's entity ID
65    /// * `topic_name` - Topic name (max 63 chars)
66    /// * `dest_locator` - Destination locator (multicast or unicast)
67    pub fn new(
68        guid_prefix: GuidPrefix,
69        entity_id: EntityId,
70        topic_name: &str,
71        dest_locator: Locator,
72    ) -> Result<Self> {
73        if topic_name.len() > 63 {
74            return Err(Error::InvalidParameter);
75        }
76
77        let mut topic_name_buf = [0u8; 64];
78        topic_name_buf[0..topic_name.len()].copy_from_slice(topic_name.as_bytes());
79
80        Ok(Self {
81            guid: GUID::new(guid_prefix, entity_id),
82            topic_name: topic_name_buf,
83            topic_len: topic_name.len(),
84            dest_locator,
85            sequence_number: SequenceNumber::MIN,
86        })
87    }
88
89    /// Get writer GUID
90    pub const fn guid(&self) -> GUID {
91        self.guid
92    }
93
94    /// Get topic name
95    pub fn topic_name(&self) -> &str {
96        core::str::from_utf8(&self.topic_name[0..self.topic_len]).unwrap_or("")
97    }
98
99    /// Get current sequence number
100    pub const fn sequence_number(&self) -> SequenceNumber {
101        self.sequence_number
102    }
103
104    /// Write a sample
105    ///
106    /// # Arguments
107    ///
108    /// * `payload` - CDR-encoded payload
109    /// * `transport` - Transport to send through
110    pub fn write<T: Transport>(&mut self, payload: &[u8], transport: &mut T) -> Result<()> {
111        // Build RTPS packet: Header + DATA submessage + payload
112        let mut packet = [0u8; MAX_PACKET_SIZE];
113
114        // 1. RTPS header (20 bytes)
115        let header = RtpsHeader::new(
116            crate::rtps::ProtocolVersion::RTPS_2_5,
117            crate::rtps::VendorId::HDDS,
118            self.guid.prefix,
119        );
120        let header_len = header.encode(&mut packet)?;
121
122        // 2. DATA submessage header (24 bytes)
123        let data = Data::new(
124            EntityId::UNKNOWN, // reader_id = UNKNOWN for multicast
125            self.guid.entity_id,
126            self.sequence_number,
127        );
128        let data_len = data.encode_header(&mut packet[header_len..])?;
129
130        // 3. Payload
131        let payload_offset = header_len + data_len;
132        if payload_offset + payload.len() > MAX_PACKET_SIZE {
133            return Err(Error::BufferTooSmall);
134        }
135        packet[payload_offset..payload_offset + payload.len()].copy_from_slice(payload);
136
137        let total_len = payload_offset + payload.len();
138
139        // Update DATA submessage header with correct octets_to_next
140        // (20 bytes fixed fields + payload length)
141        let octets_to_next = (20 + payload.len()) as u16;
142        packet[header_len + 2] = (octets_to_next & 0xff) as u8;
143        packet[header_len + 3] = ((octets_to_next >> 8) & 0xff) as u8;
144
145        // Send packet
146        transport.send(&packet[0..total_len], &self.dest_locator)?;
147
148        // Increment sequence number
149        self.sequence_number.increment();
150
151        Ok(())
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::cdr::CdrEncoder;
159    use crate::transport::NullTransport;
160
161    #[test]
162    fn test_writer_creation() {
163        let writer = MicroWriter::new(
164            GuidPrefix::new([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
165            EntityId::new([0, 0, 0, 0xc2]),
166            "TestTopic",
167            Locator::udpv4([239, 255, 0, 1], 7400),
168        )
169        .unwrap();
170
171        assert_eq!(writer.topic_name(), "TestTopic");
172        assert_eq!(writer.sequence_number(), SequenceNumber::MIN);
173    }
174
175    #[test]
176    fn test_writer_write() {
177        let mut writer = MicroWriter::new(
178            GuidPrefix::new([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
179            EntityId::new([0, 0, 0, 0xc2]),
180            "TestTopic",
181            Locator::udpv4([239, 255, 0, 1], 7400),
182        )
183        .unwrap();
184
185        let mut transport = NullTransport::default();
186
187        // Encode sample
188        let mut buf = [0u8; 128];
189        let mut encoder = CdrEncoder::new(&mut buf);
190        encoder.encode_f32(23.5).unwrap();
191        let payload = encoder.finish();
192
193        // Write sample
194        writer.write(payload, &mut transport).unwrap();
195
196        // Sequence number should increment
197        assert_eq!(writer.sequence_number(), SequenceNumber::new(2));
198    }
199
200    #[test]
201    fn test_writer_topic_name_too_long() {
202        let long_name = "a".repeat(100);
203        let result = MicroWriter::new(
204            GuidPrefix::default(),
205            EntityId::default(),
206            &long_name,
207            Locator::default(),
208        );
209
210        assert_eq!(result, Err(Error::InvalidParameter));
211    }
212}