mdf4-rs 0.6.0

mdf4-rs is a Rust library for working with Measurement Data Format (ASAM MDF4) files.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
//! MDF file structure initialization and block creation.
//!
//! This module provides methods for building the MDF4 file structure:
//!
//! - File initialization (ID and HD blocks)
//! - Data group (DG) and channel group (CG) creation
//! - Channel (CN) definition with data types and conversions
//! - Source information (SI) attachment
//! - Conversion rules (CC) for physical value scaling
//!
//! The created blocks are automatically linked according to the MDF4 specification.

use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use super::{MdfWrite, MdfWriter};
use crate::{
    Result,
    blocks::{
        BlockHeader, ChannelBlock, ChannelGroupBlock, DataGroupBlock, HeaderBlock,
        IdentificationBlock, SourceBlock, TextBlock, {ConversionBlock, ConversionType},
    },
};

impl<W: MdfWrite> MdfWriter<W> {
    /// Initializes a new MDF 4.1 file with identification and header blocks.
    pub fn init_mdf_file(&mut self) -> Result<(u64, u64)> {
        let id_block = IdentificationBlock::default();
        let id_bytes = id_block.to_bytes()?;
        let id_pos = self.write_block_with_id(&id_bytes, "id_block")?;

        let hd_block = HeaderBlock::default();
        let hd_bytes = hd_block.to_bytes()?;
        let hd_pos = self.write_block_with_id(&hd_bytes, "hd_block")?;
        Ok((id_pos, hd_pos))
    }

    /// Adds a data group block to the file and links it from the header block.
    pub fn add_data_group(&mut self, prev_dg_id: Option<&str>) -> Result<String> {
        let dg_count = self
            .block_positions
            .keys()
            .filter(|k| k.starts_with("dg_"))
            .count();
        let dg_id = format!("dg_{}", dg_count);
        let dg_block = DataGroupBlock::default();
        let dg_bytes = dg_block.to_bytes()?;
        let _pos = self.write_block_with_id(&dg_bytes, &dg_id)?;

        if let Some(prev) = prev_dg_id {
            let prev_off = 24;
            self.update_block_link(prev, prev_off, &dg_id)?;
        } else {
            let hd_dg_link_offset = 24;
            self.update_block_link("hd_block", hd_dg_link_offset, &dg_id)?;
        }
        Ok(dg_id)
    }

    /// Adds a channel group block to the specified data group and links it.
    pub fn add_channel_group_with_dg<F>(
        &mut self,
        dg_id: &str,
        prev_cg_id: Option<&str>,
        configure: F,
    ) -> Result<String>
    where
        F: FnOnce(&mut ChannelGroupBlock),
    {
        let cg_count = self
            .block_positions
            .keys()
            .filter(|k| k.starts_with("cg_"))
            .count();
        let cg_id = format!("cg_{}", cg_count);

        let mut cg_block = ChannelGroupBlock::default();
        configure(&mut cg_block);

        let cg_bytes = cg_block.to_bytes()?;
        let _pos = self.write_block_with_id(&cg_bytes, &cg_id)?;

        if let Some(prev) = prev_cg_id {
            let prev_cg_off = 24;
            self.update_block_link(prev, prev_cg_off, &cg_id)?;
        } else {
            let dg_cg_link_offset = 32;
            self.update_block_link(dg_id, dg_cg_link_offset, &cg_id)?;
        }
        Ok(cg_id)
    }

    /// Adds a channel group and automatically creates a new data group for it.
    pub fn add_channel_group<F>(&mut self, prev_cg_id: Option<&str>, configure: F) -> Result<String>
    where
        F: FnOnce(&mut ChannelGroupBlock),
    {
        let dg_id = match self.last_dg.clone() {
            Some(last) => self.add_data_group(Some(&last))?,
            None => self.add_data_group(None)?,
        };
        self.last_dg = Some(dg_id.clone());
        let cg_id = self.add_channel_group_with_dg(&dg_id, prev_cg_id, configure)?;
        self.cg_to_dg.insert(cg_id.clone(), dg_id);
        self.cg_offsets.insert(cg_id.clone(), 0);
        self.cg_channels.insert(cg_id.clone(), Vec::new());
        Ok(cg_id)
    }

