1use alloc::collections::BTreeMap;
16use alloc::string::String;
17use alloc::vec;
18use alloc::vec::Vec;
19
20use crate::codec::decode::{decode, decode_bytes, decode_raw, decode_str, Ctx};
21use crate::codec::encode::{encode, encode_bytes, encode_str, validate_write};
22use crate::codec::mask_for;
23use crate::command::{condition_met, Delay, ParamValue, DEFAULT_POLL_INTERVAL_MS, MAX_WRITE_WORDS};
24use crate::convert::{desc_bufs, point_desc, point_words};
25use crate::desc::{DateTimeEncoding, ValueKind};
26use crate::error::{DecodeError, Error};
27use crate::measurand::{measurand_matches, MeasurandQuery};
28use crate::schema;
29use crate::transport::Transport;
30use crate::value::{field_value, flag_names, DecodedValue, Value};
31
32const SUNS_MARKER: [u16; 2] = [0x5375, 0x6e53];
34
35pub struct Device<'d, T: Transport> {
38 profile: &'d schema::DeviceProfile,
39 transport: T,
40 model_base: BTreeMap<&'d str, u16>,
42}
43
44impl<'d, T: Transport> Device<'d, T> {
45 pub fn new(
47 doc: &'d schema::ModDefDocument,
48 device_id: Option<&str>,
49 transport: T,
50 ) -> Result<Self, Error<T::Error>> {
51 let profile = doc
52 .devices
53 .iter()
54 .find(|d| device_id.is_none_or(|id| d.device_id == id))
55 .ok_or(Error::DeviceNotFound)?;
56 Ok(Device::from_profile(profile, transport))
57 }
58
59 pub fn from_profile(profile: &'d schema::DeviceProfile, transport: T) -> Self {
60 Device {
61 profile,
62 transport,
63 model_base: BTreeMap::new(),
64 }
65 }
66
67 pub fn profile(&self) -> &'d schema::DeviceProfile {
68 self.profile
69 }
70
71 pub fn transport_mut(&mut self) -> &mut T {
72 &mut self.transport
73 }
74
75 pub fn into_transport(self) -> T {
76 self.transport
77 }
78
79 pub fn points(&self) -> impl Iterator<Item = &'d schema::Point> {
81 self.profile.blocks.iter().flat_map(|b| b.points.iter())
82 }
83
84 pub fn point(
86 &self,
87 id: &str,
88 ) -> Result<(&'d schema::Point, &'d schema::RegisterBlock), Error<T::Error>> {
89 for b in &self.profile.blocks {
90 if let Some(p) = b.points.iter().find(|p| p.point_id == id) {
91 return Ok((p, b));
92 }
93 }
94 Err(Error::PointNotFound)
95 }
96
97 pub async fn read_point(&mut self, id: &str) -> Result<DecodedValue, Error<T::Error>> {
99 let (p, b) = self.point(id)?;
100 let regs = self.read_registers(p, b).await?;
101 let refs = self.ref_context(p).await?;
102 decode_sized(p, b.space(), ®s, &Ctx { refs: &refs }).map_err(Error::Decode)
103 }
104
105 pub async fn read_measurand(
107 &mut self,
108 q: &MeasurandQuery<'_>,
109 ) -> Result<DecodedValue, Error<T::Error>> {
110 let mut found: Option<&'d schema::Point> = None;
111 for p in self.points() {
112 if measurand_matches(p.measurand.as_ref(), q) {
113 if found.is_some() {
114 return Err(Error::AmbiguousMeasurand);
115 }
116 found = Some(p);
117 }
118 }
119 let p = found.ok_or(Error::MeasurandNotSupported)?;
120 self.read_point(&p.point_id).await
121 }
122
123 pub async fn write_point(&mut self, id: &str, v: Value) -> Result<(), Error<T::Error>> {
125 let (p, b) = self.point(id)?;
126 let bufs = desc_bufs(p);
127 let d = point_desc(p, b.space(), &bufs);
128 if !d.writable() {
129 return Err(Error::WriteAccess);
130 }
131 validate_write(&d, &v).map_err(Error::WriteConstraint)?;
132
133 let space = effective_space(p, b);
134 let off = self.offset_of(p, b).await?;
135
136 if space == schema::AddressSpace::Coil {
137 let on = v.as_f64().map(|f| f != 0.0).unwrap_or(false);
138 return self
139 .transport
140 .write_coil(off, on)
141 .await
142 .map_err(Error::Transport);
143 }
144 if space != schema::AddressSpace::HoldingRegister {
145 return Err(Error::UnsupportedMapping("cannot write this address space"));
146 }
147 let refs = self.ref_context(p).await?;
148 let mut regs = vec![0u16; d.words()];
149 encode(&d, &v, &Ctx { refs: &refs }, &mut regs)?;
150 self.transport
151 .write_holding(off, ®s)
152 .await
153 .map_err(Error::Transport)
154 }
155
156 pub async fn write_point_str(&mut self, id: &str, s: &str) -> Result<(), Error<T::Error>> {
158 let (p, b) = self.point(id)?;
159 let bufs = desc_bufs(p);
160 let d = point_desc(p, b.space(), &bufs);
161 if !d.writable() {
162 return Err(Error::WriteAccess);
163 }
164 if effective_space(p, b) != schema::AddressSpace::HoldingRegister {
165 return Err(Error::UnsupportedMapping("cannot write this address space"));
166 }
167 let off = self.offset_of(p, b).await?;
168 let mut regs = vec![0u16; d.words()];
169 encode_str(&d, s, &mut regs)?;
170 self.transport
171 .write_holding(off, ®s)
172 .await
173 .map_err(Error::Transport)
174 }
175
176 pub async fn run_command<D: Delay>(
184 &mut self,
185 id: &str,
186 params: &[(&str, ParamValue<'_>)],
187 delay: &mut D,
188 ) -> Result<BTreeMap<&'d str, DecodedValue>, Error<T::Error>> {
189 let cmd = self
190 .profile
191 .commands
192 .iter()
193 .find(|c| c.command_id == id)
194 .ok_or(Error::CommandNotFound)?;
195
196 for cp in &cmd.params {
197 if cp.required && !params.iter().any(|(f, _)| *f == cp.field) {
198 return Err(Error::RequiredParamMissing);
199 }
200 }
201
202 let mut bindings: BTreeMap<&'d str, DecodedValue> = BTreeMap::new();
203 for st in &cmd.steps {
204 match &st.step {
205 Some(schema::command_step::Step::Write(w)) => {
206 self.run_write_step(cmd, w, params).await?;
207 }
208 Some(schema::command_step::Step::Poll(p)) => {
209 self.run_poll_step(p, delay).await?;
210 }
211 Some(schema::command_step::Step::Read(r)) => {
212 let v = self.read_command_point(&r.point_id).await?;
213 if !r.into.is_empty() {
214 bindings.insert(r.into.as_str(), v);
215 }
216 }
217 None => return Err(Error::StepReference),
218 }
219 }
220
221 let mut out: BTreeMap<&'d str, DecodedValue> = BTreeMap::new();
222 for res in &cmd.results {
223 let v = match bindings.get(res.from.as_str()) {
224 Some(v) => v.clone(),
225 None => self.read_command_point(&res.from).await?,
226 };
227 out.insert(res.field.as_str(), v);
228 }
229 Ok(out)
230 }
231
232 async fn run_write_step(
233 &mut self,
234 cmd: &'d schema::Command,
235 w: &'d schema::WriteStep,
236 params: &[(&str, ParamValue<'_>)],
237 ) -> Result<(), Error<T::Error>> {
238 match &w.target {
239 Some(schema::write_step::Target::Param(field)) => {
240 let cp = cmd
241 .params
242 .iter()
243 .find(|p| p.field == *field)
244 .ok_or(Error::StepReference)?;
245 let Some((_, pv)) = params.iter().find(|(f, _)| f == field) else {
246 return Ok(()); };
248 let pp = schema::Point {
251 point_id: cp.field.clone(),
252 storage_type: cp.storage_type,
253 value_type: cp.value_type.clone(),
254 mapping: cp.mapping.clone(),
255 ..Default::default()
256 };
257 let bufs = desc_bufs(&pp);
258 let d = point_desc(&pp, schema::AddressSpace::HoldingRegister, &bufs);
259 let mut regs = vec![0u16; d.words()];
260 match pv {
261 ParamValue::Value(v) => encode(&d, v, &Ctx::EMPTY, &mut regs)?,
262 ParamValue::Str(s) => encode_str(&d, s, &mut regs)?,
263 ParamValue::Bytes(b) => encode_bytes(&d, b, &mut regs)?,
264 }
265 let m = cp.mapping.as_ref();
266 let space = m
267 .map(|m| m.space())
268 .filter(|s| *s != schema::AddressSpace::Unspecified)
269 .unwrap_or(schema::AddressSpace::HoldingRegister);
270 let off = m.map(|m| m.offset as u16).unwrap_or(0);
271 self.write_chunked(space, off, ®s).await
272 }
273 Some(schema::write_step::Target::Trigger(tr)) => {
274 let (p, b) = self.point(&tr.point_id).map_err(|_| Error::StepReference)?;
275 let space = effective_space(p, b);
276 let off = self.offset_of(p, b).await?;
277 let rp = raw_point(p);
280 let bufs = desc_bufs(&rp);
281 let d = point_desc(&rp, b.space(), &bufs);
282 let mut regs = vec![0u16; d.words()];
283 encode(&d, &Value::I64(tr.value), &Ctx::EMPTY, &mut regs)?;
284 self.write_chunked(space, off, ®s).await
285 }
286 None => Err(Error::StepReference),
287 }
288 }
289
290 async fn run_poll_step<D: Delay>(
291 &mut self,
292 p: &'d schema::PollStep,
293 delay: &mut D,
294 ) -> Result<(), Error<T::Error>> {
295 let (pt, b) = self.point(&p.point_id).map_err(|_| Error::StepReference)?;
296 let interval = if p.interval_ms > 0 {
297 p.interval_ms
298 } else {
299 DEFAULT_POLL_INTERVAL_MS
300 };
301 let mut elapsed: u64 = 0;
302 loop {
303 let raw = self.read_raw_int(pt, b).await?;
304 if condition_met(p.until.as_ref(), raw) {
305 return Ok(());
306 }
307 if p.timeout_ms > 0 && elapsed >= p.timeout_ms as u64 {
308 return Err(Error::PollTimeout);
309 }
310 delay.delay_ms(interval).await;
311 elapsed += interval as u64;
312 }
313 }
314
315 async fn read_raw_int(
317 &mut self,
318 p: &'d schema::Point,
319 b: &'d schema::RegisterBlock,
320 ) -> Result<i64, Error<T::Error>> {
321 let regs = self.read_registers(p, b).await?;
322 let rp = raw_point(p);
323 let bufs = desc_bufs(&rp);
324 let d = point_desc(&rp, b.space(), &bufs);
325 let v = decode(&d, ®s, &Ctx::EMPTY)?;
326 v.as_i64().ok_or(Error::UnsupportedMapping(
327 "poll point is not integer-valued",
328 ))
329 }
330
331 async fn read_command_point(&mut self, id: &str) -> Result<DecodedValue, Error<T::Error>> {
333 let (p, b) = self.point(id).map_err(|_| Error::StepReference)?;
334 let regs = self.read_registers(p, b).await?;
335 let refs = self.ref_context(p).await?;
336 decode_sized(p, b.space(), ®s, &Ctx { refs: &refs }).map_err(Error::Decode)
337 }
338
339 async fn write_chunked(
341 &mut self,
342 space: schema::AddressSpace,
343 off: u16,
344 regs: &[u16],
345 ) -> Result<(), Error<T::Error>> {
346 match space {
347 schema::AddressSpace::Coil => self
348 .transport
349 .write_coil(off, regs.first().is_some_and(|w| *w != 0))
350 .await
351 .map_err(Error::Transport),
352 schema::AddressSpace::HoldingRegister => {
353 let mut o = off;
354 for chunk in regs.chunks(MAX_WRITE_WORDS) {
355 self.transport
356 .write_holding(o, chunk)
357 .await
358 .map_err(Error::Transport)?;
359 o = o.wrapping_add(chunk.len() as u16);
360 }
361 Ok(())
362 }
363 _ => Err(Error::UnsupportedMapping("cannot write this address space")),
364 }
365 }
366
367 async fn ref_context(
372 &mut self,
373 p: &'d schema::Point,
374 ) -> Result<Vec<(&'d str, i64)>, Error<T::Error>> {
375 let mut ids: Vec<&'d str> = Vec::new();
376 if let Some(sr) = p.transform.as_ref().and_then(|t| t.scale_ref.as_ref()) {
377 ids.push(&sr.point_id);
378 }
379 if let Some(sel) = p.selector_ref.as_ref() {
380 ids.push(&sel.point_id);
381 }
382 let mut refs = Vec::with_capacity(ids.len());
383 for id in ids {
384 let (rp, rb) = self.point(id)?;
385 let regs = self.read_registers(rp, rb).await?;
386 let bufs = desc_bufs(rp);
387 let d = point_desc(rp, rb.space(), &bufs);
388 let v = decode(&d, ®s, &Ctx::EMPTY)?;
389 if let Some(iv) = v.as_i64() {
390 refs.push((id, iv));
391 }
392 }
393 Ok(refs)
394 }
395
396 async fn read_registers(
397 &mut self,
398 p: &'d schema::Point,
399 b: &'d schema::RegisterBlock,
400 ) -> Result<Vec<u16>, Error<T::Error>> {
401 if p.storage_type() == schema::StorageType::Composed {
402 return Err(Error::UnsupportedMapping(
403 "composed points are not read via the facade",
404 ));
405 }
406 let space = effective_space(p, b);
407 let n = self.point_read_words(p).await?;
408 let off = self.offset_of(p, b).await?;
409 self.read_space(space, off, n).await
410 }
411
412 async fn point_read_words(&mut self, p: &'d schema::Point) -> Result<usize, Error<T::Error>> {
416 let Some(lr) = p.mapping.as_ref().and_then(|m| m.length_ref.as_ref()) else {
417 return Ok(point_words(p));
418 };
419 let (rp, rb) = self.point(&lr.point_id)?;
420 if rp
422 .mapping
423 .as_ref()
424 .and_then(|m| m.length_ref.as_ref())
425 .is_some()
426 {
427 return Err(Error::UnsupportedMapping("chained length_ref"));
428 }
429 let space = effective_space(rp, rb);
430 let off = self.offset_of(rp, rb).await?;
431 let regs = self.read_space(space, off, point_words(rp)).await?;
432 let bufs = desc_bufs(rp);
433 let d = point_desc(rp, rb.space(), &bufs);
434 let v = decode(&d, ®s, &Ctx::EMPTY)?;
435 let iv = v
436 .as_i64()
437 .filter(|iv| *iv >= 0)
438 .ok_or(Error::UnsupportedMapping(
439 "length_ref target is not a non-negative integer",
440 ))?;
441 let mut n = iv as usize;
442 let max = p
443 .mapping
444 .as_ref()
445 .map(|m| m.length_words as usize)
446 .unwrap_or(0);
447 if max > 0 && n > max {
448 n = max;
449 }
450 Ok(n)
451 }
452
453 async fn read_space(
454 &mut self,
455 space: schema::AddressSpace,
456 off: u16,
457 n: usize,
458 ) -> Result<Vec<u16>, Error<T::Error>> {
459 match space {
460 schema::AddressSpace::HoldingRegister => {
461 let mut regs = vec![0u16; n];
462 self.transport
463 .read_holding(off, &mut regs)
464 .await
465 .map_err(Error::Transport)?;
466 Ok(regs)
467 }
468 schema::AddressSpace::InputRegister => {
469 let mut regs = vec![0u16; n];
470 self.transport
471 .read_input(off, &mut regs)
472 .await
473 .map_err(Error::Transport)?;
474 Ok(regs)
475 }
476 schema::AddressSpace::Coil => {
477 let mut bits = [false];
478 self.transport
479 .read_coils(off, &mut bits)
480 .await
481 .map_err(Error::Transport)?;
482 Ok(vec![bits[0] as u16])
483 }
484 schema::AddressSpace::DiscreteInput => {
485 let mut bits = [false];
486 self.transport
487 .read_discrete(off, &mut bits)
488 .await
489 .map_err(Error::Transport)?;
490 Ok(vec![bits[0] as u16])
491 }
492 schema::AddressSpace::Unspecified => {
493 Err(Error::UnsupportedMapping("unspecified address space"))
494 }
495 }
496 }
497
498 async fn offset_of(
499 &mut self,
500 p: &schema::Point,
501 b: &'d schema::RegisterBlock,
502 ) -> Result<u16, Error<T::Error>> {
503 let m = p
504 .mapping
505 .as_ref()
506 .ok_or(Error::UnsupportedMapping("point has no mapping"))?;
507 if b.discovery.is_some() {
508 let base = self.resolve_model_base(b).await?;
509 Ok(base + m.model_relative_offset as u16)
510 } else {
511 Ok(m.offset as u16)
512 }
513 }
514
515 async fn resolve_model_base(
518 &mut self,
519 b: &'d schema::RegisterBlock,
520 ) -> Result<u16, Error<T::Error>> {
521 if let Some(base) = self.model_base.get(b.block_id.as_str()) {
522 return Ok(*base);
523 }
524 let disc = b
525 .discovery
526 .as_ref()
527 .ok_or(Error::UnsupportedMapping("block has no discovery"))?;
528 if disc.kind() != schema::DiscoveryKind::Sunspec {
529 return Err(Error::UnsupportedMapping("unsupported discovery kind"));
530 }
531 let space = b.space();
532 let defaults = [40000u32, 50000, 0];
533 let candidates: &[u32] = if disc.anchor_candidates.is_empty() {
534 &defaults
535 } else {
536 &disc.anchor_candidates
537 };
538
539 let mut anchor = None;
540 for &c in candidates {
541 if let Ok(hdr) = self.read_space(space, c as u16, 2).await {
543 if hdr[..2] == SUNS_MARKER {
544 anchor = Some(c as u16);
545 break;
546 }
547 }
548 }
549 let Some(anchor) = anchor else {
550 return Err(Error::UnsupportedMapping("SunS marker not found"));
551 };
552
553 let mut off = anchor + 2;
555 for _ in 0..256 {
556 let hdr = self.read_space(space, off, 2).await?;
557 let (id, length) = (hdr[0], hdr[1]);
558 if id == 0xffff {
559 break;
560 }
561 if id as u32 == disc.model_id {
562 self.model_base.insert(&b.block_id, off);
564 return Ok(off);
565 }
566 off = off
567 .checked_add(2 + length)
568 .ok_or(Error::UnsupportedMapping("SunSpec model chain overflows"))?;
569 }
570 Err(Error::UnsupportedMapping("SunSpec model not found"))
571 }
572}
573
574fn effective_space(p: &schema::Point, b: &schema::RegisterBlock) -> schema::AddressSpace {
576 match p.mapping.as_ref().map(|m| m.space()) {
577 Some(s) if s != schema::AddressSpace::Unspecified => s,
578 _ => b.space(),
579 }
580}
581
582fn raw_point(p: &schema::Point) -> schema::Point {
585 schema::Point {
586 point_id: p.point_id.clone(),
587 storage_type: p.storage_type,
588 mapping: p.mapping.clone(),
589 ..Default::default()
590 }
591}
592
593fn decode_sized(
597 p: &schema::Point,
598 block_space: schema::AddressSpace,
599 regs: &[u16],
600 ctx: &Ctx<'_>,
601) -> Result<DecodedValue, DecodeError> {
602 let has_length_ref = p.mapping.as_ref().is_some_and(|m| m.length_ref.is_some());
603 if has_length_ref && regs.len() != point_words(p) {
604 let mut sized = p.clone();
605 if let Some(m) = sized.mapping.as_mut() {
606 m.length_words = regs.len() as u32;
607 }
608 return decode_owned(&sized, block_space, regs, ctx);
609 }
610 decode_owned(p, block_space, regs, ctx)
611}
612
613fn decode_owned(
616 p: &schema::Point,
617 block_space: schema::AddressSpace,
618 regs: &[u16],
619 ctx: &Ctx<'_>,
620) -> Result<DecodedValue, DecodeError> {
621 let bufs = desc_bufs(p);
622 let d = point_desc(p, block_space, &bufs);
623
624 match d.value {
625 ValueKind::Str { .. } => {
626 let mut buf = vec![0u8; regs.len() * 2];
627 let s = decode_str(&d, regs, &mut buf)?;
628 Ok(DecodedValue::Str(String::from(s)))
629 }
630 ValueKind::Bytes => {
631 let mut buf = vec![0u8; regs.len() * 2];
632 let bytes = decode_bytes(&d, regs, &mut buf)?;
633 Ok(DecodedValue::Bytes(bytes.to_vec()))
634 }
635 _ => Ok(match decode(&d, regs, ctx)? {
636 Value::Bool(v) => DecodedValue::Bool(v),
637 Value::U64(v) => DecodedValue::U64(v),
638 Value::I64(v) => DecodedValue::I64(v),
639 Value::F64(v) => DecodedValue::F64(v),
640 Value::Flags(mask) => {
641 DecodedValue::Flags(flag_names(&d, mask).map(String::from).collect())
642 }
643 Value::Fields(window) => {
644 let fields = match d.value {
645 ValueKind::Fields(fs) => fs,
646 _ => &[],
647 };
648 DecodedValue::Fields(
649 fields
650 .iter()
651 .map(|f| (String::from(f.id), field_value(f, window)))
652 .collect(),
653 )
654 }
655 Value::DateTime(t) => DecodedValue::DateTime(match d.value {
656 ValueKind::DateTime(DateTimeEncoding::EpochMillis) => t,
657 _ => t.saturating_mul(1000),
658 }),
659 Value::Unavailable => {
660 let (raw, bits) = decode_raw(&d, regs)?;
662 let meaning =
663 d.na.iter()
664 .find(|na| (na.raw as u64) & mask_for(bits) == raw)
665 .map(|na| na.meaning)
666 .unwrap_or("");
667 DecodedValue::Unavailable(String::from(meaning))
668 }
669 }),
670 }
671}