Skip to main content

plc_comm_slmp/
device_ranges.rs

1use crate::client::SlmpClient;
2use crate::error::SlmpError;
3use crate::model::{SlmpDeviceAddress, SlmpDeviceCode, SlmpTypeNameInfo};
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7pub enum SlmpDeviceRangeFamily {
8    IqR,
9    IqL,
10    MxF,
11    MxR,
12    IqF,
13    QCpu,
14    LCpu,
15    QnU,
16    QnUDV,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub enum SlmpDeviceRangeCategory {
21    Bit,
22    Word,
23    TimerCounter,
24    Index,
25    FileRefresh,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub enum SlmpDeviceRangeNotation {
30    Decimal,
31    Octal,
32    Hexadecimal,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36pub struct SlmpDeviceRangeEntry {
37    pub device: String,
38    pub category: SlmpDeviceRangeCategory,
39    pub is_bit_device: bool,
40    pub supported: bool,
41    pub lower_bound: u32,
42    pub upper_bound: Option<u32>,
43    pub point_count: Option<u32>,
44    pub address_range: Option<String>,
45    pub notation: SlmpDeviceRangeNotation,
46    pub source: String,
47    pub notes: Option<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
51pub struct SlmpDeviceRangeCatalog {
52    /// PLC model text when known, or a user-selected family label.
53    pub model: String,
54    /// Model code when known. Zero when the caller selected the family explicitly.
55    pub model_code: u16,
56    pub has_model_code: bool,
57    pub family: SlmpDeviceRangeFamily,
58    pub entries: Vec<SlmpDeviceRangeEntry>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62enum SlmpRangeValueKind {
63    Unsupported,
64    Undefined,
65    Fixed,
66    WordRegister,
67    DWordRegister,
68    WordRegisterClipped,
69    DWordRegisterClipped,
70}
71
72#[derive(Debug, Clone)]
73struct SlmpRangeValueSpec {
74    kind: SlmpRangeValueKind,
75    register: u16,
76    fixed_value: u32,
77    clip_value: u32,
78    source: &'static str,
79    notes: Option<&'static str>,
80}
81
82#[derive(Debug, Clone, Copy)]
83struct SlmpDeviceRangeRow {
84    category: SlmpDeviceRangeCategory,
85    devices: &'static [(&'static str, bool)],
86    notation: SlmpDeviceRangeNotation,
87}
88
89#[derive(Debug, Clone)]
90pub(crate) struct SlmpDeviceRangeProfile {
91    pub(crate) family: SlmpDeviceRangeFamily,
92    pub(crate) register_start: u16,
93    pub(crate) register_count: u16,
94    rules: BTreeMap<&'static str, SlmpRangeValueSpec>,
95}
96
97const ORDERED_ITEMS: &[&str] = &[
98    "X", "Y", "M", "B", "SB", "F", "V", "L", "S", "D", "W", "SW", "R", "T", "ST", "C", "LT", "LST",
99    "LC", "Z", "LZ", "ZR", "RD", "SM", "SD",
100];
101
102const T_DEVICES: &[(&str, bool)] = &[("TS", true), ("TC", true), ("TN", false)];
103const ST_DEVICES: &[(&str, bool)] = &[("STS", true), ("STC", true), ("STN", false)];
104const C_DEVICES: &[(&str, bool)] = &[("CS", true), ("CC", true), ("CN", false)];
105const LT_DEVICES: &[(&str, bool)] = &[("LTS", true), ("LTC", true), ("LTN", false)];
106const LST_DEVICES: &[(&str, bool)] = &[("LSTS", true), ("LSTC", true), ("LSTN", false)];
107const LC_DEVICES: &[(&str, bool)] = &[("LCS", true), ("LCC", true), ("LCN", false)];
108
109pub(crate) fn normalize_model(model: &str) -> String {
110    model
111        .trim()
112        .trim_end_matches('\0')
113        .trim()
114        .to_ascii_uppercase()
115}
116
117#[cfg(test)]
118pub(crate) fn resolve_family(
119    type_info: &SlmpTypeNameInfo,
120) -> Result<SlmpDeviceRangeFamily, SlmpError> {
121    if type_info.has_model_code {
122        if let Some(family) = family_from_model_code(type_info.model_code) {
123            return Ok(family);
124        }
125    }
126
127    let normalized = normalize_model(&type_info.model);
128    if let Some(family) = family_from_model_name(&normalized) {
129        return Ok(family);
130    }
131
132    let code_text = if type_info.has_model_code {
133        format!("0x{:04X}", type_info.model_code)
134    } else {
135        "none".to_string()
136    };
137    Err(SlmpError::new(format!(
138        "Unsupported PLC model for device-range rules: model='{normalized}', model_code={code_text}."
139    )))
140}
141
142#[cfg(test)]
143pub(crate) fn resolve_profile(
144    type_info: &SlmpTypeNameInfo,
145) -> Result<SlmpDeviceRangeProfile, SlmpError> {
146    Ok(match resolve_family(type_info)? {
147        SlmpDeviceRangeFamily::IqR => create_iqr_profile(),
148        SlmpDeviceRangeFamily::IqL => create_iql_profile(),
149        SlmpDeviceRangeFamily::MxF => create_mxf_profile(),
150        SlmpDeviceRangeFamily::MxR => create_mxr_profile(),
151        SlmpDeviceRangeFamily::IqF => create_iqf_profile(),
152        SlmpDeviceRangeFamily::QCpu => create_qcpu_profile(),
153        SlmpDeviceRangeFamily::LCpu => create_lcpu_profile(),
154        SlmpDeviceRangeFamily::QnU => create_qnu_profile(),
155        SlmpDeviceRangeFamily::QnUDV => create_qnudv_profile(),
156    })
157}
158
159pub(crate) fn resolve_profile_for_family(family: SlmpDeviceRangeFamily) -> SlmpDeviceRangeProfile {
160    match family {
161        SlmpDeviceRangeFamily::IqR => create_iqr_profile(),
162        SlmpDeviceRangeFamily::IqL => create_iql_profile(),
163        SlmpDeviceRangeFamily::MxF => create_mxf_profile(),
164        SlmpDeviceRangeFamily::MxR => create_mxr_profile(),
165        SlmpDeviceRangeFamily::IqF => create_iqf_profile(),
166        SlmpDeviceRangeFamily::QCpu => create_qcpu_profile(),
167        SlmpDeviceRangeFamily::LCpu => create_lcpu_profile(),
168        SlmpDeviceRangeFamily::QnU => create_qnu_profile(),
169        SlmpDeviceRangeFamily::QnUDV => create_qnudv_profile(),
170    }
171}
172
173pub(crate) async fn read_registers(
174    client: &SlmpClient,
175    profile: &SlmpDeviceRangeProfile,
176) -> Result<BTreeMap<u16, u16>, SlmpError> {
177    if profile.register_count == 0 {
178        return Ok(BTreeMap::new());
179    }
180
181    let values = client
182        .read_words_raw(
183            SlmpDeviceAddress::new(SlmpDeviceCode::SD, u32::from(profile.register_start)),
184            profile.register_count,
185        )
186        .await?;
187
188    let mut map = BTreeMap::new();
189    for (index, value) in values.into_iter().enumerate() {
190        map.insert(profile.register_start + index as u16, value);
191    }
192    Ok(map)
193}
194
195pub(crate) fn build_catalog(
196    type_info: &SlmpTypeNameInfo,
197    profile: &SlmpDeviceRangeProfile,
198    registers: &BTreeMap<u16, u16>,
199) -> Result<SlmpDeviceRangeCatalog, SlmpError> {
200    let mut entries = Vec::with_capacity(64);
201    for item in ORDERED_ITEMS {
202        let row = row_for(item);
203        let spec = profile
204            .rules
205            .get(item)
206            .ok_or_else(|| SlmpError::new(format!("Missing range rule for item {item}.")))?;
207        let raw_point_count = evaluate_point_count(spec, registers)?;
208        let (point_count, family_note) =
209            apply_family_point_count_cap(profile.family, item, raw_point_count);
210        let upper_bound = point_count_to_upper_bound(point_count);
211        let supported = spec.kind != SlmpRangeValueKind::Unsupported;
212        for (device, is_bit_device) in row.devices {
213            let notation = resolve_notation(profile.family, device, row.notation);
214            entries.push(SlmpDeviceRangeEntry {
215                device: (*device).to_string(),
216                category: row.category,
217                is_bit_device: *is_bit_device,
218                supported,
219                lower_bound: 0,
220                upper_bound,
221                point_count,
222                address_range: format_address_range(device, notation, upper_bound),
223                notation,
224                source: spec.source.to_string(),
225                notes: merge_notes(spec.notes, family_note),
226            });
227        }
228    }
229
230    Ok(SlmpDeviceRangeCatalog {
231        model: normalize_model(&type_info.model),
232        model_code: type_info.model_code,
233        has_model_code: type_info.has_model_code,
234        family: profile.family,
235        entries,
236    })
237}
238
239pub(crate) fn build_catalog_for_family(
240    family: SlmpDeviceRangeFamily,
241    registers: &BTreeMap<u16, u16>,
242) -> Result<SlmpDeviceRangeCatalog, SlmpError> {
243    let profile = resolve_profile_for_family(family);
244    let mut catalog = build_catalog(
245        &SlmpTypeNameInfo {
246            model: family_label(family).to_string(),
247            model_code: 0,
248            has_model_code: false,
249        },
250        &profile,
251        registers,
252    )?;
253    catalog.model = family_label(family).to_string();
254    Ok(catalog)
255}
256
257pub(crate) fn replace_fixed_point_count(
258    mut catalog: SlmpDeviceRangeCatalog,
259    device: &str,
260    point_count: u32,
261    source: &str,
262    note: &str,
263) -> SlmpDeviceRangeCatalog {
264    let upper_bound = point_count_to_upper_bound(Some(point_count));
265    for entry in &mut catalog.entries {
266        if entry.device == device {
267            entry.upper_bound = upper_bound;
268            entry.point_count = Some(point_count);
269            entry.address_range = format_address_range(&entry.device, entry.notation, upper_bound);
270            entry.source = source.to_string();
271            entry.notes = Some(match &entry.notes {
272                Some(existing) if !existing.is_empty() => format!("{existing} {note}"),
273                _ => note.to_string(),
274            });
275        }
276    }
277    catalog
278}
279
280pub(crate) fn family_label(family: SlmpDeviceRangeFamily) -> &'static str {
281    match family {
282        SlmpDeviceRangeFamily::IqR => "IQ-R",
283        SlmpDeviceRangeFamily::IqL => "iQ-L",
284        SlmpDeviceRangeFamily::MxF => "MX-F",
285        SlmpDeviceRangeFamily::MxR => "MX-R",
286        SlmpDeviceRangeFamily::IqF => "IQ-F",
287        SlmpDeviceRangeFamily::QCpu => "QCPU",
288        SlmpDeviceRangeFamily::LCpu => "LCPU",
289        SlmpDeviceRangeFamily::QnU => "QnU",
290        SlmpDeviceRangeFamily::QnUDV => "QnUDV",
291    }
292}
293
294fn evaluate_point_count(
295    spec: &SlmpRangeValueSpec,
296    registers: &BTreeMap<u16, u16>,
297) -> Result<Option<u32>, SlmpError> {
298    Ok(match spec.kind {
299        SlmpRangeValueKind::Unsupported | SlmpRangeValueKind::Undefined => None,
300        SlmpRangeValueKind::Fixed => Some(spec.fixed_value),
301        SlmpRangeValueKind::WordRegister => Some(read_word(registers, spec.register)?),
302        SlmpRangeValueKind::DWordRegister => Some(read_dword(registers, spec.register)?),
303        SlmpRangeValueKind::WordRegisterClipped => {
304            Some(read_word(registers, spec.register)?.min(spec.clip_value))
305        }
306        SlmpRangeValueKind::DWordRegisterClipped => {
307            Some(read_dword(registers, spec.register)?.min(spec.clip_value))
308        }
309    })
310}
311
312fn apply_family_point_count_cap(
313    family: SlmpDeviceRangeFamily,
314    item: &str,
315    point_count: Option<u32>,
316) -> (Option<u32>, Option<&'static str>) {
317    let Some(value) = point_count else {
318        return (None, None);
319    };
320    let Some(cap) = family_point_count_cap(family, item) else {
321        return (Some(value), None);
322    };
323    if value > cap {
324        (
325            Some(cap),
326            Some("iQ-R SD point count is capped to the fixed family maximum."),
327        )
328    } else {
329        (Some(value), None)
330    }
331}
332
333fn family_point_count_cap(family: SlmpDeviceRangeFamily, item: &str) -> Option<u32> {
334    if family != SlmpDeviceRangeFamily::IqR {
335        return None;
336    }
337
338    Some(match item {
339        "X" | "Y" => 12_288,
340        "M" | "B" | "SB" => 94_674_944,
341        "F" | "V" | "L" => 32_768,
342        "T" | "ST" | "C" => 5_259_712,
343        "LT" | "LST" => 1_479_296,
344        "LC" => 2_784_544,
345        "D" | "W" | "SW" => 5_917_184,
346        _ => return None,
347    })
348}
349
350fn merge_notes(primary: Option<&'static str>, extra: Option<&'static str>) -> Option<String> {
351    match (primary, extra) {
352        (Some(left), Some(right)) if !left.is_empty() && !right.is_empty() => {
353            Some(format!("{left} {right}"))
354        }
355        (Some(left), _) if !left.is_empty() => Some(left.to_string()),
356        (_, Some(right)) if !right.is_empty() => Some(right.to_string()),
357        _ => None,
358    }
359}
360
361fn point_count_to_upper_bound(point_count: Option<u32>) -> Option<u32> {
362    point_count.and_then(|value| value.checked_sub(1))
363}
364
365fn resolve_notation(
366    family: SlmpDeviceRangeFamily,
367    device: &str,
368    default_notation: SlmpDeviceRangeNotation,
369) -> SlmpDeviceRangeNotation {
370    if family == SlmpDeviceRangeFamily::IqF && matches!(device, "X" | "Y") {
371        return SlmpDeviceRangeNotation::Octal;
372    }
373
374    default_notation
375}
376
377fn format_address_range(
378    device: &str,
379    notation: SlmpDeviceRangeNotation,
380    upper_bound: Option<u32>,
381) -> Option<String> {
382    let upper_bound = upper_bound?;
383    Some(match notation {
384        SlmpDeviceRangeNotation::Decimal => format!("{device}0-{device}{upper_bound}"),
385        SlmpDeviceRangeNotation::Octal => {
386            let upper_text = format!("{upper_bound:o}");
387            let width = std::cmp::max(3, upper_text.len());
388            format!(
389                "{device}{start:0width$o}-{device}{end:0width$o}",
390                start = 0u32,
391                end = upper_bound,
392                width = width
393            )
394        }
395        SlmpDeviceRangeNotation::Hexadecimal => {
396            let width = std::cmp::max(3, format!("{upper_bound:X}").len());
397            format!(
398                "{device}{start:0width$X}-{device}{end:0width$X}",
399                start = 0u32,
400                end = upper_bound,
401                width = width
402            )
403        }
404    })
405}
406
407fn read_word(registers: &BTreeMap<u16, u16>, register: u16) -> Result<u32, SlmpError> {
408    registers
409        .get(&register)
410        .copied()
411        .map(u32::from)
412        .ok_or_else(|| SlmpError::new(format!("Device-range resolver is missing SD{register}.")))
413}
414
415fn read_dword(registers: &BTreeMap<u16, u16>, register: u16) -> Result<u32, SlmpError> {
416    let low = registers
417        .get(&register)
418        .copied()
419        .ok_or_else(|| SlmpError::new(format!("Device-range resolver is missing SD{register}.")))?;
420    let high_register = register + 1;
421    let high = registers.get(&high_register).copied().ok_or_else(|| {
422        SlmpError::new(format!(
423            "Device-range resolver is missing SD{high_register}."
424        ))
425    })?;
426    Ok(u32::from(low) | (u32::from(high) << 16))
427}
428
429fn row_for(item: &str) -> SlmpDeviceRangeRow {
430    match item {
431        "X" => SlmpDeviceRangeRow {
432            category: SlmpDeviceRangeCategory::Bit,
433            devices: &[("X", true)],
434            notation: SlmpDeviceRangeNotation::Hexadecimal,
435        },
436        "Y" => SlmpDeviceRangeRow {
437            category: SlmpDeviceRangeCategory::Bit,
438            devices: &[("Y", true)],
439            notation: SlmpDeviceRangeNotation::Hexadecimal,
440        },
441        "M" => SlmpDeviceRangeRow {
442            category: SlmpDeviceRangeCategory::Bit,
443            devices: &[("M", true)],
444            notation: SlmpDeviceRangeNotation::Decimal,
445        },
446        "B" => SlmpDeviceRangeRow {
447            category: SlmpDeviceRangeCategory::Bit,
448            devices: &[("B", true)],
449            notation: SlmpDeviceRangeNotation::Hexadecimal,
450        },
451        "SB" => SlmpDeviceRangeRow {
452            category: SlmpDeviceRangeCategory::Bit,
453            devices: &[("SB", true)],
454            notation: SlmpDeviceRangeNotation::Hexadecimal,
455        },
456        "F" => SlmpDeviceRangeRow {
457            category: SlmpDeviceRangeCategory::Bit,
458            devices: &[("F", true)],
459            notation: SlmpDeviceRangeNotation::Decimal,
460        },
461        "V" => SlmpDeviceRangeRow {
462            category: SlmpDeviceRangeCategory::Bit,
463            devices: &[("V", true)],
464            notation: SlmpDeviceRangeNotation::Decimal,
465        },
466        "L" => SlmpDeviceRangeRow {
467            category: SlmpDeviceRangeCategory::Bit,
468            devices: &[("L", true)],
469            notation: SlmpDeviceRangeNotation::Decimal,
470        },
471        "S" => SlmpDeviceRangeRow {
472            category: SlmpDeviceRangeCategory::Bit,
473            devices: &[("S", true)],
474            notation: SlmpDeviceRangeNotation::Decimal,
475        },
476        "D" => SlmpDeviceRangeRow {
477            category: SlmpDeviceRangeCategory::Word,
478            devices: &[("D", false)],
479            notation: SlmpDeviceRangeNotation::Decimal,
480        },
481        "W" => SlmpDeviceRangeRow {
482            category: SlmpDeviceRangeCategory::Word,
483            devices: &[("W", false)],
484            notation: SlmpDeviceRangeNotation::Hexadecimal,
485        },
486        "SW" => SlmpDeviceRangeRow {
487            category: SlmpDeviceRangeCategory::Word,
488            devices: &[("SW", false)],
489            notation: SlmpDeviceRangeNotation::Hexadecimal,
490        },
491        "R" => SlmpDeviceRangeRow {
492            category: SlmpDeviceRangeCategory::Word,
493            devices: &[("R", false)],
494            notation: SlmpDeviceRangeNotation::Decimal,
495        },
496        "T" => multi(
497            SlmpDeviceRangeCategory::TimerCounter,
498            SlmpDeviceRangeNotation::Decimal,
499            T_DEVICES,
500        ),
501        "ST" => multi(
502            SlmpDeviceRangeCategory::TimerCounter,
503            SlmpDeviceRangeNotation::Decimal,
504            ST_DEVICES,
505        ),
506        "C" => multi(
507            SlmpDeviceRangeCategory::TimerCounter,
508            SlmpDeviceRangeNotation::Decimal,
509            C_DEVICES,
510        ),
511        "LT" => multi(
512            SlmpDeviceRangeCategory::TimerCounter,
513            SlmpDeviceRangeNotation::Decimal,
514            LT_DEVICES,
515        ),
516        "LST" => multi(
517            SlmpDeviceRangeCategory::TimerCounter,
518            SlmpDeviceRangeNotation::Decimal,
519            LST_DEVICES,
520        ),
521        "LC" => multi(
522            SlmpDeviceRangeCategory::TimerCounter,
523            SlmpDeviceRangeNotation::Decimal,
524            LC_DEVICES,
525        ),
526        "Z" => SlmpDeviceRangeRow {
527            category: SlmpDeviceRangeCategory::Index,
528            devices: &[("Z", false)],
529            notation: SlmpDeviceRangeNotation::Decimal,
530        },
531        "LZ" => SlmpDeviceRangeRow {
532            category: SlmpDeviceRangeCategory::Index,
533            devices: &[("LZ", false)],
534            notation: SlmpDeviceRangeNotation::Decimal,
535        },
536        "ZR" => SlmpDeviceRangeRow {
537            category: SlmpDeviceRangeCategory::FileRefresh,
538            devices: &[("ZR", false)],
539            notation: SlmpDeviceRangeNotation::Decimal,
540        },
541        "RD" => SlmpDeviceRangeRow {
542            category: SlmpDeviceRangeCategory::FileRefresh,
543            devices: &[("RD", false)],
544            notation: SlmpDeviceRangeNotation::Decimal,
545        },
546        "SM" => SlmpDeviceRangeRow {
547            category: SlmpDeviceRangeCategory::Bit,
548            devices: &[("SM", true)],
549            notation: SlmpDeviceRangeNotation::Decimal,
550        },
551        "SD" => SlmpDeviceRangeRow {
552            category: SlmpDeviceRangeCategory::Word,
553            devices: &[("SD", false)],
554            notation: SlmpDeviceRangeNotation::Decimal,
555        },
556        _ => unreachable!("unsupported item {item}"),
557    }
558}
559
560fn multi(
561    category: SlmpDeviceRangeCategory,
562    notation: SlmpDeviceRangeNotation,
563    devices: &'static [(&'static str, bool)],
564) -> SlmpDeviceRangeRow {
565    SlmpDeviceRangeRow {
566        category,
567        devices,
568        notation,
569    }
570}
571
572#[cfg(test)]
573fn family_from_model_code(model_code: u16) -> Option<SlmpDeviceRangeFamily> {
574    Some(match model_code {
575        0x0250 | 0x0251 | 0x0041 | 0x0042 | 0x0043 | 0x0044 | 0x004B | 0x004C | 0x0230 => {
576            SlmpDeviceRangeFamily::QCpu
577        }
578        0x0260 | 0x0261 | 0x0262 | 0x0263 | 0x0268 | 0x0269 | 0x026A | 0x0266 | 0x026B | 0x0267
579        | 0x026C | 0x026D | 0x026E => SlmpDeviceRangeFamily::QnU,
580        0x0366 | 0x0367 | 0x0368 | 0x036A | 0x036C => SlmpDeviceRangeFamily::QnUDV,
581        0x0543 | 0x0541 | 0x0544 | 0x0545 | 0x0542 | 0x0641 => SlmpDeviceRangeFamily::LCpu,
582        0x48C0 | 0x48C1 | 0x48C2 | 0x48C3 => SlmpDeviceRangeFamily::IqL,
583        0x48A0 | 0x48A1 | 0x48A2 | 0x4800 | 0x4801 | 0x4802 | 0x4803 | 0x4804 | 0x4805 | 0x4806
584        | 0x4807 | 0x4808 | 0x4809 | 0x4841 | 0x4842 | 0x4843 | 0x4844 | 0x4851 | 0x4852
585        | 0x4853 | 0x4854 | 0x4891 | 0x4892 | 0x4893 | 0x4894 | 0x4820 | 0x4E01 | 0x4860
586        | 0x4861 | 0x4862 | 0x0642 => SlmpDeviceRangeFamily::IqR,
587        0x48E9 | 0x48EA | 0x48EB | 0x48EE | 0x48EF => SlmpDeviceRangeFamily::MxR,
588        0x4A21 | 0x4A23 | 0x4A24 | 0x4A29 | 0x4A2B | 0x4A2C | 0x4A31 | 0x4A33 | 0x4A34 | 0x4A41
589        | 0x4A43 | 0x4A44 | 0x4A49 | 0x4A4B | 0x4A4C | 0x4A51 | 0x4A53 | 0x4A54 | 0x4A91
590        | 0x4A92 | 0x4A93 | 0x4A99 | 0x4A9A | 0x4A9B | 0x4AA9 | 0x4AB1 | 0x4AB9 | 0x4B0D
591        | 0x4B0E | 0x4B0F | 0x4B14 | 0x4B15 | 0x4B16 | 0x4B1B | 0x4B1C | 0x4B1D | 0x4B4E
592        | 0x4B4F | 0x4B50 | 0x4B51 | 0x4B55 | 0x4B56 | 0x4B57 | 0x4B58 | 0x4B5C | 0x4B5D
593        | 0x4B5E | 0x4B5F => SlmpDeviceRangeFamily::IqF,
594        _ => return None,
595    })
596}
597
598#[cfg(test)]
599fn family_from_model_name(model: &str) -> Option<SlmpDeviceRangeFamily> {
600    const PREFIXES: &[(&str, SlmpDeviceRangeFamily)] = &[
601        ("Q04UDPV", SlmpDeviceRangeFamily::QnUDV),
602        ("Q06UDPV", SlmpDeviceRangeFamily::QnUDV),
603        ("Q13UDPV", SlmpDeviceRangeFamily::QnUDV),
604        ("Q26UDPV", SlmpDeviceRangeFamily::QnUDV),
605        ("Q03UDV", SlmpDeviceRangeFamily::QnUDV),
606        ("Q04UDV", SlmpDeviceRangeFamily::QnUDV),
607        ("Q06UDV", SlmpDeviceRangeFamily::QnUDV),
608        ("Q13UDV", SlmpDeviceRangeFamily::QnUDV),
609        ("Q26UDV", SlmpDeviceRangeFamily::QnUDV),
610        ("Q00UJ", SlmpDeviceRangeFamily::QnU),
611        ("Q00U", SlmpDeviceRangeFamily::QnU),
612        ("Q01U", SlmpDeviceRangeFamily::QnU),
613        ("Q02U", SlmpDeviceRangeFamily::QnU),
614        ("Q03UD", SlmpDeviceRangeFamily::QnU),
615        ("Q04UD", SlmpDeviceRangeFamily::QnU),
616        ("Q06UD", SlmpDeviceRangeFamily::QnU),
617        ("Q10UD", SlmpDeviceRangeFamily::QnU),
618        ("Q13UD", SlmpDeviceRangeFamily::QnU),
619        ("Q20UD", SlmpDeviceRangeFamily::QnU),
620        ("Q26UD", SlmpDeviceRangeFamily::QnU),
621        ("Q50UDEH", SlmpDeviceRangeFamily::QnU),
622        ("Q100UDEH", SlmpDeviceRangeFamily::QnU),
623        ("FX5UC", SlmpDeviceRangeFamily::IqF),
624        ("FX5UJ", SlmpDeviceRangeFamily::IqF),
625        ("FX5U", SlmpDeviceRangeFamily::IqF),
626        ("FX5S", SlmpDeviceRangeFamily::IqF),
627        ("MXF100-", SlmpDeviceRangeFamily::MxF),
628        ("MXF", SlmpDeviceRangeFamily::MxF),
629        ("MXR", SlmpDeviceRangeFamily::MxR),
630        ("LJ72GF15-T2", SlmpDeviceRangeFamily::LCpu),
631        ("L02SCPU", SlmpDeviceRangeFamily::LCpu),
632        ("L02CPU", SlmpDeviceRangeFamily::LCpu),
633        ("L06CPU", SlmpDeviceRangeFamily::LCpu),
634        ("L26CPU", SlmpDeviceRangeFamily::LCpu),
635        ("L04HCPU", SlmpDeviceRangeFamily::IqL),
636        ("L08HCPU", SlmpDeviceRangeFamily::IqL),
637        ("L16HCPU", SlmpDeviceRangeFamily::IqL),
638        ("L32HCPU", SlmpDeviceRangeFamily::IqL),
639        ("RJ72GF15-T2", SlmpDeviceRangeFamily::IqR),
640        ("NZ2GF-ETB", SlmpDeviceRangeFamily::IqR),
641        ("MI5122-VW", SlmpDeviceRangeFamily::IqR),
642        ("QS001CPU", SlmpDeviceRangeFamily::QCpu),
643        ("Q00JCPU", SlmpDeviceRangeFamily::QCpu),
644        ("Q00CPU", SlmpDeviceRangeFamily::QCpu),
645        ("Q01CPU", SlmpDeviceRangeFamily::QCpu),
646        ("Q02", SlmpDeviceRangeFamily::QCpu),
647        ("Q06", SlmpDeviceRangeFamily::QCpu),
648        ("Q12", SlmpDeviceRangeFamily::QCpu),
649        ("Q25", SlmpDeviceRangeFamily::QCpu),
650        ("R", SlmpDeviceRangeFamily::IqR),
651    ];
652
653    PREFIXES
654        .iter()
655        .find(|(prefix, _)| model.starts_with(prefix))
656        .map(|(_, family)| *family)
657}
658
659fn create_profile(
660    family: SlmpDeviceRangeFamily,
661    register_start: u16,
662    register_count: u16,
663    rules: Vec<(&'static str, SlmpRangeValueSpec)>,
664) -> SlmpDeviceRangeProfile {
665    let mut map = BTreeMap::new();
666    for (item, spec) in rules {
667        map.insert(item, spec);
668    }
669    SlmpDeviceRangeProfile {
670        family,
671        register_start,
672        register_count,
673        rules: map,
674    }
675}
676
677fn fixed(value: u32, source: &'static str) -> SlmpRangeValueSpec {
678    SlmpRangeValueSpec {
679        kind: SlmpRangeValueKind::Fixed,
680        register: 0,
681        fixed_value: value,
682        clip_value: 0,
683        source,
684        notes: None,
685    }
686}
687
688fn word_register(
689    register: u16,
690    source: &'static str,
691    notes: Option<&'static str>,
692) -> SlmpRangeValueSpec {
693    SlmpRangeValueSpec {
694        kind: SlmpRangeValueKind::WordRegister,
695        register,
696        fixed_value: 0,
697        clip_value: 0,
698        source,
699        notes,
700    }
701}
702
703fn dword_register(
704    register: u16,
705    source: &'static str,
706    notes: Option<&'static str>,
707) -> SlmpRangeValueSpec {
708    SlmpRangeValueSpec {
709        kind: SlmpRangeValueKind::DWordRegister,
710        register,
711        fixed_value: 0,
712        clip_value: 0,
713        source,
714        notes,
715    }
716}
717
718fn word_register_clipped(
719    register: u16,
720    clip_value: u32,
721    source: &'static str,
722    notes: Option<&'static str>,
723) -> SlmpRangeValueSpec {
724    SlmpRangeValueSpec {
725        kind: SlmpRangeValueKind::WordRegisterClipped,
726        register,
727        fixed_value: 0,
728        clip_value,
729        source,
730        notes,
731    }
732}
733
734fn dword_register_clipped(
735    register: u16,
736    clip_value: u32,
737    source: &'static str,
738    notes: Option<&'static str>,
739) -> SlmpRangeValueSpec {
740    SlmpRangeValueSpec {
741        kind: SlmpRangeValueKind::DWordRegisterClipped,
742        register,
743        fixed_value: 0,
744        clip_value,
745        source,
746        notes,
747    }
748}
749
750fn undefined(notes: &'static str) -> SlmpRangeValueSpec {
751    SlmpRangeValueSpec {
752        kind: SlmpRangeValueKind::Undefined,
753        register: 0,
754        fixed_value: 0,
755        clip_value: 0,
756        source: "No finite upper-bound register",
757        notes: Some(notes),
758    }
759}
760
761fn unsupported(notes: &'static str) -> SlmpRangeValueSpec {
762    SlmpRangeValueSpec {
763        kind: SlmpRangeValueKind::Unsupported,
764        register: 0,
765        fixed_value: 0,
766        clip_value: 0,
767        source: "Not supported",
768        notes: Some(notes),
769    }
770}
771
772fn create_iqr_profile() -> SlmpDeviceRangeProfile {
773    create_profile(
774        SlmpDeviceRangeFamily::IqR,
775        260,
776        50,
777        vec![
778            ("X", dword_register(260, "SD260-SD261 (32-bit)", None)),
779            ("Y", dword_register(262, "SD262-SD263 (32-bit)", None)),
780            ("M", dword_register(264, "SD264-SD265 (32-bit)", None)),
781            ("B", dword_register(266, "SD266-SD267 (32-bit)", None)),
782            ("SB", dword_register(268, "SD268-SD269 (32-bit)", None)),
783            ("F", dword_register(270, "SD270-SD271 (32-bit)", None)),
784            ("V", dword_register(272, "SD272-SD273 (32-bit)", None)),
785            ("L", dword_register(274, "SD274-SD275 (32-bit)", None)),
786            ("S", dword_register(276, "SD276-SD277 (32-bit)", None)),
787            ("D", dword_register(280, "SD280-SD281 (32-bit)", None)),
788            ("W", dword_register(282, "SD282-SD283 (32-bit)", None)),
789            ("SW", dword_register(284, "SD284-SD285 (32-bit)", None)),
790            (
791                "R",
792                dword_register_clipped(
793                    306,
794                    32768,
795                    "SD306-SD307 (32-bit)",
796                    Some("Upper bound is clipped to 32768."),
797                ),
798            ),
799            ("T", dword_register(288, "SD288-SD289 (32-bit)", None)),
800            ("ST", dword_register(290, "SD290-SD291 (32-bit)", None)),
801            ("C", dword_register(292, "SD292-SD293 (32-bit)", None)),
802            ("LT", dword_register(294, "SD294-SD295 (32-bit)", None)),
803            ("LST", dword_register(296, "SD296-SD297 (32-bit)", None)),
804            ("LC", dword_register(298, "SD298-SD299 (32-bit)", None)),
805            ("Z", word_register(300, "SD300", None)),
806            ("LZ", word_register(302, "SD302", None)),
807            ("ZR", dword_register(306, "SD306-SD307 (32-bit)", None)),
808            ("RD", dword_register(308, "SD308-SD309 (32-bit)", None)),
809            ("SM", fixed(4096, "Fixed family limit")),
810            ("SD", fixed(4096, "Fixed family limit")),
811        ],
812    )
813}
814
815fn create_iql_profile() -> SlmpDeviceRangeProfile {
816    let mut profile = create_iqr_profile();
817    profile.family = SlmpDeviceRangeFamily::IqL;
818    profile
819}
820
821fn create_mxf_profile() -> SlmpDeviceRangeProfile {
822    let mut profile = create_iqr_profile();
823    profile.family = SlmpDeviceRangeFamily::MxF;
824    profile
825        .rules
826        .insert("S", unsupported("Not supported on MX-F."));
827    profile
828        .rules
829        .insert("SM", fixed(10000, "Fixed family limit"));
830    profile
831        .rules
832        .insert("SD", fixed(10000, "Fixed family limit"));
833    profile
834}
835
836fn create_mxr_profile() -> SlmpDeviceRangeProfile {
837    let mut profile = create_iqr_profile();
838    profile.family = SlmpDeviceRangeFamily::MxR;
839    profile
840        .rules
841        .insert("S", unsupported("Not supported on MX-R."));
842    profile
843        .rules
844        .insert("SM", fixed(4496, "Fixed family limit"));
845    profile
846        .rules
847        .insert("SD", fixed(4496, "Fixed family limit"));
848    profile
849}
850
851fn create_iqf_profile() -> SlmpDeviceRangeProfile {
852    create_profile(
853        SlmpDeviceRangeFamily::IqF,
854        260,
855        46,
856        vec![
857            (
858                "X",
859                dword_register(
860                    260,
861                    "SD260-SD261 (32-bit)",
862                    Some("Manual addressing for iQ-F X devices is octal."),
863                ),
864            ),
865            (
866                "Y",
867                dword_register(
868                    262,
869                    "SD262-SD263 (32-bit)",
870                    Some("Manual addressing for iQ-F Y devices is octal."),
871                ),
872            ),
873            ("M", dword_register(264, "SD264-SD265 (32-bit)", None)),
874            ("B", dword_register(266, "SD266-SD267 (32-bit)", None)),
875            ("SB", dword_register(268, "SD268-SD269 (32-bit)", None)),
876            ("F", dword_register(270, "SD270-SD271 (32-bit)", None)),
877            ("V", unsupported("Not supported on iQ-F.")),
878            ("L", dword_register(274, "SD274-SD275 (32-bit)", None)),
879            ("S", unsupported("Not supported on iQ-F.")),
880            ("D", dword_register(280, "SD280-SD281 (32-bit)", None)),
881            ("W", dword_register(282, "SD282-SD283 (32-bit)", None)),
882            ("SW", dword_register(284, "SD284-SD285 (32-bit)", None)),
883            ("R", dword_register(304, "SD304-SD305 (32-bit)", None)),
884            ("T", dword_register(288, "SD288-SD289 (32-bit)", None)),
885            ("ST", dword_register(290, "SD290-SD291 (32-bit)", None)),
886            ("C", dword_register(292, "SD292-SD293 (32-bit)", None)),
887            ("LT", unsupported("Not supported on iQ-F.")),
888            ("LST", unsupported("Not supported on iQ-F.")),
889            ("LC", dword_register(298, "SD298-SD299 (32-bit)", None)),
890            ("Z", word_register(300, "SD300", None)),
891            ("LZ", word_register(302, "SD302", None)),
892            ("ZR", unsupported("Not supported on iQ-F.")),
893            ("RD", unsupported("Not supported on iQ-F.")),
894            ("SM", fixed(10000, "Fixed family limit")),
895            ("SD", fixed(12000, "Fixed family limit")),
896        ],
897    )
898}
899
900fn create_qcpu_profile() -> SlmpDeviceRangeProfile {
901    create_profile(
902        SlmpDeviceRangeFamily::QCpu,
903        290,
904        15,
905        vec![
906            ("X", word_register(290, "SD290", None)),
907            ("Y", word_register(291, "SD291", None)),
908            (
909                "M",
910                word_register_clipped(
911                    292,
912                    32768,
913                    "SD292",
914                    Some("Upper bound is clipped to 32768."),
915                ),
916            ),
917            (
918                "B",
919                word_register_clipped(
920                    294,
921                    32768,
922                    "SD294",
923                    Some("Upper bound is clipped to 32768."),
924                ),
925            ),
926            ("SB", word_register(296, "SD296", None)),
927            ("F", word_register(295, "SD295", None)),
928            ("V", word_register(297, "SD297", None)),
929            ("L", word_register(293, "SD293", None)),
930            ("S", word_register(298, "SD298", None)),
931            (
932                "D",
933                word_register_clipped(
934                    302,
935                    32768,
936                    "SD302",
937                    Some("Upper bound is clipped to 32768 and excludes extended area."),
938                ),
939            ),
940            (
941                "W",
942                word_register_clipped(
943                    303,
944                    32768,
945                    "SD303",
946                    Some("Upper bound is clipped to 32768 and excludes extended area."),
947                ),
948            ),
949            ("SW", word_register(304, "SD304", None)),
950            ("R", fixed(32768, "Fixed family limit")),
951            ("T", word_register(299, "SD299", None)),
952            ("ST", word_register(300, "SD300", None)),
953            ("C", word_register(301, "SD301", None)),
954            ("LT", unsupported("Not supported on QCPU.")),
955            ("LST", unsupported("Not supported on QCPU.")),
956            ("LC", unsupported("Not supported on QCPU.")),
957            ("Z", fixed(10, "Fixed family limit")),
958            ("LZ", unsupported("Not supported on QCPU.")),
959            (
960                "ZR",
961                undefined("No finite upper-bound register is defined for QCPU ZR."),
962            ),
963            ("RD", unsupported("Not supported on QCPU.")),
964            ("SM", fixed(1024, "Fixed family limit")),
965            ("SD", fixed(1024, "Fixed family limit")),
966        ],
967    )
968}
969
970fn create_lcpu_like_profile(
971    family: SlmpDeviceRangeFamily,
972    family_name: &'static str,
973) -> SlmpDeviceRangeProfile {
974    let unsupported_note = match family {
975        SlmpDeviceRangeFamily::LCpu => "Not supported on LCPU.",
976        SlmpDeviceRangeFamily::QnU => "Not supported on QnU.",
977        SlmpDeviceRangeFamily::QnUDV => "Not supported on QnUDV.",
978        _ => family_name,
979    };
980
981    create_profile(
982        family,
983        286,
984        26,
985        vec![
986            ("X", word_register(290, "SD290", None)),
987            ("Y", word_register(291, "SD291", None)),
988            ("M", dword_register(286, "SD286-SD287 (32-bit)", None)),
989            ("B", dword_register(288, "SD288-SD289 (32-bit)", None)),
990            ("SB", word_register(296, "SD296", None)),
991            ("F", word_register(295, "SD295", None)),
992            ("V", word_register(297, "SD297", None)),
993            ("L", word_register(293, "SD293", None)),
994            ("S", word_register(298, "SD298", None)),
995            ("D", dword_register(308, "SD308-SD309 (32-bit)", None)),
996            ("W", dword_register(310, "SD310-SD311 (32-bit)", None)),
997            ("SW", word_register(304, "SD304", None)),
998            (
999                "R",
1000                dword_register_clipped(
1001                    306,
1002                    32768,
1003                    "SD306-SD307 (32-bit)",
1004                    Some("Upper bound is clipped to 32768."),
1005                ),
1006            ),
1007            ("T", word_register(299, "SD299", None)),
1008            ("ST", word_register(300, "SD300", None)),
1009            ("C", word_register(301, "SD301", None)),
1010            ("LT", unsupported(unsupported_note)),
1011            ("LST", unsupported(unsupported_note)),
1012            ("LC", unsupported(unsupported_note)),
1013            ("Z", fixed(20, "Fixed family limit")),
1014            ("LZ", unsupported(unsupported_note)),
1015            ("ZR", dword_register(306, "SD306-SD307 (32-bit)", None)),
1016            ("RD", unsupported(unsupported_note)),
1017            ("SM", fixed(2048, "Fixed family limit")),
1018            ("SD", fixed(2048, "Fixed family limit")),
1019        ],
1020    )
1021}
1022
1023fn create_lcpu_profile() -> SlmpDeviceRangeProfile {
1024    create_lcpu_like_profile(SlmpDeviceRangeFamily::LCpu, "LCPU")
1025}
1026
1027fn create_qnu_profile() -> SlmpDeviceRangeProfile {
1028    create_lcpu_like_profile(SlmpDeviceRangeFamily::QnU, "QnU")
1029}
1030
1031fn create_qnudv_profile() -> SlmpDeviceRangeProfile {
1032    create_lcpu_like_profile(SlmpDeviceRangeFamily::QnUDV, "QnUDV")
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    use super::*;
1038
1039    fn create_snapshot(profile: &SlmpDeviceRangeProfile) -> BTreeMap<u16, u16> {
1040        let mut snapshot = BTreeMap::new();
1041        for offset in 0..profile.register_count {
1042            snapshot.insert(profile.register_start + offset, 0);
1043        }
1044        snapshot
1045    }
1046
1047    fn entry<'a>(catalog: &'a SlmpDeviceRangeCatalog, device: &str) -> &'a SlmpDeviceRangeEntry {
1048        catalog
1049            .entries
1050            .iter()
1051            .find(|item| item.device == device)
1052            .unwrap()
1053    }
1054
1055    fn insert_dword(snapshot: &mut BTreeMap<u16, u16>, register: u16, value: u32) {
1056        snapshot.insert(register, value as u16);
1057        snapshot.insert(register + 1, (value >> 16) as u16);
1058    }
1059
1060    #[test]
1061    fn normalize_model_trims_and_upcases() {
1062        assert_eq!(normalize_model(" R120PCPU\0 "), "R120PCPU");
1063        assert_eq!(normalize_model("fx5u-32mr/ds"), "FX5U-32MR/DS");
1064    }
1065
1066    #[test]
1067    fn resolve_family_uses_model_code_and_name_rules() {
1068        let qnudv = resolve_family(&SlmpTypeNameInfo {
1069            model: "Q03UDVCPU".to_string(),
1070            model_code: 0x0366,
1071            has_model_code: true,
1072        })
1073        .unwrap();
1074        let mxf = resolve_family(&SlmpTypeNameInfo {
1075            model: "MXF100-8-N32".to_string(),
1076            model_code: 0,
1077            has_model_code: false,
1078        })
1079        .unwrap();
1080
1081        assert_eq!(qnudv, SlmpDeviceRangeFamily::QnUDV);
1082        assert_eq!(mxf, SlmpDeviceRangeFamily::MxF);
1083    }
1084
1085    #[test]
1086    fn build_catalog_qcpu_clips_and_leaves_conditional_bounds_open() {
1087        let type_info = SlmpTypeNameInfo {
1088            model: "Q00CPU".to_string(),
1089            model_code: 0x0251,
1090            has_model_code: true,
1091        };
1092        let profile = resolve_profile(&type_info).unwrap();
1093        let mut snapshot = create_snapshot(&profile);
1094        snapshot.insert(290, 123);
1095        snapshot.insert(292, 50000);
1096        snapshot.insert(299, 90);
1097        snapshot.insert(302, 50000);
1098        snapshot.insert(303, 60000);
1099
1100        let catalog = build_catalog(&type_info, &profile, &snapshot).unwrap();
1101
1102        assert_eq!(catalog.family, SlmpDeviceRangeFamily::QCpu);
1103        assert_eq!(entry(&catalog, "X").point_count, Some(123));
1104        assert_eq!(entry(&catalog, "X").upper_bound, Some(122));
1105        assert_eq!(
1106            entry(&catalog, "X").address_range.as_deref(),
1107            Some("X000-X07A")
1108        );
1109        assert_eq!(entry(&catalog, "M").point_count, Some(32768));
1110        assert_eq!(entry(&catalog, "M").upper_bound, Some(32767));
1111        assert_eq!(entry(&catalog, "D").point_count, Some(32768));
1112        assert_eq!(entry(&catalog, "D").upper_bound, Some(32767));
1113        assert_eq!(entry(&catalog, "TS").point_count, Some(90));
1114        assert_eq!(entry(&catalog, "TS").upper_bound, Some(89));
1115        assert_eq!(entry(&catalog, "TN").point_count, Some(90));
1116        assert_eq!(entry(&catalog, "TN").upper_bound, Some(89));
1117        assert!(entry(&catalog, "ZR").supported);
1118        assert_eq!(entry(&catalog, "ZR").point_count, None);
1119        assert_eq!(entry(&catalog, "ZR").upper_bound, None);
1120        assert_eq!(entry(&catalog, "ZR").address_range, None);
1121        assert_eq!(entry(&catalog, "Z").point_count, Some(10));
1122        assert_eq!(entry(&catalog, "Z").upper_bound, Some(9));
1123        assert_eq!(entry(&catalog, "Z").address_range.as_deref(), Some("Z0-Z9"));
1124    }
1125
1126    #[test]
1127    fn build_catalog_iqr_reads_dword_registers_and_caps_family_maximums() {
1128        let type_info = SlmpTypeNameInfo {
1129            model: "R120PCPU".to_string(),
1130            model_code: 0x4844,
1131            has_model_code: true,
1132        };
1133        let profile = resolve_profile(&type_info).unwrap();
1134        let mut snapshot = create_snapshot(&profile);
1135        insert_dword(&mut snapshot, 260, 12_289);
1136        insert_dword(&mut snapshot, 264, 94_674_945);
1137        insert_dword(&mut snapshot, 266, 94_674_945);
1138        insert_dword(&mut snapshot, 270, 32_769);
1139        insert_dword(&mut snapshot, 280, 5_917_185);
1140        insert_dword(&mut snapshot, 282, 5_917_185);
1141        insert_dword(&mut snapshot, 284, 5_917_185);
1142        insert_dword(&mut snapshot, 288, 5_259_713);
1143        insert_dword(&mut snapshot, 294, 1_479_297);
1144        insert_dword(&mut snapshot, 296, 1_479_297);
1145        insert_dword(&mut snapshot, 298, 2_784_545);
1146        insert_dword(&mut snapshot, 306, 0x0002_0001);
1147
1148        let catalog = build_catalog(&type_info, &profile, &snapshot).unwrap();
1149
1150        assert_eq!(entry(&catalog, "X").point_count, Some(12_288));
1151        assert_eq!(entry(&catalog, "X").upper_bound, Some(12_287));
1152        assert_eq!(
1153            entry(&catalog, "X").address_range.as_deref(),
1154            Some("X0000-X2FFF")
1155        );
1156        assert_eq!(entry(&catalog, "M").point_count, Some(94_674_944));
1157        assert_eq!(entry(&catalog, "M").upper_bound, Some(94_674_943));
1158        assert_eq!(entry(&catalog, "B").point_count, Some(94_674_944));
1159        assert_eq!(
1160            entry(&catalog, "B").address_range.as_deref(),
1161            Some("B0000000-B5A49FFF")
1162        );
1163        assert_eq!(entry(&catalog, "F").point_count, Some(32_768));
1164        assert_eq!(entry(&catalog, "D").point_count, Some(5_917_184));
1165        assert_eq!(entry(&catalog, "W").point_count, Some(5_917_184));
1166        assert_eq!(
1167            entry(&catalog, "SW").address_range.as_deref(),
1168            Some("SW000000-SW5A49FF")
1169        );
1170        assert_eq!(entry(&catalog, "TN").point_count, Some(5_259_712));
1171        assert_eq!(entry(&catalog, "LTN").point_count, Some(1_479_296));
1172        assert_eq!(entry(&catalog, "LSTN").point_count, Some(1_479_296));
1173        assert_eq!(entry(&catalog, "LCN").point_count, Some(2_784_544));
1174        assert_eq!(entry(&catalog, "R").point_count, Some(32768));
1175        assert_eq!(entry(&catalog, "R").upper_bound, Some(32767));
1176        assert_eq!(
1177            entry(&catalog, "R").address_range.as_deref(),
1178            Some("R0-R32767")
1179        );
1180    }
1181
1182    #[test]
1183    fn build_catalog_iqf_formats_x_and_y_in_octal() {
1184        let type_info = SlmpTypeNameInfo {
1185            model: "FX5UC-32MT/D".to_string(),
1186            model_code: 0x4A91,
1187            has_model_code: true,
1188        };
1189        let profile = resolve_profile(&type_info).unwrap();
1190        let mut snapshot = create_snapshot(&profile);
1191        snapshot.insert(260, 1024);
1192        snapshot.insert(261, 0);
1193        snapshot.insert(262, 1024);
1194        snapshot.insert(263, 0);
1195
1196        let catalog = build_catalog(&type_info, &profile, &snapshot).unwrap();
1197
1198        assert_eq!(
1199            entry(&catalog, "X").notation,
1200            SlmpDeviceRangeNotation::Octal
1201        );
1202        assert_eq!(entry(&catalog, "X").point_count, Some(1024));
1203        assert_eq!(entry(&catalog, "X").upper_bound, Some(1023));
1204        assert_eq!(
1205            entry(&catalog, "X").address_range.as_deref(),
1206            Some("X0000-X1777")
1207        );
1208
1209        assert_eq!(
1210            entry(&catalog, "Y").notation,
1211            SlmpDeviceRangeNotation::Octal
1212        );
1213        assert_eq!(entry(&catalog, "Y").point_count, Some(1024));
1214        assert_eq!(entry(&catalog, "Y").upper_bound, Some(1023));
1215        assert_eq!(
1216            entry(&catalog, "Y").address_range.as_deref(),
1217            Some("Y0000-Y1777")
1218        );
1219    }
1220
1221    #[test]
1222    fn build_catalog_qnu_uses_sd300_for_st_family_and_fixed_z_limit() {
1223        let type_info = SlmpTypeNameInfo {
1224            model: "Q03UDECPU".to_string(),
1225            model_code: 0x0268,
1226            has_model_code: true,
1227        };
1228        let profile = resolve_profile(&type_info).unwrap();
1229        let mut snapshot = create_snapshot(&profile);
1230        snapshot.insert(300, 16);
1231        snapshot.insert(301, 1024);
1232
1233        let catalog = build_catalog(&type_info, &profile, &snapshot).unwrap();
1234
1235        assert_eq!(catalog.family, SlmpDeviceRangeFamily::QnU);
1236        assert_eq!(entry(&catalog, "STS").point_count, Some(16));
1237        assert_eq!(entry(&catalog, "STS").upper_bound, Some(15));
1238        assert_eq!(
1239            entry(&catalog, "STS").address_range.as_deref(),
1240            Some("STS0-STS15")
1241        );
1242        assert_eq!(entry(&catalog, "STC").point_count, Some(16));
1243        assert_eq!(entry(&catalog, "STC").upper_bound, Some(15));
1244        assert_eq!(
1245            entry(&catalog, "STC").address_range.as_deref(),
1246            Some("STC0-STC15")
1247        );
1248        assert_eq!(entry(&catalog, "STN").point_count, Some(16));
1249        assert_eq!(entry(&catalog, "STN").upper_bound, Some(15));
1250        assert_eq!(
1251            entry(&catalog, "STN").address_range.as_deref(),
1252            Some("STN0-STN15")
1253        );
1254        assert_eq!(entry(&catalog, "CS").point_count, Some(1024));
1255        assert_eq!(entry(&catalog, "CS").upper_bound, Some(1023));
1256        assert_eq!(
1257            entry(&catalog, "CS").address_range.as_deref(),
1258            Some("CS0-CS1023")
1259        );
1260        assert_eq!(entry(&catalog, "Z").point_count, Some(20));
1261        assert_eq!(entry(&catalog, "Z").upper_bound, Some(19));
1262        assert_eq!(
1263            entry(&catalog, "Z").address_range.as_deref(),
1264            Some("Z0-Z19")
1265        );
1266    }
1267}