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,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"]