oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) 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/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.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)