Skip to main content

knx_catalog/
schema.rs

1//! Declarative row and descriptor types for the catalogue.
2//!
3//! These types hold the declarative record of each subtype - a
4//! restatement where the KNX source is plain, and the reading this
5//! catalogue settled on where the source is silent, ambiguous or
6//! prints figures that disagree - in a shape a consumer can read
7//! without heap allocation or runtime text parsing. The types
8//! deliberately distinguish structural dimensions the source
9//! distinguishes - fixed fields, fixed arrays, repeated fixed-width
10//! groups, genuinely variable length, reserved bits, named bits, special
11//! values with distinct kinds - rather than collapsing them into a single
12//! width number.
13
14use core::fmt;
15
16use crate::id::DptId;
17
18/// The transmitted width contract of a subtype.
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20pub enum PayloadWidth {
21    /// A fixed width shorter than one octet, transmitted on the low bit
22    /// positions of a data octet under the carrier rule for short types.
23    Bits(u8),
24    /// A fixed width in octets.
25    Octets(u16),
26    /// A variable octet count, bounded by the transport rather than by the
27    /// type.
28    Variable,
29}
30
31/// An exact decimal number: `num * 10^exp`.
32///
33/// The source declares ranges and resolutions in decimal; a scaled integer
34/// keeps them exact where a binary float would approximate.
35#[derive(Clone, Copy, PartialEq, Eq, Debug)]
36pub struct Decimal {
37    /// The scaled integer value.
38    pub num: i64,
39    /// The power of ten the value is scaled by.
40    pub exp: i8,
41}
42
43impl Decimal {
44    /// The value scaled to the given exponent, when that scaling fits
45    /// an `i128`.
46    fn scaled_to(self, exp: i8) -> Option<i128> {
47        let shift = u32::try_from(i32::from(self.exp) - i32::from(exp)).ok()?;
48        10i128.checked_pow(shift).and_then(|scale| i128::from(self.num).checked_mul(scale))
49    }
50
51    /// Compares two decimals by numeric value, exactly, for any
52    /// exponent pair: a scaling that cannot be represented is decided
53    /// by sign, because the unscalable side's magnitude necessarily
54    /// exceeds the other's.
55    pub fn value_cmp(self, other: Decimal) -> core::cmp::Ordering {
56        use core::cmp::Ordering;
57        if self.num == 0 || other.num == 0 || (self.num < 0) != (other.num < 0) {
58            return self.num.cmp(&other.num);
59        }
60        // The side at the minimum exponent scales by 10^0 and always
61        // fits, so at most one scaling can overflow - and a nonzero
62        // value whose scaling overflows i128 necessarily exceeds the
63        // other side's magnitude.
64        let exp = self.exp.min(other.exp);
65        match (self.scaled_to(exp), other.scaled_to(exp)) {
66            (Some(left), Some(right)) => left.cmp(&right),
67            (None, _) => {
68                if self.num > 0 {
69                    Ordering::Greater
70                } else {
71                    Ordering::Less
72                }
73            }
74            (_, None) => {
75                if other.num > 0 {
76                    Ordering::Less
77                } else {
78                    Ordering::Greater
79                }
80            }
81        }
82    }
83
84    /// The exact quotient of this value by `divisor`, when the division
85    /// is exact, every intermediate fits, and the result fits an `i64`.
86    ///
87    /// `None` means the value is off the divisor's grid - whether
88    /// because the division leaves a remainder or because a scaling
89    /// cannot be represented; an unscalable magnitude is off every
90    /// field grid, so `None` is the honest answer either way.
91    pub fn exact_quotient(self, divisor: Decimal) -> Option<i64> {
92        if divisor.num == 0 {
93            return None;
94        }
95        if self.num == 0 {
96            // Zero is on every grid whatever the exponents; the
97            // scaling below could overflow on the divisor's side.
98            return Some(0);
99        }
100        let exp = self.exp.min(divisor.exp);
101        let left = self.scaled_to(exp)?;
102        let right = divisor.scaled_to(exp)?;
103        if left % right != 0 {
104            return None;
105        }
106        i64::try_from(left / right).ok()
107    }
108
109    /// A binary floating-point approximation; exact wherever the
110    /// scaling power of ten is exactly representable (every catalogued
111    /// exponent is).
112    ///
113    /// A representation conversion only - the exact decimal remains the
114    /// recorded fact; a caller doing floating arithmetic opts into the
115    /// one rounding step here.
116    pub fn to_f64(self) -> f64 {
117        let mut scale = 1.0f64;
118        for _ in 0..self.exp.unsigned_abs() {
119            scale *= 10.0;
120        }
121        let num = self.num as f64;
122        if self.exp >= 0 {
123            num * scale
124        } else {
125            num / scale
126        }
127    }
128}
129
130/// Canonical decimal rendering: the exact value, a `.` only where the
131/// exponent is negative, no exponent notation and no trimming.
132impl fmt::Display for Decimal {
133    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134        if self.exp >= 0 {
135            write!(formatter, "{}", self.num)?;
136            if self.num != 0 {
137                for _ in 0..self.exp {
138                    formatter.write_str("0")?;
139                }
140            }
141            return Ok(());
142        }
143        if self.num < 0 {
144            formatter.write_str("-")?;
145        }
146        let magnitude = self.num.unsigned_abs();
147        let places = usize::from(self.exp.unsigned_abs());
148        let mut digits = 1usize;
149        let mut probe = magnitude;
150        while probe >= 10 {
151            probe /= 10;
152            digits += 1;
153        }
154        if digits > places {
155            // `places < digits <= 20`, so the split power fits u64.
156            let split = 10u64.pow(places as u32);
157            write!(
158                formatter,
159                "{}.{:0places$}",
160                magnitude / split,
161                magnitude % split,
162                places = places
163            )
164        } else {
165            formatter.write_str("0.")?;
166            for _ in 0..(places - digits) {
167                formatter.write_str("0")?;
168            }
169            write!(formatter, "{magnitude}")
170        }
171    }
172}
173
174/// The declared numeric range of a subtype's value.
175#[derive(Clone, Copy, PartialEq, Eq, Debug)]
176pub enum DeclaredRange {
177    /// The numeric range the reviewed data records for this field.
178    /// Where the source prints one range plainly this restates it;
179    /// where the source is silent, ambiguous, or prints figures that
180    /// disagree, it is the reading this catalogue settled on, not a
181    /// further source statement.
182    ///
183    /// Whether either direction judges against it follows the field's
184    /// carrier, so recording a range is not by itself a bound. On the
185    /// unsigned, signed and float carriers it is one: interpretation
186    /// refuses a value outside it and so does composition - except
187    /// under a linear mapping, which runs the encodable span onto it,
188    /// so composition refuses a value outside it and interpretation
189    /// produces none to refuse. The binary32 carrier keeps 1
190    /// deliberate asymmetry: a NaN is never outside a range on the
191    /// interpret side, which reports what the octets carry, and is
192    /// always outside one on the compose side, which refuses to
193    /// transmit it. No catalogued row exercises that asymmetry - no
194    /// catalogued binary32 row records a range at all. On the boolean and
195    /// enumerated carriers neither direction reads the range: a
196    /// boolean's 2 states and a declared code table are those fields'
197    /// whole domains. Generation still reads it there, checking every
198    /// code the declared table defines against it; how many rows
199    /// record both is a measured count, not a figure repeated here.
200    Declared {
201        /// The declared minimum.
202        min: Decimal,
203        /// The declared maximum.
204        max: Decimal,
205    },
206    /// The reviewed data records no range for this field. A recorded
207    /// absence, not zero - and, like a recorded range, a fact about
208    /// the record rather than a further statement about what the
209    /// source prints.
210    NotDeclared,
211}
212
213/// A declared resolution.
214#[derive(Clone, Copy, PartialEq, Eq, Debug)]
215pub struct Resolution {
216    /// The step value.
217    pub value: Decimal,
218    /// Whether the recorded step is an approximation. Set where the
219    /// source prints the figure as approximate; clear where it prints
220    /// an exact one, and clear where the figure is the reading this
221    /// catalogue settled on rather than a printed figure.
222    pub approximate: bool,
223}
224
225/// The kind of special encoded value, as the source distinguishes them.
226#[derive(Clone, Copy, PartialEq, Eq, Debug)]
227pub enum SpecialKind {
228    /// The encoded value marks the carried data as invalid.
229    ValueState,
230    /// The encoded value directs the receiver's handling of the
231    /// message; the source-assigned disposition rides the kind, so a
232    /// receiver-disposition special without one cannot exist.
233    ReceiverDisposition(ReceiveDisposition),
234    /// The value's meaning depends on a sibling validity flag.
235    FlagConditioned,
236}
237
238/// A receiver disposition the source assigns to declared data.
239///
240/// The vocabulary carries exactly the dispositions catalogued rows
241/// declare and interpretation returns. Rejection needs no declared
242/// data: an undeclared code or pattern already fails closed. The
243/// source assigns further dispositions at other layers (refuse the
244/// read, truncate, neglect the frame); those are not catalogue data
245/// and no row can declare them. A population row declaring a
246/// disposition outside this vocabulary is a source question that
247/// extends it through review.
248#[derive(Clone, Copy, PartialEq, Eq, Debug)]
249pub enum ReceiveDisposition {
250    /// Ignore the whole message.
251    IgnoreMessage,
252    /// Accept the message without acting on it.
253    AcceptWithoutAction,
254}
255
256/// A special encoded value the reviewed data records beside the
257/// field's ordinary domain: singled out by the source where the source
258/// singles one out, and the reading this catalogue settled on where a
259/// printed reference supplies it instead.
260///
261/// The kind carries the whole behavioral contract; any human-readable
262/// designation is a projection concern derived from it.
263#[derive(Debug)]
264pub struct SpecialValue {
265    /// The encoded value, as transmitted.
266    pub raw: u32,
267    /// Which kind of special value this is.
268    pub kind: SpecialKind,
269}
270
271/// One defined code in an enumeration code table.
272#[derive(Debug)]
273pub struct CodeEntry {
274    /// The encoded code.
275    pub code: u16,
276    /// A short human-readable name for the code. It is not part of the
277    /// encoding: match on `code`, never on this text, because a label may
278    /// be reworded between releases without the code's meaning changing.
279    ///
280    /// Some enumerations define their extent by a range rather than by
281    /// naming every member - 10.001's `Day` is one - and the labels
282    /// between the named anchors are derived from that range.
283    pub label: &'static str,
284}
285
286/// A band of codes that share one collective meaning.
287#[derive(Debug)]
288pub struct CodeBand {
289    /// The first code of the band, inclusive.
290    pub start: u16,
291    /// The last code of the band, inclusive.
292    pub end: u16,
293    /// A short human-readable name for what the whole band means. It is
294    /// not part of the encoding: match on the range, never on this text.
295    pub label: &'static str,
296    /// The receiver disposition the source assigns to the band, where
297    /// it states one. What silence does depends on the carrier the band
298    /// sits beside: on an enumerated carrier an undeclared band's codes
299    /// fail closed, while on an unsigned carrier the band only labels
300    /// values the declared range already carries and nothing refuses
301    /// (3.007's Step band is the precedent for the second reading).
302    pub disposition: Option<ReceiveDisposition>,
303}
304
305/// An enumeration code table: a set of defined codes, possibly sparse,
306/// with source-stated bands over the remaining code space.
307///
308/// A code domain is a set, never a range with a default.
309#[derive(Debug)]
310pub struct CodeTable {
311    /// The defined codes.
312    pub entries: &'static [CodeEntry],
313    /// Source-stated bands over the remaining code space.
314    pub bands: &'static [CodeBand],
315}
316
317/// A named bit in a bit-set layout.
318#[derive(Debug)]
319pub struct NamedBit {
320    /// The bit position, 0 = least significant.
321    pub bit: u8,
322    /// A short name for the bit. It is not part of the encoding: match
323    /// on `bit`, never on this text.
324    pub name: &'static str,
325    /// The encoded value at which the named condition holds. The source
326    /// states inverted polarities explicitly, so this is per-declaration
327    /// data rather than a convention.
328    pub active_value: u8,
329    /// A declared reference supplying this bit's interpretation
330    /// contract, where the source states one. 2.xxx's v bit prints no
331    /// encoding of its own - only the delegation to its 1.xxx twin -
332    /// so the reference is the bit's whole stated contract, exactly
333    /// as a scalar's [`ScalarField::semantics`] is. A records
334    /// statement: the generated contract derives nothing from the
335    /// target at runtime.
336    pub semantics: Option<RefTarget>,
337}
338
339/// How a receiver treats declared reserved or unused bits.
340///
341/// Per-declaration data: the source obliges receivers to check some
342/// reserved fields to be zero and to ignore other unused fields.
343#[derive(Clone, Copy, PartialEq, Eq, Debug)]
344pub enum ReservedBitRule {
345    /// Receivers check the bits are zero.
346    CheckZero,
347    /// Receivers ignore the bits.
348    Ignore,
349}
350
351/// A bit-set layout with named positions and reserved bits.
352///
353/// Bit positions are anchored to the bit-group field node that
354/// references this descriptor, not to the whole payload: bit 0 is the
355/// group's last transmitted bit, so a group of N bits spans positions
356/// N-1..0 whether or not it ends on an octet boundary. The group's
357/// placement in the payload is the field tree's data; this descriptor
358/// never restates it.
359#[derive(Debug)]
360pub struct BitSet {
361    /// The named bits.
362    pub named: &'static [NamedBit],
363    /// Mask of bits the sender transmits as zero.
364    pub reserved_mask: u32,
365    /// The receiver rule for the reserved bits.
366    pub reserved_rule: ReservedBitRule,
367}
368
369/// The numeric carrier of a scalar field, as the notation symbol
370/// declares it.
371#[derive(Clone, Copy, PartialEq, Eq, Debug)]
372pub enum Carrier {
373    /// A single-bit boolean (`B1` as a lone value field).
374    Boolean,
375    /// An enumeration code (`N`); the field carries a code table.
376    Enumerated,
377    /// An unsigned integer (`U`).
378    Unsigned,
379    /// A two's complement signed integer (`V`).
380    Signed,
381    /// The KNX 2-octet float (`F16`).
382    Float16,
383    /// IEEE binary32 (`F32`).
384    Float32,
385}
386
387impl Carrier {
388    /// All `Carrier` variants in declaration order; the canonical
389    /// single source for a consumer that must answer for every one.
390    pub const ALL: [Carrier; 6] = [
391        Carrier::Boolean,
392        Carrier::Enumerated,
393        Carrier::Unsigned,
394        Carrier::Signed,
395        Carrier::Float16,
396        Carrier::Float32,
397    ];
398}
399
400/// A declared value-to-field mapping of an integer scalar field.
401///
402/// The DPT specification form's encoding conventions distinguish how a
403/// field value relates to the value it carries, and the relationship is
404/// a per-field fact: one subtype can hold fields of different mapping
405/// kinds. The reviewed data declares the kind explicitly and generation
406/// fails when a declared kind contradicts the field's printed
407/// resolution or range, so no consumer ever re-derives it.
408///
409/// The declared range's domain follows the kind: identity, offset,
410/// century and interval-exponential ranges bound the FIELD value
411/// (19.001's Year prints 0..255), by this catalogue's own convention
412/// for all 4; resolution and linear ranges bound the mapped VALUE
413/// (7.003's 0..655350 ms, 5.001's 0..100 percent).
414#[derive(Clone, Copy, PartialEq, Eq, Debug)]
415pub enum Mapping {
416    /// The field value is the value, at a resolution of 1 in the unit
417    /// the source declares (or dimensionless).
418    Identity,
419    /// The field's encodable span maps onto the declared range by an
420    /// exact ratio; the printed resolution is approximate (5.001:
421    /// 100 percent encodes as 255, so the ratio is 100/255).
422    Linear,
423    /// The value is the field times the declared exact resolution
424    /// factor, in the unit the source declares (7.003: field x 10 ms); the encode
425    /// direction divides, and only exact multiples of the factor are
426    /// encodable. The factor is the field's declared resolution -
427    /// guaranteed present and exact by generation, never restated here.
428    Resolution,
429    /// The value is the field plus a declared base (19.001: the year is
430    /// the field plus 1900).
431    Offset {
432        /// The base added to the field value.
433        base: i32,
434    },
435    /// The date century rule (11.001): field values 90 through 99 read
436    /// as the years 1990 through 1999, values 0 through 89 as 2000
437    /// through 2089.
438    Century,
439    /// The field's value is the step code itself; the code additionally
440    /// declares an interval count of 2 to the power of the code minus 1
441    /// (3.007: step codes 1 through 7 subdivide the controlled range
442    /// into 1 through 64 intervals). Access reads and writes the code;
443    /// the recorded exponential relation is data for a consumer that
444    /// needs interval counts, and none exists yet.
445    IntervalExponential,
446}
447
448/// A scalar field's declarative record.
449///
450/// Resolution, range, codes and specials attach here because they are
451/// per-field facts: multi-field rows (251.600's 4 colour components,
452/// 204.001's main value) are only expressible at this level, and a
453/// per-field absence is recorded rather than erased by a row-level
454/// hoist. Each is a restatement where the source is plain and the
455/// reading this catalogue settled on where it is not.
456#[derive(Debug)]
457pub struct ScalarField {
458    /// The carrier the notation symbol declares.
459    pub carrier: Carrier,
460    /// The field width in bits.
461    pub bits: u8,
462    /// A declared reference supplying this field's interpretation
463    /// contract, where the source states one.
464    pub semantics: Option<RefTarget>,
465    /// The resolution the reviewed data records for this field, where
466    /// it records one: a restatement where the source prints a figure,
467    /// and the reading this catalogue settled on where it does not.
468    pub resolution: Option<Resolution>,
469    /// The declared value-to-field mapping. Present on every unsigned
470    /// and signed integer carrier; absent on boolean, enumerated and
471    /// float carriers, whose relationship is owned by the code table or
472    /// the float format itself.
473    pub mapping: Option<Mapping>,
474    /// The declared numeric range.
475    pub range: DeclaredRange,
476    /// The enumeration code table, for fields that declare one.
477    pub codes: Option<&'static CodeTable>,
478    /// Special encoded values of this field.
479    pub specials: &'static [SpecialValue],
480    /// The Z8 main-value designation: true on the one top-level
481    /// scalar the Z8 control octet couples with, on a row that
482    /// declares MORE than one top-level scalar (the source designates
483    /// the field per row). A single-scalar
484    /// Z8 row's sole scalar is the main by construction and carries
485    /// no designation; every non-Z8 field is false.
486    pub main: bool,
487}
488
489/// The shape of one field in a subtype's declared encoding.
490///
491/// Recursion appears exactly where the source structure requires it: a
492/// repeated group and an embedded record contain fields; everything
493/// else is flat. Print-level octet groupings are not nesting and are
494/// flattened.
495#[derive(Debug)]
496pub enum FieldShape {
497    /// A value-carrying scalar.
498    Scalar(ScalarField),
499    /// Reserved bits: a declared `r` field, or unused carrier bits of
500    /// the declared width that the format string leaves uncovered - two
501    /// categories the source equates while assigning them different
502    /// receiver rules per declaration. Senders transmit zero either way.
503    Reserved {
504        /// The reserved width in bits.
505        bits: u8,
506        /// The receiver rule; check-zero where the source is silent.
507        rule: ReservedBitRule,
508    },
509    /// A named-bit group. Positions in the descriptor anchor to this
510    /// group.
511    BitGroup {
512        /// The group width in bits.
513        bits: u8,
514        /// The bit-set descriptor.
515        set: &'static BitSet,
516    },
517    /// A fixed-width character array with 8-bit elements.
518    CharArray {
519        /// The element count.
520        count: u16,
521        /// The declared fill value for unused trailing positions, where
522        /// the source declares one.
523        pad: Option<u8>,
524        /// The declared character contract, as a schema token.
525        repertoire: TextRepertoire,
526    },
527    /// A fixed-width container holding a terminator-delimited character
528    /// sequence: at most `octets - 1` content octets, then the mandatory
529    /// terminator, then zero fill.
530    CharTerminated {
531        /// The container width in octets, the terminator included.
532        octets: u16,
533        /// The declared terminator value.
534        terminator: u8,
535        /// The declared character contract, as a schema token.
536        repertoire: TextRepertoire,
537    },
538    /// A variable-length character sequence with 8-bit elements and a
539    /// terminator that is part of the format.
540    CharVariable {
541        /// The declared terminator value.
542        terminator: u8,
543        /// The declared character contract, as a schema token.
544        repertoire: TextRepertoire,
545    },
546    /// The service-selected Z8 status/command octet. A row containing
547    /// this field requires service-selected context; a standalone
548    /// status-only Z8 layout (21.001) is a bit group instead, which is
549    /// per-subtype data.
550    Z8Control,
551    /// An embedded record declared same-as another subtype. The fields
552    /// are this row's own declared instantiation; per-subtype
553    /// divergences in the embedded layout are data, never options.
554    Embed {
555        /// The subtype whose record is embedded.
556        of: DptId,
557        /// The embedded fields, in transmission order.
558        fields: &'static [FieldNode],
559    },
560    /// A field group repeated a declared number of times.
561    Repeat {
562        /// The declared repetition count.
563        count: u16,
564        /// The declared base contract of the group, where the source
565        /// states one.
566        base: Option<RefTarget>,
567        /// The group fields, in transmission order.
568        group: &'static [FieldNode],
569    },
570}
571
572/// One field of a subtype's declared encoding, in transmission order
573/// (most significant, first transmitted, first).
574#[derive(Debug)]
575pub struct FieldNode {
576    /// A short name for the field. It identifies the field within its
577    /// subtype and is not part of the encoding.
578    pub name: &'static str,
579    /// The field's shape and declared facts.
580    pub shape: FieldShape,
581}
582
583/// What the gating flag's active state declares about its target.
584///
585/// The source states both polarities: 251.600's mask bits and 235.001's
586/// validity bits declare the component valid, while 19.001's "No ..."
587/// flags declare the gated fields not valid. The sense is recorded per
588/// edge because it cannot be derived from a bit name.
589#[derive(Clone, Copy, PartialEq, Eq, Debug)]
590pub enum ValiditySense {
591    /// The target is valid exactly when the flag is in its active
592    /// state.
593    AssertsValid,
594    /// The target is not valid when the flag is in its active state.
595    AssertsInvalid,
596}
597
598/// A validity association the reviewed data records: a named flag bit
599/// gates a field (or another named bit) of the same row.
600///
601/// Source-declared where the defining clause declares one, and the
602/// reading this catalogue settled on where the clause reaches its flags
603/// through a reference instead. The reviewed data records no per-edge
604/// provenance column; the kind is assigned per ROW, in the 2 paragraphs
605/// below, and every per-row count is pinned by a test - so the totals
606/// live where they are measured, not here - a moving figure restated
607/// in prose goes stale the commit the set grows. Per row, the counts
608/// below are fixed facts of each clause.
609///
610/// The clause-declared rows: 19.001 carries 8, 251.600 carries 4,
611/// and 235.001 and 235.002 carry 2 each - each row's own field table
612/// names the gating flag, its polarity and what it gates. The
613/// structured compound rows whose own bit tables print a validity
614/// declaration are clause-declared the same way, 1 edge per printed
615/// declaration - a growing membership, so it is not enumerated here;
616/// the per-row counts are pinned by tests.
617///
618/// The remainder rest on a reference. 265.001 and 247.600 carry 8 each,
619/// under 2 same-shaped readings: 265.001 declares no validity association of
620/// its own at all: its clause reaches 19.001 through a same-as cell in
621/// the column that states how a field's value relates to its coded
622/// value, and whether such a cell carries the named row's whole
623/// definition or only its field encodings is a question the source
624/// leaves open. Answering it broadly - so that the edges come across
625/// with the layout - is a decision recorded for this catalogue rather
626/// than a printed fact. 247.600's clause hands its octets 12 to 5 -
627/// the whole date-and-time block, flags included - to 19.001 through
628/// one see-cell, and its edges come across under the same broad
629/// reading (its flag octets re-package 19.001's B16 as B8 plus B1r7,
630/// so the layout could not come across as an instantiation - the
631/// contract did). The DateTime-carrying register variants take the
632/// SAME same-as reading for their date-and-time portion: each
633/// carries 19.001's 8 flag edges through its embedded record,
634/// exactly as 265.001 does. And every register-family row (277.1200,
635/// its instantiation-count variants, and the DateTime variants
636/// alike) carries its `E` and `T` pair under a qualified delegation:
637/// the
638/// family clause prints the validity octet's bit geometry and no bit
639/// label, polarity or per-bit meaning at all, handing the repeated
640/// record to 235.001 under a qualifier whose extension is printed as
641/// prose with no encoding - so the pair is read off the delegation
642/// target, and whether the referenced meanings survive the qualifier
643/// stays an open reading. A DateTime register variant therefore
644/// carries both classes: the 8 date-and-time edges and the pair.
645///
646/// The flag's active state is its declared `active_value`; the sense
647/// says what that state means for the target. The value-level
648/// consequences of an invalid target are semantic-layer concerns.
649/// Within a repeated group, the association binds per instance - and
650/// that is why both ends live in one scope: an association binding per
651/// instance has no reading where one end sits outside the group, so an
652/// edge whose flag and target are declared on opposite sides of a
653/// repeated group is refused where the row is bound rather than
654/// resolved per payload.
655#[derive(Debug)]
656pub struct ValidityEdge {
657    /// The gating flag: a named bit in one of the row's bit groups.
658    pub flag: &'static str,
659    /// The gated target: a field name, or a named bit of the same row.
660    pub target: &'static str,
661    /// What the flag's active state declares about the target.
662    pub sense: ValiditySense,
663}
664
665/// A source-declared relation between fields of one row.
666///
667/// The vocabulary is closed over the relations the reviewed rows
668/// declare; a new source-declared relation form extends it through
669/// review, never through a generic escape hatch. A rule's operands
670/// all live in one scope - the row's top scope, or one repeated
671/// group, where the relation binds every instance independently
672/// (instance i's trigger obliges instance i's targets alone), the
673/// same per-instance reading a [`ValidityEdge`] takes.
674#[derive(Debug)]
675pub enum CrossFieldRule {
676    /// When the trigger field carries the stated value, every target
677    /// field shall be zero (19.001: hour 24 obliges zero minutes and
678    /// seconds). A violating message gets the stated receiver
679    /// disposition; an encoder rejects what it cannot ignore.
680    ZeroWhen {
681        /// The field whose value conditions the rule.
682        trigger: &'static str,
683        /// The trigger's raw field value that activates the rule.
684        value: u64,
685        /// The fields obliged to be zero while the rule is active.
686        targets: &'static [&'static str],
687        /// The receiver disposition the source assigns to a violation.
688        disposition: ReceiveDisposition,
689    },
690}
691
692/// The target of a source-declared reference.
693///
694/// The source references both specific subtypes and whole families; a
695/// family-level reference is not collapsed onto an arbitrary subtype.
696#[derive(Clone, Copy, PartialEq, Eq, Debug)]
697pub enum RefTarget {
698    /// A specific subtype.
699    Subtype(DptId),
700    /// A whole main-number family.
701    Family(u16),
702}
703
704/// Emits [`TextRepertoire`] and its `ALL` census from ONE variant
705/// list, so a variant cannot exist outside the census: the
706/// variant-live-census-short state is not expressible, on stable and
707/// on this workspace's MSRV alike. A `macro_rules!` list, following
708/// the tree's own precedent, not a derive.
709macro_rules! text_repertoires {
710    (
711        $(#[$enum_doc:meta])*
712        pub enum TextRepertoire { $( $(#[$member_doc:meta])* $member:ident, )+ }
713    ) => {
714        $(#[$enum_doc])*
715        #[derive(Clone, Copy, PartialEq, Eq, Debug)]
716        pub enum TextRepertoire { $( $(#[$member_doc])* $member, )+ }
717
718        impl TextRepertoire {
719            /// The census the vocabulary tests loop over, emitted from
720            /// the same variant list as the enum itself - a member
721            /// cannot exist outside it by construction. The guard test
722            /// beside `carrier_all` still pins the census's SIZE, so a
723            /// vocabulary change stays a loudly reviewed number.
724            pub const ALL: [TextRepertoire; text_repertoires!(@count $($member)+)] =
725                [ $(TextRepertoire::$member,)+ ];
726        }
727    };
728    (@count $head:ident $($rest:ident)*) => { 1usize + text_repertoires!(@count $($rest)*) };
729    (@count) => { 0usize };
730}
731
732text_repertoires! {
733    /// The declared character contract of a character field, carried as a
734    /// datum of the schema rather than decided by a reader.
735    ///
736    /// The reviewed data declares a repertoire by reference, or - on
737    /// the 3 defining rows 4.001, 4.002 and 28.001 - by the recorded
738    /// `self` token; the generator maps either declaration to this
739    /// token when it emits the field, so the vocabulary is closed
740    /// exactly once - here. A
741    /// reader matching on this enum answers for every member or does not
742    /// compile, which is what makes a half-added repertoire unbuildable
743    /// rather than silently withdrawn: while the reference was
744    /// re-interpreted in a hand-written library arm instead, a repertoire
745    /// the generator admitted and the arm did not know registered for
746    /// encode, published 0 composable items, and sealed a zeroed payload
747    /// with no refusal at any gate.
748    pub enum TextRepertoire {
749        /// The ASCII repertoire, referenced as DPT 4.001: 0x00..=0x7F,
750        /// the most significant bit always 0 per the defining clause.
751        Ascii,
752        /// The ISO-8859-1 octet repertoire, referenced as DPT 4.002.
753        Iso8859_1,
754        /// The UTF-8 repertoire, referenced as DPT 28.001.
755        Utf8,
756    }
757}
758
759/// Whether a Z8 command's main value carries data.
760///
761/// The command table's own column: under some commands the main value
762/// is valid, under others it is don't care (the source's term) - the
763/// receiver reads no data from it.
764#[derive(Clone, Copy, PartialEq, Eq, Debug)]
765pub enum Z8MainValue {
766    /// The main value field carries the datapoint value.
767    Valid,
768    /// The main value field carries no data under this command.
769    DontCare,
770}
771
772/// One command of the Z8 command enumeration.
773#[derive(Debug)]
774pub struct Z8Command {
775    /// The encoded command value.
776    pub code: u8,
777    /// A short name for the command. It is not part of the encoding:
778    /// match on `code`, never on this text.
779    pub label: &'static str,
780    /// Whether the main value carries data under this command.
781    pub main_value: Z8MainValue,
782}
783
784/// The standardised Z8 status/command contract, declared once.
785///
786/// A general clause defines the Z8 octet for every Z8-bearing subtype:
787/// read as STATUS it is the DPT_StatusGen bit set, read as COMMAND it
788/// is this command enumeration, and the reading is selected by the
789/// Application Layer service, never by payload content. Under a set
790/// Fault status bit the main value field carries failure information
791/// whose codes this descriptor also names.
792#[derive(Debug)]
793pub struct Z8Descriptor {
794    /// The status-reading bit set (the DPT_StatusGen layout).
795    pub status: &'static BitSet,
796    /// The command enumeration, sorted by code.
797    pub commands: &'static [Z8Command],
798    /// The failure-information codes the main value carries when the
799    /// status Fault bit is set.
800    pub fault_info: &'static CodeTable,
801}
802
803/// The operation context a subtype's interpretation requires.
804#[derive(Clone, Copy, PartialEq, Eq, Debug)]
805pub enum ContextRequirement {
806    /// No context beyond the payload.
807    None,
808    /// The Application Layer service selects the reading; there is no
809    /// payload-derivable answer.
810    ServiceSelected,
811}
812
813/// The transport restriction the source states for this type.
814///
815/// The variants rest on different source statements; they are not
816/// degrees of one rule, and a refusal names the rule the source
817/// actually states.
818#[derive(Clone, Copy, PartialEq, Eq, Debug)]
819pub enum GroupTransport {
820    /// No source-stated restriction.
821    Eligible,
822    /// The source declares the row not available for standard group
823    /// communication: for Z8-bearing rows by constraint, and for the
824    /// structured HVAC subtype range by the identifier-allocation
825    /// table's LTE-only cell.
826    Forbidden,
827    /// The source does not allow this DPT for runtime communication and
828    /// confines it to parameters and diagnostic data, or to what a
829    /// Functional Block specification designates as such.
830    ParametersAndDiagnosticsOnly,
831}
832
833/// One catalogued datapoint subtype: its declarative record - a
834/// restatement where the KNX source is plain, and the reading this
835/// catalogue settled on where the source is silent, ambiguous or prints
836/// figures that disagree.
837///
838/// Per-field facts - resolutions, ranges, code tables, named bits,
839/// specials, reserved declarations - live on the field nodes that own
840/// them.
841#[derive(Debug)]
842pub struct SubtypeRow {
843    /// The identifier.
844    pub id: DptId,
845    /// The official name.
846    pub name: &'static str,
847    /// The width contract, as the source declares it. The generator
848    /// checks it equals the width the field tree computes.
849    pub width: PayloadWidth,
850    /// The declared field decomposition, in transmission order.
851    pub fields: &'static [FieldNode],
852    /// The validity associations the reviewed data records between the
853    /// row's flags and its fields: source-declared where the clause
854    /// declares them, and read off a reference where the clause reaches
855    /// its flags through one instead - the 265.001 same-as class and
856    /// the register family's qualified delegation; every count is
857    /// pinned by a test.
858    pub validity: &'static [ValidityEdge],
859    /// Source-declared relations between the row's fields.
860    pub cross_field: &'static [CrossFieldRule],
861    /// The operation context interpretation requires.
862    pub context: ContextRequirement,
863    /// Group-transport eligibility as the source states it.
864    pub group_transport: GroupTransport,
865}