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