aero-agent-roles 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +18 -0
- package/README.md +49 -0
- package/bin/aero-agent-roles.js +127 -0
- package/lib/catalog.js +97 -0
- package/lib/install.js +48 -0
- package/lib/mcp.js +199 -0
- package/manifest.json +516 -0
- package/package.json +44 -0
- package/roles/aerodynamics-engineer/ROLE.md +153 -0
- package/roles/aerodynamics-engineer/SOURCES.md +12 -0
- package/roles/aerodynamics-engineer/cli.py +143 -0
- package/roles/aerodynamics-engineer/core/aerodynamics_core.py +1218 -0
- package/roles/aerodynamics-engineer/templates/aero-design-report-template.md +72 -0
- package/roles/aerodynamics-engineer/tests/test_aerodynamics_core.py +227 -0
- package/roles/aerodynamics-engineer/tests/test_role_aerodynamics.py +94 -0
- package/roles/aircraft-design-engineer/ROLE.md +138 -0
- package/roles/aircraft-design-engineer/SOURCES.md +12 -0
- package/roles/aircraft-design-engineer/cli.py +152 -0
- package/roles/aircraft-design-engineer/core/aircraft_design_core.py +1278 -0
- package/roles/aircraft-design-engineer/templates/concept-design-template.md +99 -0
- package/roles/aircraft-design-engineer/tests/test_aircraft_design_core.py +269 -0
- package/roles/aircraft-design-engineer/tests/test_role_aircraft_design_engineer.py +106 -0
- package/roles/airworthiness-compliance-engineer/ROLE.md +122 -0
- package/roles/airworthiness-compliance-engineer/SOURCES.md +16 -0
- package/roles/airworthiness-compliance-engineer/cli.py +146 -0
- package/roles/airworthiness-compliance-engineer/core/airworthiness_core.py +894 -0
- package/roles/airworthiness-compliance-engineer/templates/compliance-matrix-template.md +46 -0
- package/roles/airworthiness-compliance-engineer/tests/test_airworthiness_core.py +224 -0
- package/roles/airworthiness-compliance-engineer/tests/test_role_airworthiness.py +85 -0
- package/roles/as9100-quality-auditor/ROLE.md +128 -0
- package/roles/as9100-quality-auditor/SOURCES.md +16 -0
- package/roles/as9100-quality-auditor/cli.py +136 -0
- package/roles/as9100-quality-auditor/core/as9100_core.py +857 -0
- package/roles/as9100-quality-auditor/templates/findings-report-template.md +119 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_cli_profile.py +105 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_core.py +204 -0
- package/roles/as9100-quality-auditor/tests/test_role_as9100.py +90 -0
- package/roles/data-bus-avionics-engineer/ROLE.md +146 -0
- package/roles/data-bus-avionics-engineer/SOURCES.md +23 -0
- package/roles/data-bus-avionics-engineer/cli.py +311 -0
- package/roles/data-bus-avionics-engineer/core/data_bus_avionics_core.py +1121 -0
- package/roles/data-bus-avionics-engineer/templates/bus-assessment-template.md +134 -0
- package/roles/data-bus-avionics-engineer/tests/test_bundle_protocol.py +137 -0
- package/roles/data-bus-avionics-engineer/tests/test_data_bus_avionics_core.py +261 -0
- package/roles/data-bus-avionics-engineer/tests/test_role_data_bus_avionics_engineer.py +121 -0
- package/roles/do160-environmental-engineer/ROLE.md +136 -0
- package/roles/do160-environmental-engineer/SOURCES.md +30 -0
- package/roles/do160-environmental-engineer/cli.py +147 -0
- package/roles/do160-environmental-engineer/core/do160_environmental_core.py +1246 -0
- package/roles/do160-environmental-engineer/templates/environmental-qualification-plan-template.md +114 -0
- package/roles/do160-environmental-engineer/tests/test_do160_environmental_core.py +312 -0
- package/roles/do160-environmental-engineer/tests/test_role_do160_environmental_engineer.py +281 -0
- package/roles/do178c-cert-engineer/ROLE.md +120 -0
- package/roles/do178c-cert-engineer/SOURCES.md +16 -0
- package/roles/do178c-cert-engineer/cli.py +129 -0
- package/roles/do178c-cert-engineer/core/do178c_core.py +411 -0
- package/roles/do178c-cert-engineer/templates/psac-template.md +74 -0
- package/roles/do178c-cert-engineer/tests/test_do178c_cert_engineer_core.py +91 -0
- package/roles/do178c-cert-engineer/tests/test_role_do178c.py +109 -0
- package/roles/do254-hardware-engineer/ROLE.md +127 -0
- package/roles/do254-hardware-engineer/SOURCES.md +19 -0
- package/roles/do254-hardware-engineer/cli.py +308 -0
- package/roles/do254-hardware-engineer/core/do254_hardware_core.py +761 -0
- package/roles/do254-hardware-engineer/templates/phac-template.md +87 -0
- package/roles/do254-hardware-engineer/tests/test_do254_bundle_profile.py +152 -0
- package/roles/do254-hardware-engineer/tests/test_do254_hardware_core.py +200 -0
- package/roles/do254-hardware-engineer/tests/test_role_do254_hardware.py +108 -0
- package/roles/engineering-analysis-engineer/ROLE.md +154 -0
- package/roles/engineering-analysis-engineer/SOURCES.md +15 -0
- package/roles/engineering-analysis-engineer/cli.py +151 -0
- package/roles/engineering-analysis-engineer/core/engineering_analysis_engineer_core.py +866 -0
- package/roles/engineering-analysis-engineer/templates/analysis-memo-template.md +73 -0
- package/roles/engineering-analysis-engineer/tests/test_engineering_analysis_engineer_core.py +367 -0
- package/roles/engineering-analysis-engineer/tests/test_role_engineering_analysis_engineer.py +113 -0
- package/roles/flight-management-engineer/ROLE.md +160 -0
- package/roles/flight-management-engineer/SOURCES.md +30 -0
- package/roles/flight-management-engineer/cli.py +258 -0
- package/roles/flight-management-engineer/core/flight_management_core.py +1473 -0
- package/roles/flight-management-engineer/templates/route-assessment-template.md +94 -0
- package/roles/flight-management-engineer/tests/test_flight_management_core.py +437 -0
- package/roles/flight-management-engineer/tests/test_role_flight_management_engineer.py +123 -0
- package/roles/flight-mechanics-engineer/ROLE.md +162 -0
- package/roles/flight-mechanics-engineer/SOURCES.md +51 -0
- package/roles/flight-mechanics-engineer/cli.py +149 -0
- package/roles/flight-mechanics-engineer/core/flight_mechanics_core.py +1507 -0
- package/roles/flight-mechanics-engineer/templates/perf-sc-report-template.md +82 -0
- package/roles/flight-mechanics-engineer/tests/test_flight_mechanics_core.py +277 -0
- package/roles/flight-mechanics-engineer/tests/test_role_flight_mechanics_engineer.py +141 -0
- package/roles/flight-test-engineer/ROLE.md +137 -0
- package/roles/flight-test-engineer/SOURCES.md +24 -0
- package/roles/flight-test-engineer/cli.py +135 -0
- package/roles/flight-test-engineer/core/flight_test_core.py +931 -0
- package/roles/flight-test-engineer/templates/flight-test-template.md +143 -0
- package/roles/flight-test-engineer/tests/test_flight_test_engineer_core.py +227 -0
- package/roles/flight-test-engineer/tests/test_role_flight_test_engineer.py +150 -0
- package/roles/flight-test-performance-engineer/ROLE.md +165 -0
- package/roles/flight-test-performance-engineer/SOURCES.md +20 -0
- package/roles/flight-test-performance-engineer/cli.py +319 -0
- package/roles/flight-test-performance-engineer/core/flight_test_performance_core.py +1398 -0
- package/roles/flight-test-performance-engineer/templates/performance-report-template.md +105 -0
- package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py +308 -0
- package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py +165 -0
- package/roles/gnc-engineer/ROLE.md +141 -0
- package/roles/gnc-engineer/SOURCES.md +35 -0
- package/roles/gnc-engineer/cli.py +141 -0
- package/roles/gnc-engineer/core/gnc_core.py +1074 -0
- package/roles/gnc-engineer/templates/gnc-report-template.md +105 -0
- package/roles/gnc-engineer/tests/test_gnc_engineer_core.py +238 -0
- package/roles/gnc-engineer/tests/test_role_gnc_engineer.py +142 -0
- package/roles/guidance-engineer/ROLE.md +156 -0
- package/roles/guidance-engineer/SOURCES.md +43 -0
- package/roles/guidance-engineer/cli.py +280 -0
- package/roles/guidance-engineer/core/guidance_core.py +820 -0
- package/roles/guidance-engineer/templates/guidance-report-template.md +70 -0
- package/roles/guidance-engineer/tests/test_guidance_engineer_core.py +269 -0
- package/roles/guidance-engineer/tests/test_role_guidance_engineer.py +146 -0
- package/roles/high-speed-aerodynamics-engineer/ROLE.md +161 -0
- package/roles/high-speed-aerodynamics-engineer/SOURCES.md +18 -0
- package/roles/high-speed-aerodynamics-engineer/cli.py +282 -0
- package/roles/high-speed-aerodynamics-engineer/core/high_speed_aero_core.py +1566 -0
- package/roles/high-speed-aerodynamics-engineer/templates/high-speed-memo-template.md +75 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_bundle_protocol.py +103 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_high_speed_aero_core.py +280 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_role_high_speed_aerodynamics.py +125 -0
- package/roles/ndt-engineer/ROLE.md +157 -0
- package/roles/ndt-engineer/SOURCES.md +18 -0
- package/roles/ndt-engineer/cli.py +316 -0
- package/roles/ndt-engineer/core/ndt_core.py +1590 -0
- package/roles/ndt-engineer/templates/ndt-plan-template.md +109 -0
- package/roles/ndt-engineer/tests/test_ndt_cli_bundle.py +183 -0
- package/roles/ndt-engineer/tests/test_ndt_core.py +437 -0
- package/roles/ndt-engineer/tests/test_role_ndt_engineer.py +148 -0
- package/roles/numerical-analysis-engineer/ROLE.md +158 -0
- package/roles/numerical-analysis-engineer/SOURCES.md +19 -0
- package/roles/numerical-analysis-engineer/cli.py +414 -0
- package/roles/numerical-analysis-engineer/core/numerical_analysis_core.py +1220 -0
- package/roles/numerical-analysis-engineer/templates/verification-memo-template.md +120 -0
- package/roles/numerical-analysis-engineer/tests/test_bundle_protocol.py +157 -0
- package/roles/numerical-analysis-engineer/tests/test_numerical_analysis_core.py +337 -0
- package/roles/numerical-analysis-engineer/tests/test_role_numerical_analysis_engineer.py +117 -0
- package/roles/propulsion-engineer/ROLE.md +163 -0
- package/roles/propulsion-engineer/SOURCES.md +15 -0
- package/roles/propulsion-engineer/cli.py +168 -0
- package/roles/propulsion-engineer/core/propulsion_core.py +1291 -0
- package/roles/propulsion-engineer/templates/propulsion-report-template.md +92 -0
- package/roles/propulsion-engineer/tests/test_propulsion_engineer_core.py +294 -0
- package/roles/propulsion-engineer/tests/test_role_propulsion_engineer.py +106 -0
- package/roles/space-systems-engineer/ROLE.md +161 -0
- package/roles/space-systems-engineer/SOURCES.md +49 -0
- package/roles/space-systems-engineer/cli.py +173 -0
- package/roles/space-systems-engineer/core/space_systems_core.py +980 -0
- package/roles/space-systems-engineer/templates/space-systems-template.md +88 -0
- package/roles/space-systems-engineer/tests/test_role_space_systems_engineer.py +117 -0
- package/roles/space-systems-engineer/tests/test_space_systems_engineer_core.py +247 -0
- package/roles/state-estimation-engineer/ROLE.md +154 -0
- package/roles/state-estimation-engineer/SOURCES.md +27 -0
- package/roles/state-estimation-engineer/cli.py +326 -0
- package/roles/state-estimation-engineer/core/state_estimation_core.py +958 -0
- package/roles/state-estimation-engineer/templates/nav-state-estimator-design-template.md +76 -0
- package/roles/state-estimation-engineer/tests/test_bundle_protocol.py +110 -0
- package/roles/state-estimation-engineer/tests/test_role_state_estimation_engineer.py +127 -0
- package/roles/state-estimation-engineer/tests/test_state_estimation_engineer_core.py +231 -0
- package/roles/structures-loads-engineer/ROLE.md +171 -0
- package/roles/structures-loads-engineer/SOURCES.md +44 -0
- package/roles/structures-loads-engineer/cli.py +219 -0
- package/roles/structures-loads-engineer/core/structures_loads_core.py +1112 -0
- package/roles/structures-loads-engineer/templates/loads-strength-report-template.md +96 -0
- package/roles/structures-loads-engineer/tests/test_bundle_protocol.py +101 -0
- package/roles/structures-loads-engineer/tests/test_role_structures_loads_engineer.py +147 -0
- package/roles/structures-loads-engineer/tests/test_structures_loads_core.py +261 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""data_bus_avionics_core.py - Data Bus / Avionics Network Engineer core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given an aircraft bus architecture it decodes
|
|
5
|
+
ARINC 429 and MIL-STD-1553 words, budgets ARINC 429 label-rate schedules
|
|
6
|
+
into bus load and percent utilization, computes MIL-STD-1553 bus loading
|
|
7
|
+
from a bus-controller minor-frame schedule, sizes ARINC 664 AFDX virtual
|
|
8
|
+
links against the 100 Mbps link, and BUILDS the Avionics Data Bus Loading
|
|
9
|
+
and Protocol Assessment deliverable. It also gate-checks deliverables.
|
|
10
|
+
Standalone: no external repo needed.
|
|
11
|
+
|
|
12
|
+
Domain rules encoded here mirror the bound AeroSkills data-bus leaves
|
|
13
|
+
(AeroSkills avionics/data-bus/*, themselves paraphrases of the public
|
|
14
|
+
protocol practice) so the same quantity can be computed two independent
|
|
15
|
+
ways and cross-checked in provenance:
|
|
16
|
+
|
|
17
|
+
- ARINC 429 (Mark 33 DITS): every transmitted word occupies 36 bit-times
|
|
18
|
+
on the bus (32 data bits + 4-bit inter-word gap); high-speed link is
|
|
19
|
+
100 kbps (about 2777.8 words/s), low-speed 12.5 kbps (about 347.2
|
|
20
|
+
words/s); common design guideline 80% utilization. Word: bits 1-8
|
|
21
|
+
octal label, 9-10 SDI, 11-29 data, 30-31 SSM, 32 odd parity (integer
|
|
22
|
+
bit 0 = first bit transmitted).
|
|
23
|
+
- MIL-STD-1553B: 1 Mbps command/response bus; 20-bit words (16 info +
|
|
24
|
+
3-bit sync + odd parity); message = command/status overhead + data
|
|
25
|
+
words; each wire word occupies a 24 us slot (20 us word + 4 us gap);
|
|
26
|
+
loading is schedule time over the minor frame against an 80% budget.
|
|
27
|
+
- ARINC 664 Part 7 AFDX: VL bandwidth = max_frame_bytes*8/(BAG seconds)
|
|
28
|
+
with BAG in {1,2,4,8,16,32,64,128} ms and frame 64-1518 bytes; the VL
|
|
29
|
+
set must fit the 100 Mbps link; latency = 2 serializations + switches
|
|
30
|
+
x switch delay; jitter slack = budget - measured.
|
|
31
|
+
|
|
32
|
+
ARINC 429 / ARINC 664 text is proprietary and never reproduced; the
|
|
33
|
+
field layouts and timing figures above are common-knowledge protocol
|
|
34
|
+
summaries. MIL-STD-1553 is US public-domain work, paraphrase used.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import re
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from datetime import date
|
|
41
|
+
|
|
42
|
+
def _today() -> str:
|
|
43
|
+
import os
|
|
44
|
+
from datetime import date
|
|
45
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Domain constants (grounded in the bound data-bus leaves)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
# ARINC 429 loading model (leaf avionics/data-bus/arinc429-bus-loading)
|
|
52
|
+
A429_BITS_PER_WORD = 36.0 # 32 data bits + 4-bit gap
|
|
53
|
+
A429_RATE_100_KBPS = 100000.0 # high-speed link
|
|
54
|
+
A429_RATE_12_5_KBPS = 12500.0 # low-speed link
|
|
55
|
+
A429_GUIDELINE_PCT = 80.0 # common design guideline
|
|
56
|
+
|
|
57
|
+
# ARINC 429 word layout (leaf avionics/data-bus/arinc429-protocol).
|
|
58
|
+
# Integer bit 0 is the first bit transmitted (ARINC bit 1).
|
|
59
|
+
A429_LABEL_MASK = 0xFF # ARINC bits 1-8
|
|
60
|
+
A429_SDI_MASK = 0b11 # ARINC bits 9-10
|
|
61
|
+
A429_DATA_MASK = 0x7FFFF # ARINC bits 11-29 (19-bit field)
|
|
62
|
+
A429_SSM_MASK = 0b11 # ARINC bits 30-31
|
|
63
|
+
A429_PARITY_BIT = 31 # ARINC bit 32
|
|
64
|
+
A429_BNR_SIGN_BIT = 1 << 18
|
|
65
|
+
A429_BNR_POS_MAX = (1 << 18) - 1
|
|
66
|
+
A429_BNR_NEG_MIN = -(1 << 18)
|
|
67
|
+
|
|
68
|
+
# MIL-STD-1553 loading model (leaf avionics/data-bus/mil-std-1553-bus-loading)
|
|
69
|
+
M1553_WORD_TIME_US = 20.0 # 20 bit times at 1 Mbps
|
|
70
|
+
M1553_WORD_GAP_US = 4.0 # inter-word gap
|
|
71
|
+
M1553_WORD_SLOT_US = M1553_WORD_TIME_US + M1553_WORD_GAP_US # 24.0 us
|
|
72
|
+
M1553_FRAME_US_DEFAULT = 5000.0 # 5 ms minor frame
|
|
73
|
+
M1553_BUDGET_FRACTION = 0.80
|
|
74
|
+
M1553_KINDS = ("BCRT", "RTBC", "RTRT")
|
|
75
|
+
|
|
76
|
+
# MIL-STD-1553 word layout (leaf avionics/data-bus/mil-std-1553)
|
|
77
|
+
M1553_WORD_MASK = (1 << 20) - 1
|
|
78
|
+
M1553_RT_MASK = 0x1F
|
|
79
|
+
M1553_DATA16_MASK = 0xFFFF
|
|
80
|
+
M1553_SYNC_COMMAND = 0b100
|
|
81
|
+
M1553_SYNC_DATA = 0b011
|
|
82
|
+
M1553_MODE_SUBADDRESSES = (0, 0x1F)
|
|
83
|
+
M1553_BROADCAST_RT = 0x1F
|
|
84
|
+
|
|
85
|
+
# ARINC 664 AFDX (leaf avionics/data-bus/arinc664-afdx)
|
|
86
|
+
AFDX_LEGAL_BAGS = (1, 2, 4, 8, 16, 32, 64, 128)
|
|
87
|
+
AFDX_MIN_FRAME = 64
|
|
88
|
+
AFDX_MAX_FRAME = 1518
|
|
89
|
+
AFDX_LINK_RATE = 100_000_000.0 # 100 Mbps full-duplex link
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Guards (stdlib, shared)
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _number(value, name):
|
|
97
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
98
|
+
raise ValueError("%s must be a number, got %r" % (name, value))
|
|
99
|
+
return float(value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _require_int(value, name, lo, hi):
|
|
103
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
104
|
+
raise ValueError("%s must be an integer, got %r" % (name, value))
|
|
105
|
+
if not (lo <= value <= hi):
|
|
106
|
+
raise ValueError("%s %d out of range [%d, %d]" % (name, value, lo, hi))
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# ARINC 429 protocol (word decode/encode, parity, BNR)
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def a429_parity_ok(word):
|
|
115
|
+
"""True when a 32-bit word carries odd parity (odd number of 1 bits)."""
|
|
116
|
+
word = _require_int(word, "word", 0, 0xFFFFFFFF)
|
|
117
|
+
return (bin(word).count("1") % 2) == 1
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def a429_decode_word(word):
|
|
121
|
+
"""Split a 32-bit ARINC 429 word into label, SDI, data, SSM, parity."""
|
|
122
|
+
word = _require_int(word, "word", 0, 0xFFFFFFFF)
|
|
123
|
+
return {
|
|
124
|
+
"label": word & A429_LABEL_MASK,
|
|
125
|
+
"sdi": (word >> 8) & A429_SDI_MASK,
|
|
126
|
+
"data": (word >> 10) & A429_DATA_MASK,
|
|
127
|
+
"ssm": (word >> 29) & A429_SSM_MASK,
|
|
128
|
+
"parity": (word >> A429_PARITY_BIT) & 1,
|
|
129
|
+
"parity_ok": a429_parity_ok(word),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def a429_build_word(label, sdi, data, ssm, parity=None):
|
|
134
|
+
"""Pack label, SDI, 19-bit data, SSM, and odd parity into 32 bits.
|
|
135
|
+
|
|
136
|
+
Worked anchor: build_word(8, 1, 1234, 3) -> 1611876616 (0x60134908).
|
|
137
|
+
"""
|
|
138
|
+
if isinstance(label, str):
|
|
139
|
+
text = label.strip()
|
|
140
|
+
if not re.match(r"^[0-7]+$", text):
|
|
141
|
+
raise ValueError("label %r is not octal digits (0-7)" % (label,))
|
|
142
|
+
label = int(text, 8)
|
|
143
|
+
label = _require_int(label, "label", 0, A429_LABEL_MASK)
|
|
144
|
+
sdi = _require_int(sdi, "sdi", 0, A429_SDI_MASK)
|
|
145
|
+
data = _require_int(data, "data", 0, A429_DATA_MASK)
|
|
146
|
+
ssm = _require_int(ssm, "ssm", 0, A429_SSM_MASK)
|
|
147
|
+
payload = label | (sdi << 8) | (data << 10) | (ssm << 29)
|
|
148
|
+
if parity is None:
|
|
149
|
+
parity = 0 if (bin(payload & 0x7FFFFFFF).count("1") % 2) else 1
|
|
150
|
+
else:
|
|
151
|
+
parity = _require_int(parity, "parity", 0, 1)
|
|
152
|
+
return payload | (parity << A429_PARITY_BIT)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def a429_bnr_encode(value, scale_factor):
|
|
156
|
+
"""Encode a signed BNR value into the 19-bit data field (two's complement).
|
|
157
|
+
|
|
158
|
+
Worked anchor: 123.4 at LSB 0.1 -> field 1234; -12.3 -> field 524165.
|
|
159
|
+
"""
|
|
160
|
+
value = _number(value, "value")
|
|
161
|
+
scale = _number(scale_factor, "scale_factor")
|
|
162
|
+
if scale <= 0.0:
|
|
163
|
+
raise ValueError("scale_factor must be positive, got %r" % (scale,))
|
|
164
|
+
raw = int(round(value / scale))
|
|
165
|
+
if not (A429_BNR_NEG_MIN <= raw <= A429_BNR_POS_MAX):
|
|
166
|
+
raise ValueError(
|
|
167
|
+
"value %.6f does not fit BNR range at scale %.6f" % (value, scale))
|
|
168
|
+
if raw < 0:
|
|
169
|
+
raw += 1 << 19
|
|
170
|
+
return raw
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def a429_bnr_decode(data_field, scale_factor):
|
|
174
|
+
"""Decode a 19-bit BNR data field to its signed engineering value."""
|
|
175
|
+
data_field = _require_int(data_field, "data_field", 0, A429_DATA_MASK)
|
|
176
|
+
scale = _number(scale_factor, "scale_factor")
|
|
177
|
+
if scale <= 0.0:
|
|
178
|
+
raise ValueError("scale_factor must be positive, got %r" % (scale,))
|
|
179
|
+
raw = data_field - (1 << 19) if data_field & A429_BNR_SIGN_BIT else data_field
|
|
180
|
+
return raw * scale
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# ARINC 429 bus loading (label rate schedule -> utilization)
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def a429_total_word_rate(label_rates):
|
|
188
|
+
"""Sum a label rate schedule (dict {label: words/s} or list) into wps."""
|
|
189
|
+
rates = list(label_rates.values()) if isinstance(label_rates, dict) \
|
|
190
|
+
else list(label_rates)
|
|
191
|
+
if not rates:
|
|
192
|
+
raise ValueError("the label rate schedule is empty")
|
|
193
|
+
for r in rates:
|
|
194
|
+
if r < 0:
|
|
195
|
+
raise ValueError("label rates must be non-negative, got %r" % (r,))
|
|
196
|
+
return sum(float(r) for r in rates)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def a429_bus_load_bps(total_words_per_s):
|
|
200
|
+
"""Bus load in bits/s: every word occupies 36 bit-times on the bus."""
|
|
201
|
+
if total_words_per_s < 0:
|
|
202
|
+
raise ValueError("words per second must be non-negative")
|
|
203
|
+
return float(total_words_per_s) * A429_BITS_PER_WORD
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def a429_word_capacity(link_rate_bps=A429_RATE_100_KBPS):
|
|
207
|
+
"""Word-per-second capacity of the link: rate / 36 bit-times.
|
|
208
|
+
|
|
209
|
+
About 2777.8 words/s at 100 kbps, 347.2 words/s at 12.5 kbps.
|
|
210
|
+
"""
|
|
211
|
+
if link_rate_bps <= 0:
|
|
212
|
+
raise ValueError("link rate must be positive")
|
|
213
|
+
return float(link_rate_bps) / A429_BITS_PER_WORD
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def a429_utilization_pct(total_words_per_s, link_rate_bps=A429_RATE_100_KBPS):
|
|
217
|
+
"""Percent of the link occupied by the word schedule."""
|
|
218
|
+
if total_words_per_s < 0:
|
|
219
|
+
raise ValueError("words per second must be non-negative")
|
|
220
|
+
if link_rate_bps <= 0:
|
|
221
|
+
raise ValueError("link rate must be positive")
|
|
222
|
+
return a429_bus_load_bps(total_words_per_s) / float(link_rate_bps) * 100.0
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def a429_loading_summary(label_rates, link_rate_bps=A429_RATE_100_KBPS):
|
|
226
|
+
"""Full ARINC 429 bus loading budget for a label rate schedule.
|
|
227
|
+
|
|
228
|
+
Returns total_words_per_s, load_bps, utilization_pct, capacity_wps,
|
|
229
|
+
capacity_verdict (FITS/OVER against 100% of the link), guideline_pct,
|
|
230
|
+
guideline_verdict (FITS/VIOLATION against the 80% design guideline),
|
|
231
|
+
headroom_pct (margin to the guideline, floored at 0).
|
|
232
|
+
"""
|
|
233
|
+
total = a429_total_word_rate(label_rates)
|
|
234
|
+
if link_rate_bps <= 0:
|
|
235
|
+
raise ValueError("link rate must be positive")
|
|
236
|
+
load_bps = a429_bus_load_bps(total)
|
|
237
|
+
utilization_pct = a429_utilization_pct(total, link_rate_bps)
|
|
238
|
+
capacity_wps = a429_word_capacity(link_rate_bps)
|
|
239
|
+
return {
|
|
240
|
+
"total_words_per_s": total,
|
|
241
|
+
"load_bps": load_bps,
|
|
242
|
+
"utilization_pct": utilization_pct,
|
|
243
|
+
"capacity_wps": capacity_wps,
|
|
244
|
+
"capacity_verdict": "OVER" if utilization_pct > 100.0 else "FITS",
|
|
245
|
+
"guideline_pct": A429_GUIDELINE_PCT,
|
|
246
|
+
"guideline_verdict": "VIOLATION"
|
|
247
|
+
if utilization_pct > A429_GUIDELINE_PCT else "FITS",
|
|
248
|
+
"headroom_pct": max(0.0, A429_GUIDELINE_PCT - utilization_pct),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# MIL-STD-1553 protocol (command / status / data words, classification)
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
def m1553_parity_ok(word):
|
|
257
|
+
"""True when a 20-bit word carries odd parity."""
|
|
258
|
+
word = _require_int(word, "word", 0, M1553_WORD_MASK)
|
|
259
|
+
return (bin(word).count("1") % 2) == 1
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _m1553_parity_bit(data16, sync):
|
|
263
|
+
ones = bin(data16 & M1553_DATA16_MASK).count("1") \
|
|
264
|
+
+ bin(sync & 0b111).count("1")
|
|
265
|
+
return 0 if (ones % 2) else 1
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def m1553_encode_command_word(rt_address, subaddress, word_count,
|
|
269
|
+
transmit_receive):
|
|
270
|
+
"""Pack RT address, T/R, subaddress, word count, odd parity (20 bits).
|
|
271
|
+
|
|
272
|
+
Worked anchor: (5, 12, 16, 1) -> 93316, odd parity.
|
|
273
|
+
"""
|
|
274
|
+
rt = _require_int(rt_address, "rt_address", 0, M1553_RT_MASK)
|
|
275
|
+
sa = _require_int(subaddress, "subaddress", 0, M1553_RT_MASK)
|
|
276
|
+
wc = _require_int(word_count, "word_count", 0, M1553_RT_MASK)
|
|
277
|
+
tr = _require_int(transmit_receive, "transmit_receive", 0, 1)
|
|
278
|
+
data16 = (rt << 11) | (tr << 10) | (sa << 5) | wc
|
|
279
|
+
parity = _m1553_parity_bit(data16, M1553_SYNC_COMMAND)
|
|
280
|
+
return (parity << 19) | (data16 << 3) | M1553_SYNC_COMMAND
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def m1553_decode_command_word(word):
|
|
284
|
+
"""Split a 20-bit command word; raises on a non-command sync pattern."""
|
|
285
|
+
word = _require_int(word, "word", 0, M1553_WORD_MASK)
|
|
286
|
+
if (word & 0b111) != M1553_SYNC_COMMAND:
|
|
287
|
+
raise ValueError("word 0x%X is not a command/status sync word" % word)
|
|
288
|
+
data16 = (word >> 3) & M1553_DATA16_MASK
|
|
289
|
+
return {
|
|
290
|
+
"rt_address": (data16 >> 11) & M1553_RT_MASK,
|
|
291
|
+
"transmit_receive": (data16 >> 10) & 1,
|
|
292
|
+
"subaddress": (data16 >> 5) & M1553_RT_MASK,
|
|
293
|
+
"word_count": data16 & M1553_RT_MASK,
|
|
294
|
+
"parity": (word >> 19) & 1,
|
|
295
|
+
"parity_ok": m1553_parity_ok(word),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def m1553_encode_data_word(data):
|
|
300
|
+
"""Pack 16 data bits and odd parity into a 20-bit data word.
|
|
301
|
+
|
|
302
|
+
Worked anchor: 0x7FFF -> 262139.
|
|
303
|
+
"""
|
|
304
|
+
data = _require_int(data, "data", 0, M1553_DATA16_MASK)
|
|
305
|
+
parity = _m1553_parity_bit(data, M1553_SYNC_DATA)
|
|
306
|
+
return (parity << 19) | (data << 3) | M1553_SYNC_DATA
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def m1553_encode_status_word(rt_address, message_error=0, instrumentation=0,
|
|
310
|
+
service_request=0, broadcast_received=0, busy=0,
|
|
311
|
+
subsystem_flag=0,
|
|
312
|
+
dynamic_bus_control_acceptance=0,
|
|
313
|
+
terminal_flag=0):
|
|
314
|
+
"""Pack RT address and status flags with odd parity.
|
|
315
|
+
|
|
316
|
+
Worked anchor: encode_status_word(5, busy=1) -> 606276.
|
|
317
|
+
"""
|
|
318
|
+
rt = _require_int(rt_address, "rt_address", 0, M1553_RT_MASK)
|
|
319
|
+
me = _require_int(message_error, "message_error", 0, 1)
|
|
320
|
+
inst = _require_int(instrumentation, "instrumentation", 0, 1)
|
|
321
|
+
sr = _require_int(service_request, "service_request", 0, 1)
|
|
322
|
+
bcr = _require_int(broadcast_received, "broadcast_received", 0, 1)
|
|
323
|
+
bsy = _require_int(busy, "busy", 0, 1)
|
|
324
|
+
ssf = _require_int(subsystem_flag, "subsystem_flag", 0, 1)
|
|
325
|
+
dbca = _require_int(dynamic_bus_control_acceptance,
|
|
326
|
+
"dynamic_bus_control_acceptance", 0, 1)
|
|
327
|
+
tfl = _require_int(terminal_flag, "terminal_flag", 0, 1)
|
|
328
|
+
data16 = ((rt << 11) | (me << 10) | (inst << 9) | (sr << 8) | (bcr << 4)
|
|
329
|
+
| (bsy << 3) | (ssf << 2) | (dbca << 1) | tfl)
|
|
330
|
+
parity = _m1553_parity_bit(data16, M1553_SYNC_COMMAND)
|
|
331
|
+
return (parity << 19) | (data16 << 3) | M1553_SYNC_COMMAND
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def m1553_decode_status_word(word):
|
|
335
|
+
"""Split a 20-bit status word into RT address and status flags.
|
|
336
|
+
|
|
337
|
+
Flag layout in transmission order: message error, instrumentation,
|
|
338
|
+
service request, 3 reserved zeros, broadcast received, busy,
|
|
339
|
+
subsystem flag, dynamic bus control acceptance, terminal flag.
|
|
340
|
+
Worked anchor: 606276 -> RT 5 with busy=1.
|
|
341
|
+
"""
|
|
342
|
+
word = _require_int(word, "word", 0, M1553_WORD_MASK)
|
|
343
|
+
if (word & 0b111) != M1553_SYNC_COMMAND:
|
|
344
|
+
raise ValueError("word 0x%X is not a status sync word" % word)
|
|
345
|
+
data16 = (word >> 3) & M1553_DATA16_MASK
|
|
346
|
+
return {
|
|
347
|
+
"rt_address": (data16 >> 11) & M1553_RT_MASK,
|
|
348
|
+
"message_error": (data16 >> 10) & 1,
|
|
349
|
+
"instrumentation": (data16 >> 9) & 1,
|
|
350
|
+
"service_request": (data16 >> 8) & 1,
|
|
351
|
+
"broadcast_received": (data16 >> 4) & 1,
|
|
352
|
+
"busy": (data16 >> 3) & 1,
|
|
353
|
+
"subsystem_flag": (data16 >> 2) & 1,
|
|
354
|
+
"dynamic_bus_control_acceptance": (data16 >> 1) & 1,
|
|
355
|
+
"terminal_flag": data16 & 1,
|
|
356
|
+
"parity": (word >> 19) & 1,
|
|
357
|
+
"parity_ok": m1553_parity_ok(word),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def m1553_classify_message(rt_address, subaddress, word_count,
|
|
362
|
+
transmit_receive):
|
|
363
|
+
"""Classify the message a command word initiates.
|
|
364
|
+
|
|
365
|
+
Returns broadcast (RT 31), mode-code (subaddress 0/31), rt-to-bc
|
|
366
|
+
(T/R 1), or bc-to-rt (T/R 0).
|
|
367
|
+
"""
|
|
368
|
+
rt = _require_int(rt_address, "rt_address", 0, M1553_RT_MASK)
|
|
369
|
+
sa = _require_int(subaddress, "subaddress", 0, M1553_RT_MASK)
|
|
370
|
+
_require_int(word_count, "word_count", 0, M1553_RT_MASK)
|
|
371
|
+
tr = _require_int(transmit_receive, "transmit_receive", 0, 1)
|
|
372
|
+
if rt == M1553_BROADCAST_RT:
|
|
373
|
+
return "broadcast"
|
|
374
|
+
if sa in M1553_MODE_SUBADDRESSES:
|
|
375
|
+
return "mode-code"
|
|
376
|
+
if tr == 1:
|
|
377
|
+
return "rt-to-bc"
|
|
378
|
+
return "bc-to-rt"
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# MIL-STD-1553 bus loading (BC minor-frame schedule -> utilization)
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def m1553_wire_words(kind, data_words):
|
|
386
|
+
"""Wire words on the bus for one message: overhead + data words.
|
|
387
|
+
|
|
388
|
+
BCRT/RTBC = 1 command + 1 status + data; RTRT = 2 commands + 1 status
|
|
389
|
+
+ data. data_words must be an integer in 1..32.
|
|
390
|
+
"""
|
|
391
|
+
if kind not in M1553_KINDS:
|
|
392
|
+
raise ValueError("unknown message kind %r, use BCRT, RTBC or RTRT"
|
|
393
|
+
% (kind,))
|
|
394
|
+
if not isinstance(data_words, int):
|
|
395
|
+
raise ValueError("data_words must be an integer")
|
|
396
|
+
if data_words < 1 or data_words > 32:
|
|
397
|
+
raise ValueError("data_words must be in 1..32")
|
|
398
|
+
return data_words + (3 if kind == "RTRT" else 2)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def m1553_message_time_us(kind, data_words):
|
|
402
|
+
"""Bus time in us for one message: wire words x 24 us slot."""
|
|
403
|
+
return float(m1553_wire_words(kind, data_words)) * M1553_WORD_SLOT_US
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def m1553_schedule_utilization(messages, frame_us=M1553_FRAME_US_DEFAULT):
|
|
407
|
+
"""Utilization of a minor frame by a (kind, data_words) schedule.
|
|
408
|
+
|
|
409
|
+
Returns total_us, utilization_fraction, utilization_pct, budget_us
|
|
410
|
+
(80% of the frame), headroom_us, headroom_pct, verdict (FITS when the
|
|
411
|
+
schedule fits the 80% budget, OVER otherwise).
|
|
412
|
+
"""
|
|
413
|
+
if frame_us <= 0:
|
|
414
|
+
raise ValueError("frame_us must be positive")
|
|
415
|
+
if not messages:
|
|
416
|
+
raise ValueError("schedule is empty, nothing to load")
|
|
417
|
+
total_us = sum(m1553_message_time_us(kind, words)
|
|
418
|
+
for kind, words in messages)
|
|
419
|
+
budget_us = M1553_BUDGET_FRACTION * frame_us
|
|
420
|
+
utilization_fraction = total_us / frame_us
|
|
421
|
+
headroom_us = budget_us - total_us
|
|
422
|
+
return {
|
|
423
|
+
"total_us": total_us,
|
|
424
|
+
"total_wire_words": sum(m1553_wire_words(kind, words)
|
|
425
|
+
for kind, words in messages),
|
|
426
|
+
"utilization_fraction": utilization_fraction,
|
|
427
|
+
"utilization_pct": utilization_fraction * 100.0,
|
|
428
|
+
"budget_us": budget_us,
|
|
429
|
+
"headroom_us": headroom_us,
|
|
430
|
+
"headroom_pct": headroom_us / frame_us * 100.0,
|
|
431
|
+
"verdict": "FITS" if total_us <= budget_us else "OVER",
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
# ARINC 664 AFDX (VL bandwidth, link budget, timing)
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def _require_bag(bag_ms):
|
|
440
|
+
bag_ms = _require_int(bag_ms, "bag_ms", 1, 128)
|
|
441
|
+
if bag_ms not in AFDX_LEGAL_BAGS:
|
|
442
|
+
raise ValueError("bag_ms %d is not a legal BAG (%s ms)"
|
|
443
|
+
% (bag_ms, ", ".join(str(b) for b in AFDX_LEGAL_BAGS)))
|
|
444
|
+
return bag_ms
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _require_frame(frame_bytes):
|
|
448
|
+
return _require_int(frame_bytes, "max_frame_bytes",
|
|
449
|
+
AFDX_MIN_FRAME, AFDX_MAX_FRAME)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def afdx_vl_bandwidth(bag_ms, max_frame_bytes):
|
|
453
|
+
"""Bandwidth of one virtual link: frame*8/(BAG seconds).
|
|
454
|
+
|
|
455
|
+
Worked anchors: (4 ms, 1518) -> 3036000.0 bps; (128 ms, 1518) ->
|
|
456
|
+
94875.0 bps.
|
|
457
|
+
"""
|
|
458
|
+
bag = _require_bag(bag_ms)
|
|
459
|
+
frame = _require_frame(max_frame_bytes)
|
|
460
|
+
return frame * 8 / (bag / 1000.0)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def afdx_link_utilization_pct(vl_specs, link_rate_bps=AFDX_LINK_RATE):
|
|
464
|
+
"""Percent of the link consumed by a (bag_ms, frame_bytes) VL set.
|
|
465
|
+
|
|
466
|
+
Raises ValueError when the set oversubscribes the link (mirrors the
|
|
467
|
+
leaf: an over-budget set is a configuration error, not a percentage).
|
|
468
|
+
"""
|
|
469
|
+
link = _number(link_rate_bps, "link_rate_bps")
|
|
470
|
+
if link <= 0.0:
|
|
471
|
+
raise ValueError("link_rate_bps must be positive")
|
|
472
|
+
total = 0.0
|
|
473
|
+
for spec in vl_specs:
|
|
474
|
+
try:
|
|
475
|
+
bag_ms, frame_bytes = spec
|
|
476
|
+
except (TypeError, ValueError):
|
|
477
|
+
raise ValueError("vl entry %r is not a (bag_ms, frame_bytes) pair"
|
|
478
|
+
% (spec,))
|
|
479
|
+
total += afdx_vl_bandwidth(bag_ms, frame_bytes)
|
|
480
|
+
if total > link:
|
|
481
|
+
raise ValueError(
|
|
482
|
+
"virtual link set needs %.0f bps but the link carries %.0f bps"
|
|
483
|
+
" (oversubscribed)" % (total, link))
|
|
484
|
+
return total / link * 100.0
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def afdx_transmission_time_us(frame_bytes, link_rate_bps=AFDX_LINK_RATE):
|
|
488
|
+
"""Serialization time of one frame in us: frame*8/link.
|
|
489
|
+
|
|
490
|
+
Worked anchor: 1518 bytes at 100 Mbps -> 121.44 us; 64 bytes -> 5.12 us.
|
|
491
|
+
"""
|
|
492
|
+
frame = _require_frame(frame_bytes)
|
|
493
|
+
link = _number(link_rate_bps, "link_rate_bps")
|
|
494
|
+
if link <= 0.0:
|
|
495
|
+
raise ValueError("link_rate_bps must be positive")
|
|
496
|
+
return frame * 8 / link * 1e6
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def afdx_end_to_end_latency_us(frame_bytes, switch_count, switch_delay_us,
|
|
500
|
+
link_rate_bps=AFDX_LINK_RATE):
|
|
501
|
+
"""Worst-case one-way latency in us: 2 serializations + switch delays.
|
|
502
|
+
|
|
503
|
+
Worked anchor: 1518 bytes, 2 switches at 150 us -> 542.88 us.
|
|
504
|
+
"""
|
|
505
|
+
tx_us = afdx_transmission_time_us(frame_bytes, link_rate_bps)
|
|
506
|
+
switches = _require_int(switch_count, "switch_count", 0, 100)
|
|
507
|
+
delay = _number(switch_delay_us, "switch_delay_us")
|
|
508
|
+
if delay < 0.0:
|
|
509
|
+
raise ValueError("switch_delay_us must be non-negative")
|
|
510
|
+
return 2 * tx_us + switches * delay
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def afdx_jitter_slack(measured_max_jitter_us, jitter_budget_us):
|
|
514
|
+
"""Slack between measured max jitter and the budget, in us.
|
|
515
|
+
|
|
516
|
+
Worked anchor: (420.0, 500.0) -> 80.0 (compliant); negative = violation.
|
|
517
|
+
"""
|
|
518
|
+
measured = _number(measured_max_jitter_us, "measured_max_jitter_us")
|
|
519
|
+
budget = _number(jitter_budget_us, "jitter_budget_us")
|
|
520
|
+
if measured < 0.0:
|
|
521
|
+
raise ValueError("measured jitter must be non-negative")
|
|
522
|
+
if budget <= 0.0:
|
|
523
|
+
raise ValueError("jitter budget must be positive")
|
|
524
|
+
return budget - measured
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def afdx_largest_bag_for_bandwidth(bandwidth_bps, max_frame_bytes):
|
|
528
|
+
"""Largest legal BAG (ms) whose VL bandwidth meets the requirement.
|
|
529
|
+
|
|
530
|
+
Worked anchor: 1.0 Mbps at 1518 bytes -> 8 ms; raises when even the
|
|
531
|
+
1 ms BAG is insufficient.
|
|
532
|
+
"""
|
|
533
|
+
needed = _number(bandwidth_bps, "bandwidth_bps")
|
|
534
|
+
frame = _require_frame(max_frame_bytes)
|
|
535
|
+
if needed <= 0.0:
|
|
536
|
+
raise ValueError("bandwidth_bps must be positive")
|
|
537
|
+
max_rate = afdx_vl_bandwidth(1, frame)
|
|
538
|
+
if needed > max_rate:
|
|
539
|
+
raise ValueError(
|
|
540
|
+
"bandwidth %.0f bps exceeds the 1 ms BAG capacity at %d bytes"
|
|
541
|
+
% (needed, frame))
|
|
542
|
+
chosen = AFDX_LEGAL_BAGS[0]
|
|
543
|
+
for bag in AFDX_LEGAL_BAGS:
|
|
544
|
+
if afdx_vl_bandwidth(bag, frame) >= needed:
|
|
545
|
+
chosen = bag
|
|
546
|
+
return chosen
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ---------------------------------------------------------------------------
|
|
550
|
+
# Example architecture + analysis + deliverable
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
@dataclass
|
|
554
|
+
class A429Line:
|
|
555
|
+
"""One ARINC 429 transmit line: single transmitter, N receivers."""
|
|
556
|
+
name: str
|
|
557
|
+
transmitter: str
|
|
558
|
+
receivers: str
|
|
559
|
+
link_rate_bps: float = A429_RATE_100_KBPS
|
|
560
|
+
label_rates: dict = field(default_factory=dict) # {label: words/s}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@dataclass
|
|
564
|
+
class M1553Bus:
|
|
565
|
+
"""One MIL-STD-1553 bus: BC schedule per minor frame."""
|
|
566
|
+
name: str
|
|
567
|
+
controller: str
|
|
568
|
+
remote_terminals: str
|
|
569
|
+
frame_us: float = M1553_FRAME_US_DEFAULT
|
|
570
|
+
messages: list = field(default_factory=list) # [(kind, data_words)]
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@dataclass
|
|
574
|
+
class AfdxNetwork:
|
|
575
|
+
"""One AFDX network: dual-redundant VL set (network A and B)."""
|
|
576
|
+
name: str
|
|
577
|
+
vl_specs: list = field(default_factory=list) # [(bag_ms, frame_bytes)]
|
|
578
|
+
jitter_measured_us: float = 0.0
|
|
579
|
+
jitter_budget_us: float = 0.0
|
|
580
|
+
switch_count: int = 0
|
|
581
|
+
switch_delay_us: float = 0.0
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@dataclass
|
|
585
|
+
class DataBusItem:
|
|
586
|
+
"""Project facts the role needs to build the assessment."""
|
|
587
|
+
aircraft: str
|
|
588
|
+
description: str = ""
|
|
589
|
+
arinc429_lines: list = field(default_factory=list)
|
|
590
|
+
m1553_bus: M1553Bus | None = None
|
|
591
|
+
afdx_network: AfdxNetwork | None = None
|
|
592
|
+
a429_decode_sample: int = 0 # received word for conformance
|
|
593
|
+
a429_sample_expected_label: str = ""
|
|
594
|
+
a429_bnr_scale: float = 0.1
|
|
595
|
+
m1553_command_sample: int = 0 # received command word
|
|
596
|
+
m1553_status_sample: int = 0 # received status word
|
|
597
|
+
certification_basis: str = "MIL-STD-1553B / ARINC 429 / ARINC 664 practice"
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def analyze_architecture(item: DataBusItem) -> dict:
|
|
601
|
+
"""Run every computation for the architecture; returns the results."""
|
|
602
|
+
lines = []
|
|
603
|
+
for bus in item.arinc429_lines:
|
|
604
|
+
summary = a429_loading_summary(bus.label_rates, bus.link_rate_bps)
|
|
605
|
+
lines.append({
|
|
606
|
+
"name": bus.name,
|
|
607
|
+
"transmitter": bus.transmitter,
|
|
608
|
+
"receivers": bus.receivers,
|
|
609
|
+
"link_rate_bps": bus.link_rate_bps,
|
|
610
|
+
"label_rates": {str(k): float(v) for k, v in bus.label_rates.items()},
|
|
611
|
+
"summary": summary,
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
m1553 = None
|
|
615
|
+
if item.m1553_bus:
|
|
616
|
+
bus = item.m1553_bus
|
|
617
|
+
rows = [{
|
|
618
|
+
"kind": kind, "data_words": words,
|
|
619
|
+
"wire_words": m1553_wire_words(kind, words),
|
|
620
|
+
"time_us": m1553_message_time_us(kind, words),
|
|
621
|
+
} for kind, words in bus.messages]
|
|
622
|
+
schedule = m1553_schedule_utilization(bus.messages, bus.frame_us)
|
|
623
|
+
m1553 = {
|
|
624
|
+
"name": bus.name,
|
|
625
|
+
"controller": bus.controller,
|
|
626
|
+
"remote_terminals": bus.remote_terminals,
|
|
627
|
+
"frame_us": bus.frame_us,
|
|
628
|
+
"messages": rows,
|
|
629
|
+
"schedule": schedule,
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
afdx = None
|
|
633
|
+
if item.afdx_network:
|
|
634
|
+
net = item.afdx_network
|
|
635
|
+
vl_rows = [{
|
|
636
|
+
"bag_ms": bag, "frame_bytes": frame,
|
|
637
|
+
"bandwidth_bps": afdx_vl_bandwidth(bag, frame),
|
|
638
|
+
} for bag, frame in net.vl_specs]
|
|
639
|
+
total_bw = sum(r["bandwidth_bps"] for r in vl_rows)
|
|
640
|
+
try:
|
|
641
|
+
utilization_pct = afdx_link_utilization_pct(net.vl_specs)
|
|
642
|
+
over = False
|
|
643
|
+
error = ""
|
|
644
|
+
except ValueError as e:
|
|
645
|
+
utilization_pct = total_bw / AFDX_LINK_RATE * 100.0
|
|
646
|
+
over = True
|
|
647
|
+
error = str(e)
|
|
648
|
+
check = None
|
|
649
|
+
if net.vl_specs:
|
|
650
|
+
check = {
|
|
651
|
+
"frame_bytes": net.vl_specs[0][1],
|
|
652
|
+
"latency_us": afdx_end_to_end_latency_us(
|
|
653
|
+
net.vl_specs[0][1], net.switch_count,
|
|
654
|
+
net.switch_delay_us),
|
|
655
|
+
"jitter_slack_us": afdx_jitter_slack(
|
|
656
|
+
net.jitter_measured_us, net.jitter_budget_us),
|
|
657
|
+
}
|
|
658
|
+
afdx = {
|
|
659
|
+
"name": net.name,
|
|
660
|
+
"vl_count": len(vl_rows),
|
|
661
|
+
"vl_rows": vl_rows,
|
|
662
|
+
"total_bandwidth_bps": total_bw,
|
|
663
|
+
"link_rate_bps": AFDX_LINK_RATE,
|
|
664
|
+
"utilization_pct": utilization_pct,
|
|
665
|
+
"oversubscribed": over,
|
|
666
|
+
"error": error,
|
|
667
|
+
"redundancy": "dual independent networks A and B, same VL set",
|
|
668
|
+
"switch_count": net.switch_count,
|
|
669
|
+
"switch_delay_us": net.switch_delay_us,
|
|
670
|
+
"jitter_measured_us": net.jitter_measured_us,
|
|
671
|
+
"jitter_budget_us": net.jitter_budget_us,
|
|
672
|
+
"checks": check,
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
# Protocol conformance samples (decode received words on the bench)
|
|
676
|
+
a429_word = a429_decode_word(item.a429_decode_sample) if \
|
|
677
|
+
item.a429_decode_sample else None
|
|
678
|
+
a429_bnr = None
|
|
679
|
+
if a429_word is not None:
|
|
680
|
+
a429_bnr = {
|
|
681
|
+
"data_field": a429_word["data"],
|
|
682
|
+
"scale": item.a429_bnr_scale,
|
|
683
|
+
"engineering_value": a429_bnr_decode(a429_word["data"],
|
|
684
|
+
item.a429_bnr_scale),
|
|
685
|
+
}
|
|
686
|
+
m1553_cmd = m1553_decode_command_word(item.m1553_command_sample) if \
|
|
687
|
+
item.m1553_command_sample else None
|
|
688
|
+
m1553_st = m1553_decode_status_word(item.m1553_status_sample) if \
|
|
689
|
+
item.m1553_status_sample else None
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
"arinc429_lines": lines,
|
|
693
|
+
"m1553": m1553,
|
|
694
|
+
"afdx": afdx,
|
|
695
|
+
"conformance": {
|
|
696
|
+
"a429_word": a429_word,
|
|
697
|
+
"a429_word_hex": "0x%08X" % item.a429_decode_sample
|
|
698
|
+
if item.a429_decode_sample else "",
|
|
699
|
+
"a429_expected_label": item.a429_sample_expected_label,
|
|
700
|
+
"a429_bnr": a429_bnr,
|
|
701
|
+
"m1553_command": m1553_cmd,
|
|
702
|
+
"m1553_status": m1553_st,
|
|
703
|
+
},
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def build_assessment(item: DataBusItem, results: dict) -> dict:
|
|
708
|
+
"""Build the complete content model from facts + computed results."""
|
|
709
|
+
observations = []
|
|
710
|
+
for line in results["arinc429_lines"]:
|
|
711
|
+
s = line["summary"]
|
|
712
|
+
if s["guideline_verdict"] == "VIOLATION":
|
|
713
|
+
observations.append(
|
|
714
|
+
"%s exceeds the %g%% design guideline at %.2f%% utilization "
|
|
715
|
+
"(headroom 0): re-rate or offload labels."
|
|
716
|
+
% (line["name"], s["guideline_pct"], s["utilization_pct"]))
|
|
717
|
+
elif s["headroom_pct"] < 10.0:
|
|
718
|
+
observations.append(
|
|
719
|
+
"%s sits at %.2f%% utilization with only %.2f%% headroom to "
|
|
720
|
+
"the %g%% guideline: growth requires a re-rate or a new line."
|
|
721
|
+
% (line["name"], s["utilization_pct"], s["headroom_pct"],
|
|
722
|
+
s["guideline_pct"]))
|
|
723
|
+
m1553 = results.get("m1553") or {}
|
|
724
|
+
if m1553:
|
|
725
|
+
sch = m1553["schedule"]
|
|
726
|
+
if sch["verdict"] == "OVER":
|
|
727
|
+
observations.append(
|
|
728
|
+
"%s schedule OVER the %g%% budget (%.2f%%): move messages "
|
|
729
|
+
"to the spare bus or lengthen the minor frame."
|
|
730
|
+
% (m1553["name"], M1553_BUDGET_FRACTION * 100.0,
|
|
731
|
+
sch["utilization_pct"]))
|
|
732
|
+
afdx = results.get("afdx")
|
|
733
|
+
if afdx and afdx["oversubscribed"]:
|
|
734
|
+
observations.append(
|
|
735
|
+
"%s VL set oversubscribes the 100 Mbps link (%.2f%%): increase "
|
|
736
|
+
"BAGs or reduce frame sizes. %s" % (afdx["name"],
|
|
737
|
+
afdx["utilization_pct"],
|
|
738
|
+
afdx["error"]))
|
|
739
|
+
return {
|
|
740
|
+
"document_type": "Avionics Data Bus Loading and Protocol Assessment",
|
|
741
|
+
"status": "draft-for-review",
|
|
742
|
+
"aircraft": item.aircraft,
|
|
743
|
+
"description": item.description,
|
|
744
|
+
"certification_basis": item.certification_basis,
|
|
745
|
+
"arinc429": results["arinc429_lines"],
|
|
746
|
+
"m1553": m1553,
|
|
747
|
+
"afdx": afdx,
|
|
748
|
+
"conformance": results["conformance"],
|
|
749
|
+
"observations": observations,
|
|
750
|
+
"generated": _today(),
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _fmt_link(rate):
|
|
755
|
+
if rate >= 1e6:
|
|
756
|
+
return "%g Mbps" % (rate / 1e6)
|
|
757
|
+
if rate >= 1e3:
|
|
758
|
+
return "%g kbps" % (rate / 1e3)
|
|
759
|
+
return "%g bps" % rate
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def render_assessment_markdown(model: dict) -> str:
|
|
763
|
+
"""Render the content model as the deliverable markdown document."""
|
|
764
|
+
lines = [
|
|
765
|
+
"# Avionics Data Bus Loading and Protocol Assessment",
|
|
766
|
+
"",
|
|
767
|
+
f"**Aircraft / program:** {model['aircraft']}",
|
|
768
|
+
f"**Status:** {model['status']}",
|
|
769
|
+
f"**Protocol basis:** {model['certification_basis']}",
|
|
770
|
+
"",
|
|
771
|
+
"## 1. Scope",
|
|
772
|
+
"",
|
|
773
|
+
"This assessment budgets every ARINC 429 transmit line, the "
|
|
774
|
+
"MIL-STD-1553 bus-controller minor-frame schedule, and the ARINC "
|
|
775
|
+
"664 AFDX virtual-link set of the aircraft bus architecture, and "
|
|
776
|
+
"checks received-word conformance (parity, label, word fields) "
|
|
777
|
+
"against the interface control documents."
|
|
778
|
+
+ (f" {model['description']}" if model.get("description") else ""),
|
|
779
|
+
"",
|
|
780
|
+
"Loading gates: ARINC 429 and MIL-STD-1553 utilization against the "
|
|
781
|
+
f"{A429_GUIDELINE_PCT:.0f}% design guideline "
|
|
782
|
+
"(the leaf practice; ARINC 429 capacity is the link itself), "
|
|
783
|
+
"AFDX VL set against the 100 Mbps link.",
|
|
784
|
+
"",
|
|
785
|
+
"## 2. ARINC 429 bus loading",
|
|
786
|
+
"",
|
|
787
|
+
]
|
|
788
|
+
for bus in model["arinc429"]:
|
|
789
|
+
s = bus["summary"]
|
|
790
|
+
lines += [
|
|
791
|
+
f"### {bus['name']} ({bus['transmitter']} -> "
|
|
792
|
+
f"{bus['receivers']}, {_fmt_link(bus['link_rate_bps'])})",
|
|
793
|
+
"",
|
|
794
|
+
"| Label | Rate (words/s) |",
|
|
795
|
+
"|---|---|",
|
|
796
|
+
]
|
|
797
|
+
for label, rate in sorted(bus["label_rates"].items()):
|
|
798
|
+
lines.append(f"| {label} | {rate:g} |")
|
|
799
|
+
lines += [
|
|
800
|
+
"",
|
|
801
|
+
f"- Total word rate: **{s['total_words_per_s']:.2f} words/s**",
|
|
802
|
+
f"- Bus load: **{s['load_bps']:.0f} bps** "
|
|
803
|
+
f"({A429_BITS_PER_WORD:.0f} bit-times per word)",
|
|
804
|
+
f"- Bus utilization: **{s['utilization_pct']:.2f}%** of "
|
|
805
|
+
f"{_fmt_link(bus['link_rate_bps'])} "
|
|
806
|
+
f"(capacity {s['capacity_wps']:.1f} words/s)",
|
|
807
|
+
f"- Capacity verdict: **{s['capacity_verdict']}**; design "
|
|
808
|
+
f"guideline ({s['guideline_pct']:.0f}%): "
|
|
809
|
+
f"**{s['guideline_verdict']}**",
|
|
810
|
+
f"- Headroom to guideline: **{s['headroom_pct']:.2f}%**",
|
|
811
|
+
"",
|
|
812
|
+
]
|
|
813
|
+
lines += ["## 3. MIL-STD-1553 bus loading", ""]
|
|
814
|
+
m1553 = model.get("m1553")
|
|
815
|
+
if m1553:
|
|
816
|
+
sch = m1553["schedule"]
|
|
817
|
+
lines += [
|
|
818
|
+
f"### {m1553['name']} (BC {m1553['controller']}; RTs "
|
|
819
|
+
f"{m1553['remote_terminals']}; minor frame "
|
|
820
|
+
f"{m1553['frame_us']:.0f} us)",
|
|
821
|
+
"",
|
|
822
|
+
"| Message | Data words | Wire words | Bus time (us) |",
|
|
823
|
+
"|---|---|---|---|",
|
|
824
|
+
]
|
|
825
|
+
for row in m1553["messages"]:
|
|
826
|
+
lines.append(f"| {row['kind']} | {row['data_words']} | "
|
|
827
|
+
f"{row['wire_words']} | {row['time_us']:.0f} |")
|
|
828
|
+
lines += [
|
|
829
|
+
"",
|
|
830
|
+
f"- Schedule time: **{sch['total_us']:.0f} us** over "
|
|
831
|
+
f"{m1553['frame_us']:.0f} us "
|
|
832
|
+
f"({sch['total_wire_words']} wire words x "
|
|
833
|
+
f"{M1553_WORD_SLOT_US:.0f} us)",
|
|
834
|
+
f"- Bus utilization: **{sch['utilization_pct']:.2f}%**",
|
|
835
|
+
f"- Budget ({M1553_BUDGET_FRACTION * 100:.0f}% of frame): "
|
|
836
|
+
f"{sch['budget_us']:.0f} us; headroom "
|
|
837
|
+
f"{sch['headroom_us']:.0f} us "
|
|
838
|
+
f"({sch['headroom_pct']:.2f}%)",
|
|
839
|
+
f"- Verdict: **{sch['verdict']}**",
|
|
840
|
+
"",
|
|
841
|
+
]
|
|
842
|
+
else:
|
|
843
|
+
lines += ["No MIL-STD-1553 bus in this architecture.", ""]
|
|
844
|
+
|
|
845
|
+
lines += ["## 4. ARINC 664 AFDX network", ""]
|
|
846
|
+
afdx = model.get("afdx")
|
|
847
|
+
if afdx:
|
|
848
|
+
lines += [
|
|
849
|
+
f"### {afdx['name']} ({afdx['redundancy']})",
|
|
850
|
+
"",
|
|
851
|
+
f"- Virtual links: {afdx['vl_count']}; total bandwidth "
|
|
852
|
+
f"**{afdx['total_bandwidth_bps'] / 1e6:.3f} Mbps**",
|
|
853
|
+
f"- Link utilization: **{afdx['utilization_pct']:.2f}%** of "
|
|
854
|
+
f"100 Mbps",
|
|
855
|
+
f"- Link budget verdict: "
|
|
856
|
+
f"**{'OVER' if afdx['oversubscribed'] else 'FITS'}**",
|
|
857
|
+
"",
|
|
858
|
+
"| VLs | BAG (ms) | Frame (bytes) | Bandwidth (Mbps) |",
|
|
859
|
+
"|---|---|---|---|",
|
|
860
|
+
]
|
|
861
|
+
# summarize repeated (bag, frame) pairs
|
|
862
|
+
counts = {}
|
|
863
|
+
for row in afdx["vl_rows"]:
|
|
864
|
+
key = (row["bag_ms"], row["frame_bytes"])
|
|
865
|
+
counts[key] = counts.get(key, 0) + 1
|
|
866
|
+
for (bag, frame), n in sorted(counts.items()):
|
|
867
|
+
bw = afdx_vl_bandwidth(bag, frame) / 1e6
|
|
868
|
+
lines.append(f"| {n} | {bag} | {frame} | {bw:.3f} |")
|
|
869
|
+
if afdx["checks"]:
|
|
870
|
+
chk = afdx["checks"]
|
|
871
|
+
lines += [
|
|
872
|
+
"",
|
|
873
|
+
f"- Worst-case latency ({chk['frame_bytes']}-byte frame, "
|
|
874
|
+
f"{afdx['switch_count']} switch(es) at "
|
|
875
|
+
f"{afdx['switch_delay_us']:.0f} us): "
|
|
876
|
+
f"**{chk['latency_us']:.2f} us**",
|
|
877
|
+
f"- Jitter: measured {afdx['jitter_measured_us']:.0f} us vs "
|
|
878
|
+
f"budget {afdx['jitter_budget_us']:.0f} us -> slack "
|
|
879
|
+
f"**{chk['jitter_slack_us']:.1f} us**",
|
|
880
|
+
"",
|
|
881
|
+
]
|
|
882
|
+
else:
|
|
883
|
+
lines += ["No AFDX network in this architecture.", ""]
|
|
884
|
+
|
|
885
|
+
lines += ["## 5. Protocol conformance checks", ""]
|
|
886
|
+
conf = model["conformance"]
|
|
887
|
+
aw = conf.get("a429_word")
|
|
888
|
+
if a429_word_present := aw:
|
|
889
|
+
bnr = conf.get("a429_bnr") or {}
|
|
890
|
+
label_ok = (aw["label"] == _oct_label(conf.get("a429_expected_label", "")))
|
|
891
|
+
lines += [
|
|
892
|
+
f"- ARINC 429 received word {conf.get('a429_word_hex', '')}: "
|
|
893
|
+
f"label {aw['label']:03o} ({aw['label']}), SDI {aw['sdi']}, "
|
|
894
|
+
f"data field {aw['data']}"
|
|
895
|
+
+ (f" (BNR {bnr.get('engineering_value', 0):g} at "
|
|
896
|
+
f"LSB {bnr.get('scale', 0.1):g})" if bnr else "")
|
|
897
|
+
+ f", SSM {aw['ssm']}, parity {'OK' if aw['parity_ok'] else 'FAIL'}",
|
|
898
|
+
f" - Expected label {conf.get('a429_expected_label', '')} "
|
|
899
|
+
f"({_oct_label(conf.get('a429_expected_label', ''))}): "
|
|
900
|
+
f"{'MATCH' if label_ok else 'MISMATCH'}",
|
|
901
|
+
]
|
|
902
|
+
mc = conf.get("m1553_command")
|
|
903
|
+
if mc:
|
|
904
|
+
cls = m1553_classify_message(mc["rt_address"], mc["subaddress"],
|
|
905
|
+
mc["word_count"], mc["transmit_receive"])
|
|
906
|
+
lines += [
|
|
907
|
+
f"- MIL-STD-1553 command word {mc['word_count']} data words, "
|
|
908
|
+
f"RT {mc['rt_address']} subaddress {mc['subaddress']}, "
|
|
909
|
+
f"{'RT-to-BC' if mc['transmit_receive'] else 'BC-to-RT'}: "
|
|
910
|
+
f"classified **{cls}**, parity "
|
|
911
|
+
f"{'OK' if mc['parity_ok'] else 'FAIL'}",
|
|
912
|
+
]
|
|
913
|
+
ms = conf.get("m1553_status")
|
|
914
|
+
if ms:
|
|
915
|
+
lines += [
|
|
916
|
+
f"- MIL-STD-1553 status word RT {ms['rt_address']}: busy="
|
|
917
|
+
f"{ms['busy']}, terminal_flag={ms['terminal_flag']}, parity "
|
|
918
|
+
f"{'OK' if ms['parity_ok'] else 'FAIL'}",
|
|
919
|
+
]
|
|
920
|
+
lines += [""]
|
|
921
|
+
|
|
922
|
+
lines += ["## 6. Findings and observations", ""]
|
|
923
|
+
obs = model.get("observations") or []
|
|
924
|
+
if obs:
|
|
925
|
+
for i, o in enumerate(obs, 1):
|
|
926
|
+
lines.append(f"- **O-{i}** {o}")
|
|
927
|
+
else:
|
|
928
|
+
lines.append("- No loading or conformance observations: all buses "
|
|
929
|
+
"fit their design guidelines.")
|
|
930
|
+
lines += [
|
|
931
|
+
"",
|
|
932
|
+
"## 7. Conclusion",
|
|
933
|
+
"",
|
|
934
|
+
"The bus architecture loading and received-word conformance results "
|
|
935
|
+
"above are computed by the data-bus-avionics-engineer core from the "
|
|
936
|
+
"bound ARINC 429, MIL-STD-1553 and ARINC 664 AFDX leaf rules "
|
|
937
|
+
"(36 bit-times per ARINC 429 word; 24 us per MIL-STD-1553 wire "
|
|
938
|
+
"word; frame x 8 / BAG for AFDX VL bandwidth). Numbers are "
|
|
939
|
+
"cross-checked against the AeroSkills leaf logic when the skills "
|
|
940
|
+
"library is present and recorded in the evidence bundle.",
|
|
941
|
+
"",
|
|
942
|
+
"---",
|
|
943
|
+
f"*Generated by Aero Agent Roles data-bus-avionics-engineer core "
|
|
944
|
+
f"({model['generated']}). DRAFT for human avionics/data-bus "
|
|
945
|
+
"engineer review. Not an approval document.*",
|
|
946
|
+
]
|
|
947
|
+
return "\n".join(lines)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _oct_label(label_str: str) -> int:
|
|
951
|
+
"""Octal label string -> int (empty -> 0)."""
|
|
952
|
+
if not label_str:
|
|
953
|
+
return 0
|
|
954
|
+
return int(label_str, 8)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
# ---------------------------------------------------------------------------
|
|
958
|
+
# Evidence gates
|
|
959
|
+
# ---------------------------------------------------------------------------
|
|
960
|
+
|
|
961
|
+
GATE_CHECKS = {
|
|
962
|
+
"arinc429_loads_computed": "every ARINC 429 line has numeric load + utilization",
|
|
963
|
+
"arinc429_conformance_ok": "received word parity OK and label matches ICD",
|
|
964
|
+
"m1553_schedule_computed": "1553 schedule utilization is a number in range",
|
|
965
|
+
"m1553_conformance_ok": "command/status words decode with odd parity OK",
|
|
966
|
+
"afdx_budget_checkable": "AFDX VL set present and link budget verdict known",
|
|
967
|
+
"observations_listed": "findings/observations section is present",
|
|
968
|
+
"sign_off_honest": "document is marked draft-for-review, not approval",
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def check_assessment(model: dict) -> dict:
|
|
973
|
+
"""Run the evidence gates against a content model."""
|
|
974
|
+
a429 = model.get("arinc429") or []
|
|
975
|
+
conf = model.get("conformance") or {}
|
|
976
|
+
a429_ok = bool(a429) and all(
|
|
977
|
+
isinstance(b.get("summary", {}).get("utilization_pct"), (int, float))
|
|
978
|
+
for b in a429)
|
|
979
|
+
aw = conf.get("a429_word") or {}
|
|
980
|
+
a429_conf = bool(aw.get("parity_ok")) and bool(aw) and (
|
|
981
|
+
aw.get("label") == _oct_label(conf.get("a429_expected_label", "")))
|
|
982
|
+
m1553 = model.get("m1553") or {}
|
|
983
|
+
sch = m1553.get("schedule") or {}
|
|
984
|
+
m1553_ok = bool(m1553) and isinstance(sch.get("utilization_pct"),
|
|
985
|
+
(int, float)) and \
|
|
986
|
+
0.0 <= sch.get("utilization_pct", -1) <= 100.0
|
|
987
|
+
mc = conf.get("m1553_command") or {}
|
|
988
|
+
ms = conf.get("m1553_status") or {}
|
|
989
|
+
m1553_conf = bool(mc.get("parity_ok")) and bool(ms.get("parity_ok"))
|
|
990
|
+
afdx = model.get("afdx")
|
|
991
|
+
afdx_ok = bool(afdx) and "oversubscribed" in afdx
|
|
992
|
+
results = {
|
|
993
|
+
"arinc429_loads_computed": a429_ok,
|
|
994
|
+
"arinc429_conformance_ok": a429_conf,
|
|
995
|
+
"m1553_schedule_computed": m1553_ok,
|
|
996
|
+
"m1553_conformance_ok": m1553_conf,
|
|
997
|
+
"afdx_budget_checkable": afdx_ok,
|
|
998
|
+
"observations_listed": isinstance(model.get("observations"), list),
|
|
999
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
1000
|
+
}
|
|
1001
|
+
results["all_pass"] = all(results.values())
|
|
1002
|
+
return results
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def check_assessment_markdown(md_text: str) -> dict:
|
|
1006
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
1007
|
+
low = md_text.lower()
|
|
1008
|
+
checks = {
|
|
1009
|
+
"has_title": "avionics data bus loading and protocol assessment" in low,
|
|
1010
|
+
"has_a429": "arinc 429" in low and "utilization" in low,
|
|
1011
|
+
"has_m1553": "mil-std-1553" in low and "utilization" in low,
|
|
1012
|
+
"has_afdx": "afdx" in low,
|
|
1013
|
+
"has_percent": "%" in low,
|
|
1014
|
+
"has_conformance": "parity" in low,
|
|
1015
|
+
"has_draft_marker": "draft" in low,
|
|
1016
|
+
"has_not_approval": "not an approval" in low,
|
|
1017
|
+
}
|
|
1018
|
+
checks["all_pass"] = all(checks.values())
|
|
1019
|
+
return checks
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
1023
|
+
"""Public entry point used by gate tooling: check an assessment doc."""
|
|
1024
|
+
return check_assessment_markdown(md_text)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
# ---------------------------------------------------------------------------
|
|
1028
|
+
# Example item: EX-100 reference bus architecture (worked example)
|
|
1029
|
+
# ---------------------------------------------------------------------------
|
|
1030
|
+
|
|
1031
|
+
def example_item() -> DataBusItem:
|
|
1032
|
+
"""Reference example: transport avionics bus architecture with
|
|
1033
|
+
ARINC 429 TX lines, one MIL-STD-1553 bus, and an AFDX VL set."""
|
|
1034
|
+
return DataBusItem(
|
|
1035
|
+
aircraft="EX-100 Reference Example Transport",
|
|
1036
|
+
description="Flight deck avionics data bus architecture: ADC, IRS "
|
|
1037
|
+
"and legacy utility concentrator ARINC 429 transmit "
|
|
1038
|
+
"lines, a dual-redundant MIL-STD-1553 bus under one "
|
|
1039
|
+
"bus controller, and a growth AFDX core network.",
|
|
1040
|
+
arinc429_lines=[
|
|
1041
|
+
A429Line(
|
|
1042
|
+
name="A429-1 ADC/FMS data",
|
|
1043
|
+
transmitter="ADC-1",
|
|
1044
|
+
receivers="FMS-1, EFIS-1",
|
|
1045
|
+
link_rate_bps=A429_RATE_100_KBPS,
|
|
1046
|
+
label_rates={
|
|
1047
|
+
"010": 25.0, "011": 25.0, "036": 50.0, "100": 50.0,
|
|
1048
|
+
"203": 12.5, "210": 25.0, "231": 25.0, "320": 12.5,
|
|
1049
|
+
"365": 12.5, "370": 12.5,
|
|
1050
|
+
}),
|
|
1051
|
+
A429Line(
|
|
1052
|
+
name="A429-2 IRS/FCC data",
|
|
1053
|
+
transmitter="IRS-1",
|
|
1054
|
+
receivers="FCC-1, EFIS-1",
|
|
1055
|
+
link_rate_bps=A429_RATE_100_KBPS,
|
|
1056
|
+
label_rates={
|
|
1057
|
+
"320": 25.0, "365": 25.0, "366": 25.0, "367": 25.0,
|
|
1058
|
+
"370": 25.0, "371": 25.0, "372": 25.0, "375": 12.5,
|
|
1059
|
+
"376": 12.5,
|
|
1060
|
+
}),
|
|
1061
|
+
A429Line(
|
|
1062
|
+
name="A429-3 fuel/utility (low speed)",
|
|
1063
|
+
transmitter="UTA-1",
|
|
1064
|
+
receivers="EICAS-1",
|
|
1065
|
+
link_rate_bps=A429_RATE_12_5_KBPS,
|
|
1066
|
+
label_rates={
|
|
1067
|
+
"250": 20.0, "251": 20.0, "252": 20.0, "253": 20.0,
|
|
1068
|
+
"254": 20.0, "255": 20.0, "256": 20.0, "257": 20.0,
|
|
1069
|
+
"260": 20.0, "261": 20.0, "262": 20.0, "263": 20.0,
|
|
1070
|
+
"264": 20.0,
|
|
1071
|
+
}),
|
|
1072
|
+
],
|
|
1073
|
+
m1553_bus=M1553Bus(
|
|
1074
|
+
name="1553-B1 mission bus",
|
|
1075
|
+
controller="Mission Computer (MC-1)",
|
|
1076
|
+
remote_terminals="RT 1 (FMS-1), RT 2 (NAV-1), RT 3 (FCC-1)",
|
|
1077
|
+
frame_us=M1553_FRAME_US_DEFAULT,
|
|
1078
|
+
messages=[
|
|
1079
|
+
("BCRT", 32), ("RTBC", 16), ("BCRT", 16), ("RTRT", 8),
|
|
1080
|
+
("RTBC", 8), ("BCRT", 8), ("BCRT", 4), ("RTRT", 4),
|
|
1081
|
+
]),
|
|
1082
|
+
afdx_network=AfdxNetwork(
|
|
1083
|
+
name="AFDX core network (growth)",
|
|
1084
|
+
vl_specs=[(16, 1518)] * 6 + [(32, 1518)] * 4 + [(128, 1518)] * 2
|
|
1085
|
+
+ [(4, 1518), (8, 1518)],
|
|
1086
|
+
jitter_measured_us=420.0,
|
|
1087
|
+
jitter_budget_us=500.0,
|
|
1088
|
+
switch_count=2,
|
|
1089
|
+
switch_delay_us=150.0,
|
|
1090
|
+
),
|
|
1091
|
+
a429_decode_sample=0x60134908, # label 010, SDI 1, data 1234
|
|
1092
|
+
a429_sample_expected_label="010",
|
|
1093
|
+
a429_bnr_scale=0.1,
|
|
1094
|
+
m1553_command_sample=93316, # RT5 SA12 WC16 RT-to-BC
|
|
1095
|
+
m1553_status_sample=606276, # RT5 busy
|
|
1096
|
+
certification_basis="MIL-STD-1553B / ARINC 429 Mark 33 DITS / "
|
|
1097
|
+
"ARINC 664 Part 7 protocol practice",
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def example_assessment_markdown() -> str:
|
|
1102
|
+
item = example_item()
|
|
1103
|
+
return render_assessment_markdown(build_assessment(item,
|
|
1104
|
+
analyze_architecture(item)))
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
if __name__ == "__main__":
|
|
1108
|
+
item = example_item()
|
|
1109
|
+
results = analyze_architecture(item)
|
|
1110
|
+
model = build_assessment(item, results)
|
|
1111
|
+
md = render_assessment_markdown(model)
|
|
1112
|
+
a429_max = max(b["summary"]["utilization_pct"]
|
|
1113
|
+
for b in model["arinc429"])
|
|
1114
|
+
print("ARINC 429 lines: %d, max utilization %.2f%%"
|
|
1115
|
+
% (len(model["arinc429"]), a429_max))
|
|
1116
|
+
print("MIL-STD-1553 utilization: %.2f%%"
|
|
1117
|
+
% model["m1553"]["schedule"]["utilization_pct"])
|
|
1118
|
+
print("AFDX utilization: %.2f%%, oversubscribed=%s"
|
|
1119
|
+
% (model["afdx"]["utilization_pct"], model["afdx"]["oversubscribed"]))
|
|
1120
|
+
print("GATES: %s" % check_assessment(model))
|
|
1121
|
+
print("RENDERED: %d chars" % len(md))
|