oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

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.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,708 @@
1
+ """BACnet protocol analyzer for IP and MSTP variants.
2
+
3
+ This module provides comprehensive BACnet (Building Automation and Control Networks)
4
+ protocol analysis supporting both BACnet/IP (UDP port 47808) and BACnet/MSTP
5
+ (Master-Slave/Token-Passing serial) variants. Decodes NPDU/APDU layers, all service
6
+ types, and discovers devices and objects for HVAC and building automation systems.
7
+
8
+ Example:
9
+ >>> from oscura.analyzers.protocols.industrial.bacnet import BACnetAnalyzer
10
+ >>> analyzer = BACnetAnalyzer()
11
+ >>> # Parse BACnet/IP message from UDP packet
12
+ >>> udp_payload = bytes([0x81, 0x0A, 0x00, 0x11, 0x01, 0x20, 0x00, 0x08, ...])
13
+ >>> message = analyzer.parse_bacnet_ip(udp_payload, timestamp=0.0)
14
+ >>> print(f"{message.service_name}: {message.decoded_service}")
15
+ >>> # Export discovered devices
16
+ >>> analyzer.export_devices(Path("bacnet_devices.json"))
17
+
18
+ References:
19
+ ANSI/ASHRAE Standard 135-2020 (BACnet):
20
+ https://www.ashrae.org/technical-resources/bookstore/bacnet
21
+
22
+ BACnet/IP (Annex J):
23
+ http://www.bacnet.org/Addenda/Add-135-2016bj-1_chair_approved.pdf
24
+
25
+ BACnet MS/TP (Annex G):
26
+ http://www.bacnet.org/Addenda/Add-135-2012g-5_PPR2-redline.pdf
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+ from typing import Any, ClassVar
35
+
36
+ from oscura.analyzers.protocols.industrial.bacnet.services import (
37
+ decode_i_am,
38
+ decode_i_have,
39
+ decode_read_property_ack,
40
+ decode_read_property_request,
41
+ decode_who_has,
42
+ decode_who_is,
43
+ decode_write_property_request,
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class BACnetMessage:
49
+ """BACnet message representation.
50
+
51
+ Attributes:
52
+ timestamp: Message timestamp in seconds.
53
+ protocol: Protocol variant ("BACnet/IP" or "BACnet/MSTP").
54
+ npdu: Network Protocol Data Unit parsed fields.
55
+ apdu_type: APDU type name (Confirmed-REQ, Unconfirmed-REQ, etc.).
56
+ service_choice: Service choice number.
57
+ service_name: Human-readable service name.
58
+ invoke_id: Invoke ID for confirmed services.
59
+ payload: Raw APDU payload bytes.
60
+ decoded_service: Service-specific decoded data.
61
+ """
62
+
63
+ timestamp: float
64
+ protocol: str # "BACnet/IP" or "BACnet/MSTP"
65
+ npdu: dict[str, Any]
66
+ apdu_type: str
67
+ service_choice: int | None = None
68
+ service_name: str | None = None
69
+ invoke_id: int | None = None
70
+ payload: bytes = b""
71
+ decoded_service: dict[str, Any] = field(default_factory=dict)
72
+
73
+
74
+ @dataclass
75
+ class BACnetObject:
76
+ """BACnet object representation.
77
+
78
+ Attributes:
79
+ object_type: Object type name (analog-input, binary-output, device, etc.).
80
+ instance_number: Object instance number.
81
+ properties: Observed property values (property_id -> value).
82
+ """
83
+
84
+ object_type: str
85
+ instance_number: int
86
+ properties: dict[str, Any] = field(default_factory=dict)
87
+
88
+
89
+ @dataclass
90
+ class BACnetDevice:
91
+ """BACnet device information.
92
+
93
+ Attributes:
94
+ device_instance: Device instance number.
95
+ device_name: Device name (if discovered).
96
+ vendor_id: Vendor identifier (if discovered).
97
+ model_name: Model name (if discovered).
98
+ objects: List of discovered objects on this device.
99
+ """
100
+
101
+ device_instance: int
102
+ device_name: str | None = None
103
+ vendor_id: int | None = None
104
+ model_name: str | None = None
105
+ objects: list[BACnetObject] = field(default_factory=list)
106
+
107
+
108
+ class BACnetAnalyzer:
109
+ """BACnet protocol analyzer for IP and MSTP variants.
110
+
111
+ Supports parsing BACnet/IP (UDP) and BACnet/MSTP (serial) messages,
112
+ decoding NPDU/APDU layers, all service types, and discovering devices
113
+ and objects on the network.
114
+
115
+ Attributes:
116
+ messages: List of all parsed BACnet messages.
117
+ devices: Dictionary of discovered devices (device_instance -> BACnetDevice).
118
+ """
119
+
120
+ # APDU types (ASHRAE 135-2020, Clause 20.1.2)
121
+ APDU_TYPES: ClassVar[dict[int, str]] = {
122
+ 0: "Confirmed-REQ",
123
+ 1: "Unconfirmed-REQ",
124
+ 2: "SimpleACK",
125
+ 3: "ComplexACK",
126
+ 4: "SegmentACK",
127
+ 5: "Error",
128
+ 6: "Reject",
129
+ 7: "Abort",
130
+ }
131
+
132
+ # Confirmed services (ASHRAE 135-2020, Clause 21.1)
133
+ CONFIRMED_SERVICES: ClassVar[dict[int, str]] = {
134
+ 0: "acknowledgeAlarm",
135
+ 1: "confirmedCOVNotification",
136
+ 2: "confirmedEventNotification",
137
+ 3: "getAlarmSummary",
138
+ 4: "getEnrollmentSummary",
139
+ 5: "subscribeCOV",
140
+ 6: "atomicReadFile",
141
+ 7: "atomicWriteFile",
142
+ 8: "addListElement",
143
+ 9: "removeListElement",
144
+ 10: "createObject",
145
+ 11: "deleteObject",
146
+ 12: "readProperty",
147
+ 13: "readPropertyConditional",
148
+ 14: "readPropertyMultiple",
149
+ 15: "writeProperty",
150
+ 16: "writePropertyMultiple",
151
+ 17: "deviceCommunicationControl",
152
+ 18: "confirmedPrivateTransfer",
153
+ 19: "confirmedTextMessage",
154
+ 20: "reinitializeDevice",
155
+ }
156
+
157
+ # Unconfirmed services (ASHRAE 135-2020, Clause 21.2)
158
+ UNCONFIRMED_SERVICES: ClassVar[dict[int, str]] = {
159
+ 0: "i-Am",
160
+ 1: "i-Have",
161
+ 2: "unconfirmedCOVNotification",
162
+ 3: "unconfirmedEventNotification",
163
+ 4: "unconfirmedPrivateTransfer",
164
+ 5: "unconfirmedTextMessage",
165
+ 6: "timeSynchronization",
166
+ 7: "who-Has",
167
+ 8: "who-Is",
168
+ 9: "utcTimeSynchronization",
169
+ }
170
+
171
+ def __init__(self) -> None:
172
+ """Initialize BACnet analyzer."""
173
+ self.messages: list[BACnetMessage] = []
174
+ self.devices: dict[int, BACnetDevice] = {}
175
+
176
+ def parse_bacnet_ip(self, udp_payload: bytes, timestamp: float = 0.0) -> BACnetMessage:
177
+ """Parse BACnet/IP message from UDP payload (port 47808).
178
+
179
+ BACnet/IP messages use the BACnet Virtual Link Control (BVLC) layer
180
+ before the NPDU/APDU layers.
181
+
182
+ Args:
183
+ udp_payload: Raw UDP payload bytes.
184
+ timestamp: Message timestamp in seconds.
185
+
186
+ Returns:
187
+ Parsed BACnet message.
188
+
189
+ Raises:
190
+ ValueError: If message is too short or has invalid BVLC header.
191
+
192
+ Example:
193
+ >>> analyzer = BACnetAnalyzer()
194
+ >>> udp_data = bytes([0x81, 0x0A, 0x00, 0x11, ...])
195
+ >>> msg = analyzer.parse_bacnet_ip(udp_data, timestamp=1.23)
196
+ >>> print(f"{msg.service_name}: {msg.decoded_service}")
197
+ """
198
+ if len(udp_payload) < 4:
199
+ raise ValueError("BACnet/IP message too short")
200
+
201
+ # Parse BVLC header (Annex J)
202
+ bvlc_type = udp_payload[0]
203
+ bvlc_function = udp_payload[1]
204
+ # bvlc_length = int.from_bytes(udp_payload[2:4], "big") # Not used in current implementation
205
+
206
+ if bvlc_type != 0x81:
207
+ raise ValueError(f"Invalid BACnet/IP type: 0x{bvlc_type:02X} (expected 0x81)")
208
+
209
+ # BVLC function 0x0A = Original-Unicast-NPDU
210
+ # BVLC function 0x0B = Original-Broadcast-NPDU
211
+ if bvlc_function not in (0x0A, 0x0B, 0x04):
212
+ raise ValueError(f"Unsupported BVLC function: 0x{bvlc_function:02X}")
213
+
214
+ # Parse NPDU starting at offset 4
215
+ npdu_data = udp_payload[4:]
216
+ npdu, npdu_len = self._parse_npdu(npdu_data)
217
+
218
+ # Parse APDU
219
+ apdu_data = npdu_data[npdu_len:]
220
+ apdu_dict = self._parse_apdu(apdu_data)
221
+
222
+ # Decode service-specific payload
223
+ decoded_service = self._decode_service(
224
+ apdu_dict["apdu_type"],
225
+ apdu_dict.get("service_choice"),
226
+ apdu_dict.get("service_data", b""),
227
+ )
228
+
229
+ message = BACnetMessage(
230
+ timestamp=timestamp,
231
+ protocol="BACnet/IP",
232
+ npdu=npdu,
233
+ apdu_type=self.APDU_TYPES.get(apdu_dict["apdu_type"], "Unknown"),
234
+ service_choice=apdu_dict.get("service_choice"),
235
+ service_name=apdu_dict.get("service_name"),
236
+ invoke_id=apdu_dict.get("invoke_id"),
237
+ payload=apdu_data,
238
+ decoded_service=decoded_service,
239
+ )
240
+
241
+ self.messages.append(message)
242
+ self._update_device_info(message)
243
+
244
+ return message
245
+
246
+ def parse_bacnet_mstp(self, serial_data: bytes, timestamp: float = 0.0) -> BACnetMessage:
247
+ """Parse BACnet MS/TP (Master-Slave/Token-Passing) frame from serial data.
248
+
249
+ BACnet MS/TP is a token-passing protocol for serial (RS-485) links.
250
+ Frame format: Preamble (0x55 0xFF) + Header (6 bytes) + Data + CRC.
251
+
252
+ Args:
253
+ serial_data: Raw serial data bytes.
254
+ timestamp: Message timestamp in seconds.
255
+
256
+ Returns:
257
+ Parsed BACnet message.
258
+
259
+ Raises:
260
+ ValueError: If frame is too short or has invalid preamble/CRC.
261
+
262
+ Example:
263
+ >>> analyzer = BACnetAnalyzer()
264
+ >>> serial_frame = bytes([0x55, 0xFF, 0x05, 0x01, 0x00, ...])
265
+ >>> msg = analyzer.parse_bacnet_mstp(serial_frame, timestamp=2.34)
266
+ """
267
+ if len(serial_data) < 8:
268
+ raise ValueError("BACnet MSTP frame too short")
269
+
270
+ # Check preamble (Annex G)
271
+ if serial_data[0] != 0x55 or serial_data[1] != 0xFF:
272
+ raise ValueError("Invalid MSTP preamble (expected 0x55 0xFF)")
273
+
274
+ # Parse MSTP header (6 bytes after preamble)
275
+ frame_type = serial_data[2]
276
+ # destination_address = serial_data[3] # Not used in current implementation
277
+ # source_address = serial_data[4] # Not used in current implementation
278
+ data_length = int.from_bytes(serial_data[5:7], "big")
279
+ header_crc = serial_data[7]
280
+
281
+ # Verify header CRC (simple XOR for demonstration; real MSTP uses proper CRC)
282
+ calculated_header_crc = self._mstp_header_crc(serial_data[2:7])
283
+ if calculated_header_crc != header_crc:
284
+ raise ValueError(
285
+ f"MSTP header CRC mismatch: {header_crc:02X} != {calculated_header_crc:02X}"
286
+ )
287
+
288
+ # Extract data and verify data CRC if present
289
+ if data_length > 0:
290
+ if len(serial_data) < 8 + data_length + 2:
291
+ raise ValueError("MSTP frame truncated")
292
+ data_payload = serial_data[8 : 8 + data_length]
293
+ data_crc = int.from_bytes(serial_data[8 + data_length : 8 + data_length + 2], "big")
294
+
295
+ calculated_data_crc = self._mstp_data_crc(data_payload)
296
+ if calculated_data_crc != data_crc:
297
+ raise ValueError(
298
+ f"MSTP data CRC mismatch: {data_crc:04X} != {calculated_data_crc:04X}"
299
+ )
300
+ else:
301
+ data_payload = b""
302
+
303
+ # Parse NPDU from data payload (frame type 0x05 = BACnet Data Expecting Reply)
304
+ if frame_type in (0x00, 0x01, 0x05): # Data frames
305
+ npdu, npdu_len = self._parse_npdu(data_payload)
306
+ apdu_data = data_payload[npdu_len:]
307
+ apdu_dict = self._parse_apdu(apdu_data)
308
+
309
+ decoded_service = self._decode_service(
310
+ apdu_dict["apdu_type"],
311
+ apdu_dict.get("service_choice"),
312
+ apdu_dict.get("service_data", b""),
313
+ )
314
+
315
+ message = BACnetMessage(
316
+ timestamp=timestamp,
317
+ protocol="BACnet/MSTP",
318
+ npdu=npdu,
319
+ apdu_type=self.APDU_TYPES.get(apdu_dict["apdu_type"], "Unknown"),
320
+ service_choice=apdu_dict.get("service_choice"),
321
+ service_name=apdu_dict.get("service_name"),
322
+ invoke_id=apdu_dict.get("invoke_id"),
323
+ payload=apdu_data,
324
+ decoded_service=decoded_service,
325
+ )
326
+
327
+ self.messages.append(message)
328
+ self._update_device_info(message)
329
+
330
+ return message
331
+ else:
332
+ # Non-data frame (token, poll, etc.)
333
+ raise ValueError(f"MSTP frame type {frame_type:02X} not supported")
334
+
335
+ def _parse_npdu(self, data: bytes) -> tuple[dict[str, Any], int]:
336
+ """Parse NPDU (Network Protocol Data Unit).
337
+
338
+ Args:
339
+ data: Raw NPDU bytes.
340
+
341
+ Returns:
342
+ Tuple of (npdu_dict, bytes_consumed).
343
+
344
+ Raises:
345
+ ValueError: If NPDU is too short or has invalid version.
346
+ """
347
+ if len(data) < 2:
348
+ raise ValueError("NPDU too short")
349
+
350
+ version = data[0]
351
+ control = data[1]
352
+ offset = 2
353
+
354
+ if version != 0x01:
355
+ raise ValueError(f"Invalid NPDU version: {version} (expected 0x01)")
356
+
357
+ npdu_dict: dict[str, Any] = {
358
+ "version": version,
359
+ "control": control,
360
+ "network_priority": (control >> 0) & 0x03,
361
+ "dest_specifier": bool(control & 0x20),
362
+ "source_specifier": bool(control & 0x08),
363
+ "expects_reply": bool(control & 0x04),
364
+ "is_network_message": bool(control & 0x80),
365
+ }
366
+
367
+ # Parse destination network address if present
368
+ if npdu_dict["dest_specifier"]:
369
+ if offset + 3 > len(data):
370
+ return npdu_dict, offset
371
+ dest_network = int.from_bytes(data[offset : offset + 2], "big")
372
+ dest_mac_len = data[offset + 2]
373
+ offset += 3
374
+
375
+ if offset + dest_mac_len > len(data):
376
+ return npdu_dict, offset
377
+ dest_mac = data[offset : offset + dest_mac_len]
378
+ offset += dest_mac_len
379
+
380
+ npdu_dict["dest_network"] = dest_network
381
+ npdu_dict["dest_mac"] = dest_mac.hex()
382
+
383
+ # Parse source network address if present
384
+ if npdu_dict["source_specifier"]:
385
+ if offset + 3 > len(data):
386
+ return npdu_dict, offset
387
+ source_network = int.from_bytes(data[offset : offset + 2], "big")
388
+ source_mac_len = data[offset + 2]
389
+ offset += 3
390
+
391
+ if offset + source_mac_len > len(data):
392
+ return npdu_dict, offset
393
+ source_mac = data[offset : offset + source_mac_len]
394
+ offset += source_mac_len
395
+
396
+ npdu_dict["source_network"] = source_network
397
+ npdu_dict["source_mac"] = source_mac.hex()
398
+
399
+ # Parse hop count if destination specified
400
+ if npdu_dict["dest_specifier"]:
401
+ if offset < len(data):
402
+ npdu_dict["hop_count"] = data[offset]
403
+ offset += 1
404
+
405
+ # Parse network message type if network message
406
+ if npdu_dict["is_network_message"]:
407
+ if offset < len(data):
408
+ npdu_dict["network_message_type"] = data[offset]
409
+ offset += 1
410
+
411
+ return npdu_dict, offset
412
+
413
+ def _parse_apdu(self, data: bytes) -> dict[str, Any]:
414
+ """Parse APDU (Application Protocol Data Unit).
415
+
416
+ Args:
417
+ data: Raw APDU bytes.
418
+
419
+ Returns:
420
+ Dictionary with apdu_type, service_choice, invoke_id, and service_data.
421
+
422
+ Raises:
423
+ ValueError: If APDU is too short.
424
+ """
425
+ if len(data) < 1:
426
+ raise ValueError("APDU too short")
427
+
428
+ apdu_type = (data[0] >> 4) & 0x0F
429
+ apdu_dict: dict[str, Any] = {"apdu_type": apdu_type}
430
+
431
+ if apdu_type == 0: # Confirmed-REQ
432
+ self._parse_confirmed_req(data, apdu_dict)
433
+ elif apdu_type == 1: # Unconfirmed-REQ
434
+ self._parse_unconfirmed_req(data, apdu_dict)
435
+ elif apdu_type == 2: # SimpleACK
436
+ self._parse_simple_ack(data, apdu_dict)
437
+ elif apdu_type == 3: # ComplexACK
438
+ self._parse_complex_ack(data, apdu_dict)
439
+ elif apdu_type == 5: # Error
440
+ self._parse_error(data, apdu_dict)
441
+ elif apdu_type == 6: # Reject
442
+ self._parse_reject(data, apdu_dict)
443
+ elif apdu_type == 7: # Abort
444
+ self._parse_abort(data, apdu_dict)
445
+
446
+ return apdu_dict
447
+
448
+ def _parse_confirmed_req(self, data: bytes, apdu_dict: dict[str, Any]) -> None:
449
+ """Parse Confirmed-REQ APDU type."""
450
+ if len(data) < 3:
451
+ raise ValueError("Confirmed-REQ APDU too short")
452
+
453
+ segmented = bool(data[0] & 0x08)
454
+ more_follows = bool(data[0] & 0x04)
455
+ segmented_response_accepted = bool(data[0] & 0x02)
456
+ max_segments = data[1] >> 4
457
+ max_apdu = data[1] & 0x0F
458
+ invoke_id = data[2]
459
+ service_choice = data[3] if len(data) > 3 else 0
460
+
461
+ apdu_dict.update(
462
+ {
463
+ "segmented": segmented,
464
+ "more_follows": more_follows,
465
+ "segmented_response_accepted": segmented_response_accepted,
466
+ "max_segments": max_segments,
467
+ "max_apdu": max_apdu,
468
+ "invoke_id": invoke_id,
469
+ "service_choice": service_choice,
470
+ "service_name": self.CONFIRMED_SERVICES.get(
471
+ service_choice, f"service-{service_choice}"
472
+ ),
473
+ "service_data": data[4:] if len(data) > 4 else b"",
474
+ }
475
+ )
476
+
477
+ def _parse_unconfirmed_req(self, data: bytes, apdu_dict: dict[str, Any]) -> None:
478
+ """Parse Unconfirmed-REQ APDU type."""
479
+ if len(data) < 2:
480
+ raise ValueError("Unconfirmed-REQ APDU too short")
481
+
482
+ service_choice = data[1]
483
+ apdu_dict.update(
484
+ {
485
+ "service_choice": service_choice,
486
+ "service_name": self.UNCONFIRMED_SERVICES.get(
487
+ service_choice, f"service-{service_choice}"
488
+ ),
489
+ "service_data": data[2:] if len(data) > 2 else b"",
490
+ }
491
+ )
492
+
493
+ def _parse_simple_ack(self, data: bytes, apdu_dict: dict[str, Any]) -> None:
494
+ """Parse SimpleACK APDU type."""
495
+ if len(data) < 3:
496
+ raise ValueError("SimpleACK APDU too short")
497
+
498
+ invoke_id = data[1]
499
+ service_choice = data[2]
500
+ apdu_dict.update(
501
+ {
502
+ "invoke_id": invoke_id,
503
+ "service_choice": service_choice,
504
+ "service_name": self.CONFIRMED_SERVICES.get(
505
+ service_choice, f"service-{service_choice}"
506
+ ),
507
+ }
508
+ )
509
+
510
+ def _parse_complex_ack(self, data: bytes, apdu_dict: dict[str, Any]) -> None:
511
+ """Parse ComplexACK APDU type."""
512
+ if len(data) < 3:
513
+ raise ValueError("ComplexACK APDU too short")
514
+
515
+ segmented = bool(data[0] & 0x08)
516
+ more_follows = bool(data[0] & 0x04)
517
+ invoke_id = data[1]
518
+ service_choice = data[2]
519
+
520
+ apdu_dict.update(
521
+ {
522
+ "segmented": segmented,
523
+ "more_follows": more_follows,
524
+ "invoke_id": invoke_id,
525
+ "service_choice": service_choice,
526
+ "service_name": self.CONFIRMED_SERVICES.get(
527
+ service_choice, f"service-{service_choice}"
528
+ ),
529
+ "service_data": data[3:] if len(data) > 3 else b"",
530
+ }
531
+ )
532
+
533
+ def _parse_error(self, data: bytes, apdu_dict: dict[str, Any]) -> None:
534
+ """Parse Error APDU type."""
535
+ if len(data) < 3:
536
+ raise ValueError("Error APDU too short")
537
+
538
+ invoke_id = data[1]
539
+ service_choice = data[2]
540
+ apdu_dict.update(
541
+ {
542
+ "invoke_id": invoke_id,
543
+ "service_choice": service_choice,
544
+ "service_name": self.CONFIRMED_SERVICES.get(
545
+ service_choice, f"service-{service_choice}"
546
+ ),
547
+ "service_data": data[3:] if len(data) > 3 else b"",
548
+ }
549
+ )
550
+
551
+ def _parse_reject(self, data: bytes, apdu_dict: dict[str, Any]) -> None:
552
+ """Parse Reject APDU type."""
553
+ if len(data) < 3:
554
+ raise ValueError("Reject APDU too short")
555
+
556
+ invoke_id = data[1]
557
+ reject_reason = data[2]
558
+ apdu_dict.update({"invoke_id": invoke_id, "reject_reason": reject_reason})
559
+
560
+ def _parse_abort(self, data: bytes, apdu_dict: dict[str, Any]) -> None:
561
+ """Parse Abort APDU type."""
562
+ if len(data) < 3:
563
+ raise ValueError("Abort APDU too short")
564
+
565
+ invoke_id = data[1]
566
+ abort_reason = data[2]
567
+ apdu_dict.update({"invoke_id": invoke_id, "abort_reason": abort_reason})
568
+
569
+ def _decode_service(
570
+ self, apdu_type: int, service_choice: int | None, data: bytes
571
+ ) -> dict[str, Any]:
572
+ """Decode service-specific payload based on APDU type and service choice.
573
+
574
+ Args:
575
+ apdu_type: APDU type number.
576
+ service_choice: Service choice number.
577
+ data: Service payload bytes.
578
+
579
+ Returns:
580
+ Decoded service data dictionary.
581
+ """
582
+ if service_choice is None:
583
+ return {}
584
+
585
+ decoders = {
586
+ 1: self._decode_unconfirmed_service,
587
+ 0: self._decode_confirmed_service,
588
+ 3: self._decode_complex_ack,
589
+ }
590
+
591
+ decoder = decoders.get(apdu_type)
592
+ return decoder(service_choice, data) if decoder else {}
593
+
594
+ def _decode_unconfirmed_service(self, service_choice: int, data: bytes) -> dict[str, Any]:
595
+ """Decode unconfirmed service payloads."""
596
+ unconfirmed_decoders = {
597
+ 0: decode_i_am,
598
+ 1: decode_i_have,
599
+ 7: decode_who_has,
600
+ 8: decode_who_is,
601
+ }
602
+ decoder = unconfirmed_decoders.get(service_choice)
603
+ return decoder(data) if decoder else {}
604
+
605
+ def _decode_confirmed_service(self, service_choice: int, data: bytes) -> dict[str, Any]:
606
+ """Decode confirmed service request payloads."""
607
+ confirmed_decoders = {
608
+ 12: decode_read_property_request,
609
+ 15: decode_write_property_request,
610
+ }
611
+ decoder = confirmed_decoders.get(service_choice)
612
+ return decoder(data) if decoder else {}
613
+
614
+ def _decode_complex_ack(self, service_choice: int, data: bytes) -> dict[str, Any]:
615
+ """Decode ComplexACK response payloads."""
616
+ if service_choice == 12:
617
+ return decode_read_property_ack(data)
618
+ return {}
619
+
620
+ def _update_device_info(self, message: BACnetMessage) -> None:
621
+ """Update device information from parsed message.
622
+
623
+ Args:
624
+ message: Parsed BACnet message.
625
+ """
626
+ # Extract device info from I-Am messages
627
+ if message.service_name == "i-Am" and "device_instance" in message.decoded_service:
628
+ device_instance = message.decoded_service["device_instance"]
629
+
630
+ if device_instance not in self.devices:
631
+ self.devices[device_instance] = BACnetDevice(device_instance=device_instance)
632
+
633
+ device = self.devices[device_instance]
634
+
635
+ # Update device properties from I-Am
636
+ if "vendor_id" in message.decoded_service:
637
+ device.vendor_id = message.decoded_service["vendor_id"]
638
+
639
+ def _mstp_header_crc(self, header: bytes) -> int:
640
+ """Calculate MSTP header CRC (simplified XOR for demonstration).
641
+
642
+ Args:
643
+ header: Header bytes (5 bytes).
644
+
645
+ Returns:
646
+ CRC value.
647
+ """
648
+ # Real implementation should use proper CRC-8 (CCITT)
649
+ # This is a simplified version for demonstration
650
+ crc = 0xFF
651
+ for byte in header:
652
+ crc ^= byte
653
+ return crc
654
+
655
+ def _mstp_data_crc(self, data: bytes) -> int:
656
+ """Calculate MSTP data CRC (simplified for demonstration).
657
+
658
+ Args:
659
+ data: Data bytes.
660
+
661
+ Returns:
662
+ CRC value (16-bit).
663
+ """
664
+ # Real implementation should use proper CRC-16 (CCITT)
665
+ # This is a simplified version for demonstration
666
+ crc = 0xFFFF
667
+ for byte in data:
668
+ crc ^= byte << 8
669
+ for _ in range(8):
670
+ if crc & 0x8000:
671
+ crc = (crc << 1) ^ 0x1021
672
+ else:
673
+ crc <<= 1
674
+ crc &= 0xFFFF
675
+ return crc
676
+
677
+ def export_devices(self, output_path: Path) -> None:
678
+ """Export discovered devices and object lists as JSON.
679
+
680
+ Args:
681
+ output_path: Output file path for JSON export.
682
+
683
+ Example:
684
+ >>> analyzer = BACnetAnalyzer()
685
+ >>> # ... parse messages ...
686
+ >>> analyzer.export_devices(Path("bacnet_devices.json"))
687
+ """
688
+ devices_data = []
689
+
690
+ for device in self.devices.values():
691
+ device_dict = {
692
+ "device_instance": device.device_instance,
693
+ "device_name": device.device_name,
694
+ "vendor_id": device.vendor_id,
695
+ "model_name": device.model_name,
696
+ "objects": [
697
+ {
698
+ "object_type": obj.object_type,
699
+ "instance_number": obj.instance_number,
700
+ "properties": obj.properties,
701
+ }
702
+ for obj in device.objects
703
+ ],
704
+ }
705
+ devices_data.append(device_dict)
706
+
707
+ with output_path.open("w") as f:
708
+ json.dump(devices_data, f, indent=2)