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