    /// Creates and writes a simple value-to-text conversion block.
    pub fn add_value_to_text_conversion(
        &mut self,
        mapping: &[(i64, &str)],
        default_text: &str,
        channel_id: Option<&str>,
    ) -> Result<(String, u64)> {
        let cc_count = self
            .block_positions
            .keys()
            .filter(|k| k.starts_with("cc_"))
            .count();
        let cc_id = format!("cc_{}", cc_count);

        let mut refs = Vec::new();
        for (idx, (_, txt)) in mapping.iter().enumerate() {
            let tx_id = format!("tx_{}_{}", cc_id, idx);
            let tx_block = TextBlock::new(txt);
            let tx_bytes = tx_block.to_bytes()?;
            let pos = self.write_block_with_id(&tx_bytes, &tx_id)?;
            refs.push(pos);
        }
        let tx_default_id = format!("tx_{}_default", cc_id);
        let tx_default = TextBlock::new(default_text);
        let tx_bytes = tx_default.to_bytes()?;
        let default_pos = self.write_block_with_id(&tx_bytes, &tx_default_id)?;
        refs.push(default_pos);

        let vals: Vec<f64> = mapping.iter().map(|(v, _)| *v as f64).collect();

        let block = ConversionBlock {
            header: BlockHeader {
                id: "##CC".into(),
                reserved: 0,
                length: 0,
                link_count: 0,
            },
            name_addr: None,
            unit_addr: None,
            comment_addr: None,
            inverse_addr: None,
            refs,
            conversion_type: ConversionType::ValueToText,
            precision: 0,
            flags: 0b10,
            ref_count: (mapping.len() + 1) as u16,
            value_count: mapping.len() as u16,
            phys_range_min: Some(0.0),
            phys_range_max: Some(0.0),
            values: vals,
            formula: None,
            resolved_texts: None,
            resolved_conversions: None,
            default_conversion: None,
        };
        let cc_bytes = block.to_bytes()?;
        let pos = self.write_block_with_id(&cc_bytes, &cc_id)?;

        if let Some(cn) = channel_id {
            let conv_offset = 56u64;
            self.update_block_link(cn, conv_offset, &cc_id)?;
        }
        Ok((cc_id, pos))
    }

    /// Adds a channel block to the specified channel group and links it.
    pub fn add_channel<F>(
        &mut self,
        cg_id: &str,
        prev_cn_id: Option<&str>,
        configure: F,
    ) -> Result<String>
    where
        F: FnOnce(&mut ChannelBlock),
    {
        let cn_count = self
            .block_positions
            .keys()
            .filter(|k| k.starts_with("cn_"))
            .count();
        let cn_id = format!("cn_{}", cn_count);

        let mut ch = ChannelBlock::default();
        configure(&mut ch);
        if ch.bit_count == 0 {
            ch.bit_count = ch.data_type.default_bits();
        }
        if let Some(off) = self.cg_offsets.get_mut(cg_id) {
            if ch.byte_offset == 0 {
                ch.byte_offset = *off as u32;
            }
            let used = (ch.bit_offset as usize + ch.bit_count as usize).div_ceil(8);
            *off = ch.byte_offset as usize + used;
        }

        let cn_bytes = ch.to_bytes()?;
        let cn_pos = self.write_block_with_id(&cn_bytes, &cn_id)?;
        if let Some(channel_name) = &ch.name {
            let tx_id = format!("tx_name_{}", cn_id);
            let tx_block = TextBlock::new(channel_name);
            let tx_bytes = tx_block.to_bytes()?;
            let tx_pos = self.write_block_with_id(&tx_bytes, &tx_id)?;
            let name_link_offset = 40;
            self.update_link(cn_pos + name_link_offset, tx_pos)?;
        }

        let entry = self.cg_channels.entry(cg_id.to_string()).or_default();
        entry.push(ch.clone());
        let idx = entry.len() - 1;
        self.channel_map
            .insert(cn_id.clone(), (cg_id.to_string(), idx));

        if let Some(prev_cn) = prev_cn_id {
            let prev_cn_next_link_offset = 24;
            self.update_block_link(prev_cn, prev_cn_next_link_offset, &cn_id)?;
        } else {
            let cg_cn_link_offset = 32;
            self.update_block_link(cg_id, cg_cn_link_offset, &cn_id)?;
        }
        Ok(cn_id)
    }

