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,622 @@
1
+ """BACnet service decoders.
2
+
3
+ This module provides service-specific decoders for BACnet confirmed and unconfirmed
4
+ services according to ASHRAE 135-2020.
5
+
6
+ References:
7
+ ANSI/ASHRAE Standard 135-2020, Clause 15-19: Application Layer Services
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from oscura.analyzers.protocols.industrial.bacnet.encoding import (
15
+ parse_application_tag,
16
+ parse_object_identifier,
17
+ parse_tag,
18
+ )
19
+
20
+
21
+ def decode_who_is(data: bytes) -> dict[str, Any]:
22
+ """Decode Who-Is service (unconfirmed service for device discovery).
23
+
24
+ Args:
25
+ data: Service payload bytes.
26
+
27
+ Returns:
28
+ Dictionary with optional device_instance_range_low_limit and
29
+ device_instance_range_high_limit if specified.
30
+
31
+ Example:
32
+ >>> data = bytes([0x09, 0x00, 0x19, 0xFF]) # Range 0-255
33
+ >>> result = decode_who_is(data)
34
+ """
35
+ result: dict[str, Any] = {}
36
+ offset = 0
37
+
38
+ # Optional device instance range (context tags 0 and 1)
39
+ if offset < len(data):
40
+ try:
41
+ tag, tag_size = parse_tag(data, offset)
42
+ if tag["context_specific"] and tag["tag_number"] == 0:
43
+ # Low limit
44
+ value_offset = offset + tag_size
45
+ low_limit = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
46
+ result["device_instance_range_low_limit"] = low_limit
47
+ offset = value_offset + tag["length"]
48
+
49
+ # High limit (context tag 1)
50
+ if offset < len(data):
51
+ tag, tag_size = parse_tag(data, offset)
52
+ if tag["context_specific"] and tag["tag_number"] == 1:
53
+ value_offset = offset + tag_size
54
+ high_limit = int.from_bytes(
55
+ data[value_offset : value_offset + tag["length"]], "big"
56
+ )
57
+ result["device_instance_range_high_limit"] = high_limit
58
+ except (ValueError, IndexError):
59
+ pass
60
+
61
+ return result
62
+
63
+
64
+ def decode_i_am(data: bytes) -> dict[str, Any]:
65
+ """Decode I-Am service (unconfirmed device announcement).
66
+
67
+ Args:
68
+ data: Service payload bytes.
69
+
70
+ Returns:
71
+ Dictionary with device_instance, max_apdu_length, segmentation, and vendor_id.
72
+
73
+ Example:
74
+ >>> data = bytes([0xC4, 0x02, 0x00, 0x00, 0x08, 0x22, 0x05, 0x00, ...])
75
+ >>> result = decode_i_am(data)
76
+ >>> print(f"Device {result['device_instance']}")
77
+ """
78
+ result: dict[str, Any] = {}
79
+ offset = 0
80
+
81
+ try:
82
+ # Device object identifier (application tag 12, object identifier)
83
+ value, consumed = parse_application_tag(data, offset)
84
+ if isinstance(value, dict) and "instance" in value:
85
+ result["device_instance"] = value["instance"]
86
+ result["device_object_type"] = value.get("object_type_name", "unknown")
87
+ offset += consumed
88
+
89
+ # Max APDU length accepted (unsigned)
90
+ if offset < len(data):
91
+ value, consumed = parse_application_tag(data, offset)
92
+ result["max_apdu_length"] = value
93
+ offset += consumed
94
+
95
+ # Segmentation supported (enumerated)
96
+ if offset < len(data):
97
+ value, consumed = parse_application_tag(data, offset)
98
+ segmentation_names = {
99
+ 0: "both",
100
+ 1: "transmit",
101
+ 2: "receive",
102
+ 3: "no-segmentation",
103
+ }
104
+ result["segmentation"] = segmentation_names.get(value, value)
105
+ offset += consumed
106
+
107
+ # Vendor ID (unsigned)
108
+ if offset < len(data):
109
+ value, consumed = parse_application_tag(data, offset)
110
+ result["vendor_id"] = value
111
+ offset += consumed
112
+
113
+ except (ValueError, IndexError):
114
+ pass
115
+
116
+ return result
117
+
118
+
119
+ def decode_who_has(data: bytes) -> dict[str, Any]:
120
+ """Decode Who-Has service (unconfirmed service to find objects).
121
+
122
+ Args:
123
+ data: Service payload bytes.
124
+
125
+ Returns:
126
+ Dictionary with optional device range and object identifier or name.
127
+
128
+ Example:
129
+ >>> data = bytes([0x3C, 0x02, 0x00, 0x00, 0x01, 0x3E, ...])
130
+ >>> result = decode_who_has(data)
131
+ """
132
+ result: dict[str, Any] = {}
133
+ offset = 0
134
+
135
+ try:
136
+ # Optional device instance range (context tags 0 and 1)
137
+ tag, tag_size = parse_tag(data, offset)
138
+ if tag["context_specific"] and tag["tag_number"] == 0:
139
+ value_offset = offset + tag_size
140
+ low_limit = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
141
+ result["device_instance_range_low_limit"] = low_limit
142
+ offset = value_offset + tag["length"]
143
+
144
+ tag, tag_size = parse_tag(data, offset)
145
+ if tag["context_specific"] and tag["tag_number"] == 1:
146
+ value_offset = offset + tag_size
147
+ high_limit = int.from_bytes(
148
+ data[value_offset : value_offset + tag["length"]], "big"
149
+ )
150
+ result["device_instance_range_high_limit"] = high_limit
151
+ offset = value_offset + tag["length"]
152
+
153
+ # Object identifier or object name (choice: context 2 or 3)
154
+ if offset < len(data):
155
+ tag, tag_size = parse_tag(data, offset)
156
+ if tag["context_specific"] and tag["tag_number"] == 2:
157
+ # Object identifier
158
+ obj_id, _ = parse_object_identifier(data, offset + tag_size)
159
+ result["object_identifier"] = obj_id
160
+ elif tag["context_specific"] and tag["tag_number"] == 3:
161
+ # Object name
162
+ value_offset = offset + tag_size
163
+ name = data[value_offset : value_offset + tag["length"]].decode(
164
+ "utf-8", errors="replace"
165
+ )
166
+ result["object_name"] = name
167
+
168
+ except (ValueError, IndexError):
169
+ pass
170
+
171
+ return result
172
+
173
+
174
+ def decode_i_have(data: bytes) -> dict[str, Any]:
175
+ """Decode I-Have service (unconfirmed response to Who-Has).
176
+
177
+ Args:
178
+ data: Service payload bytes.
179
+
180
+ Returns:
181
+ Dictionary with device_identifier, object_identifier, and object_name.
182
+
183
+ Example:
184
+ >>> result = decode_i_have(data)
185
+ >>> print(f"Device has {result['object_name']}")
186
+ """
187
+ result: dict[str, Any] = {}
188
+ offset = 0
189
+
190
+ try:
191
+ # Device identifier
192
+ value, consumed = parse_application_tag(data, offset)
193
+ result["device_identifier"] = value
194
+ offset += consumed
195
+
196
+ # Object identifier
197
+ value, consumed = parse_application_tag(data, offset)
198
+ result["object_identifier"] = value
199
+ offset += consumed
200
+
201
+ # Object name
202
+ value, consumed = parse_application_tag(data, offset)
203
+ result["object_name"] = value
204
+ offset += consumed
205
+
206
+ except (ValueError, IndexError):
207
+ pass
208
+
209
+ return result
210
+
211
+
212
+ def decode_read_property_request(data: bytes) -> dict[str, Any]:
213
+ """Decode ReadProperty service request.
214
+
215
+ Args:
216
+ data: Service payload bytes.
217
+
218
+ Returns:
219
+ Dictionary with object_identifier, property_identifier, and optional
220
+ property_array_index.
221
+
222
+ Example:
223
+ >>> result = decode_read_property_request(data)
224
+ >>> print(f"Read {result['property_identifier']} from {result['object_identifier']}")
225
+ """
226
+ result: dict[str, Any] = {}
227
+ offset = 0
228
+
229
+ try:
230
+ # Object identifier (context tag 0)
231
+ tag, tag_size = parse_tag(data, offset)
232
+ if tag["context_specific"] and tag["tag_number"] == 0:
233
+ obj_id, _ = parse_object_identifier(data, offset + tag_size)
234
+ result["object_identifier"] = obj_id
235
+ offset += tag_size + 4
236
+
237
+ # Property identifier (context tag 1)
238
+ if offset < len(data):
239
+ tag, tag_size = parse_tag(data, offset)
240
+ if tag["context_specific"] and tag["tag_number"] == 1:
241
+ value_offset = offset + tag_size
242
+ prop_id = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
243
+ result["property_identifier"] = prop_id
244
+ result["property_name"] = get_property_name(prop_id)
245
+ offset = value_offset + tag["length"]
246
+
247
+ # Optional property array index (context tag 2)
248
+ if offset < len(data):
249
+ tag, tag_size = parse_tag(data, offset)
250
+ if tag["context_specific"] and tag["tag_number"] == 2:
251
+ value_offset = offset + tag_size
252
+ array_index = int.from_bytes(
253
+ data[value_offset : value_offset + tag["length"]], "big"
254
+ )
255
+ result["property_array_index"] = array_index
256
+
257
+ except (ValueError, IndexError):
258
+ pass
259
+
260
+ return result
261
+
262
+
263
+ def decode_read_property_ack(data: bytes) -> dict[str, Any]:
264
+ """Decode ReadProperty-ACK service response.
265
+
266
+ Args:
267
+ data: Service payload bytes.
268
+
269
+ Returns:
270
+ Dictionary with object_identifier, property_identifier, and property_value.
271
+
272
+ Example:
273
+ >>> result = decode_read_property_ack(data)
274
+ >>> print(f"Value: {result['property_value']}")
275
+ """
276
+ result: dict[str, Any] = {}
277
+ offset = 0
278
+
279
+ try:
280
+ # Parse object identifier (context tag 0)
281
+ obj_id, offset = _parse_bacnet_object_id(data, offset)
282
+ if obj_id is not None:
283
+ result["object_identifier"] = obj_id
284
+
285
+ # Parse property identifier (context tag 1)
286
+ prop_id, prop_name, offset = _parse_bacnet_property_id(data, offset)
287
+ if prop_id is not None:
288
+ result["property_identifier"] = prop_id
289
+ result["property_name"] = prop_name
290
+
291
+ # Parse optional property array index (context tag 2)
292
+ array_index, offset = _parse_bacnet_array_index(data, offset)
293
+ if array_index is not None:
294
+ result["property_array_index"] = array_index
295
+
296
+ # Parse property value (context tag 3)
297
+ prop_value = _parse_bacnet_property_value(data, offset)
298
+ if prop_value is not None:
299
+ result["property_value"] = prop_value
300
+
301
+ except (ValueError, IndexError):
302
+ pass
303
+
304
+ return result
305
+
306
+
307
+ def _parse_bacnet_object_id(data: bytes, offset: int) -> tuple[dict[str, Any] | None, int]:
308
+ """Parse BACnet object identifier from context tag 0.
309
+
310
+ Args:
311
+ data: Payload bytes.
312
+ offset: Current parsing offset.
313
+
314
+ Returns:
315
+ Tuple of (object_id_dict, new_offset). None if tag not found.
316
+ """
317
+ if offset >= len(data):
318
+ return (None, offset)
319
+
320
+ tag, tag_size = parse_tag(data, offset)
321
+ if tag["context_specific"] and tag["tag_number"] == 0:
322
+ obj_id, _ = parse_object_identifier(data, offset + tag_size)
323
+ return (obj_id, offset + tag_size + 4)
324
+
325
+ return (None, offset)
326
+
327
+
328
+ def _parse_bacnet_property_id(data: bytes, offset: int) -> tuple[int | None, str | None, int]:
329
+ """Parse BACnet property identifier from context tag 1.
330
+
331
+ Args:
332
+ data: Payload bytes.
333
+ offset: Current parsing offset.
334
+
335
+ Returns:
336
+ Tuple of (property_id, property_name, new_offset). None if tag not found.
337
+ """
338
+ if offset >= len(data):
339
+ return (None, None, offset)
340
+
341
+ tag, tag_size = parse_tag(data, offset)
342
+ if tag["context_specific"] and tag["tag_number"] == 1:
343
+ value_offset = offset + tag_size
344
+ prop_id = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
345
+ prop_name = get_property_name(prop_id)
346
+ return (prop_id, prop_name, value_offset + tag["length"])
347
+
348
+ return (None, None, offset)
349
+
350
+
351
+ def _parse_bacnet_array_index(data: bytes, offset: int) -> tuple[int | None, int]:
352
+ """Parse optional BACnet array index from context tag 2.
353
+
354
+ Args:
355
+ data: Payload bytes.
356
+ offset: Current parsing offset.
357
+
358
+ Returns:
359
+ Tuple of (array_index, new_offset). None if tag not found.
360
+ """
361
+ if offset >= len(data):
362
+ return (None, offset)
363
+
364
+ tag, tag_size = parse_tag(data, offset)
365
+ if tag["context_specific"] and tag["tag_number"] == 2:
366
+ value_offset = offset + tag_size
367
+ array_index = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
368
+ return (array_index, value_offset + tag["length"])
369
+
370
+ return (None, offset)
371
+
372
+
373
+ def _parse_bacnet_property_value(data: bytes, offset: int) -> Any:
374
+ """Parse BACnet property value from context tag 3.
375
+
376
+ Args:
377
+ data: Payload bytes.
378
+ offset: Current parsing offset.
379
+
380
+ Returns:
381
+ Property value (single value or list). None if tag not found.
382
+ """
383
+ if offset >= len(data):
384
+ return None
385
+
386
+ tag, tag_size = parse_tag(data, offset)
387
+ if not (tag["context_specific"] and tag["tag_number"] == 3 and tag["is_opening"]):
388
+ return None
389
+
390
+ offset += tag_size
391
+
392
+ # Parse values until closing tag
393
+ values = []
394
+ while offset < len(data):
395
+ tag, tag_size = parse_tag(data, offset)
396
+ if tag["is_closing"] and tag["tag_number"] == 3:
397
+ break
398
+
399
+ try:
400
+ value, consumed = parse_application_tag(data, offset)
401
+ values.append(value)
402
+ offset += consumed
403
+ except ValueError:
404
+ # Skip unparseable data
405
+ offset += tag_size + tag.get("length", 0)
406
+ break
407
+
408
+ return values[0] if len(values) == 1 else (values if values else None)
409
+
410
+
411
+ def decode_write_property_request(data: bytes) -> dict[str, Any]:
412
+ """Decode WriteProperty service request.
413
+
414
+ Args:
415
+ data: Service payload bytes.
416
+
417
+ Returns:
418
+ Dictionary with object_identifier, property_identifier, property_value,
419
+ and optional priority.
420
+
421
+ Example:
422
+ >>> result = decode_write_property_request(data)
423
+ >>> print(f"Write {result['property_value']} to {result['property_name']}")
424
+ """
425
+ result: dict[str, Any] = {}
426
+ offset = 0
427
+
428
+ try:
429
+ # Parse object identifier (context tag 0)
430
+ obj_id, offset = _parse_write_property_object_id(data, offset, result)
431
+
432
+ # Parse property identifier (context tag 1)
433
+ offset = _parse_write_property_id(data, offset, result)
434
+
435
+ # Parse optional property array index (context tag 2)
436
+ offset = _parse_write_property_array_index(data, offset, result)
437
+
438
+ # Parse property value (context tag 3)
439
+ offset = _parse_write_property_value(data, offset, result)
440
+
441
+ # Parse optional priority (context tag 4)
442
+ _parse_write_property_priority(data, offset, result)
443
+
444
+ except (ValueError, IndexError):
445
+ pass
446
+
447
+ return result
448
+
449
+
450
+ def _parse_write_property_object_id(
451
+ data: bytes, offset: int, result: dict[str, Any]
452
+ ) -> tuple[dict[str, Any] | None, int]:
453
+ """Parse object identifier from WriteProperty request.
454
+
455
+ Args:
456
+ data: Payload bytes.
457
+ offset: Current offset.
458
+ result: Result dictionary to populate.
459
+
460
+ Returns:
461
+ Tuple of (object_id, new_offset).
462
+ """
463
+ tag, tag_size = parse_tag(data, offset)
464
+ if tag["context_specific"] and tag["tag_number"] == 0:
465
+ obj_id, _ = parse_object_identifier(data, offset + tag_size)
466
+ result["object_identifier"] = obj_id
467
+ return obj_id, offset + tag_size + 4
468
+ return None, offset
469
+
470
+
471
+ def _parse_write_property_id(data: bytes, offset: int, result: dict[str, Any]) -> int:
472
+ """Parse property identifier from WriteProperty request.
473
+
474
+ Args:
475
+ data: Payload bytes.
476
+ offset: Current offset.
477
+ result: Result dictionary to populate.
478
+
479
+ Returns:
480
+ New offset.
481
+ """
482
+ if offset >= len(data):
483
+ return offset
484
+
485
+ tag, tag_size = parse_tag(data, offset)
486
+ if tag["context_specific"] and tag["tag_number"] == 1:
487
+ value_offset = offset + tag_size
488
+ length: int = int(tag["length"])
489
+ prop_id = int.from_bytes(data[value_offset : value_offset + length], "big")
490
+ result["property_identifier"] = prop_id
491
+ result["property_name"] = get_property_name(prop_id)
492
+ return value_offset + length
493
+
494
+ return offset
495
+
496
+
497
+ def _parse_write_property_array_index(data: bytes, offset: int, result: dict[str, Any]) -> int:
498
+ """Parse optional array index from WriteProperty request.
499
+
500
+ Args:
501
+ data: Payload bytes.
502
+ offset: Current offset.
503
+ result: Result dictionary to populate.
504
+
505
+ Returns:
506
+ New offset.
507
+ """
508
+ if offset >= len(data):
509
+ return offset
510
+
511
+ tag, tag_size = parse_tag(data, offset)
512
+ if tag["context_specific"] and tag["tag_number"] == 2:
513
+ value_offset = offset + tag_size
514
+ length: int = int(tag["length"])
515
+ array_index = int.from_bytes(data[value_offset : value_offset + length], "big")
516
+ result["property_array_index"] = array_index
517
+ return value_offset + length
518
+
519
+ return offset
520
+
521
+
522
+ def _parse_write_property_value(data: bytes, offset: int, result: dict[str, Any]) -> int:
523
+ """Parse property value from WriteProperty request.
524
+
525
+ Args:
526
+ data: Payload bytes.
527
+ offset: Current offset.
528
+ result: Result dictionary to populate.
529
+
530
+ Returns:
531
+ New offset.
532
+ """
533
+ if offset >= len(data):
534
+ return offset
535
+
536
+ tag, tag_size = parse_tag(data, offset)
537
+ if tag["context_specific"] and tag["tag_number"] == 3 and tag["is_opening"]:
538
+ offset += tag_size
539
+
540
+ # Parse value(s) until closing tag
541
+ values = []
542
+ while offset < len(data):
543
+ tag, tag_size = parse_tag(data, offset)
544
+ if tag["is_closing"] and tag["tag_number"] == 3:
545
+ offset += tag_size
546
+ break
547
+
548
+ try:
549
+ value, consumed = parse_application_tag(data, offset)
550
+ values.append(value)
551
+ offset += consumed
552
+ except ValueError:
553
+ offset += tag_size + tag.get("length", 0)
554
+ break
555
+
556
+ result["property_value"] = values[0] if len(values) == 1 else values
557
+
558
+ return offset
559
+
560
+
561
+ def _parse_write_property_priority(data: bytes, offset: int, result: dict[str, Any]) -> None:
562
+ """Parse optional priority from WriteProperty request.
563
+
564
+ Args:
565
+ data: Payload bytes.
566
+ offset: Current offset.
567
+ result: Result dictionary to populate.
568
+ """
569
+ if offset >= len(data):
570
+ return
571
+
572
+ tag, tag_size = parse_tag(data, offset)
573
+ if tag["context_specific"] and tag["tag_number"] == 4:
574
+ value_offset = offset + tag_size
575
+ priority = int.from_bytes(data[value_offset : value_offset + tag["length"]], "big")
576
+ result["priority"] = priority
577
+
578
+
579
+ def get_property_name(property_id: int) -> str:
580
+ """Get human-readable property name from property identifier.
581
+
582
+ Args:
583
+ property_id: BACnet property identifier.
584
+
585
+ Returns:
586
+ Property name string.
587
+
588
+ Example:
589
+ >>> name = get_property_name(85) # "present-value"
590
+ """
591
+ # Common BACnet property identifiers (ASHRAE 135-2020, Clause 21)
592
+ property_names = {
593
+ 0: "acked-transitions",
594
+ 1: "ack-required",
595
+ 4: "action",
596
+ 8: "all",
597
+ 28: "description",
598
+ 36: "event-state",
599
+ 41: "high-limit",
600
+ 44: "limit-enable",
601
+ 45: "local-date",
602
+ 46: "local-time",
603
+ 52: "low-limit",
604
+ 56: "max-pres-value",
605
+ 59: "min-pres-value",
606
+ 62: "notify-type",
607
+ 65: "object-identifier",
608
+ 77: "object-name",
609
+ 79: "object-type",
610
+ 85: "present-value",
611
+ 103: "reliability",
612
+ 107: "segmentation-supported",
613
+ 111: "status-flags",
614
+ 112: "system-status",
615
+ 117: "units",
616
+ 120: "vendor-identifier",
617
+ 121: "vendor-name",
618
+ 122: "vt-classes-supported",
619
+ 155: "event-enable",
620
+ 158: "ack-mode",
621
+ }
622
+ return property_names.get(property_id, f"property-{property_id}")
@@ -0,0 +1,30 @@
1
+ """EtherCAT protocol analyzer package.
2
+
3
+ EtherCAT (Ethernet for Control Automation Technology) is a high-performance
4
+ industrial fieldbus system based on Ethernet.
5
+
6
+ Example:
7
+ >>> from oscura.analyzers.protocols.industrial.ethercat import EtherCATAnalyzer
8
+ >>> analyzer = EtherCATAnalyzer()
9
+ >>> frame = analyzer.parse_frame(ethernet_payload, timestamp=0.0)
10
+ >>> print(f"Datagrams: {len(frame.datagrams)}")
11
+
12
+ References:
13
+ IEC 61158 Type 12: https://www.iec.ch/
14
+ ETG.1000 EtherCAT Protocol Specification
15
+ ETG.2000 EtherCAT AL Protocol
16
+ """
17
+
18
+ from oscura.analyzers.protocols.industrial.ethercat.analyzer import (
19
+ EtherCATAnalyzer,
20
+ EtherCATDatagram,
21
+ EtherCATFrame,
22
+ EtherCATSlave,
23
+ )
24
+
25
+ __all__ = [
26
+ "EtherCATAnalyzer",
27
+ "EtherCATDatagram",
28
+ "EtherCATFrame",
29
+ "EtherCATSlave",
30
+ ]