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,474 @@
1
+ """EtherCAT protocol analyzer.
2
+
3
+ This module provides comprehensive EtherCAT (Ethernet for Control Automation Technology)
4
+ protocol analysis supporting frame parsing, datagram decoding, topology discovery,
5
+ and slave configuration export.
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.protocols.industrial.ethercat.analyzer import EtherCATAnalyzer
9
+ >>> analyzer = EtherCATAnalyzer()
10
+ >>> # Parse EtherCAT frame (Ethertype 0x88A4)
11
+ >>> ethernet_payload = bytes([0x11, 0x10, 0x01, 0x00, ...])
12
+ >>> frame = analyzer.parse_frame(ethernet_payload, timestamp=0.0)
13
+ >>> print(f"Datagrams: {len(frame.datagrams)}")
14
+ >>> # Access slave information
15
+ >>> slave = analyzer.read_slave_info(station_address=1)
16
+ >>> if slave:
17
+ ... print(f"State: {slave.state}")
18
+
19
+ References:
20
+ IEC 61158 Type 12: Industrial communication networks
21
+ ETG.1000 EtherCAT Protocol Specification
22
+ ETG.2000 EtherCAT AL Protocol (Application Layer)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import xml.etree.ElementTree as ET
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import ClassVar
31
+
32
+
33
+ @dataclass
34
+ class EtherCATDatagram:
35
+ """EtherCAT datagram within frame.
36
+
37
+ A single EtherCAT frame can contain multiple datagrams. Each datagram
38
+ represents a command (read/write) to EtherCAT slave devices.
39
+
40
+ Attributes:
41
+ cmd: Command type code (0x00-0x0E).
42
+ cmd_name: Human-readable command name.
43
+ idx: Index field (lower 4 bits of first byte).
44
+ adp: Auto-increment address (2 bytes).
45
+ ado: Address offset (2 bytes).
46
+ len_: Data length (11 bits).
47
+ irq: Interrupt requested (4 bits).
48
+ data: Datagram payload data.
49
+ wkc: Working counter (2 bytes).
50
+ more_follows: True if more datagrams follow in frame.
51
+ """
52
+
53
+ cmd: int
54
+ cmd_name: str
55
+ idx: int
56
+ adp: int
57
+ ado: int
58
+ len_: int
59
+ irq: int
60
+ data: bytes
61
+ wkc: int
62
+ more_follows: bool
63
+
64
+
65
+ @dataclass
66
+ class EtherCATFrame:
67
+ """EtherCAT frame representation.
68
+
69
+ Attributes:
70
+ timestamp: Frame timestamp in seconds.
71
+ length: Frame length field (11 bits).
72
+ datagrams: List of datagrams in frame.
73
+ """
74
+
75
+ timestamp: float
76
+ length: int
77
+ datagrams: list[EtherCATDatagram]
78
+
79
+
80
+ @dataclass
81
+ class EtherCATSlave:
82
+ """EtherCAT slave device.
83
+
84
+ Attributes:
85
+ station_address: Configured station address.
86
+ alias_address: Alias address (if configured).
87
+ vendor_id: Vendor identification.
88
+ product_code: Product code.
89
+ revision: Revision number.
90
+ serial_number: Serial number.
91
+ state: Current slave state (INIT, PRE-OP, SAFE-OP, OP).
92
+ dc_supported: Distributed Clock support.
93
+ mailbox_protocols: Supported mailbox protocols (CoE, FoE, SoE, EoE).
94
+ """
95
+
96
+ station_address: int
97
+ alias_address: int | None = None
98
+ vendor_id: int | None = None
99
+ product_code: int | None = None
100
+ revision: int | None = None
101
+ serial_number: int | None = None
102
+ state: str = "UNKNOWN"
103
+ dc_supported: bool = False
104
+ mailbox_protocols: list[str] = field(default_factory=list)
105
+
106
+
107
+ class EtherCATAnalyzer:
108
+ """EtherCAT protocol analyzer.
109
+
110
+ Provides comprehensive EtherCAT protocol analysis including frame parsing,
111
+ datagram decoding, topology discovery, and slave state tracking.
112
+
113
+ Attributes:
114
+ frames: List of parsed EtherCAT frames.
115
+ slaves: Dictionary of discovered slaves by station address.
116
+
117
+ Example:
118
+ >>> analyzer = EtherCATAnalyzer()
119
+ >>> # Parse frame
120
+ >>> frame = analyzer.parse_frame(ethernet_payload, timestamp=0.0)
121
+ >>> # Discover topology
122
+ >>> slave_addresses = analyzer.discover_topology()
123
+ >>> # Export configuration
124
+ >>> from pathlib import Path
125
+ >>> analyzer.export_configuration(Path("ethercat_config.xml"))
126
+ """
127
+
128
+ # Command types (EtherCAT datagram commands)
129
+ COMMANDS: ClassVar[dict[int, str]] = {
130
+ 0x00: "NOP",
131
+ 0x01: "APRD", # Auto-increment Physical Read
132
+ 0x02: "APWR", # Auto-increment Physical Write
133
+ 0x03: "APRW", # Auto-increment Physical Read/Write
134
+ 0x04: "FPRD", # Configured address Physical Read
135
+ 0x05: "FPWR", # Configured address Physical Write
136
+ 0x06: "FPRW", # Configured address Physical Read/Write
137
+ 0x07: "BRD", # Broadcast Read
138
+ 0x08: "BWR", # Broadcast Write
139
+ 0x09: "BRW", # Broadcast Read/Write
140
+ 0x0A: "LRD", # Logical Memory Read
141
+ 0x0B: "LWR", # Logical Memory Write
142
+ 0x0C: "LRW", # Logical Memory Read/Write
143
+ 0x0D: "ARMW", # Auto-increment physical Read Multiple Write
144
+ 0x0E: "FRMW", # Configured address physical Read Multiple Write
145
+ }
146
+
147
+ # Slave state machine states
148
+ STATES: ClassVar[dict[int, str]] = {
149
+ 0x01: "INIT",
150
+ 0x02: "PRE-OP",
151
+ 0x04: "SAFE-OP",
152
+ 0x08: "OP",
153
+ }
154
+
155
+ # Mailbox protocol types
156
+ MAILBOX_PROTOCOLS: ClassVar[dict[int, str]] = {
157
+ 0x02: "CoE", # CAN application protocol over EtherCAT
158
+ 0x03: "FoE", # File access over EtherCAT
159
+ 0x04: "SoE", # Servo drive profile over EtherCAT
160
+ 0x05: "EoE", # Ethernet over EtherCAT
161
+ }
162
+
163
+ # Well-known EtherCAT register addresses
164
+ REG_TYPE: ClassVar[int] = 0x0000 # Type
165
+ REG_REVISION: ClassVar[int] = 0x0001 # Revision
166
+ REG_BUILD: ClassVar[int] = 0x0002 # Build
167
+ REG_FMMU_COUNT: ClassVar[int] = 0x0004 # FMMU count
168
+ REG_SYNC_COUNT: ClassVar[int] = 0x0005 # SyncManager count
169
+ REG_PORT_DESC: ClassVar[int] = 0x0007 # Port descriptor
170
+ REG_ESC_FEATURES: ClassVar[int] = 0x0008 # ESC features
171
+ REG_STATION_ADDRESS: ClassVar[int] = 0x0010 # Configured Station Address
172
+ REG_ALIAS_ADDRESS: ClassVar[int] = 0x0012 # Alias Address
173
+ REG_DL_CONTROL: ClassVar[int] = 0x0100 # DL Control
174
+ REG_DL_STATUS: ClassVar[int] = 0x0110 # DL Status
175
+ REG_AL_CONTROL: ClassVar[int] = 0x0120 # AL Control
176
+ REG_AL_STATUS: ClassVar[int] = 0x0130 # AL Status
177
+ REG_AL_STATUS_CODE: ClassVar[int] = 0x0134 # AL Status Code
178
+ REG_DC_SYSTEM_TIME: ClassVar[int] = 0x0910 # Distributed Clock System Time
179
+
180
+ def __init__(self) -> None:
181
+ """Initialize EtherCAT analyzer."""
182
+ self.frames: list[EtherCATFrame] = []
183
+ self.slaves: dict[int, EtherCATSlave] = {}
184
+
185
+ def parse_frame(self, ethernet_payload: bytes, timestamp: float = 0.0) -> EtherCATFrame:
186
+ """Parse EtherCAT frame (Ethertype 0x88A4).
187
+
188
+ Frame Format:
189
+ - Length (2 bytes) - lower 11 bits, upper 5 bits reserved
190
+ - Type (1 byte) - Protocol type (0x01 for EtherCAT commands)
191
+ - Datagrams (variable) - One or more datagrams
192
+
193
+ Args:
194
+ ethernet_payload: EtherCAT frame payload (after Ethernet header).
195
+ timestamp: Frame timestamp in seconds.
196
+
197
+ Returns:
198
+ Parsed EtherCAT frame.
199
+
200
+ Raises:
201
+ ValueError: If frame is invalid.
202
+
203
+ Example:
204
+ >>> analyzer = EtherCATAnalyzer()
205
+ >>> # Simple frame with one datagram
206
+ >>> payload = bytes([0x0E, 0x10, 0x01, 0x00, 0x00, 0x00, ...])
207
+ >>> frame = analyzer.parse_frame(payload, timestamp=0.0)
208
+ >>> assert len(frame.datagrams) >= 1
209
+ """
210
+ if len(ethernet_payload) < 2:
211
+ raise ValueError(f"EtherCAT frame too short: {len(ethernet_payload)} bytes (minimum 2)")
212
+
213
+ # Parse length field (11 bits)
214
+ length_and_reserved = int.from_bytes(ethernet_payload[0:2], "little")
215
+ length = length_and_reserved & 0x07FF
216
+
217
+ datagrams: list[EtherCATDatagram] = []
218
+ offset = 2
219
+
220
+ # Parse all datagrams
221
+ while offset < len(ethernet_payload):
222
+ try:
223
+ datagram, consumed = self._parse_datagram(ethernet_payload, offset)
224
+ datagrams.append(datagram)
225
+ offset += consumed
226
+
227
+ if not datagram.more_follows:
228
+ break
229
+ except ValueError:
230
+ # If we fail to parse a datagram but have already parsed some, stop gracefully
231
+ if len(datagrams) > 0:
232
+ break
233
+ raise
234
+
235
+ frame = EtherCATFrame(
236
+ timestamp=timestamp,
237
+ length=length,
238
+ datagrams=datagrams,
239
+ )
240
+
241
+ self.frames.append(frame)
242
+ self._update_slave_state(frame)
243
+
244
+ return frame
245
+
246
+ def _parse_datagram(self, data: bytes, offset: int) -> tuple[EtherCATDatagram, int]:
247
+ """Parse single EtherCAT datagram.
248
+
249
+ Datagram Format:
250
+ - Cmd (1 byte) - Command type
251
+ - Idx (1 byte) - Index
252
+ - ADP (2 bytes) - Auto-increment address (little-endian)
253
+ - ADO (2 bytes) - Address offset (little-endian)
254
+ - Len/M/IRQ (2 bytes) - Length (11 bits), More (1 bit), IRQ (4 bits)
255
+ - Data (Len bytes)
256
+ - WKC (2 bytes) - Working counter (little-endian)
257
+
258
+ Args:
259
+ data: Frame data containing datagram.
260
+ offset: Offset to start of datagram.
261
+
262
+ Returns:
263
+ Tuple of (datagram, bytes_consumed).
264
+
265
+ Raises:
266
+ ValueError: If datagram is invalid.
267
+ """
268
+ if offset + 10 > len(data):
269
+ raise ValueError(
270
+ f"Insufficient data for datagram header at offset {offset}: "
271
+ f"need 10 bytes, have {len(data) - offset}"
272
+ )
273
+
274
+ cmd = data[offset]
275
+ idx = data[offset + 1]
276
+ adp = int.from_bytes(data[offset + 2 : offset + 4], "little")
277
+ ado = int.from_bytes(data[offset + 4 : offset + 6], "little")
278
+ len_m_irq = int.from_bytes(data[offset + 6 : offset + 8], "little")
279
+
280
+ # Extract fields from len_m_irq
281
+ data_len = len_m_irq & 0x07FF # Lower 11 bits
282
+ more_follows = bool(len_m_irq & 0x8000) # Bit 15
283
+ irq = (len_m_irq >> 12) & 0x0F # Bits 12-15 (but bit 15 is M flag)
284
+
285
+ # Validate data length
286
+ if offset + 10 + data_len > len(data):
287
+ raise ValueError(
288
+ f"Insufficient data for datagram payload at offset {offset}: "
289
+ f"need {data_len} bytes, have {len(data) - offset - 10}"
290
+ )
291
+
292
+ datagram_data = data[offset + 8 : offset + 8 + data_len]
293
+ wkc = int.from_bytes(data[offset + 8 + data_len : offset + 10 + data_len], "little")
294
+
295
+ total_consumed = 10 + data_len
296
+
297
+ datagram = EtherCATDatagram(
298
+ cmd=cmd,
299
+ cmd_name=self.COMMANDS.get(cmd, f"Unknown (0x{cmd:02X})"),
300
+ idx=idx,
301
+ adp=adp,
302
+ ado=ado,
303
+ len_=data_len,
304
+ irq=irq,
305
+ data=datagram_data,
306
+ wkc=wkc,
307
+ more_follows=more_follows,
308
+ )
309
+
310
+ return datagram, total_consumed
311
+
312
+ def _update_slave_state(self, frame: EtherCATFrame) -> None:
313
+ """Update slave state based on parsed frame.
314
+
315
+ Args:
316
+ frame: Parsed EtherCAT frame.
317
+ """
318
+ for datagram in frame.datagrams:
319
+ # Check for AL Status register reads (address 0x0130)
320
+ if datagram.ado == self.REG_AL_STATUS and len(datagram.data) >= 2:
321
+ state_code = datagram.data[0] & 0x0F # Lower 4 bits
322
+ state_name = self.STATES.get(state_code, "UNKNOWN")
323
+
324
+ # For auto-increment addressing, adp indicates position
325
+ if datagram.cmd in {0x01, 0x02, 0x03}: # APRD, APWR, APRW
326
+ station_address = datagram.adp
327
+ if station_address not in self.slaves:
328
+ self.slaves[station_address] = EtherCATSlave(
329
+ station_address=station_address
330
+ )
331
+ self.slaves[station_address].state = state_name
332
+
333
+ # Check for configured station address reads (address 0x0010)
334
+ if datagram.ado == self.REG_STATION_ADDRESS and len(datagram.data) >= 2:
335
+ station_address = int.from_bytes(datagram.data[0:2], "little")
336
+ if station_address not in self.slaves and station_address != 0:
337
+ self.slaves[station_address] = EtherCATSlave(station_address=station_address)
338
+
339
+ def discover_topology(self) -> list[int]:
340
+ """Discover slave topology using auto-increment addressing.
341
+
342
+ Uses the auto-increment addressing mechanism to enumerate all slaves
343
+ in the EtherCAT segment. This method analyzes already-parsed frames
344
+ to extract slave addresses.
345
+
346
+ Returns:
347
+ List of discovered slave station addresses.
348
+
349
+ Example:
350
+ >>> analyzer = EtherCATAnalyzer()
351
+ >>> # Parse some frames first...
352
+ >>> addresses = analyzer.discover_topology()
353
+ >>> print(f"Found {len(addresses)} slaves")
354
+ """
355
+ discovered: set[int] = set()
356
+
357
+ for frame in self.frames:
358
+ for datagram in frame.datagrams:
359
+ # Auto-increment commands (APRD, APWR, APRW)
360
+ if datagram.cmd in {0x01, 0x02, 0x03}:
361
+ # ADP field is the position, WKC indicates success
362
+ if datagram.wkc > 0:
363
+ discovered.add(datagram.adp)
364
+
365
+ # Configured address reads
366
+ elif datagram.cmd in {0x04, 0x05, 0x06}: # FPRD, FPWR, FPRW
367
+ if datagram.wkc > 0:
368
+ discovered.add(datagram.adp)
369
+
370
+ return sorted(discovered)
371
+
372
+ def read_slave_info(self, station_address: int) -> EtherCATSlave | None:
373
+ """Read slave information from analysis.
374
+
375
+ Args:
376
+ station_address: Slave station address.
377
+
378
+ Returns:
379
+ Slave information if found, None otherwise.
380
+
381
+ Example:
382
+ >>> analyzer = EtherCATAnalyzer()
383
+ >>> # After parsing frames...
384
+ >>> slave = analyzer.read_slave_info(station_address=1)
385
+ >>> if slave:
386
+ ... print(f"State: {slave.state}")
387
+ """
388
+ return self.slaves.get(station_address)
389
+
390
+ def export_configuration(self, output_path: Path) -> None:
391
+ """Export slave configuration as ENI (EtherCAT Network Information) XML.
392
+
393
+ Creates a simplified ENI XML file containing discovered slave configuration.
394
+ This is compatible with EtherCAT master implementations.
395
+
396
+ Args:
397
+ output_path: Path to output XML file.
398
+
399
+ Example:
400
+ >>> analyzer = EtherCATAnalyzer()
401
+ >>> # After parsing frames...
402
+ >>> from pathlib import Path
403
+ >>> analyzer.export_configuration(Path("ethercat_network.xml"))
404
+ """
405
+ # Create root element
406
+ root = ET.Element("EtherCATConfig")
407
+ root.set("Version", "1.0")
408
+
409
+ # Create Config section
410
+ config = ET.SubElement(root, "Config")
411
+
412
+ # Create Master section
413
+ master = ET.SubElement(config, "Master")
414
+
415
+ # Add slaves
416
+ for slave in sorted(self.slaves.values(), key=lambda s: s.station_address):
417
+ slave_elem = ET.SubElement(master, "Slave")
418
+
419
+ # Add slave information
420
+ info = ET.SubElement(slave_elem, "Info")
421
+
422
+ station = ET.SubElement(info, "StationAddress")
423
+ station.text = str(slave.station_address)
424
+
425
+ if slave.alias_address is not None:
426
+ alias = ET.SubElement(info, "AliasAddress")
427
+ alias.text = str(slave.alias_address)
428
+
429
+ if slave.vendor_id is not None:
430
+ vendor = ET.SubElement(info, "VendorId")
431
+ vendor.text = f"0x{slave.vendor_id:08X}"
432
+
433
+ if slave.product_code is not None:
434
+ product = ET.SubElement(info, "ProductCode")
435
+ product.text = f"0x{slave.product_code:08X}"
436
+
437
+ if slave.revision is not None:
438
+ revision = ET.SubElement(info, "Revision")
439
+ revision.text = f"0x{slave.revision:08X}"
440
+
441
+ if slave.serial_number is not None:
442
+ serial = ET.SubElement(info, "SerialNumber")
443
+ serial.text = str(slave.serial_number)
444
+
445
+ # Add state information
446
+ state = ET.SubElement(info, "State")
447
+ state.text = slave.state
448
+
449
+ # Add DC support
450
+ if slave.dc_supported:
451
+ dc = ET.SubElement(info, "Dc")
452
+ dc.set("supported", "true")
453
+
454
+ # Add mailbox protocols
455
+ if slave.mailbox_protocols:
456
+ mailbox = ET.SubElement(info, "Mailbox")
457
+ for protocol in slave.mailbox_protocols:
458
+ proto_elem = ET.SubElement(mailbox, "Protocol")
459
+ proto_elem.text = protocol
460
+
461
+ # Create ElementTree and write to file
462
+ tree = ET.ElementTree(root)
463
+ ET.indent(tree, space=" ")
464
+
465
+ with output_path.open("wb") as f:
466
+ tree.write(f, encoding="utf-8", xml_declaration=True)
467
+
468
+
469
+ __all__ = [
470
+ "EtherCATAnalyzer",
471
+ "EtherCATDatagram",
472
+ "EtherCATFrame",
473
+ "EtherCATSlave",
474
+ ]