Skip to main content

moddef_core/
device.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Runtime device facade (spec §32.4): binds a [`Transport`] to one parsed
4//! device profile for point- and measurand-based reads/writes, with no
5//! codegen. Port of moddef-ts `Device` / go/client/client.go.
6//!
7//! SunSpec `model_relative_offset` is resolved against the model *ID
8//! register* (offset 0 = model id, 1 = length, data at 2+) per spec §7.3 —
9//! the same convention as the Go/TS clients and the profiles in devices/.
10//!
11//! Shared limitation kept in lockstep with Go/TS: composed (multi-register
12//! mantissa/exponent) points decode via the codec directly, not through the
13//! facade.
14
15use 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
32/// "SunS" marker as two big-endian 16-bit words.
33const SUNS_MARKER: [u16; 2] = [0x5375, 0x6e53];
34
35/// The untyped runtime facade over one device profile. Generated typed
36/// clients (moddef-codegen) skip this and use `static` descriptor tables.
37pub struct Device<'d, T: Transport> {
38    profile: &'d schema::DeviceProfile,
39    transport: T,
40    /// Resolved SunSpec model ID-register offsets, cached per block (§7.3).
41    model_base: BTreeMap<&'d str, u16>,
42}
43
44impl<'d, T: Transport> Device<'d, T> {
45    /// Bind a transport to the named device profile in `doc` (or the only one).
46    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    /// All points in block order (spec §32.1).
80    pub fn points(&self) -> impl Iterator<Item = &'d schema::Point> {
81        self.profile.blocks.iter().flat_map(|b| b.points.iter())
82    }
83
84    /// Look up a point and its owning block by id.
85    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    /// Read and decode a single point by id.
98    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(), &regs, &Ctx { refs: &refs }).map_err(Error::Decode)
103    }
104
105    /// Read a point by its semantic measurand tuple (spec §26.1).
106    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    /// Encode and write a value, validating access mode and §11.4 constraints.
124    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, &regs)
152            .await
153            .map_err(Error::Transport)
154    }
155
156    /// Write a string point (STRING_ASCII / STRING_UTF8), §15.
157    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, &regs)
172            .await
173            .map_err(Error::Transport)
174    }
175
176    /// Execute a §11.7 command: `params` are the caller's inputs keyed by
177    /// `CommandParam.field`; the returned map holds results keyed by
178    /// `CommandResult.field`. Steps run strictly in declaration order; a
179    /// poll step past its `timeout_ms` fails with [`Error::PollTimeout`]
180    /// (elapsed time is accounted by accumulating the delays requested from
181    /// `delay` — no wall clock). Poll conditions and trigger writes use raw
182    /// (pre-transform) register values.
183    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(()); // optional param not supplied — skip its write
247                };
248                // A param carries its own wire mapping; encode through a
249                // synthetic point so the shared codec handles storage/order.
250                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, &regs).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                // Trigger values are raw register values (§11.7): encode via
278                // storage/mapping only, bypassing transform/value_type.
279                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, &regs).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    /// Read a point's raw (pre-transform) integer value (poll conditions).
316    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, &regs, &Ctx::EMPTY)?;
326        v.as_i64().ok_or(Error::UnsupportedMapping(
327            "poll point is not integer-valued",
328        ))
329    }
330
331    /// Read/decode a point for a read step or result (length_ref-aware).
332    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(), &regs, &Ctx { refs: &refs }).map_err(Error::Decode)
337    }
338
339    /// Write registers in ≤123-word slices (single-PDU FC16 cap).
340    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    // --- internals -------------------------------------------------------- //
368
369    /// Read the points referenced by p's scale_ref / selector_ref
370    /// (spec §10.4/§10.5), decoded to integers for the codec context.
371    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, &regs, &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    /// Effective register count for reading p: the static [`point_words`],
413    /// or — when the mapping sets `length_ref` (§11.7.1) — the decoded value
414    /// of the referenced point, clamped to `length_words` as an upper bound.
415    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        // MDE506 forbids chains/cycles; guard so a bad document cannot recurse.
421        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, &regs, &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    /// Probe discovery anchors for the SunS marker, walk the (model_id,
516    /// length) chain, and return the offset of the target model's ID register.
517    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            // Devices answer exceptions off-anchor; try the next candidate.
542            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        // Walk model headers starting just after the marker.
554        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                // Base is the model ID register (model_relative_offset 0, §7.3).
563                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
574/// The effective address space: the mapping's, else the owning block's.
575fn 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
582/// Storage/mapping-only copy of a point: trigger writes and poll reads are
583/// raw register values (§11.7), bypassing transform and value_type.
584fn 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
593/// [`decode_owned`], but honouring a `length_ref`-sized read: when the
594/// register window is shorter than the mapping's declared `length_words`
595/// clamp, decode at the runtime length (string/bytes lengths follow it).
596fn 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
613/// Decode a point's registers into an owned [`DecodedValue`]. Date/times are
614/// normalized to epoch **milliseconds** (parity with the TS `Date` surface).
615fn 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                // Recover the sentinel's meaning for the owned value.
661                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}