    /// Mark an existing channel as the time (master) channel.
    pub fn set_time_channel(&mut self, cn_id: &str) -> Result<()> {
        const CHANNEL_TYPE_OFFSET: u64 = 88;
        const SYNC_TYPE_OFFSET: u64 = 89;
        self.update_block_u8(cn_id, CHANNEL_TYPE_OFFSET, 2)?;
        self.update_block_u8(cn_id, SYNC_TYPE_OFFSET, 1)?;

        if let Some((cg, idx)) = self.channel_map.get(cn_id).cloned() {
            if let Some(chs) = self.cg_channels.get_mut(&cg) {
                if let Some(ch) = chs.get_mut(idx) {
                    ch.channel_type = 2;
                    ch.sync_type = 1;
                }
            }
        }
        Ok(())
    }

    /// Sets the unit string for an existing channel.
    ///
    /// This creates a text block containing the unit string and links it
    /// to the channel's unit_addr field.
    ///
    /// # Arguments
    /// * `cn_id` - The channel ID returned from `add_channel()`
    /// * `unit` - The unit string (e.g., "rpm", "°C", "km/h")
    ///
    /// # Example
    /// ```ignore
    /// let ch = writer.add_channel(&cg, None, |ch| {
    ///     ch.name = Some("Temperature".into());
    ///     ch.data_type = DataType::FloatLE;
    ///     ch.bit_count = 64;
    /// })?;
    /// writer.set_channel_unit(&ch, "°C")?;
    /// ```
    pub fn set_channel_unit(&mut self, cn_id: &str, unit: &str) -> Result<()> {
        if unit.is_empty() {
            return Ok(());
        }

        let cn_pos = self.get_block_position(cn_id).ok_or_else(|| {
            crate::Error::BlockLinkError(format!("Channel '{}' not found", cn_id))
        })?;

        let tx_id = format!("tx_unit_{}", cn_id);
        let tx_block = TextBlock::new(unit);
        let tx_bytes = tx_block.to_bytes()?;
        let tx_pos = self.write_block_with_id(&tx_bytes, &tx_id)?;

        // unit_addr is at offset 72 in ChannelBlock (after header + 6 links)
        const UNIT_ADDR_OFFSET: u64 = 72;
        self.update_link(cn_pos + UNIT_ADDR_OFFSET, tx_pos)?;

        Ok(())
    }

    /// Sets the comment/description for an existing channel.
    ///
    /// This creates a text block containing the comment and links it
    /// to the channel's comment_addr field.
    ///
    /// # Arguments
    /// * `cn_id` - The channel ID returned from `add_channel()`
    /// * `comment` - The comment/description string
    pub fn set_channel_comment(&mut self, cn_id: &str, comment: &str) -> Result<()> {
        if comment.is_empty() {
            return Ok(());
        }

        let cn_pos = self.get_block_position(cn_id).ok_or_else(|| {
            crate::Error::BlockLinkError(format!("Channel '{}' not found", cn_id))
        })?;

        let tx_id = format!("tx_comment_{}", cn_id);
        let tx_block = TextBlock::new(comment);
        let tx_bytes = tx_block.to_bytes()?;
        let tx_pos = self.write_block_with_id(&tx_bytes, &tx_id)?;

        // comment_addr is at offset 80 in ChannelBlock
        const COMMENT_ADDR_OFFSET: u64 = 80;
        self.update_link(cn_pos + COMMENT_ADDR_OFFSET, tx_pos)?;

        Ok(())
    }

    /// Sets the conversion block for an existing channel.
    ///
    /// This writes the conversion block and links it to the channel's
    /// conversion_addr field. Use `ConversionBlock::linear()` or other
    /// constructors to create the conversion.
    ///
    /// # Arguments
    /// * `cn_id` - The channel ID returned from `add_channel()`
    /// * `conversion` - The conversion block to attach
    ///
    /// # Example
    /// ```ignore
    /// use mdf4_rs::blocks::ConversionBlock;
    ///
    /// let ch = writer.add_channel(&cg, None, |ch| {
    ///     ch.name = Some("Temperature".into());
    ///     ch.data_type = DataType::SignedIntegerLE;
    ///     ch.bit_count = 16;
    /// })?;
    ///
    /// // Raw value to Celsius: physical = -40 + 0.1 * raw
    /// let conv = ConversionBlock::linear(-40.0, 0.1);
    /// writer.set_channel_conversion(&ch, &conv)?;
    /// ```
    pub fn set_channel_conversion(
        &mut self,
        cn_id: &str,
        conversion: &ConversionBlock,
    ) -> Result<()> {
        // Skip identity conversions (they're redundant)
        if conversion.is_identity() {
            return Ok(());
        }

        let cn_pos = self.get_block_position(cn_id).ok_or_else(|| {
            crate::Error::BlockLinkError(format!("Channel '{}' not found", cn_id))
        })?;

        let cc_count = self
            .block_positions
            .keys()
            .filter(|k| k.starts_with("cc_"))
            .count();
        let cc_id = format!("cc_{}", cc_count);

        let cc_bytes = conversion.to_bytes()?;
        let cc_pos = self.write_block_with_id(&cc_bytes, &cc_id)?;

        // conversion_addr is at offset 56 in ChannelBlock
        const CONVERSION_ADDR_OFFSET: u64 = 56;
        self.update_link(cn_pos + CONVERSION_ADDR_OFFSET, cc_pos)?;

        Ok(())
    }

