Skip to content

Commit cf5e511

Browse files
committed
rewrite Parameter model with explicit 717/767 separation, add typed factory constructors for ARINC 717 and ARINC 767 parameters
1 parent 5f2e12a commit cf5e511

1 file changed

Lines changed: 107 additions & 6 deletions

File tree

pyarinc/models/parameter.py

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,30 @@ class Parameter:
2525
2. ARINC 717 (word-based indexing):
2626
- subframe, word, bit_offset: locate parameter in frame structure
2727
- decode_from_frame(frame_bytes, ...) -> (decoded_value, valid)
28+
29+
Note on start_bit:
30+
start_bit is intentionally Optional. 717-style parameters should leave
31+
it as None and rely on subframe/word/bit_offset; decode_from_frame()
32+
only falls back to computing the position from those fields when
33+
start_bit is None. Setting start_bit=0 on a 717 parameter (instead of
34+
leaving it None) silently forces decoding from bit 0 of the frame
35+
regardless of subframe/word/bit_offset -- do not do that.
2836
"""
2937

3038
name: str
3139
"""Parameter identifier/name."""
3240

33-
start_bit: int
34-
"""Absolute bit position (0-based, MSB-first) for ARINC 767."""
35-
3641
bit_length: int
3742
"""Number of bits occupied by parameter (1-32)."""
3843

3944
data_type: str
4045
"""Data type: 'BNR', 'BCD', 'DISCRETE', 'CHAR', 'ASCII', 'UTC', 'PACKED', 'COB'."""
4146

47+
start_bit: int | None = None
48+
"""Absolute bit position (0-based, MSB-first). Required for ARINC 767
49+
(decode_raw_from_bytes). For ARINC 717, leave as None so
50+
decode_from_frame() computes the position from subframe/word/bit_offset."""
51+
4252
scale: float | None = None
4353
"""Scale factor for BNR decoding (multiplier)."""
4454

@@ -73,10 +83,85 @@ class Parameter:
7383

7484
cob_formula: str | None = None
7585
"""Optional formula for COB (Computed On Board) parameters.
76-
86+
7787
Evaluated with restricted namespace: {"raw": <bit_value>, "scale": <scale>, "offset": <offset>}
7888
Example: "raw * 0.00390625" for Mach computation.
7989
"""
90+
# ----------------------------------------------------------------------
91+
# Factory constructors
92+
# ----------------------------------------------------------------------
93+
94+
@classmethod
95+
def from_717(
96+
cls,
97+
name: str,
98+
bit_length: int,
99+
data_type: str,
100+
*,
101+
subframe: int,
102+
word: int,
103+
bit_offset: int,
104+
rate: float = 1.0,
105+
scale: float | None = None,
106+
offset: float | None = None,
107+
signed: bool = False,
108+
superframe: int | None = None,
109+
superframe_group_count: int = 4,
110+
) -> "Parameter":
111+
"""
112+
Construct an ARINC 717 parameter.
113+
114+
start_bit is intentionally left as None so decode_from_frame()
115+
computes the absolute bit position from subframe/word/bit_offset.
116+
"""
117+
return cls(
118+
name=name,
119+
bit_length=bit_length,
120+
data_type=data_type,
121+
start_bit=None, # critical: 717 parameters must NOT set start_bit
122+
scale=scale,
123+
offset=offset,
124+
signed=signed,
125+
rate=rate,
126+
subframe=subframe,
127+
word=word,
128+
bit_offset=bit_offset,
129+
superframe=superframe,
130+
superframe_group_count=superframe_group_count,
131+
)
132+
133+
@classmethod
134+
def from_767(
135+
cls,
136+
name: str,
137+
bit_length: int,
138+
data_type: str,
139+
*,
140+
start_bit: int,
141+
frame_id_767: int | None = None,
142+
scale: float | None = None,
143+
offset: float | None = None,
144+
signed: bool = False,
145+
rate: float = 1.0,
146+
cob_formula: str | None = None,
147+
) -> "Parameter":
148+
"""
149+
Construct an ARINC 767 parameter.
150+
151+
Requires an absolute start_bit (0-based MSB-first).
152+
"""
153+
return cls(
154+
name=name,
155+
bit_length=bit_length,
156+
data_type=data_type,
157+
start_bit=start_bit,
158+
frame_id_767=frame_id_767,
159+
scale=scale,
160+
offset=offset,
161+
signed=signed,
162+
rate=rate,
163+
cob_formula=cob_formula,
164+
)
80165

81166
def decode_raw_from_bytes(self, data: bytes) -> Any:
82167
"""Decode from frame data using absolute start_bit (ARINC 767 style).
@@ -89,7 +174,18 @@ def decode_raw_from_bytes(self, data: bytes) -> Any:
89174
90175
Returns:
91176
Decoded value (float, int, str, bool, etc. depending on data_type)
177+
178+
Raises:
179+
ValueError: if start_bit is not set, or extraction is out of bounds.
92180
"""
181+
if self.start_bit is None:
182+
raise ValueError(
183+
f"Parameter {self.name}: start_bit is not set; "
184+
"decode_raw_from_bytes() requires an absolute start_bit "
185+
"(ARINC 767 style). Use decode_from_frame() for "
186+
"subframe/word/bit_offset-based (ARINC 717) parameters."
187+
)
188+
93189
total_bits = len(data) * 8
94190
if self.start_bit < 0 or (self.start_bit + self.bit_length) > total_bits:
95191
raise ValueError(
@@ -290,12 +386,13 @@ def _decode_char(self, raw_bits: int) -> str:
290386
def _decode_utc(self, raw_bits: int) -> str:
291387
"""Decode UTC time format.
292388
293-
UTC is typically encoded as BCD HH:MM:SS (6 decimal digits).
389+
UTC is typically encoded as BCD HH:MM:SS (6 decimal digits) packed
390+
directly into nibbles, e.g. 23:59:59 -> raw_bits == 0x235959.
294391
295392
Returns:
296393
str in format "HH:MM:SS"
297394
"""
298-
s = str(self._bcd_from_int(raw_bits)).rjust(6, "0")
395+
s = self._decode_bcd(raw_bits).rjust(6, "0")
299396
hh = int(s[0:2])
300397
mm = int(s[2:4])
301398
ss = int(s[4:6])
@@ -322,6 +419,10 @@ def _decode_cob(self, raw_bits: int) -> Any:
322419
323420
Safety:
324421
Uses restricted eval() with no __builtins__ access.
422+
NOTE: this is not a true sandbox -- attribute-chain sandbox
423+
escapes are possible in plain Python eval(). Do not load
424+
cob_formula values from untrusted sources without further
425+
hardening (e.g. an AST allowlist).
325426
"""
326427
if self.cob_formula:
327428
ctx = {

0 commit comments

Comments
 (0)