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 pub model: String,
54 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(®ister)
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(®ister)
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}