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,23 @@
1
+ """PROFINET IO protocol analyzer package.
2
+
3
+ This package provides comprehensive PROFINET protocol analysis for real-time
4
+ industrial Ethernet communication, including RT, IRT, DCP, and PTCP protocols.
5
+
6
+ Example:
7
+ >>> from oscura.analyzers.protocols.industrial.profinet import ProfinetAnalyzer
8
+ >>> analyzer = ProfinetAnalyzer()
9
+ >>> frame = analyzer.parse_frame(ethernet_data, timestamp=0.0)
10
+ >>> devices = analyzer.discover_devices()
11
+
12
+ References:
13
+ PROFINET Specification V2.4 (IEC 61158 / IEC 61784):
14
+ https://www.profibus.com/download/profinet-specification/
15
+ """
16
+
17
+ from oscura.analyzers.protocols.industrial.profinet.analyzer import (
18
+ ProfinetAnalyzer,
19
+ ProfinetDevice,
20
+ ProfinetFrame,
21
+ )
22
+
23
+ __all__ = ["ProfinetAnalyzer", "ProfinetDevice", "ProfinetFrame"]
@@ -0,0 +1,441 @@
1
+ """PROFINET IO protocol analyzer.
2
+
3
+ This module provides comprehensive PROFINET protocol analysis for real-time
4
+ industrial Ethernet communication. Supports RT (Real-Time), IRT (Isochronous
5
+ Real-Time), DCP (Discovery and Configuration), and PTCP (Precision Time) protocols.
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.protocols.industrial.profinet import ProfinetAnalyzer
9
+ >>> analyzer = ProfinetAnalyzer()
10
+ >>> # Parse Ethernet frame containing PROFINET data
11
+ >>> frame = analyzer.parse_frame(ethernet_frame, timestamp=0.0)
12
+ >>> print(f"Frame Type: {frame.frame_type}, Frame ID: 0x{frame.frame_id:04X}")
13
+ >>> # Discover devices from DCP Identify responses
14
+ >>> devices = analyzer.discover_devices()
15
+ >>> for device in devices:
16
+ ... print(f"Device: {device.device_name} at {device.mac_address}")
17
+ >>> # Export topology
18
+ >>> analyzer.export_topology(Path("profinet_topology.json"))
19
+
20
+ References:
21
+ PROFINET Specification V2.4:
22
+ https://www.profibus.com/download/profinet-specification/
23
+
24
+ IEC 61158 / IEC 61784 (Industrial communication networks)
25
+ Wireshark PROFINET dissector
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ from dataclasses import dataclass, field
32
+ from pathlib import Path
33
+ from typing import Any, ClassVar
34
+
35
+ from oscura.analyzers.protocols.industrial.profinet.dcp import DCPParser
36
+ from oscura.analyzers.protocols.industrial.profinet.ptcp import PTCPParser
37
+
38
+
39
+ @dataclass
40
+ class ProfinetFrame:
41
+ """PROFINET frame representation.
42
+
43
+ Attributes:
44
+ timestamp: Frame timestamp in seconds.
45
+ frame_type: Frame type name (e.g., "RT_CLASS_1", "DCP", "PTCP").
46
+ frame_id: PROFINET frame ID (0x8000-0xFFFF).
47
+ source_mac: Source MAC address (XX:XX:XX:XX:XX:XX format).
48
+ dest_mac: Destination MAC address.
49
+ cycle_counter: Cycle counter for RT frames.
50
+ data_status: Data status byte for RT frames.
51
+ payload: Raw payload bytes.
52
+ decoded: Decoded frame data (varies by frame type).
53
+ """
54
+
55
+ timestamp: float
56
+ frame_type: str
57
+ frame_id: int
58
+ source_mac: str
59
+ dest_mac: str
60
+ cycle_counter: int | None = None
61
+ data_status: int | None = None
62
+ payload: bytes = b""
63
+ decoded: dict[str, Any] = field(default_factory=dict)
64
+
65
+
66
+ @dataclass
67
+ class ProfinetDevice:
68
+ """PROFINET device information.
69
+
70
+ Attributes:
71
+ mac_address: Device MAC address.
72
+ device_name: Device name from DCP (Name of Station).
73
+ device_type: Device type description.
74
+ vendor_id: Vendor ID from DCP Device ID block.
75
+ device_id: Device ID from DCP Device ID block.
76
+ station_type: Station type (IO-Controller, IO-Device, etc.).
77
+ modules: List of device modules/submodules.
78
+ ip_address: Device IP address (if configured).
79
+ subnet_mask: Subnet mask.
80
+ gateway: Gateway address.
81
+ """
82
+
83
+ mac_address: str
84
+ device_name: str | None = None
85
+ device_type: str | None = None
86
+ vendor_id: int | None = None
87
+ device_id: int | None = None
88
+ station_type: str = "DEVICE"
89
+ modules: list[dict[str, Any]] = field(default_factory=list)
90
+ ip_address: str | None = None
91
+ subnet_mask: str | None = None
92
+ gateway: str | None = None
93
+
94
+
95
+ class ProfinetAnalyzer:
96
+ """PROFINET IO protocol analyzer.
97
+
98
+ Provides comprehensive analysis of PROFINET frames including RT (Real-Time),
99
+ IRT (Isochronous Real-Time), DCP (Discovery and Configuration Protocol),
100
+ and PTCP (Precision Transparent Clock Protocol).
101
+
102
+ Attributes:
103
+ frames: List of parsed PROFINET frames.
104
+ devices: Dictionary of discovered devices by MAC address.
105
+
106
+ Example:
107
+ >>> analyzer = ProfinetAnalyzer()
108
+ >>> # Parse frame from raw Ethernet data
109
+ >>> frame = analyzer.parse_frame(eth_frame, timestamp=1.234)
110
+ >>> # Discover devices
111
+ >>> devices = analyzer.discover_devices()
112
+ >>> print(f"Found {len(devices)} PROFINET devices")
113
+ """
114
+
115
+ # PROFINET Frame ID ranges (0x0000-0xFFFF)
116
+ FRAME_ID_RANGES: ClassVar[list[tuple[tuple[int, int], str]]] = [
117
+ ((0x0000, 0x7FFF), "Reserved"),
118
+ ((0x8000, 0xBFFF), "RT_CLASS_1"), # Cyclic Real-Time data
119
+ ((0xC000, 0xFBFF), "RT_CLASS_UDP"), # RT over UDP
120
+ ((0xFC00, 0xFCFF), "RT_CLASS_2"), # IRT
121
+ ((0xFD00, 0xFDFF), "RT_CLASS_3"), # IRT with fragmentation
122
+ ((0xFE00, 0xFEFF), "Reserved for profiles"),
123
+ ((0xFF00, 0xFF1F), "Multicast MAC range"),
124
+ ((0xFF20, 0xFF3F), "PTCP"), # Precision Time Protocol - Delay
125
+ ((0xFF40, 0xFF4F), "PTCP"), # Precision Time Protocol - Sync
126
+ ((0xFF50, 0xFF8F), "PTCP"), # Reserved for PTCP
127
+ ((0xFF90, 0xFFFF), "Reserved"),
128
+ ]
129
+
130
+ # PROFINET EtherType
131
+ ETHERTYPE_PROFINET: ClassVar[int] = 0x8892
132
+
133
+ def __init__(self) -> None:
134
+ """Initialize PROFINET analyzer."""
135
+ self.frames: list[ProfinetFrame] = []
136
+ self.devices: dict[str, ProfinetDevice] = {}
137
+ self._dcp_parser = DCPParser()
138
+ self._ptcp_parser = PTCPParser()
139
+
140
+ def parse_frame(self, ethernet_frame: bytes, timestamp: float = 0.0) -> ProfinetFrame:
141
+ """Parse PROFINET frame from Ethernet payload.
142
+
143
+ Extracts PROFINET-specific data from raw Ethernet frame and decodes
144
+ based on frame type (RT, DCP, PTCP, etc.).
145
+
146
+ Ethernet Frame Format:
147
+ - Destination MAC (6 bytes)
148
+ - Source MAC (6 bytes)
149
+ - EtherType/Length (2 bytes) - should be 0x8892 for PROFINET
150
+ - PROFINET Data (variable)
151
+ - FCS (4 bytes, typically stripped by capture)
152
+
153
+ Args:
154
+ ethernet_frame: Complete Ethernet frame including headers.
155
+ timestamp: Frame timestamp in seconds.
156
+
157
+ Returns:
158
+ Parsed PROFINET frame.
159
+
160
+ Raises:
161
+ ValueError: If frame is too short or not a valid PROFINET frame.
162
+
163
+ Example:
164
+ >>> analyzer = ProfinetAnalyzer()
165
+ >>> eth_frame = bytes([...]) # Raw Ethernet frame
166
+ >>> frame = analyzer.parse_frame(eth_frame, timestamp=1.5)
167
+ >>> print(f"Frame type: {frame.frame_type}")
168
+ """
169
+ if len(ethernet_frame) < 14:
170
+ raise ValueError(f"Ethernet frame too short: {len(ethernet_frame)} bytes (minimum 14)")
171
+
172
+ # Parse Ethernet header and extract PROFINET payload
173
+ dest_mac, source_mac, profinet_data = self._parse_ethernet_header(ethernet_frame)
174
+
175
+ # Extract Frame ID and classify
176
+ frame_id = int.from_bytes(profinet_data[0:2], "big")
177
+ frame_type = self._classify_frame_id(frame_id)
178
+ frame_payload = profinet_data[2:]
179
+
180
+ # Decode frame based on type (may update frame_type for DCP)
181
+ frame_type, decoded, cycle_counter, data_status = self._decode_frame_payload(
182
+ frame_id, frame_type, frame_payload
183
+ )
184
+
185
+ frame = ProfinetFrame(
186
+ timestamp=timestamp,
187
+ frame_type=frame_type,
188
+ frame_id=frame_id,
189
+ source_mac=source_mac,
190
+ dest_mac=dest_mac,
191
+ cycle_counter=cycle_counter,
192
+ data_status=data_status,
193
+ payload=frame_payload,
194
+ decoded=decoded,
195
+ )
196
+
197
+ self.frames.append(frame)
198
+ self._update_device_info(frame)
199
+ return frame
200
+
201
+ def _parse_ethernet_header(self, ethernet_frame: bytes) -> tuple[str, str, bytes]:
202
+ """Parse Ethernet header and extract PROFINET payload."""
203
+ dest_mac = ":".join(f"{b:02x}" for b in ethernet_frame[0:6])
204
+ source_mac = ":".join(f"{b:02x}" for b in ethernet_frame[6:12])
205
+ ethertype = int.from_bytes(ethernet_frame[12:14], "big")
206
+
207
+ # Handle VLAN-tagged frames
208
+ payload_offset = 14
209
+ if ethertype == 0x8100:
210
+ if len(ethernet_frame) < 18:
211
+ raise ValueError("VLAN-tagged frame too short")
212
+ ethertype = int.from_bytes(ethernet_frame[16:18], "big")
213
+ payload_offset = 18
214
+
215
+ if ethertype != self.ETHERTYPE_PROFINET:
216
+ raise ValueError(
217
+ f"Not a PROFINET frame: EtherType 0x{ethertype:04X} "
218
+ f"(expected 0x{self.ETHERTYPE_PROFINET:04X})"
219
+ )
220
+
221
+ profinet_data = ethernet_frame[payload_offset:]
222
+ if len(profinet_data) < 2:
223
+ raise ValueError(f"PROFINET data too short: {len(profinet_data)} bytes")
224
+
225
+ return dest_mac, source_mac, profinet_data
226
+
227
+ def _decode_frame_payload(
228
+ self, frame_id: int, frame_type: str, frame_payload: bytes
229
+ ) -> tuple[str, dict[str, Any], int | None, int | None]:
230
+ """Decode frame payload based on frame type.
231
+
232
+ Returns:
233
+ Tuple of (frame_type, decoded, cycle_counter, data_status).
234
+ frame_type may be updated for DCP frames.
235
+ """
236
+ decoded: dict[str, Any] = {}
237
+ cycle_counter, data_status = None, None
238
+
239
+ if frame_type.startswith("RT_CLASS"):
240
+ decoded = self._parse_rt_frame(frame_id, frame_payload)
241
+ cycle_counter = decoded.get("cycle_counter")
242
+ data_status = decoded.get("data_status")
243
+
244
+ elif frame_type == "PTCP":
245
+ try:
246
+ decoded = self._ptcp_parser.parse_frame(frame_id, frame_payload)
247
+ except ValueError as e:
248
+ decoded = {"parse_error": str(e)}
249
+
250
+ elif frame_id in (0xFEFC, 0xFEFD):
251
+ frame_type = "DCP"
252
+ try:
253
+ decoded = self._dcp_parser.parse_frame(frame_payload)
254
+ except ValueError as e:
255
+ decoded = {"parse_error": str(e)}
256
+
257
+ return frame_type, decoded, cycle_counter, data_status
258
+
259
+ def _classify_frame_id(self, frame_id: int) -> str:
260
+ """Classify frame based on Frame ID range.
261
+
262
+ Args:
263
+ frame_id: PROFINET frame ID (0x0000-0xFFFF).
264
+
265
+ Returns:
266
+ Frame type classification string.
267
+ """
268
+ for (start, end), frame_type in self.FRAME_ID_RANGES:
269
+ if start <= frame_id <= end:
270
+ return frame_type
271
+ return f"Unknown (0x{frame_id:04X})"
272
+
273
+ def _parse_rt_frame(self, frame_id: int, data: bytes) -> dict[str, Any]:
274
+ """Parse PROFINET Real-Time frame.
275
+
276
+ RT Frame Format:
277
+ - Cycle Counter (2 bytes) - for RT_CLASS_1 and higher
278
+ - Data Status (1 byte)
279
+ - Transfer Status (1 byte) - optional for some classes
280
+ - I/O Data (variable)
281
+
282
+ Args:
283
+ frame_id: PROFINET frame ID.
284
+ data: RT frame payload.
285
+
286
+ Returns:
287
+ Parsed RT frame data.
288
+ """
289
+ if len(data) < 4:
290
+ return {"error": "RT frame too short", "raw_data": data.hex()}
291
+
292
+ result: dict[str, Any] = {"frame_id": frame_id}
293
+
294
+ # Check if this is cyclic data (has cycle counter)
295
+ if 0x8000 <= frame_id <= 0xFDFF:
296
+ cycle_counter = int.from_bytes(data[0:2], "big")
297
+ data_status = data[2]
298
+ io_data = data[3:]
299
+
300
+ # Parse Data Status byte (IEC 61158-6-10)
301
+ # Bit 7: State (0=BACKUP, 1=PRIMARY)
302
+ # Bit 6: Redundancy (0=No redundancy, 1=Redundancy enabled)
303
+ # Bit 5: DataValid (0=Invalid, 1=Valid)
304
+ # Bit 4: Reserved
305
+ # Bit 3: Provider State (0=STOP, 1=RUN)
306
+ # Bit 2: Station Problem Indicator (0=Normal, 1=Problem)
307
+ # Bit 1: Reserved
308
+ # Bit 0: Reserved
309
+
310
+ result.update(
311
+ {
312
+ "cycle_counter": cycle_counter,
313
+ "data_status": data_status,
314
+ "data_status_flags": {
315
+ "primary": bool(data_status & 0x80),
316
+ "redundancy": bool(data_status & 0x40),
317
+ "data_valid": bool(data_status & 0x20),
318
+ "provider_state": "RUN" if (data_status & 0x08) else "STOP",
319
+ "station_problem": bool(data_status & 0x04),
320
+ },
321
+ "io_data": io_data.hex(),
322
+ "io_data_length": len(io_data),
323
+ }
324
+ )
325
+
326
+ return result
327
+
328
+ def _update_device_info(self, frame: ProfinetFrame) -> None:
329
+ """Update device information from parsed frame.
330
+
331
+ Extracts and updates device information from DCP frames.
332
+
333
+ Args:
334
+ frame: Parsed PROFINET frame.
335
+ """
336
+ if frame.frame_type != "DCP" or "blocks" not in frame.decoded:
337
+ return
338
+
339
+ # Extract device information from DCP blocks
340
+ mac = frame.source_mac
341
+ if mac not in self.devices:
342
+ self.devices[mac] = ProfinetDevice(mac_address=mac)
343
+
344
+ device = self.devices[mac]
345
+
346
+ for block in frame.decoded.get("blocks", []):
347
+ # Device name
348
+ if "device_name" in block:
349
+ device.device_name = block["device_name"]
350
+
351
+ # Device ID (Vendor ID + Device ID)
352
+ if "vendor_id" in block and "device_id" in block:
353
+ device.vendor_id = block["vendor_id"]
354
+ device.device_id = block["device_id"]
355
+
356
+ # Device role
357
+ if "role_names" in block:
358
+ roles = block["role_names"]
359
+ if "IO-Controller" in roles:
360
+ device.station_type = "IO-Controller"
361
+ elif "IO-Device" in roles:
362
+ device.station_type = "IO-Device"
363
+ elif "IO-Supervisor" in roles:
364
+ device.station_type = "IO-Supervisor"
365
+
366
+ # IP configuration
367
+ if "ip_address" in block:
368
+ device.ip_address = block["ip_address"]
369
+ if "subnet_mask" in block:
370
+ device.subnet_mask = block["subnet_mask"]
371
+ if "gateway" in block:
372
+ device.gateway = block["gateway"]
373
+
374
+ def discover_devices(self) -> list[ProfinetDevice]:
375
+ """Discover PROFINET devices from DCP Identify responses.
376
+
377
+ Analyzes all parsed frames to extract device information from
378
+ DCP protocol messages.
379
+
380
+ Returns:
381
+ List of discovered PROFINET devices.
382
+
383
+ Example:
384
+ >>> analyzer = ProfinetAnalyzer()
385
+ >>> # ... parse frames ...
386
+ >>> devices = analyzer.discover_devices()
387
+ >>> for device in devices:
388
+ ... print(f"{device.mac_address}: {device.device_name}")
389
+ """
390
+ return list(self.devices.values())
391
+
392
+ def export_topology(self, output_path: Path) -> None:
393
+ """Export device topology as JSON.
394
+
395
+ Exports all discovered devices and their configuration to a JSON file.
396
+
397
+ Args:
398
+ output_path: Path to output JSON file.
399
+
400
+ Example:
401
+ >>> analyzer = ProfinetAnalyzer()
402
+ >>> # ... parse frames and discover devices ...
403
+ >>> analyzer.export_topology(Path("profinet_network.json"))
404
+ """
405
+ topology = {
406
+ "network_type": "PROFINET IO",
407
+ "devices": [
408
+ {
409
+ "mac_address": device.mac_address,
410
+ "device_name": device.device_name,
411
+ "device_type": device.device_type,
412
+ "vendor_id": device.vendor_id,
413
+ "device_id": device.device_id,
414
+ "station_type": device.station_type,
415
+ "ip_address": device.ip_address,
416
+ "subnet_mask": device.subnet_mask,
417
+ "gateway": device.gateway,
418
+ "modules": device.modules,
419
+ }
420
+ for device in self.devices.values()
421
+ ],
422
+ "frame_count": len(self.frames),
423
+ "frame_types": self._get_frame_type_statistics(),
424
+ }
425
+
426
+ with output_path.open("w") as f:
427
+ json.dump(topology, f, indent=2)
428
+
429
+ def _get_frame_type_statistics(self) -> dict[str, int]:
430
+ """Get statistics of frame types seen.
431
+
432
+ Returns:
433
+ Dictionary mapping frame type to count.
434
+ """
435
+ stats: dict[str, int] = {}
436
+ for frame in self.frames:
437
+ stats[frame.frame_type] = stats.get(frame.frame_type, 0) + 1
438
+ return stats
439
+
440
+
441
+ __all__ = ["ProfinetAnalyzer", "ProfinetDevice", "ProfinetFrame"]