Skip to main content

plc_comm_slmp/
helpers.rs

1use crate::address::{SlmpAddress, parse_named_address};
2use crate::client::SlmpClient;
3use crate::error::SlmpError;
4use crate::model::{SlmpDeviceAddress, SlmpDeviceCode, SlmpLongTimerResult};
5use async_stream::try_stream;
6use futures_core::stream::Stream;
7use std::collections::{BTreeMap, HashMap};
8use std::time::Duration;
9
10#[derive(Debug, Clone, PartialEq, serde::Serialize)]
11#[serde(untagged)]
12pub enum SlmpValue {
13    Bool(bool),
14    U16(u16),
15    I16(i16),
16    U32(u32),
17    I32(i32),
18    F32(f32),
19}
20
21impl SlmpValue {
22    pub fn as_bool(&self) -> Result<bool, SlmpError> {
23        match self {
24            Self::Bool(value) => Ok(*value),
25            _ => Err(SlmpError::new("Expected bool value.")),
26        }
27    }
28}
29
30pub type NamedAddress = BTreeMap<String, SlmpValue>;
31
32#[derive(Debug, Clone)]
33struct LongTimerReadSpec {
34    base_code: SlmpDeviceCode,
35    kind: LongTimerReadKind,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum LongTimerReadKind {
40    Current,
41    Contact,
42    Coil,
43}
44
45#[derive(Debug, Clone)]
46struct NamedReadEntry {
47    address: String,
48    device: SlmpDeviceAddress,
49    dtype: String,
50    bit_index: Option<u8>,
51    long_timer_read: Option<LongTimerReadSpec>,
52}
53
54#[derive(Debug, Clone)]
55struct NamedReadPlan {
56    entries: Vec<NamedReadEntry>,
57    word_devices: Vec<SlmpDeviceAddress>,
58    dword_devices: Vec<SlmpDeviceAddress>,
59}
60
61pub async fn read_typed(
62    client: &SlmpClient,
63    device: SlmpDeviceAddress,
64    dtype: &str,
65) -> Result<SlmpValue, SlmpError> {
66    let normalized_dtype = dtype.to_uppercase();
67    if let Some(spec) = long_timer_read_spec(device.code) {
68        let timer = read_long_like_point(client, spec.base_code, device.number).await?;
69        return decode_long_like_value(&normalized_dtype, &spec, &timer);
70    }
71
72    match normalized_dtype.as_str() {
73        "BIT" => Ok(SlmpValue::Bool(client.read_bits(device, 1).await?[0])),
74        "F" => Ok(SlmpValue::F32(f32::from_bits(
75            client.read_dwords_raw(device, 1).await?[0],
76        ))),
77        "D" => Ok(SlmpValue::U32(client.read_dwords_raw(device, 1).await?[0])),
78        "L" => Ok(SlmpValue::I32(
79            client.read_dwords_raw(device, 1).await?[0] as i32,
80        )),
81        "S" => Ok(SlmpValue::I16(
82            client.read_words_raw(device, 1).await?[0] as i16,
83        )),
84        _ => Ok(SlmpValue::U16(client.read_words_raw(device, 1).await?[0])),
85    }
86}
87
88pub async fn write_typed(
89    client: &SlmpClient,
90    device: SlmpDeviceAddress,
91    dtype: &str,
92    value: &SlmpValue,
93) -> Result<(), SlmpError> {
94    match resolve_write_route(device, dtype) {
95        NamedWriteRoute::RandomBits => {
96            client
97                .write_random_bits(&[(device, scalar_to_bool(value)?)])
98                .await
99        }
100        NamedWriteRoute::ContiguousBits => {
101            client.write_bits(device, &[scalar_to_bool(value)?]).await
102        }
103        NamedWriteRoute::RandomDWords | NamedWriteRoute::ContiguousDWords => {
104            let raw = match dtype.to_uppercase().as_str() {
105                "F" => scalar_to_f32(value)?.to_bits(),
106                "L" => scalar_to_i32(value)? as u32,
107                _ => scalar_to_u32(value)?,
108            };
109            if matches!(
110                resolve_write_route(device, dtype),
111                NamedWriteRoute::RandomDWords
112            ) {
113                client.write_random_words(&[], &[(device, raw)]).await
114            } else {
115                client.write_dwords(device, &[raw]).await
116            }
117        }
118        NamedWriteRoute::ContiguousWords => {
119            client.write_words(device, &[scalar_to_u16(value)?]).await
120        }
121    }
122}
123
124pub async fn write_bit_in_word(
125    client: &SlmpClient,
126    device: SlmpDeviceAddress,
127    bit_index: u8,
128    value: bool,
129) -> Result<(), SlmpError> {
130    if bit_index > 15 {
131        return Err(SlmpError::new("bit_index must be 0-15."));
132    }
133    let mut current = client.read_words_raw(device, 1).await?[0];
134    if value {
135        current |= 1 << bit_index;
136    } else {
137        current &= !(1 << bit_index);
138    }
139    client.write_words(device, &[current]).await
140}
141
142pub async fn read_words_single_request(
143    client: &SlmpClient,
144    start: SlmpDeviceAddress,
145    count: usize,
146) -> Result<Vec<u16>, SlmpError> {
147    validate_single_request_count(count, 960)?;
148    client.read_words_raw(start, count as u16).await
149}
150
151pub async fn read_dwords_single_request(
152    client: &SlmpClient,
153    start: SlmpDeviceAddress,
154    count: usize,
155) -> Result<Vec<u32>, SlmpError> {
156    validate_single_request_count(count, 480)?;
157    client.read_dwords_raw(start, count as u16).await
158}
159
160pub async fn write_words_single_request(
161    client: &SlmpClient,
162    start: SlmpDeviceAddress,
163    values: &[u16],
164) -> Result<(), SlmpError> {
165    validate_single_request_values(values.len(), 960)?;
166    client.write_words(start, values).await
167}
168
169pub async fn write_dwords_single_request(
170    client: &SlmpClient,
171    start: SlmpDeviceAddress,
172    values: &[u32],
173) -> Result<(), SlmpError> {
174    validate_single_request_values(values.len(), 480)?;
175    client.write_dwords(start, values).await
176}
177
178pub async fn read_words_chunked(
179    client: &SlmpClient,
180    start: SlmpDeviceAddress,
181    count: usize,
182    max_words_per_request: usize,
183) -> Result<Vec<u16>, SlmpError> {
184    let chunk = (max_words_per_request / 2) * 2;
185    if chunk == 0 {
186        return Err(SlmpError::new("max_words_per_request must be at least 2."));
187    }
188    let mut remaining = count;
189    let mut offset = 0u32;
190    let mut result = Vec::with_capacity(count);
191    while remaining > 0 {
192        let next = remaining.min(chunk);
193        result.extend(
194            client
195                .read_words_raw(
196                    SlmpDeviceAddress::new(start.code, start.number + offset),
197                    next as u16,
198                )
199                .await?,
200        );
201        remaining -= next;
202        offset += next as u32;
203    }
204    Ok(result)
205}
206
207pub async fn read_dwords_chunked(
208    client: &SlmpClient,
209    start: SlmpDeviceAddress,
210    count: usize,
211    max_dwords_per_request: usize,
212) -> Result<Vec<u32>, SlmpError> {
213    let mut remaining = count;
214    let mut offset = 0u32;
215    let mut result = Vec::with_capacity(count);
216    while remaining > 0 {
217        let next = remaining.min(max_dwords_per_request);
218        result.extend(
219            client
220                .read_dwords_raw(
221                    SlmpDeviceAddress::new(start.code, start.number + offset * 2),
222                    next as u16,
223                )
224                .await?,
225        );
226        remaining -= next;
227        offset += next as u32;
228    }
229    Ok(result)
230}
231
232pub async fn write_words_chunked(
233    client: &SlmpClient,
234    start: SlmpDeviceAddress,
235    values: &[u16],
236    max_words_per_request: usize,
237) -> Result<(), SlmpError> {
238    if max_words_per_request == 0 {
239        return Err(SlmpError::new("chunk size must be positive."));
240    }
241    let mut offset = 0usize;
242    while offset < values.len() {
243        let end = (offset + max_words_per_request).min(values.len());
244        client
245            .write_words(
246                SlmpDeviceAddress::new(start.code, start.number + offset as u32),
247                &values[offset..end],
248            )
249            .await?;
250        offset = end;
251    }
252    Ok(())
253}
254
255pub async fn write_dwords_chunked(
256    client: &SlmpClient,
257    start: SlmpDeviceAddress,
258    values: &[u32],
259    max_dwords_per_request: usize,
260) -> Result<(), SlmpError> {
261    if max_dwords_per_request == 0 {
262        return Err(SlmpError::new("chunk size must be positive."));
263    }
264    let mut offset = 0usize;
265    while offset < values.len() {
266        let end = (offset + max_dwords_per_request).min(values.len());
267        client
268            .write_dwords(
269                SlmpDeviceAddress::new(start.code, start.number + (offset * 2) as u32),
270                &values[offset..end],
271            )
272            .await?;
273        offset = end;
274    }
275    Ok(())
276}
277
278pub async fn read_named(
279    client: &SlmpClient,
280    addresses: &[String],
281) -> Result<NamedAddress, SlmpError> {
282    let plan = compile_read_plan(addresses)?;
283    read_named_compiled(client, &plan).await
284}
285
286pub async fn write_named(client: &SlmpClient, updates: &NamedAddress) -> Result<(), SlmpError> {
287    for (address, value) in updates {
288        let parts = parse_named_address(address)?;
289        let device = SlmpAddress::parse(&parts.base)?;
290        let resolved_dtype =
291            resolve_dtype_for_address(address, device, &parts.dtype, parts.bit_index);
292        validate_long_timer_entry(address, device, &resolved_dtype)?;
293        if parts.dtype == "BIT_IN_WORD" {
294            validate_bit_in_word_target(address, device)?;
295            write_bit_in_word(
296                client,
297                device,
298                parts.bit_index.unwrap_or(0),
299                scalar_to_bool(value)?,
300            )
301            .await?;
302            continue;
303        }
304        write_typed(client, device, &resolved_dtype, value).await?;
305    }
306    Ok(())
307}
308
309pub fn poll_named<'a>(
310    client: &'a SlmpClient,
311    addresses: &'a [String],
312    interval: Duration,
313) -> impl Stream<Item = Result<NamedAddress, SlmpError>> + 'a {
314    try_stream! {
315        loop {
316            yield read_named(client, addresses).await?;
317            tokio::time::sleep(interval).await;
318        }
319    }
320}
321
322fn validate_single_request_count(count: usize, max: usize) -> Result<(), SlmpError> {
323    if count == 0 || count > max {
324        return Err(SlmpError::new(format!(
325            "count must be in the range 1-{max}."
326        )));
327    }
328    Ok(())
329}
330
331fn validate_single_request_values(count: usize, max: usize) -> Result<(), SlmpError> {
332    if count == 0 || count > max {
333        return Err(SlmpError::new(format!(
334            "values.len() must be in the range 1-{max}."
335        )));
336    }
337    Ok(())
338}
339
340fn compile_read_plan(addresses: &[String]) -> Result<NamedReadPlan, SlmpError> {
341    let mut entries = Vec::new();
342    let mut word_devices = Vec::new();
343    let mut dword_devices = Vec::new();
344    for address in addresses {
345        let parts = parse_named_address(address)?;
346        let device = SlmpAddress::parse(&parts.base)?;
347        let dtype = resolve_dtype_for_address(address, device, &parts.dtype, parts.bit_index);
348        validate_long_timer_entry(address, device, &dtype)?;
349
350        if parts.dtype == "BIT_IN_WORD" {
351            validate_bit_in_word_target(address, device)?;
352            if device.code.is_word_batchable() && !word_devices.contains(&device) {
353                word_devices.push(device);
354            }
355        } else if matches!(dtype.as_str(), "U" | "S") && device.code.is_word_batchable() {
356            if !word_devices.contains(&device) {
357                word_devices.push(device);
358            }
359        } else if matches!(dtype.as_str(), "D" | "L" | "F") && device.code.is_word_batchable() {
360            if !dword_devices.contains(&device) {
361                dword_devices.push(device);
362            }
363        }
364
365        entries.push(NamedReadEntry {
366            address: address.clone(),
367            device,
368            dtype,
369            bit_index: parts.bit_index,
370            long_timer_read: long_timer_read_spec(device.code),
371        });
372    }
373
374    Ok(NamedReadPlan {
375        entries,
376        word_devices,
377        dword_devices,
378    })
379}
380
381async fn read_named_compiled(
382    client: &SlmpClient,
383    plan: &NamedReadPlan,
384) -> Result<NamedAddress, SlmpError> {
385    let mut result = NamedAddress::new();
386    let (word_values, dword_values) =
387        read_random_maps(client, &plan.word_devices, &plan.dword_devices).await?;
388    let mut long_timer_cache: HashMap<(SlmpDeviceCode, u32), SlmpLongTimerResult> = HashMap::new();
389
390    for entry in &plan.entries {
391        let value = if let Some(spec) = &entry.long_timer_read {
392            let key = (spec.base_code, entry.device.number);
393            if !long_timer_cache.contains_key(&key) {
394                let timer =
395                    read_long_like_point(client, spec.base_code, entry.device.number).await?;
396                long_timer_cache.insert(key, timer);
397            }
398            decode_long_like_value(&entry.dtype, spec, long_timer_cache.get(&key).unwrap())?
399        } else if entry.dtype == "BIT_IN_WORD" {
400            let word = if let Some(word) = word_values.get(&entry.device) {
401                *word
402            } else {
403                client.read_words_raw(entry.device, 1).await?[0]
404            };
405            SlmpValue::Bool(((word >> entry.bit_index.unwrap_or(0)) & 1) != 0)
406        } else if entry.dtype == "S" {
407            if let Some(value) = word_values.get(&entry.device) {
408                SlmpValue::I16(*value as i16)
409            } else {
410                read_typed(client, entry.device, &entry.dtype).await?
411            }
412        } else if entry.dtype == "U" {
413            if let Some(value) = word_values.get(&entry.device) {
414                SlmpValue::U16(*value)
415            } else {
416                read_typed(client, entry.device, &entry.dtype).await?
417            }
418        } else if entry.dtype == "F" {
419            if let Some(value) = dword_values.get(&entry.device) {
420                SlmpValue::F32(f32::from_bits(*value))
421            } else {
422                read_typed(client, entry.device, &entry.dtype).await?
423            }
424        } else if entry.dtype == "L" {
425            if let Some(value) = dword_values.get(&entry.device) {
426                SlmpValue::I32(*value as i32)
427            } else {
428                read_typed(client, entry.device, &entry.dtype).await?
429            }
430        } else if entry.dtype == "D" {
431            if let Some(value) = dword_values.get(&entry.device) {
432                SlmpValue::U32(*value)
433            } else {
434                read_typed(client, entry.device, &entry.dtype).await?
435            }
436        } else {
437            read_typed(client, entry.device, &entry.dtype).await?
438        };
439        result.insert(entry.address.clone(), value);
440    }
441
442    Ok(result)
443}
444
445async fn read_random_maps(
446    client: &SlmpClient,
447    word_devices: &[SlmpDeviceAddress],
448    dword_devices: &[SlmpDeviceAddress],
449) -> Result<
450    (
451        HashMap<SlmpDeviceAddress, u16>,
452        HashMap<SlmpDeviceAddress, u32>,
453    ),
454    SlmpError,
455> {
456    let mut words = HashMap::new();
457    let mut dwords = HashMap::new();
458    let mut word_index = 0usize;
459    let mut dword_index = 0usize;
460
461    while word_index < word_devices.len() || dword_index < dword_devices.len() {
462        let word_end = (word_index + 0xFF).min(word_devices.len());
463        let dword_end = (dword_index + 0xFF).min(dword_devices.len());
464        let random = client
465            .read_random(
466                &word_devices[word_index..word_end],
467                &dword_devices[dword_index..dword_end],
468            )
469            .await?;
470        for (device, value) in word_devices[word_index..word_end]
471            .iter()
472            .copied()
473            .zip(random.word_values.into_iter())
474        {
475            words.insert(device, value);
476        }
477        for (device, value) in dword_devices[dword_index..dword_end]
478            .iter()
479            .copied()
480            .zip(random.dword_values.into_iter())
481        {
482            dwords.insert(device, value);
483        }
484        word_index = word_end;
485        dword_index = dword_end;
486    }
487
488    Ok((words, dwords))
489}
490
491async fn read_long_like_point(
492    client: &SlmpClient,
493    base_code: SlmpDeviceCode,
494    number: u32,
495) -> Result<SlmpLongTimerResult, SlmpError> {
496    match base_code {
497        SlmpDeviceCode::LTN => Ok(client.read_long_timer(number, 1).await?.remove(0)),
498        SlmpDeviceCode::LSTN => Ok(client.read_long_retentive_timer(number, 1).await?.remove(0)),
499        SlmpDeviceCode::LCN => {
500            let raw_words = client
501                .read_words_raw(SlmpDeviceAddress::new(SlmpDeviceCode::LCN, number), 4)
502                .await?;
503            Ok(SlmpLongTimerResult {
504                index: number,
505                device: format!("LCN{number}"),
506                current_value: raw_words[0] as u32 | ((raw_words[1] as u32) << 16),
507                contact: (raw_words[2] & 0x0002) != 0,
508                coil: (raw_words[2] & 0x0001) != 0,
509                status_word: raw_words[2],
510                raw_words,
511            })
512        }
513        _ => Err(SlmpError::new("Unsupported long-family base code.")),
514    }
515}
516
517fn decode_long_like_value(
518    dtype: &str,
519    spec: &LongTimerReadSpec,
520    timer: &SlmpLongTimerResult,
521) -> Result<SlmpValue, SlmpError> {
522    Ok(match spec.kind {
523        LongTimerReadKind::Current => {
524            if dtype.eq_ignore_ascii_case("L") {
525                SlmpValue::I32(timer.current_value as i32)
526            } else {
527                SlmpValue::U32(timer.current_value)
528            }
529        }
530        LongTimerReadKind::Contact => SlmpValue::Bool(timer.contact),
531        LongTimerReadKind::Coil => SlmpValue::Bool(timer.coil),
532    })
533}
534
535fn validate_bit_in_word_target(address: &str, device: SlmpDeviceAddress) -> Result<(), SlmpError> {
536    if !device.code.is_word_device() {
537        return Err(SlmpError::new(format!(
538            "Address '{address}' uses '.bit' notation, which is only valid for word devices."
539        )));
540    }
541    Ok(())
542}
543
544fn resolve_dtype_for_address(
545    address: &str,
546    device: SlmpDeviceAddress,
547    dtype: &str,
548    bit_index: Option<u8>,
549) -> String {
550    let normalized = if dtype == "U" && device.code.is_bit_device() {
551        "BIT".to_string()
552    } else {
553        dtype.to_uppercase()
554    };
555    if !address.contains(':')
556        && bit_index.is_none()
557        && matches!(
558            device.code,
559            SlmpDeviceCode::LTN | SlmpDeviceCode::LSTN | SlmpDeviceCode::LCN | SlmpDeviceCode::LZ
560        )
561    {
562        "D".to_string()
563    } else {
564        normalized
565    }
566}
567
568fn resolve_write_route(device: SlmpDeviceAddress, dtype: &str) -> NamedWriteRoute {
569    let normalized = if dtype.eq_ignore_ascii_case("U") && device.code.is_bit_device() {
570        "BIT".to_string()
571    } else {
572        dtype.to_uppercase()
573    };
574    match normalized.as_str() {
575        "BIT"
576            if matches!(
577                device.code,
578                SlmpDeviceCode::LTS
579                    | SlmpDeviceCode::LTC
580                    | SlmpDeviceCode::LSTS
581                    | SlmpDeviceCode::LSTC
582            ) =>
583        {
584            NamedWriteRoute::RandomBits
585        }
586        "BIT" => NamedWriteRoute::ContiguousBits,
587        "D" | "L"
588            if matches!(
589                device.code,
590                SlmpDeviceCode::LTN | SlmpDeviceCode::LSTN | SlmpDeviceCode::LZ
591            ) =>
592        {
593            NamedWriteRoute::RandomDWords
594        }
595        "D" | "L" | "F" => NamedWriteRoute::ContiguousDWords,
596        _ => NamedWriteRoute::ContiguousWords,
597    }
598}
599
600fn long_timer_read_spec(code: SlmpDeviceCode) -> Option<LongTimerReadSpec> {
601    let (base_code, kind) = match code {
602        SlmpDeviceCode::LTN => (SlmpDeviceCode::LTN, LongTimerReadKind::Current),
603        SlmpDeviceCode::LTS => (SlmpDeviceCode::LTN, LongTimerReadKind::Contact),
604        SlmpDeviceCode::LTC => (SlmpDeviceCode::LTN, LongTimerReadKind::Coil),
605        SlmpDeviceCode::LSTN => (SlmpDeviceCode::LSTN, LongTimerReadKind::Current),
606        SlmpDeviceCode::LSTS => (SlmpDeviceCode::LSTN, LongTimerReadKind::Contact),
607        SlmpDeviceCode::LSTC => (SlmpDeviceCode::LSTN, LongTimerReadKind::Coil),
608        SlmpDeviceCode::LCN => (SlmpDeviceCode::LCN, LongTimerReadKind::Current),
609        SlmpDeviceCode::LCS => (SlmpDeviceCode::LCN, LongTimerReadKind::Contact),
610        SlmpDeviceCode::LCC => (SlmpDeviceCode::LCN, LongTimerReadKind::Coil),
611        _ => return None,
612    };
613    Some(LongTimerReadSpec { base_code, kind })
614}
615
616fn validate_long_timer_entry(
617    address: &str,
618    device: SlmpDeviceAddress,
619    dtype: &str,
620) -> Result<(), SlmpError> {
621    let Some(spec) = long_timer_read_spec(device.code) else {
622        return Ok(());
623    };
624    if matches!(spec.kind, LongTimerReadKind::Current) {
625        if dtype != "D" && dtype != "L" {
626            return Err(SlmpError::new(format!(
627                "Address '{address}' uses a 32-bit long current value. Use the plain form or ':D' / ':L'."
628            )));
629        }
630        return Ok(());
631    }
632    if !dtype.eq_ignore_ascii_case("BIT") {
633        return Err(SlmpError::new(format!(
634            "Address '{address}' is a long timer state device. Use the plain device form without a dtype override."
635        )));
636    }
637    Ok(())
638}
639
640fn scalar_to_bool(value: &SlmpValue) -> Result<bool, SlmpError> {
641    match value {
642        SlmpValue::Bool(v) => Ok(*v),
643        SlmpValue::U16(v) => Ok(*v != 0),
644        SlmpValue::I16(v) => Ok(*v != 0),
645        SlmpValue::U32(v) => Ok(*v != 0),
646        SlmpValue::I32(v) => Ok(*v != 0),
647        SlmpValue::F32(v) => Ok(*v != 0.0),
648    }
649}
650
651fn scalar_to_u16(value: &SlmpValue) -> Result<u16, SlmpError> {
652    match value {
653        SlmpValue::U16(v) => Ok(*v),
654        SlmpValue::I16(v) => Ok(*v as u16),
655        SlmpValue::Bool(v) => Ok(u16::from(*v)),
656        SlmpValue::U32(v) => Ok(*v as u16),
657        SlmpValue::I32(v) => Ok(*v as u16),
658        SlmpValue::F32(v) => Ok(*v as u16),
659    }
660}
661
662fn scalar_to_u32(value: &SlmpValue) -> Result<u32, SlmpError> {
663    match value {
664        SlmpValue::U32(v) => Ok(*v),
665        SlmpValue::I32(v) => Ok(*v as u32),
666        SlmpValue::U16(v) => Ok(*v as u32),
667        SlmpValue::I16(v) => Ok(*v as u32),
668        SlmpValue::Bool(v) => Ok(u32::from(*v)),
669        SlmpValue::F32(v) => Ok(*v as u32),
670    }
671}
672
673fn scalar_to_i32(value: &SlmpValue) -> Result<i32, SlmpError> {
674    match value {
675        SlmpValue::I32(v) => Ok(*v),
676        SlmpValue::U32(v) => Ok(*v as i32),
677        SlmpValue::U16(v) => Ok(*v as i32),
678        SlmpValue::I16(v) => Ok(*v as i32),
679        SlmpValue::Bool(v) => Ok(i32::from(*v)),
680        SlmpValue::F32(v) => Ok(*v as i32),
681    }
682}
683
684fn scalar_to_f32(value: &SlmpValue) -> Result<f32, SlmpError> {
685    match value {
686        SlmpValue::F32(v) => Ok(*v),
687        SlmpValue::U32(v) => Ok(*v as f32),
688        SlmpValue::I32(v) => Ok(*v as f32),
689        SlmpValue::U16(v) => Ok(*v as f32),
690        SlmpValue::I16(v) => Ok(*v as f32),
691        SlmpValue::Bool(v) => Ok(if *v { 1.0 } else { 0.0 }),
692    }
693}
694
695#[derive(Debug, Clone, Copy, PartialEq, Eq)]
696enum NamedWriteRoute {
697    ContiguousBits,
698    ContiguousWords,
699    ContiguousDWords,
700    RandomBits,
701    RandomDWords,
702}
703
704pub fn parse_scalar_for_named(address: &str, value: &str) -> Result<SlmpValue, SlmpError> {
705    let parts = parse_named_address(address)?;
706    let device = SlmpAddress::parse(&parts.base)?;
707    if parts.bit_index.is_some() || device.code.is_bit_device() {
708        return Ok(SlmpValue::Bool(matches!(
709            value,
710            "1" | "true" | "TRUE" | "True"
711        )));
712    }
713    if parts.dtype.eq_ignore_ascii_case("F") {
714        return value
715            .parse::<f32>()
716            .map(SlmpValue::F32)
717            .map_err(|_| SlmpError::new("Invalid float value."));
718    }
719    let parsed = if let Some(hex) = value
720        .strip_prefix("0x")
721        .or_else(|| value.strip_prefix("0X"))
722    {
723        i64::from_str_radix(hex, 16).map_err(|_| SlmpError::new("Invalid integer value."))?
724    } else {
725        value
726            .parse::<i64>()
727            .map_err(|_| SlmpError::new("Invalid integer value."))?
728    };
729    Ok(
730        match resolve_dtype_for_address(address, device, &parts.dtype, parts.bit_index).as_str() {
731            "L" => SlmpValue::I32(parsed as i32),
732            "D" => SlmpValue::U32(parsed as u32),
733            "S" => SlmpValue::I16(parsed as i16),
734            _ => SlmpValue::U16(parsed as u16),
735        },
736    )
737}