    /// Sets channel limits (min/max physical values).
    ///
    /// Updates the lower_limit and upper_limit fields in the channel block.
    ///
    /// # Arguments
    /// * `cn_id` - The channel ID returned from `add_channel()`
    /// * `min` - Minimum physical value
    /// * `max` - Maximum physical value
    pub fn set_channel_limits(&mut self, cn_id: &str, min: f64, max: f64) -> Result<()> {
        let cn_pos = self.get_block_position(cn_id).ok_or_else(|| {
            crate::Error::BlockLinkError(format!("Channel '{}' not found", cn_id))
        })?;

        // lower_limit is at offset 128, upper_limit at 136
        const LOWER_LIMIT_OFFSET: u64 = 128;
        const UPPER_LIMIT_OFFSET: u64 = 136;

        self.update_link(cn_pos + LOWER_LIMIT_OFFSET, min.to_bits())?;
        self.update_link(cn_pos + UPPER_LIMIT_OFFSET, max.to_bits())?;

        // Update in-memory copy
        if let Some((cg, idx)) = self.channel_map.get(cn_id).cloned() {
            if let Some(chs) = self.cg_channels.get_mut(&cg) {
                if let Some(ch) = chs.get_mut(idx) {
                    ch.lower_limit = min;
                    ch.upper_limit = max;
                }
            }
        }

        Ok(())
    }

    /// Adds a linear conversion to a channel.
    ///
    /// Convenience method that combines `set_channel_conversion` with
    /// `ConversionBlock::linear()`.
    ///
    /// # Arguments
    /// * `cn_id` - The channel ID returned from `add_channel()`
    /// * `offset` - The offset value (P1): physical = offset + factor * raw
    /// * `factor` - The scaling factor (P2)
    ///
    /// # Example
    /// ```ignore
    /// // Raw to RPM: physical = 0 + 0.25 * raw
    /// writer.add_linear_conversion(&ch, 0.0, 0.25)?;
    /// ```
    pub fn add_linear_conversion(&mut self, cn_id: &str, offset: f64, factor: f64) -> Result<()> {
        // Skip identity conversions
        if offset == 0.0 && factor == 1.0 {
            return Ok(());
        }

        let conversion = ConversionBlock::linear(offset, factor);
        self.set_channel_conversion(cn_id, &conversion)
    }

    /// Sets the acquisition name for an existing channel group.
    ///
    /// This creates a text block containing the name and links it
    /// to the channel group's acq_name_addr field.
    ///
    /// # Arguments
    /// * `cg_id` - The channel group ID returned from `add_channel_group()`
    /// * `name` - The acquisition/group name (e.g., "Engine", "Transmission")
    ///
    /// # Example
    /// ```ignore
    /// let cg = writer.add_channel_group(None, |_| {})?;
    /// writer.set_channel_group_name(&cg, "Engine_0x100")?;
    /// ```
    pub fn set_channel_group_name(&mut self, cg_id: &str, name: &str) -> Result<()> {
        if name.is_empty() {
            return Ok(());
        }

        let cg_pos = self.get_block_position(cg_id).ok_or_else(|| {
            crate::Error::BlockLinkError(format!("Channel group '{}' not found", cg_id))
        })?;

        let tx_id = format!("tx_cgname_{}", cg_id);
        let tx_block = TextBlock::new(name);
        let tx_bytes = tx_block.to_bytes()?;
        let tx_pos = self.write_block_with_id(&tx_bytes, &tx_id)?;

        // acq_name_addr is at offset 40 in ChannelGroupBlock (after header + 2 links)
        const ACQ_NAME_ADDR_OFFSET: u64 = 40;
        self.update_link(cg_pos + ACQ_NAME_ADDR_OFFSET, tx_pos)?;

        Ok(())
    }

