Skip to main content

sig_net/
send.rs

1#![allow(clippy::too_many_arguments)]
2use crate::*;
3use crate::{coap, crypto, security, tlv};
4
5fn compute_hmac_opt(
6    uri_string: &str,
7    options: &SigNetOptions,
8    payload: &[u8],
9    signing_key: &[u8],
10) -> Result<[u8; HMAC_SHA256_LENGTH]> {
11    crypto::compute_packet_hmac(uri_string, options, payload, signing_key)
12}
13
14fn write_options_2076_2204(
15    buffer: &mut PacketBuffer,
16    tuid: &[u8; TUID_LENGTH],
17    endpoint: u16,
18    mfg_code: u16,
19    session_id: u32,
20    seq_num: u32,
21) -> Result<SigNetOptions> {
22    let mut options = SigNetOptions {
23        security_mode: SECURITY_MODE_HMAC_SHA256,
24        ..Default::default()
25    };
26    security::build_sender_id(tuid, endpoint, &mut options.sender_id);
27    options.mfg_code = mfg_code;
28    options.session_id = session_id;
29    options.seq_num = seq_num;
30    security::build_signet_options_without_hmac(buffer, &options, COAP_OPTION_URI_PATH)?;
31    Ok(options)
32}
33
34fn write_uri_path_segments(buffer: &mut PacketBuffer, segments: &[&str]) -> Result<()> {
35    let mut prev: u16 = 0;
36    for &seg in segments {
37        coap::encode_coap_option(buffer, COAP_OPTION_URI_PATH, prev, seg.as_bytes())?;
38        prev = COAP_OPTION_URI_PATH;
39    }
40    Ok(())
41}
42
43pub fn build_dmx_packet(
44    buffer: &mut PacketBuffer,
45    universe: u16,
46    dmx_data: &[u8],
47    slot_count: u16,
48    tuid: &[u8; TUID_LENGTH],
49    endpoint: u16,
50    mfg_code: u16,
51    session_id: u32,
52    seq_num: u32,
53    sender_key: &[u8],
54    message_id: u16,
55    scope: &str,
56) -> Result<()> {
57    if slot_count == 0 || slot_count > MAX_DMX_SLOTS || dmx_data.len() < slot_count as usize {
58        return Err(SigNetError::InvalidArgument);
59    }
60    let slots = &dmx_data[..slot_count as usize];
61
62    buffer.reset();
63    coap::build_coap_header(buffer, message_id)?;
64    coap::build_uri_path_options(buffer, universe, scope)?;
65
66    let options = write_options_2076_2204(buffer, tuid, endpoint, mfg_code, session_id, seq_num)?;
67
68    // Bug 1 fix: build TLV payload first so HMAC covers the full TLV (type + length + data),
69    // matching the C++ FinalizePacketWithHMACAndPayload behaviour.
70    let mut payload_buf = PacketBuffer::new();
71    tlv::encode_tid_level(&mut payload_buf, slots)?;
72
73    let mut uri_buf = [0u8; URI_STRING_MIN_BUFFER as usize];
74    let uri_len = coap::build_uri_string(universe, scope, &mut uri_buf)?;
75    let uri_string = core::str::from_utf8(&uri_buf[..uri_len])
76        .map_err(|_| SigNetError::Encode)?;
77
78    let hmac = compute_hmac_opt(uri_string, &options, payload_buf.as_slice(), sender_key)?;
79    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SEQ_NUM, &hmac)?;
80
81    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
82    buffer.write_bytes(payload_buf.as_slice())
83}
84
85pub fn build_announce_packet(
86    buffer: &mut PacketBuffer,
87    tuid: &[u8; TUID_LENGTH],
88    soem_code: SoemCode,
89    firmware_version_id: u32,
90    firmware_version_string: &str,
91    protocol_version: u8,
92    role_capability_bits: u8,
93    endpoint_count: u16,
94    change_count: u16,
95    session_id: u32,
96    seq_num: u32,
97    citizen_key: &[u8],
98    message_id: u16,
99    scope: &str,
100) -> Result<()> {
101    buffer.reset();
102
103    coap::build_coap_header(buffer, message_id)?;
104
105    let segments = [
106        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, scope, SIGNET_URI_NODE,
107    ];
108    write_uri_path_segments(buffer, &segments)?;
109
110    let hex = TUID(*tuid).to_hex_upper();
111    let hex_str = core::str::from_utf8(&hex).map_err(|_| SigNetError::Encode)?;
112    let mut prev: u16 = COAP_OPTION_URI_PATH;
113    coap::encode_coap_option(buffer, COAP_OPTION_URI_PATH, prev, hex_str.as_bytes())?;
114    prev = COAP_OPTION_URI_PATH;
115    coap::encode_coap_option(buffer, COAP_OPTION_URI_PATH, prev, b"0")?;
116
117    let mfg_code = soem_code_mfg(soem_code);
118    let options = write_options_2076_2204(buffer, tuid, 0, mfg_code, session_id, seq_num)?;
119
120    // Build payload in a temp buffer for HMAC calculation
121    let mut payload_buf = PacketBuffer::new();
122    tlv::build_startup_announce_payload(
123        &mut payload_buf, tuid,
124        soem_code,
125        firmware_version_id,
126        firmware_version_string,
127        protocol_version, role_capability_bits, endpoint_count, change_count,
128        0,  // mult_override_state: default
129        None,  // otw_capability: not supported
130    )?;
131
132    let mut uri_buf = [0u8; URI_STRING_MIN_BUFFER as usize];
133    let uri_len = coap::build_node_uri_string(tuid, 0, scope, &mut uri_buf)?;
134    let uri_string = core::str::from_utf8(&uri_buf[..uri_len])
135        .map_err(|_| SigNetError::Encode)?;
136
137    let hmac = compute_hmac_opt(uri_string, &options, payload_buf.as_slice(), citizen_key)?;
138    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SEQ_NUM, &hmac)?;
139
140    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
141    buffer.write_bytes(payload_buf.as_slice())
142}
143
144pub fn build_poll_packet(
145    buffer: &mut PacketBuffer,
146    manager_tuid: &[u8; TUID_LENGTH],
147    soem_code: SoemCode,
148    tuid_lo: &[u8; TUID_LENGTH],
149    tuid_hi: &[u8; TUID_LENGTH],
150    target_endpoint: u16,
151    query_level: u8,
152    session_id: u32,
153    seq_num: u32,
154    manager_global_key: &[u8],
155    message_id: u16,
156    scope: &str,
157) -> Result<()> {
158    buffer.reset();
159
160    coap::build_coap_header(buffer, message_id)?;
161
162    let segments = [
163        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, scope, SIGNET_URI_POLL,
164    ];
165    write_uri_path_segments(buffer, &segments)?;
166
167    let mfg_code = soem_code_mfg(soem_code);
168    let options = write_options_2076_2204(buffer, manager_tuid, 0, mfg_code, session_id, seq_num)?;
169
170    // Build payload in temp buffer
171    let mut payload_buf = PacketBuffer::new();
172    tlv::encode_tid_poll(
173        &mut payload_buf, manager_tuid,
174        soem_code,
175        tuid_lo, tuid_hi, target_endpoint, query_level,
176    )?;
177
178    let poll_uri = format!(
179        "/{}/{}/{}/{}",
180        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, scope, SIGNET_URI_POLL
181    );
182
183    let hmac = compute_hmac_opt(&poll_uri, &options, payload_buf.as_slice(), manager_global_key)?;
184    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SEQ_NUM, &hmac)?;
185
186    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
187    buffer.write_bytes(payload_buf.as_slice())
188}
189
190/// Build timecode packet (§11.2.5).
191/// URI: /sig-net/v1/<scope>/timecode/{stream} → <mult_time> = 239.254.255.250
192pub fn build_timecode_packet(
193    buffer: &mut PacketBuffer,
194    stream: u8,
195    hours: u8,
196    minutes: u8,
197    seconds: u8,
198    frames: u8,
199    tc_type: u8,
200    tuid: &[u8; TUID_LENGTH],
201    endpoint: u16,
202    mfg_code: u16,
203    session_id: u32,
204    seq_num: u32,
205    sender_key: &[u8],
206    message_id: u16,
207    scope: &str,
208) -> Result<()> {
209    buffer.reset();
210    coap::build_coap_header(buffer, message_id)?;
211
212    let stream_str = stream.to_string();
213    let segments = [
214        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, scope, "timecode", &stream_str,
215    ];
216    write_uri_path_segments(buffer, &segments)?;
217
218    let options = write_options_2076_2204(buffer, tuid, endpoint, mfg_code, session_id, seq_num)?;
219
220    let mut payload_buf = PacketBuffer::new();
221    tlv::encode_tid_timecode(&mut payload_buf, hours, minutes, seconds, frames, tc_type)?;
222
223    let mut uri_buf = [0u8; URI_STRING_MIN_BUFFER as usize];
224    let uri_len = coap::build_timecode_uri_string(stream, scope, &mut uri_buf)?;
225    let uri_string = core::str::from_utf8(&uri_buf[..uri_len])
226        .map_err(|_| SigNetError::Encode)?;
227
228    let hmac = compute_hmac_opt(uri_string, &options, payload_buf.as_slice(), sender_key)?;
229    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SEQ_NUM, &hmac)?;
230
231    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
232    buffer.write_bytes(payload_buf.as_slice())
233}
234
235/// Build preview packet (§11.2.3).
236/// URI: /sig-net/v1/<scope>/preview/{universe} → <mult_preview> = 239.254.255.249
237pub fn build_preview_packet(
238    buffer: &mut PacketBuffer,
239    universe: u16,
240    dmx_data: &[u8],
241    tuid: &[u8; TUID_LENGTH],
242    endpoint: u16,
243    mfg_code: u16,
244    session_id: u32,
245    seq_num: u32,
246    sender_key: &[u8],
247    message_id: u16,
248    scope: &str,
249) -> Result<()> {
250    if dmx_data.is_empty() || dmx_data.len() > MAX_DMX_SLOTS as usize {
251        return Err(SigNetError::InvalidArgument);
252    }
253
254    buffer.reset();
255    coap::build_coap_header(buffer, message_id)?;
256
257    let universe_str = universe.to_string();
258    let segments = [
259        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, scope, "preview", &universe_str,
260    ];
261    write_uri_path_segments(buffer, &segments)?;
262
263    let options = write_options_2076_2204(buffer, tuid, endpoint, mfg_code, session_id, seq_num)?;
264
265    let mut payload_buf = PacketBuffer::new();
266    tlv::encode_tid_preview(&mut payload_buf, dmx_data)?;
267
268    let mut uri_buf = [0u8; URI_STRING_MIN_BUFFER as usize];
269    let uri_len = coap::build_preview_uri_string(universe, scope, &mut uri_buf)?;
270    let uri_string = core::str::from_utf8(&uri_buf[..uri_len])
271        .map_err(|_| SigNetError::Encode)?;
272
273    let hmac = compute_hmac_opt(uri_string, &options, payload_buf.as_slice(), sender_key)?;
274    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SEQ_NUM, &hmac)?;
275
276    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
277    buffer.write_bytes(payload_buf.as_slice())
278}
279
280/// Build beacon packet (§10.2.1). Security-Mode = 0xFF (unprovisioned), HMAC = zeros.
281/// URI: /sig-net/v1/local/node_beacon/{TUID}/0 → <mult_node_beacon> = 239.254.255.255
282pub fn build_beacon_packet(
283    buffer: &mut PacketBuffer,
284    tuid: &[u8; TUID_LENGTH],
285    soem_code: SoemCode,
286    device_label: &str,
287    endpoint_count: u16,
288    otw_capability: Option<(u16, u8)>,
289    message_id: u16,
290) -> Result<()> {
291    buffer.reset();
292    coap::build_coap_header(buffer, message_id)?;
293
294    let hex = TUID(*tuid).to_hex_upper();
295    let hex_str = core::str::from_utf8(&hex).map_err(|_| SigNetError::Encode)?;
296    let segments = [
297        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, "local", "node_beacon", hex_str, "0",
298    ];
299    write_uri_path_segments(buffer, &segments)?;
300
301    // Security-Mode 0xFF: unprovisioned, only option 2076 + HMAC 2236
302    coap::encode_coap_option(buffer, SIGNET_OPTION_SECURITY_MODE, COAP_OPTION_URI_PATH, &[SECURITY_MODE_UNPROVISIONED])?;
303
304    let zero_hmac = [0u8; HMAC_SHA256_LENGTH];
305    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SECURITY_MODE, &zero_hmac)?;
306
307    // Payload: POLL_REPLY(change_count=0), DEVICE_LABEL, ENDPOINT_COUNT, OTW_CAPABILITY(optional)
308    let mut payload_buf = PacketBuffer::new();
309    tlv::encode_tid_poll_reply(&mut payload_buf, tuid, soem_code, 0)?;
310
311    let dl = device_label.as_bytes();
312    let label_tlv = TLVBlock { type_id: TID_RT_DEVICE_LABEL, value: dl };
313    tlv::encode_tlv(&mut payload_buf, &label_tlv)?;
314
315    let ec_bytes = endpoint_count.to_be_bytes();
316    let ec_tlv = TLVBlock { type_id: TID_RT_ENDPOINT_COUNT, value: &ec_bytes };
317    tlv::encode_tlv(&mut payload_buf, &ec_tlv)?;
318
319    if let Some((port, protocols)) = otw_capability {
320        tlv::encode_tid_rt_otw_capability(&mut payload_buf, port, protocols)?;
321    }
322
323    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
324    buffer.write_bytes(payload_buf.as_slice())
325}
326
327/// Build node_lost packet (§10.2.6).
328/// URI: /sig-net/v1/<scope>/node_lost/{TUID}/0 → <mult_node_lost> = 239.254.255.254
329pub fn build_node_lost_packet(
330    buffer: &mut PacketBuffer,
331    tuid: &[u8; TUID_LENGTH],
332    soem_code: SoemCode,
333    endpoint_count: u16,
334    protocol_version: u8,
335    role_capability_bits: u8,
336    mult_override_state: u8,
337    otw_capability: Option<(u16, u8)>,
338    session_id: u32,
339    seq_num: u32,
340    citizen_key: &[u8],
341    message_id: u16,
342    scope: &str,
343) -> Result<()> {
344    buffer.reset();
345    coap::build_coap_header(buffer, message_id)?;
346
347    let hex = TUID(*tuid).to_hex_upper();
348    let hex_str = core::str::from_utf8(&hex).map_err(|_| SigNetError::Encode)?;
349    let segments = [
350        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, scope, "node_lost", hex_str, "0",
351    ];
352    write_uri_path_segments(buffer, &segments)?;
353
354    let mfg_code = soem_code_mfg(soem_code);
355    let options = write_options_2076_2204(buffer, tuid, 0, mfg_code, session_id, seq_num)?;
356
357    let mut payload_buf = PacketBuffer::new();
358    // Canonical order (§10.2.6):
359    // 1. TID_POLL_REPLY
360    tlv::encode_tid_poll_reply(&mut payload_buf, tuid, soem_code, 0)?;
361    // 2. TID_RT_PROTOCOL_VERSION
362    tlv::encode_tid_rt_protocol_version(&mut payload_buf, protocol_version)?;
363    // 3. TID_RT_ROLE_CAPABILITY
364    tlv::encode_tid_rt_role_capability(&mut payload_buf, role_capability_bits)?;
365    // 4. TID_RT_ENDPOINT_COUNT
366    let ec_bytes = endpoint_count.to_be_bytes();
367    let ec_tlv = TLVBlock { type_id: TID_RT_ENDPOINT_COUNT, value: &ec_bytes };
368    tlv::encode_tlv(&mut payload_buf, &ec_tlv)?;
369    // 5. TID_RT_MULT_OVERRIDE
370    tlv::encode_tid_rt_mult_override(&mut payload_buf, mult_override_state)?;
371    // 6. TID_RT_OTW_CAPABILITY (optional)
372    if let Some((port, protocols)) = otw_capability {
373        tlv::encode_tid_rt_otw_capability(&mut payload_buf, port, protocols)?;
374    }
375
376    let mut uri_buf = [0u8; URI_STRING_MIN_BUFFER as usize];
377    let uri_len = coap::build_node_lost_uri_string(tuid, scope, &mut uri_buf)?;
378    let uri_string = core::str::from_utf8(&uri_buf[..uri_len])
379        .map_err(|_| SigNetError::Encode)?;
380
381    let hmac = compute_hmac_opt(uri_string, &options, payload_buf.as_slice(), citizen_key)?;
382    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SEQ_NUM, &hmac)?;
383
384    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
385    buffer.write_bytes(payload_buf.as_slice())
386}
387
388/// Build manager command packet (§11.3).
389/// URI: /sig-net/v1/<scope>/manager/{target_TUID}/{endpoint} → <mult_manager_send> = 239.254.255.251
390pub fn build_manager_command_packet(
391    buffer: &mut PacketBuffer,
392    target_tuid: &[u8; TUID_LENGTH],
393    endpoint: u16,
394    tlv_payload: &[u8],
395    manager_tuid: &[u8; TUID_LENGTH],
396    mfg_code: u16,
397    session_id: u32,
398    seq_num: u32,
399    km_local: &[u8],
400    message_id: u16,
401    scope: &str,
402) -> Result<()> {
403    buffer.reset();
404    coap::build_coap_header(buffer, message_id)?;
405
406    let hex = TUID(*target_tuid).to_hex_upper();
407    let hex_str = core::str::from_utf8(&hex).map_err(|_| SigNetError::Encode)?;
408    let ep_str = endpoint.to_string();
409    let segments = [
410        SIGNET_URI_PREFIX, SIGNET_URI_VERSION, scope, "manager", hex_str, &ep_str,
411    ];
412    write_uri_path_segments(buffer, &segments)?;
413
414    let options = write_options_2076_2204(buffer, manager_tuid, endpoint, mfg_code, session_id, seq_num)?;
415
416    let mut uri_buf = [0u8; URI_STRING_MIN_BUFFER as usize];
417    let uri_len = coap::build_manager_uri_string(target_tuid, endpoint, scope, &mut uri_buf)?;
418    let uri_string = core::str::from_utf8(&uri_buf[..uri_len])
419        .map_err(|_| SigNetError::Encode)?;
420
421    let hmac = compute_hmac_opt(uri_string, &options, tlv_payload, km_local)?;
422    coap::encode_coap_option(buffer, SIGNET_OPTION_HMAC, SIGNET_OPTION_SEQ_NUM, &hmac)?;
423
424    buffer.write_byte(COAP_PAYLOAD_MARKER)?;
425    buffer.write_bytes(tlv_payload)
426}