Skip to main content

plc_comm_hostlink/
helpers.rs

1use crate::address::{
2    KvDeviceAddress, bit_bank_logical_number, is_direct_bit_device_type,
3    is_optimizable_read_named_device_type, offset_device, parse_device, parse_logical_address,
4    parse_named_address_parts, resolve_effective_format, uses_bit_bank_address,
5    validate_device_count, validate_device_span,
6};
7use crate::client::{HostLinkClient, HostLinkPayloadValue};
8use crate::error::HostLinkError;
9use futures_core::Stream;
10use indexmap::IndexMap;
11use std::str::FromStr;
12use std::time::Duration;
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum HostLinkValue {
16    U16(u16),
17    I16(i16),
18    U32(u32),
19    I32(i32),
20    F32(f32),
21    Bool(bool),
22    Text(String),
23}
24
25pub type NamedSnapshot = IndexMap<String, HostLinkValue>;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct TimerCounterValue {
29    pub status: u32,
30    pub current: u32,
31    pub preset: u32,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum ReadPlanValueKind {
36    Unsigned16,
37    Signed16,
38    Unsigned32,
39    Signed32,
40    Float32,
41    BitInWord,
42    DirectBit,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum ReadPlanSegmentMode {
47    Words,
48    DirectBits,
49}
50
51#[derive(Debug, Clone)]
52struct ReadPlanRequest {
53    index: usize,
54    address: String,
55    base_address: KvDeviceAddress,
56    kind: ReadPlanValueKind,
57    bit_index: u8,
58}
59
60#[derive(Debug, Clone)]
61struct ReadPlanSegment {
62    start_address: KvDeviceAddress,
63    start_number: u32,
64    count: usize,
65    mode: ReadPlanSegmentMode,
66    requests: Vec<ReadPlanRequest>,
67}
68
69#[derive(Debug, Clone)]
70pub(crate) struct CompiledReadNamedPlan {
71    requests_in_input_order: Vec<ReadPlanRequest>,
72    segments: Vec<ReadPlanSegment>,
73}
74
75impl From<HostLinkValue> for u16 {
76    fn from(value: HostLinkValue) -> Self {
77        match value {
78            HostLinkValue::U16(value) => value,
79            _ => 0,
80        }
81    }
82}
83
84impl HostLinkPayloadValue for HostLinkValue {
85    fn format_for_suffix(&self, data_format: &str) -> String {
86        let mut value = String::new();
87        self.append_to_payload(data_format, &mut value);
88        value
89    }
90
91    fn append_to_payload(&self, data_format: &str, output: &mut String) {
92        match self {
93            HostLinkValue::U16(value) => value.append_to_payload(data_format, output),
94            HostLinkValue::I16(value) => value.append_to_payload(data_format, output),
95            HostLinkValue::U32(value) => value.append_to_payload(data_format, output),
96            HostLinkValue::I32(value) => value.append_to_payload(data_format, output),
97            HostLinkValue::F32(value) => value.append_to_payload(data_format, output),
98            HostLinkValue::Bool(value) => value.append_to_payload(data_format, output),
99            HostLinkValue::Text(value) => value.append_to_payload(data_format, output),
100        }
101    }
102}
103
104pub async fn read_comments(
105    client: &HostLinkClient,
106    device: &str,
107    strip_padding: bool,
108) -> Result<String, HostLinkError> {
109    client.read_comments(device, strip_padding).await
110}
111
112pub async fn read_typed(
113    client: &HostLinkClient,
114    device: &str,
115    dtype: &str,
116) -> Result<HostLinkValue, HostLinkError> {
117    let (device, dtype) = if dtype.trim().is_empty() {
118        let logical = parse_logical_address(device)?;
119        (logical.base_address.to_text()?, logical.data_type)
120    } else {
121        (
122            device.trim().to_ascii_uppercase(),
123            dtype.trim_start_matches('.').to_ascii_uppercase(),
124        )
125    };
126
127    match dtype.as_str() {
128        "F" => {
129            let words = read_words(client, &device, 2).await?;
130            let bits = (words[0] as u32) | ((words[1] as u32) << 16);
131            Ok(HostLinkValue::F32(f32::from_bits(bits)))
132        }
133        "S" => Ok(HostLinkValue::I16(
134            read_single_parsed(client, &device, Some("S"), "Invalid signed 16-bit response")
135                .await?,
136        )),
137        "D" => {
138            if is_timer_counter_composite_device(&device)? {
139                let response = read_single_response(client, &device, Some("D")).await?;
140                Ok(HostLinkValue::U32(parse_last_token(
141                    &response,
142                    "Invalid unsigned 32-bit response",
143                )?))
144            } else {
145                Ok(HostLinkValue::U32(
146                    read_single_parsed::<u32>(
147                        client,
148                        &device,
149                        Some("D"),
150                        "Invalid unsigned 32-bit response",
151                    )
152                    .await?,
153                ))
154            }
155        }
156        "L" => {
157            if is_timer_counter_composite_device(&device)? {
158                let response = read_single_response(client, &device, Some("L")).await?;
159                Ok(HostLinkValue::I32(parse_last_token(
160                    &response,
161                    "Invalid signed 32-bit response",
162                )?))
163            } else {
164                Ok(HostLinkValue::I32(
165                    read_single_parsed(
166                        client,
167                        &device,
168                        Some("L"),
169                        "Invalid signed 32-bit response",
170                    )
171                    .await?,
172                ))
173            }
174        }
175        "U" => Ok(HostLinkValue::U16(
176            read_single_parsed::<u16>(
177                client,
178                &device,
179                Some("U"),
180                "Invalid unsigned 16-bit response",
181            )
182            .await?,
183        )),
184        "" => Ok(HostLinkValue::Bool(
185            read_single_bool(client, &device, None).await?,
186        )),
187        other => Err(HostLinkError::protocol(format!(
188            "Unsupported logical data type '{other}'."
189        ))),
190    }
191}
192
193pub async fn read_timer_counter(
194    client: &HostLinkClient,
195    device: &str,
196) -> Result<TimerCounterValue, HostLinkError> {
197    let mut address = parse_device(device)?;
198    if !matches!(address.device_type.as_str(), "T" | "C") {
199        return Err(HostLinkError::protocol(
200            "read_timer_counter requires a T or C device.",
201        ));
202    }
203
204    address.suffix.clear();
205    let target = address.to_text()?;
206    let response = read_single_response(client, &target, Some("D")).await?;
207    let values = parse_all_tokens::<u32>(
208        &response,
209        "Invalid timer/counter status/current/preset response",
210    )?;
211    if values.len() < 3 {
212        return Err(HostLinkError::protocol(
213            "Timer/counter response did not contain status/current/preset.",
214        ));
215    }
216    Ok(TimerCounterValue {
217        status: values[0],
218        current: values[1],
219        preset: values[2],
220    })
221}
222
223pub async fn read_timer(
224    client: &HostLinkClient,
225    device: &str,
226) -> Result<TimerCounterValue, HostLinkError> {
227    if parse_device(device)?.device_type != "T" {
228        return Err(HostLinkError::protocol("read_timer requires a T device."));
229    }
230    read_timer_counter(client, device).await
231}
232
233pub async fn read_counter(
234    client: &HostLinkClient,
235    device: &str,
236) -> Result<TimerCounterValue, HostLinkError> {
237    if parse_device(device)?.device_type != "C" {
238        return Err(HostLinkError::protocol("read_counter requires a C device."));
239    }
240    read_timer_counter(client, device).await
241}
242
243pub async fn write_typed<T: HostLinkPayloadValue>(
244    client: &HostLinkClient,
245    device: &str,
246    dtype: &str,
247    value: &T,
248) -> Result<(), HostLinkError> {
249    match dtype.trim_start_matches('.').to_ascii_uppercase().as_str() {
250        "F" => {
251            let single = value
252                .format_for_suffix("")
253                .parse::<f32>()
254                .map_err(|_| HostLinkError::protocol("Invalid float32 input"))?;
255            let bits = single.to_bits();
256            let words = [(bits & 0xFFFF) as u16, (bits >> 16) as u16];
257            client.write_consecutive(device, &words, Some("U")).await
258        }
259        "" => client.write(device, value, None).await,
260        "S" | "D" | "L" | "U" => client.write(device, value, Some(dtype)).await,
261        other => Err(HostLinkError::protocol(format!(
262            "Unsupported logical data type '{other}'."
263        ))),
264    }
265}
266
267fn parse_bool_token(token: &str) -> Result<bool, HostLinkError> {
268    let token = token.trim();
269    if token == "1" || token.eq_ignore_ascii_case("ON") || token.eq_ignore_ascii_case("TRUE") {
270        Ok(true)
271    } else if token == "0"
272        || token.eq_ignore_ascii_case("OFF")
273        || token.eq_ignore_ascii_case("FALSE")
274    {
275        Ok(false)
276    } else {
277        Err(HostLinkError::protocol(format!(
278            "Invalid direct bit response token: {token}"
279        )))
280    }
281}
282
283fn response_tokens(response_text: &str) -> impl Iterator<Item = &str> {
284    response_text
285        .split([' ', ','])
286        .filter(|token| !token.is_empty())
287}
288
289fn first_response_token(response_text: &str) -> Result<&str, HostLinkError> {
290    response_tokens(response_text)
291        .next()
292        .ok_or_else(|| HostLinkError::protocol("Missing response token"))
293}
294
295fn last_response_token(response_text: &str) -> Result<&str, HostLinkError> {
296    response_tokens(response_text)
297        .last()
298        .ok_or_else(|| HostLinkError::protocol("Missing response token"))
299}
300
301fn is_timer_counter_composite_device(device: &str) -> Result<bool, HostLinkError> {
302    let address = parse_device(device)?;
303    Ok(matches!(address.device_type.as_str(), "T" | "C"))
304}
305
306fn parse_first_token<T: FromStr>(
307    response_text: &str,
308    invalid_message: &'static str,
309) -> Result<T, HostLinkError> {
310    first_response_token(response_text)?
311        .parse::<T>()
312        .map_err(|_| HostLinkError::protocol(invalid_message))
313}
314
315fn parse_last_token<T: FromStr>(
316    response_text: &str,
317    invalid_message: &'static str,
318) -> Result<T, HostLinkError> {
319    last_response_token(response_text)?
320        .parse::<T>()
321        .map_err(|_| HostLinkError::protocol(invalid_message))
322}
323
324fn parse_all_tokens<T: FromStr>(
325    response_text: &str,
326    invalid_message: &'static str,
327) -> Result<Vec<T>, HostLinkError> {
328    let mut values = Vec::new();
329    for token in response_tokens(response_text) {
330        values.push(
331            token
332                .parse::<T>()
333                .map_err(|_| HostLinkError::protocol(invalid_message))?,
334        );
335    }
336    if values.is_empty() {
337        return Err(HostLinkError::protocol("Missing response token"));
338    }
339    Ok(values)
340}
341
342fn prepare_read_address(
343    device: &str,
344    data_format: Option<&str>,
345    count: usize,
346) -> Result<KvDeviceAddress, HostLinkError> {
347    let mut address = parse_device(device)?;
348    let suffix = if let Some(data_format) = data_format {
349        crate::address::normalize_suffix(data_format)?
350    } else {
351        address.suffix.clone()
352    };
353    let suffix = resolve_effective_format(&address.device_type, &suffix);
354    if count > 1 {
355        validate_device_count(&address.device_type, &suffix, count)?;
356    }
357    validate_device_span(&address.device_type, address.number, &suffix, count)?;
358    address.suffix = suffix;
359    Ok(address)
360}
361
362async fn read_single_response(
363    client: &HostLinkClient,
364    device: &str,
365    data_format: Option<&str>,
366) -> Result<String, HostLinkError> {
367    let address = prepare_read_address(device, data_format, 1)?;
368    client.send_raw(&format!("RD {}", address.to_text()?)).await
369}
370
371async fn read_single_parsed<T: FromStr>(
372    client: &HostLinkClient,
373    device: &str,
374    data_format: Option<&str>,
375    invalid_message: &'static str,
376) -> Result<T, HostLinkError> {
377    let response = read_single_response(client, device, data_format).await?;
378    parse_first_token(&response, invalid_message)
379}
380
381async fn read_single_bool(
382    client: &HostLinkClient,
383    device: &str,
384    data_format: Option<&str>,
385) -> Result<bool, HostLinkError> {
386    let response = read_single_response(client, device, data_format).await?;
387    parse_bool_token(first_response_token(&response)?)
388}
389
390async fn read_consecutive_parsed<T: FromStr>(
391    client: &HostLinkClient,
392    device: &str,
393    count: usize,
394    data_format: Option<&str>,
395    invalid_message: &'static str,
396) -> Result<Vec<T>, HostLinkError> {
397    let address = prepare_read_address(device, data_format, count)?;
398    let response = client
399        .send_raw(&format!("RDS {} {}", address.to_text()?, count))
400        .await?;
401    parse_all_tokens(&response, invalid_message)
402}
403
404pub async fn write_bit_in_word(
405    client: &HostLinkClient,
406    device: &str,
407    bit_index: u8,
408    value: bool,
409) -> Result<(), HostLinkError> {
410    if bit_index > 15 {
411        return Err(HostLinkError::protocol("bitIndex must be 0-15."));
412    }
413
414    let mut current = read_single_parsed::<u16>(
415        client,
416        device,
417        Some("U"),
418        "Invalid unsigned 16-bit response",
419    )
420    .await?;
421    if value {
422        current |= 1 << bit_index;
423    } else {
424        current &= !(1 << bit_index);
425    }
426    client.write(device, current, Some("U")).await
427}
428
429pub async fn read_named<S: AsRef<str>>(
430    client: &HostLinkClient,
431    addresses: &[S],
432) -> Result<NamedSnapshot, HostLinkError> {
433    let addr_list = addresses
434        .iter()
435        .map(|item| item.as_ref().to_owned())
436        .collect::<Vec<_>>();
437    if addr_list.is_empty() {
438        return Ok(NamedSnapshot::new());
439    }
440
441    if let Some(plan) = compile_read_named_plan(&addr_list) {
442        execute_read_named_plan(client, &plan).await
443    } else {
444        read_named_sequential(client, &addr_list).await
445    }
446}
447
448pub(crate) async fn read_named_sequential(
449    client: &HostLinkClient,
450    addresses: &[String],
451) -> Result<NamedSnapshot, HostLinkError> {
452    let mut result = NamedSnapshot::new();
453    for address in addresses {
454        let (base_address, dtype, bit_index) = parse_named_address_parts(address)?;
455        if dtype == "BIT_IN_WORD" {
456            let word = read_single_parsed::<u16>(
457                client,
458                &base_address,
459                Some("U"),
460                "Invalid unsigned 16-bit response",
461            )
462            .await?;
463            let bit_index = bit_index.unwrap_or(0);
464            result.insert(
465                address.clone(),
466                HostLinkValue::Bool(((word >> bit_index) & 1) != 0),
467            );
468        } else if dtype == "COMMENT" {
469            result.insert(
470                address.clone(),
471                HostLinkValue::Text(read_comments(client, &base_address, true).await?),
472            );
473        } else {
474            result.insert(
475                address.clone(),
476                read_typed(client, &base_address, &dtype).await?,
477            );
478        }
479    }
480    Ok(result)
481}
482
483pub(crate) fn compile_read_named_plan(addresses: &[String]) -> Option<CompiledReadNamedPlan> {
484    let mut requests_in_input_order = Vec::new();
485    let mut requests_by_device_type: IndexMap<String, Vec<ReadPlanRequest>> = IndexMap::new();
486
487    for (index, address) in addresses.iter().enumerate() {
488        let request = try_parse_optimizable_read_named_request(address, index)?;
489        requests_by_device_type
490            .entry(request.base_address.device_type.clone())
491            .or_default()
492            .push(request.clone());
493        requests_in_input_order.push(request);
494    }
495
496    let mut segments = Vec::new();
497    for bucket in requests_by_device_type.values() {
498        let mut sorted = bucket.clone();
499        sorted.sort_by_key(|request| {
500            (
501                read_plan_number(request),
502                usize::MAX - get_word_width(request.kind),
503            )
504        });
505
506        let mut pending = Vec::new();
507        let mut current_start: Option<KvDeviceAddress> = None;
508        let mut current_start_number = 0u32;
509        let mut current_end_exclusive = 0u32;
510        let mut current_mode: Option<ReadPlanSegmentMode> = None;
511
512        for request in sorted {
513            let request_start = read_plan_number(&request);
514            let request_end_exclusive = request_start + get_word_width(request.kind) as u32;
515            let request_mode = segment_mode_for_kind(request.kind);
516            if current_start.is_none()
517                || request_start > current_end_exclusive
518                || current_mode != Some(request_mode)
519            {
520                if let Some(start_address) = current_start.take() {
521                    segments.push(ReadPlanSegment {
522                        start_address,
523                        start_number: current_start_number,
524                        count: (current_end_exclusive - current_start_number) as usize,
525                        mode: current_mode.unwrap_or(ReadPlanSegmentMode::Words),
526                        requests: pending.clone(),
527                    });
528                    pending.clear();
529                }
530                current_start = Some(KvDeviceAddress {
531                    device_type: request.base_address.device_type.clone(),
532                    number: request.base_address.number,
533                    suffix: String::new(),
534                });
535                current_start_number = request_start;
536                current_end_exclusive = request_end_exclusive;
537                current_mode = Some(request_mode);
538            } else if request_end_exclusive > current_end_exclusive {
539                current_end_exclusive = request_end_exclusive;
540            }
541            pending.push(request);
542        }
543
544        if let Some(start_address) = current_start {
545            segments.push(ReadPlanSegment {
546                start_address,
547                start_number: current_start_number,
548                count: (current_end_exclusive - current_start_number) as usize,
549                mode: current_mode.unwrap_or(ReadPlanSegmentMode::Words),
550                requests: pending,
551            });
552        }
553    }
554
555    Some(CompiledReadNamedPlan {
556        requests_in_input_order,
557        segments,
558    })
559}
560
561pub(crate) async fn execute_read_named_plan(
562    client: &HostLinkClient,
563    plan: &CompiledReadNamedPlan,
564) -> Result<NamedSnapshot, HostLinkError> {
565    let mut resolved = vec![HostLinkValue::U16(0); plan.requests_in_input_order.len()];
566    for segment in &plan.segments {
567        match segment.mode {
568            ReadPlanSegmentMode::Words => {
569                let words =
570                    read_words(client, &segment.start_address.to_text()?, segment.count).await?;
571                for request in &segment.requests {
572                    let offset = (read_plan_number(request) - segment.start_number) as usize;
573                    resolved[request.index] =
574                        resolve_planned_value(&words, offset, request.kind, request.bit_index)?;
575                }
576            }
577            ReadPlanSegmentMode::DirectBits => {
578                let tokens = client
579                    .read_consecutive(&segment.start_address.to_text()?, segment.count, None)
580                    .await?;
581                for request in &segment.requests {
582                    let offset = (read_plan_number(request) - segment.start_number) as usize;
583                    resolved[request.index] = resolve_direct_bit_value(&tokens, offset)?;
584                }
585            }
586        }
587    }
588
589    let mut result = NamedSnapshot::new();
590    for request in &plan.requests_in_input_order {
591        result.insert(request.address.clone(), resolved[request.index].clone());
592    }
593    Ok(result)
594}
595
596pub fn poll<'a, S: AsRef<str> + 'a>(
597    client: &'a HostLinkClient,
598    addresses: &'a [S],
599    interval: Duration,
600) -> impl Stream<Item = Result<NamedSnapshot, HostLinkError>> + 'a {
601    async_stream::try_stream! {
602        let addr_list = addresses.iter().map(|item| item.as_ref().to_owned()).collect::<Vec<_>>();
603        let compiled = compile_read_named_plan(&addr_list);
604        loop {
605            let snapshot = if let Some(plan) = &compiled {
606                execute_read_named_plan(client, plan).await?
607            } else {
608                read_named_sequential(client, &addr_list).await?
609            };
610            yield snapshot;
611            tokio::time::sleep(interval).await;
612        }
613    }
614}
615
616pub async fn read_words(
617    client: &HostLinkClient,
618    device: &str,
619    count: usize,
620) -> Result<Vec<u16>, HostLinkError> {
621    read_words_single_request(client, device, count).await
622}
623
624pub async fn read_dwords(
625    client: &HostLinkClient,
626    device: &str,
627    count: usize,
628) -> Result<Vec<u32>, HostLinkError> {
629    read_dwords_single_request(client, device, count).await
630}
631
632pub async fn read_words_single_request(
633    client: &HostLinkClient,
634    device: &str,
635    count: usize,
636) -> Result<Vec<u16>, HostLinkError> {
637    if count == 0 {
638        return Err(HostLinkError::protocol("count must be 1 or greater."));
639    }
640    read_consecutive_parsed::<u16>(
641        client,
642        device,
643        count,
644        Some("U"),
645        "Invalid unsigned 16-bit response",
646    )
647    .await
648}
649
650pub async fn read_dwords_single_request(
651    client: &HostLinkClient,
652    device: &str,
653    count: usize,
654) -> Result<Vec<u32>, HostLinkError> {
655    if count == 0 {
656        return Err(HostLinkError::protocol("count must be 1 or greater."));
657    }
658    let words = read_words_single_request(client, device, count * 2).await?;
659    let mut result = Vec::with_capacity(count);
660    for index in 0..count {
661        let lo = words[index * 2] as u32;
662        let hi = words[(index * 2) + 1] as u32;
663        result.push(lo | (hi << 16));
664    }
665    Ok(result)
666}
667
668pub async fn write_words_single_request(
669    client: &HostLinkClient,
670    device: &str,
671    values: &[u16],
672) -> Result<(), HostLinkError> {
673    if values.is_empty() {
674        return Err(HostLinkError::protocol("values must not be empty"));
675    }
676    client.write_consecutive(device, values, Some("U")).await
677}
678
679pub async fn write_dwords_single_request(
680    client: &HostLinkClient,
681    device: &str,
682    values: &[u32],
683) -> Result<(), HostLinkError> {
684    if values.is_empty() {
685        return Err(HostLinkError::protocol("values must not be empty"));
686    }
687    let mut words = Vec::with_capacity(values.len() * 2);
688    for value in values {
689        words.push((value & 0xFFFF) as u16);
690        words.push((value >> 16) as u16);
691    }
692    write_words_single_request(client, device, &words).await
693}
694
695pub async fn read_words_chunked(
696    client: &HostLinkClient,
697    device: &str,
698    count: usize,
699    max_words_per_request: usize,
700) -> Result<Vec<u16>, HostLinkError> {
701    validate_chunk_arguments(count, max_words_per_request, "count", "maxWordsPerRequest")?;
702    let mut start = parse_device(device)?;
703    start.suffix.clear();
704    let mut result = vec![0u16; count];
705    let mut offset = 0usize;
706    while offset < count {
707        let chunk_count = max_words_per_request.min(count - offset);
708        let chunk_start = offset_device(&start, offset as u32)?;
709        let chunk = read_words_single_request(client, &chunk_start, chunk_count).await?;
710        result[offset..offset + chunk_count].copy_from_slice(&chunk);
711        offset += chunk_count;
712    }
713    Ok(result)
714}
715
716pub async fn read_dwords_chunked(
717    client: &HostLinkClient,
718    device: &str,
719    count: usize,
720    max_dwords_per_request: usize,
721) -> Result<Vec<u32>, HostLinkError> {
722    validate_chunk_arguments(
723        count,
724        max_dwords_per_request,
725        "count",
726        "maxDwordsPerRequest",
727    )?;
728    let mut start = parse_device(device)?;
729    start.suffix.clear();
730    let mut result = vec![0u32; count];
731    let mut offset = 0usize;
732    while offset < count {
733        let chunk_count = max_dwords_per_request.min(count - offset);
734        let chunk_start = offset_device(&start, (offset * 2) as u32)?;
735        let chunk = read_dwords_single_request(client, &chunk_start, chunk_count).await?;
736        result[offset..offset + chunk_count].copy_from_slice(&chunk);
737        offset += chunk_count;
738    }
739    Ok(result)
740}
741
742pub async fn write_words_chunked(
743    client: &HostLinkClient,
744    device: &str,
745    values: &[u16],
746    max_words_per_request: usize,
747) -> Result<(), HostLinkError> {
748    if values.is_empty() {
749        return Err(HostLinkError::protocol("values must not be empty"));
750    }
751    validate_chunk_size(max_words_per_request, "maxWordsPerRequest")?;
752    let mut start = parse_device(device)?;
753    start.suffix.clear();
754    let mut offset = 0usize;
755    while offset < values.len() {
756        let chunk_count = max_words_per_request.min(values.len() - offset);
757        let chunk_start = offset_device(&start, offset as u32)?;
758        write_words_single_request(client, &chunk_start, &values[offset..offset + chunk_count])
759            .await?;
760        offset += chunk_count;
761    }
762    Ok(())
763}
764
765pub async fn write_dwords_chunked(
766    client: &HostLinkClient,
767    device: &str,
768    values: &[u32],
769    max_dwords_per_request: usize,
770) -> Result<(), HostLinkError> {
771    if values.is_empty() {
772        return Err(HostLinkError::protocol("values must not be empty"));
773    }
774    validate_chunk_size(max_dwords_per_request, "maxDwordsPerRequest")?;
775    let mut start = parse_device(device)?;
776    start.suffix.clear();
777    let mut offset = 0usize;
778    while offset < values.len() {
779        let chunk_count = max_dwords_per_request.min(values.len() - offset);
780        let chunk_start = offset_device(&start, (offset * 2) as u32)?;
781        write_dwords_single_request(client, &chunk_start, &values[offset..offset + chunk_count])
782            .await?;
783        offset += chunk_count;
784    }
785    Ok(())
786}
787
788fn try_parse_optimizable_read_named_request(
789    address: &str,
790    index: usize,
791) -> Option<ReadPlanRequest> {
792    let (base_address, dtype, bit_index) = parse_named_address_parts(address).ok()?;
793    let mut base_address = parse_device(&base_address).ok()?;
794    if !is_optimizable_read_named_device_type(&base_address.device_type)
795        && !is_direct_bit_device_type(&base_address.device_type)
796    {
797        return None;
798    }
799    base_address.suffix.clear();
800
801    let (kind, bit_index) =
802        if dtype.is_empty() && is_direct_bit_device_type(&base_address.device_type) {
803            (ReadPlanValueKind::DirectBit, 0)
804        } else if dtype == "BIT_IN_WORD" {
805            (ReadPlanValueKind::BitInWord, bit_index.unwrap_or(0))
806        } else {
807            (try_map_read_plan_value_kind(&dtype)?, 0)
808        };
809
810    Some(ReadPlanRequest {
811        index,
812        address: address.to_owned(),
813        base_address,
814        kind,
815        bit_index,
816    })
817}
818
819fn try_map_read_plan_value_kind(dtype: &str) -> Option<ReadPlanValueKind> {
820    match dtype.trim_start_matches('.').to_ascii_uppercase().as_str() {
821        "U" => Some(ReadPlanValueKind::Unsigned16),
822        "S" => Some(ReadPlanValueKind::Signed16),
823        "D" => Some(ReadPlanValueKind::Unsigned32),
824        "L" => Some(ReadPlanValueKind::Signed32),
825        "F" => Some(ReadPlanValueKind::Float32),
826        _ => None,
827    }
828}
829
830fn segment_mode_for_kind(kind: ReadPlanValueKind) -> ReadPlanSegmentMode {
831    when_direct_bit(
832        kind,
833        ReadPlanSegmentMode::DirectBits,
834        ReadPlanSegmentMode::Words,
835    )
836}
837
838fn when_direct_bit<T>(kind: ReadPlanValueKind, direct: T, other: T) -> T {
839    match kind {
840        ReadPlanValueKind::DirectBit => direct,
841        _ => other,
842    }
843}
844
845fn get_word_width(kind: ReadPlanValueKind) -> usize {
846    match kind {
847        ReadPlanValueKind::Unsigned32
848        | ReadPlanValueKind::Signed32
849        | ReadPlanValueKind::Float32 => 2,
850        _ => 1,
851    }
852}
853
854fn read_plan_number(request: &ReadPlanRequest) -> u32 {
855    if request.kind == ReadPlanValueKind::DirectBit
856        && uses_bit_bank_address(&request.base_address.device_type)
857    {
858        bit_bank_logical_number(request.base_address.number)
859    } else {
860        request.base_address.number
861    }
862}
863
864fn resolve_planned_value(
865    words: &[u16],
866    offset: usize,
867    kind: ReadPlanValueKind,
868    bit_index: u8,
869) -> Result<HostLinkValue, HostLinkError> {
870    let word = *words
871        .get(offset)
872        .ok_or_else(|| HostLinkError::protocol("Batched read response was too short"))?;
873    let next_word = || {
874        words
875            .get(offset + 1)
876            .copied()
877            .ok_or_else(|| HostLinkError::protocol("Batched read response was too short"))
878    };
879
880    Ok(match kind {
881        ReadPlanValueKind::Unsigned16 => HostLinkValue::U16(word),
882        ReadPlanValueKind::Signed16 => HostLinkValue::I16(word as i16),
883        ReadPlanValueKind::Unsigned32 => {
884            let hi = next_word()? as u32;
885            HostLinkValue::U32((word as u32) | (hi << 16))
886        }
887        ReadPlanValueKind::Signed32 => {
888            let hi = next_word()? as u32;
889            HostLinkValue::I32(((word as u32) | (hi << 16)) as i32)
890        }
891        ReadPlanValueKind::Float32 => {
892            let hi = next_word()? as u32;
893            HostLinkValue::F32(f32::from_bits((word as u32) | (hi << 16)))
894        }
895        ReadPlanValueKind::BitInWord => HostLinkValue::Bool(((word >> bit_index) & 1) != 0),
896        ReadPlanValueKind::DirectBit => {
897            return Err(HostLinkError::protocol(
898                "Direct bit values must be resolved from bit tokens.",
899            ));
900        }
901    })
902}
903
904fn resolve_direct_bit_value(
905    tokens: &[String],
906    offset: usize,
907) -> Result<HostLinkValue, HostLinkError> {
908    let token = tokens
909        .get(offset)
910        .ok_or_else(|| HostLinkError::protocol("Batched direct bit response was too short"))?;
911    Ok(HostLinkValue::Bool(parse_bool_token(token)?))
912}
913
914fn validate_chunk_arguments(
915    count: usize,
916    max_per_request: usize,
917    count_name: &str,
918    chunk_name: &str,
919) -> Result<(), HostLinkError> {
920    if count == 0 {
921        return Err(HostLinkError::protocol(format!(
922            "{count_name} must be 1 or greater."
923        )));
924    }
925    validate_chunk_size(max_per_request, chunk_name)
926}
927
928fn validate_chunk_size(max_per_request: usize, param_name: &str) -> Result<(), HostLinkError> {
929    if max_per_request == 0 {
930        return Err(HostLinkError::protocol(format!(
931            "{param_name} must be 1 or greater."
932        )));
933    }
934    Ok(())
935}