    /// Sets the comment for an existing channel group.
    ///
    /// This creates a text block containing the comment and links it
    /// to the channel group's comment_addr field.
    ///
    /// # Arguments
    /// * `cg_id` - The channel group ID returned from `add_channel_group()`
    /// * `comment` - The comment/description string
    pub fn set_channel_group_comment(&mut self, cg_id: &str, comment: &str) -> Result<()> {
        if comment.is_empty() {
            return Ok(());
        }

        let cg_pos = self.get_block_position(cg_id).ok_or_else(|| {
            crate::Error::BlockLinkError(format!("Channel group '{}' not found", cg_id))
        })?;

        let tx_id = format!("tx_cgcomment_{}", cg_id);
        let tx_block = TextBlock::new(comment);
        let tx_bytes = tx_block.to_bytes()?;
        let tx_pos = self.write_block_with_id(&tx_bytes, &tx_id)?;

        // comment_addr is at offset 64 in ChannelGroupBlock
        const COMMENT_ADDR_OFFSET: u64 = 64;
        self.update_link(cg_pos + COMMENT_ADDR_OFFSET, tx_pos)?;

        Ok(())
    }

    /// Sets the acquisition source for an existing channel group.
    ///
    /// This writes a source block with the given name and links it
    /// to the channel group's acq_source_addr field.
    ///
    /// # Arguments
    /// * `cg_id` - The channel group ID returned from `add_channel_group()`
    /// * `source` - The source block to attach
    /// * `source_name` - Optional name for the source (e.g., ECU name)
    ///
    /// # Example
    /// ```ignore
    /// use mdf4_rs::blocks::{SourceBlock, SourceType, BusType};
    ///
    /// let cg = writer.add_channel_group(None, |_| {})?;
    /// let source = SourceBlock::can_ecu();
    /// writer.set_channel_group_source(&cg, &source, Some("ECM"))?;
    /// ```
    pub fn set_channel_group_source(
        &mut self,
        cg_id: &str,
        source: &SourceBlock,
        source_name: Option<&str>,
    ) -> Result<()> {
        let cg_pos = self.get_block_position(cg_id).ok_or_else(|| {
            crate::Error::BlockLinkError(format!("Channel group '{}' not found", cg_id))
        })?;

        let si_count = self
            .block_positions
            .keys()
            .filter(|k| k.starts_with("si_"))
            .count();
        let si_id = format!("si_{}", si_count);

        // Clone source and optionally set name
        let mut source = source.clone();

        // Write source name text block if provided
        if let Some(name) = source_name {
            if !name.is_empty() {
                let tx_id = format!("tx_siname_{}", si_id);
                let tx_block = TextBlock::new(name);
                let tx_bytes = tx_block.to_bytes()?;
                let tx_pos = self.write_block_with_id(&tx_bytes, &tx_id)?;
                source.name_addr = tx_pos;
            }
        }

        let si_bytes = source.to_bytes()?;
        let si_pos = self.write_block_with_id(&si_bytes, &si_id)?;

        // Update name link in the source block if we wrote one
        if source.name_addr != 0 {
            // name_addr is at offset 24 in SourceBlock
            self.update_link(si_pos + 24, source.name_addr)?;
        }

        // acq_source_addr is at offset 48 in ChannelGroupBlock
        const ACQ_SOURCE_ADDR_OFFSET: u64 = 48;
        self.update_link(cg_pos + ACQ_SOURCE_ADDR_OFFSET, si_pos)?;

        Ok(())
    }

    /// Convenience method to set channel group source with just a name.
    ///
    /// Creates a CAN ECU source block with the given name.
    ///
    /// # Arguments
    /// * `cg_id` - The channel group ID
    /// * `ecu_name` - The ECU/sender name
    pub fn set_channel_group_source_name(&mut self, cg_id: &str, ecu_name: &str) -> Result<()> {
        if ecu_name.is_empty() {
            return Ok(());
        }
        let source = SourceBlock::can_ecu();
        self.set_channel_group_source(cg_id, &source, Some(ecu_name))
    }
}