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,668 @@
1
+ """CoAP protocol analyzer with RFC 7252 support and extensions.
2
+
3
+ Provides comprehensive CoAP message parsing, request/response matching,
4
+ blockwise transfer support (RFC 7959), and observe extension (RFC 7641).
5
+
6
+ Example:
7
+ >>> from oscura.iot.coap import CoAPAnalyzer
8
+ >>> analyzer = CoAPAnalyzer()
9
+ >>> data = bytes([0x40, 0x01, 0x12, 0x34]) # CON GET
10
+ >>> message = analyzer.parse_message(data, timestamp=0.0)
11
+ >>> print(message.msg_type, message.code)
12
+ CON GET
13
+
14
+ References:
15
+ RFC 7252: CoAP (Constrained Application Protocol)
16
+ RFC 7959: Block-Wise Transfers in CoAP
17
+ RFC 7641: Observing Resources in CoAP
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Any, ClassVar
26
+
27
+ from oscura.iot.coap.options import (
28
+ CONTENT_FORMATS,
29
+ OPTIONS,
30
+ OptionParser,
31
+ format_block_option,
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class CoAPMessage:
37
+ """CoAP message representation.
38
+
39
+ Attributes:
40
+ timestamp: Message timestamp in seconds.
41
+ version: CoAP version (always 1 for RFC 7252).
42
+ msg_type: Message type ("CON", "NON", "ACK", "RST").
43
+ code: Method code or response code (e.g., "GET", "2.05 Content").
44
+ message_id: 16-bit message identifier for duplicate detection.
45
+ token: Token for matching requests/responses (0-8 bytes).
46
+ options: Parsed options dictionary (option name -> list of values).
47
+ payload: Message payload bytes.
48
+ is_request: True if request, False if response.
49
+ uri: Reconstructed URI from Uri-* options (optional).
50
+
51
+ Example:
52
+ >>> msg = CoAPMessage(
53
+ ... timestamp=1.0,
54
+ ... version=1,
55
+ ... msg_type="CON",
56
+ ... code="GET",
57
+ ... message_id=0x1234,
58
+ ... token=b"\\x01",
59
+ ... )
60
+ >>> msg.is_request
61
+ True
62
+ """
63
+
64
+ timestamp: float
65
+ version: int
66
+ msg_type: str
67
+ code: str
68
+ message_id: int
69
+ token: bytes
70
+ options: dict[str, list[Any]] = field(default_factory=dict)
71
+ payload: bytes = b""
72
+ is_request: bool = True
73
+ uri: str | None = None
74
+
75
+
76
+ @dataclass
77
+ class CoAPExchange:
78
+ """CoAP request-response exchange.
79
+
80
+ Represents a complete request-response transaction, including
81
+ support for multiple responses (observe pattern).
82
+
83
+ Attributes:
84
+ request: Initial CoAP request message.
85
+ responses: List of response messages (multiple for observe).
86
+ complete: True if exchange is complete (no more responses expected).
87
+ observe: True if this is an observe relationship.
88
+
89
+ Example:
90
+ >>> request = CoAPMessage(...)
91
+ >>> exchange = CoAPExchange(request=request)
92
+ >>> exchange.responses.append(response_msg)
93
+ """
94
+
95
+ request: CoAPMessage
96
+ responses: list[CoAPMessage] = field(default_factory=list)
97
+ complete: bool = False
98
+ observe: bool = False
99
+
100
+
101
+ class CoAPAnalyzer:
102
+ """CoAP protocol analyzer supporting RFC 7252 and extensions.
103
+
104
+ Analyzes CoAP messages including parsing message format, decoding options,
105
+ matching requests with responses, and handling blockwise transfers and
106
+ observe relationships.
107
+
108
+ Attributes:
109
+ MSG_TYPES: Message type code to name mapping.
110
+ METHODS: Method code to name mapping.
111
+ RESPONSE_CLASSES: Response class descriptions.
112
+
113
+ Example:
114
+ >>> analyzer = CoAPAnalyzer()
115
+ >>> msg = analyzer.parse_message(data, timestamp=0.0)
116
+ >>> analyzer.match_request_response()
117
+ >>> analyzer.export_exchanges(Path("coap_traffic.json"))
118
+ """
119
+
120
+ # Message types (RFC 7252 Section 3)
121
+ MSG_TYPES: ClassVar[dict[int, str]] = {
122
+ 0: "CON", # Confirmable
123
+ 1: "NON", # Non-confirmable
124
+ 2: "ACK", # Acknowledgement
125
+ 3: "RST", # Reset
126
+ }
127
+
128
+ # Method codes 0.xx (RFC 7252 Section 5.8)
129
+ METHODS: ClassVar[dict[int, str]] = {
130
+ 1: "GET",
131
+ 2: "POST",
132
+ 3: "PUT",
133
+ 4: "DELETE",
134
+ 5: "FETCH", # RFC 8132
135
+ 6: "PATCH", # RFC 8132
136
+ 7: "iPATCH", # RFC 8132
137
+ }
138
+
139
+ # Response code classes
140
+ RESPONSE_CLASSES: ClassVar[dict[int, str]] = {
141
+ 2: "Success",
142
+ 4: "Client Error",
143
+ 5: "Server Error",
144
+ }
145
+
146
+ def __init__(self) -> None:
147
+ """Initialize CoAP analyzer.
148
+
149
+ Example:
150
+ >>> analyzer = CoAPAnalyzer()
151
+ >>> len(analyzer.messages)
152
+ 0
153
+ """
154
+ self.messages: list[CoAPMessage] = []
155
+ self.exchanges: dict[bytes, CoAPExchange] = {} # Token -> exchange
156
+ self.message_id_map: dict[int, CoAPMessage] = {} # Message ID -> message
157
+ self.option_parser = OptionParser()
158
+
159
+ def parse_message(self, data: bytes, timestamp: float = 0.0) -> CoAPMessage:
160
+ """Parse CoAP message from bytes.
161
+
162
+ Parses complete CoAP message including header, token, options, and payload
163
+ according to RFC 7252 Section 3.
164
+
165
+ Message Format:
166
+ 0 1 2 3
167
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
168
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
169
+ |Ver| T | TKL | Code | Message ID |
170
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
171
+ | Token (if any, TKL bytes) ...
172
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
173
+ | Options (if any) ...
174
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
175
+ |1 1 1 1 1 1 1 1| Payload (if any) ...
176
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
177
+
178
+ Args:
179
+ data: Raw CoAP message bytes.
180
+ timestamp: Message timestamp in seconds.
181
+
182
+ Returns:
183
+ Parsed CoAP message.
184
+
185
+ Raises:
186
+ ValueError: If message is malformed or too short.
187
+
188
+ Example:
189
+ >>> analyzer = CoAPAnalyzer()
190
+ >>> data = bytes([0x40, 0x01, 0x12, 0x34]) # CON GET
191
+ >>> msg = analyzer.parse_message(data)
192
+ >>> msg.msg_type
193
+ 'CON'
194
+ >>> msg.code
195
+ 'GET'
196
+ """
197
+ if len(data) < 4:
198
+ raise ValueError(f"CoAP message too short: {len(data)} bytes (minimum 4)")
199
+
200
+ # Parse header and token
201
+ version, msg_type, code_str, is_request, message_id, token, offset = (
202
+ self._parse_header_and_token(data)
203
+ )
204
+
205
+ # Parse options
206
+ options = self._parse_options(data, offset)
207
+
208
+ # Find payload boundary
209
+ payload_start = self._find_payload_start(data, offset)
210
+
211
+ # Extract payload
212
+ payload = data[payload_start:] if payload_start < len(data) else b""
213
+
214
+ # Reconstruct URI from options
215
+ uri = self._reconstruct_uri(options)
216
+
217
+ message = CoAPMessage(
218
+ timestamp=timestamp,
219
+ version=version,
220
+ msg_type=msg_type,
221
+ code=code_str,
222
+ message_id=message_id,
223
+ token=token,
224
+ options=options,
225
+ payload=payload,
226
+ is_request=is_request,
227
+ uri=uri,
228
+ )
229
+
230
+ self.messages.append(message)
231
+ self.message_id_map[message_id] = message
232
+
233
+ return message
234
+
235
+ def _parse_header_and_token(self, data: bytes) -> tuple[int, str, str, bool, int, bytes, int]:
236
+ """Parse CoAP header and token fields.
237
+
238
+ Args:
239
+ data: Raw message bytes.
240
+
241
+ Returns:
242
+ Tuple of (version, msg_type, code_str, is_request, message_id, token, offset).
243
+
244
+ Raises:
245
+ ValueError: If header or token is invalid.
246
+ """
247
+ byte0 = data[0]
248
+ version = (byte0 >> 6) & 0x03
249
+ msg_type_val = (byte0 >> 4) & 0x03
250
+ tkl = byte0 & 0x0F
251
+
252
+ if version != 1:
253
+ raise ValueError(f"Unsupported CoAP version: {version} (expected 1)")
254
+
255
+ if tkl > 8:
256
+ raise ValueError(f"Invalid token length: {tkl} (maximum 8)")
257
+
258
+ code_byte = data[1]
259
+ message_id = int.from_bytes(data[2:4], "big")
260
+
261
+ msg_type = self.MSG_TYPES.get(msg_type_val, f"UNKNOWN({msg_type_val})")
262
+ code_str, is_request = self._parse_code(code_byte)
263
+
264
+ offset = 4
265
+
266
+ # Parse token (TKL bytes)
267
+ token = b""
268
+ if tkl > 0:
269
+ if len(data) < offset + tkl:
270
+ raise ValueError(f"Insufficient data for token: need {tkl} bytes")
271
+ token = data[offset : offset + tkl]
272
+ offset += tkl
273
+
274
+ return version, msg_type, code_str, is_request, message_id, token, offset
275
+
276
+ def _find_payload_start(self, data: bytes, start_offset: int) -> int:
277
+ """Find start of payload after options.
278
+
279
+ Args:
280
+ data: Complete message data.
281
+ start_offset: Offset where options start.
282
+
283
+ Returns:
284
+ Offset where payload starts.
285
+
286
+ Raises:
287
+ ValueError: If option encoding is invalid.
288
+ """
289
+ payload_start = start_offset
290
+ while payload_start < len(data):
291
+ if data[payload_start] == 0xFF:
292
+ payload_start += 1 # Skip marker
293
+ break
294
+ if payload_start >= len(data):
295
+ break
296
+
297
+ option_byte = data[payload_start]
298
+ delta = (option_byte >> 4) & 0x0F
299
+ length = option_byte & 0x0F
300
+
301
+ payload_start += 1
302
+
303
+ # Handle extended delta
304
+ if delta == 13:
305
+ payload_start += 1
306
+ elif delta == 14:
307
+ payload_start += 2
308
+ elif delta == 15:
309
+ break # Payload marker
310
+
311
+ # Handle extended length
312
+ if length == 13:
313
+ payload_start += 1
314
+ elif length == 14:
315
+ payload_start += 2
316
+ elif length == 15:
317
+ raise ValueError("Invalid option length encoding (15)")
318
+
319
+ # Skip option value
320
+ if delta < 15:
321
+ actual_length = self._calculate_option_length(data, payload_start, length)
322
+ payload_start += actual_length
323
+
324
+ return payload_start
325
+
326
+ def _calculate_option_length(self, data: bytes, offset: int, length_base: int) -> int:
327
+ """Calculate actual option length from extended encoding.
328
+
329
+ Args:
330
+ data: Message data.
331
+ offset: Current offset (after length encoding bytes).
332
+ length_base: Base length value from option byte.
333
+
334
+ Returns:
335
+ Actual option length in bytes.
336
+ """
337
+ if length_base < 13:
338
+ return length_base
339
+ elif length_base == 13 and offset <= len(data):
340
+ return data[offset - 1] + 13
341
+ elif length_base == 14 and offset + 1 <= len(data):
342
+ return int.from_bytes(data[offset - 2 : offset], "big") + 269
343
+ return 0
344
+
345
+ def _parse_code(self, code: int) -> tuple[str, bool]:
346
+ """Parse code byte into human-readable string and request flag.
347
+
348
+ Code byte format: class (3 bits) . detail (5 bits)
349
+ - 0.xx: Request methods
350
+ - 2.xx: Success responses
351
+ - 4.xx: Client error responses
352
+ - 5.xx: Server error responses
353
+
354
+ Args:
355
+ code: Code byte value (0-255).
356
+
357
+ Returns:
358
+ Tuple of (code_string, is_request).
359
+
360
+ Example:
361
+ >>> analyzer = CoAPAnalyzer()
362
+ >>> analyzer._parse_code(0x01)
363
+ ('GET', True)
364
+ >>> analyzer._parse_code(0x45)
365
+ ('2.05 Content', False)
366
+ """
367
+ code_class = (code >> 5) & 0x07
368
+ code_detail = code & 0x1F
369
+
370
+ if code_class == 0:
371
+ # Request method
372
+ method = self.METHODS.get(code_detail, f"0.{code_detail:02d}")
373
+ return method, True
374
+
375
+ # Response code
376
+ code_str = f"{code_class}.{code_detail:02d}"
377
+
378
+ # Add common response names
379
+ response_names = {
380
+ 0x41: "2.01 Created",
381
+ 0x42: "2.02 Deleted",
382
+ 0x43: "2.03 Valid",
383
+ 0x44: "2.04 Changed",
384
+ 0x45: "2.05 Content",
385
+ 0x5F: "2.31 Continue",
386
+ 0x80: "4.00 Bad Request",
387
+ 0x81: "4.01 Unauthorized",
388
+ 0x82: "4.02 Bad Option",
389
+ 0x83: "4.03 Forbidden",
390
+ 0x84: "4.04 Not Found",
391
+ 0x85: "4.05 Method Not Allowed",
392
+ 0x86: "4.06 Not Acceptable",
393
+ 0x8C: "4.12 Precondition Failed",
394
+ 0x8D: "4.13 Request Entity Too Large",
395
+ 0x8F: "4.15 Unsupported Content-Format",
396
+ 0xA0: "5.00 Internal Server Error",
397
+ 0xA1: "5.01 Not Implemented",
398
+ 0xA2: "5.02 Bad Gateway",
399
+ 0xA3: "5.03 Service Unavailable",
400
+ 0xA4: "5.04 Gateway Timeout",
401
+ 0xA5: "5.05 Proxying Not Supported",
402
+ }
403
+
404
+ return response_names.get(code, code_str), False
405
+
406
+ def _parse_options(self, data: bytes, start_offset: int) -> dict[str, list[Any]]:
407
+ """Parse CoAP options using delta encoding.
408
+
409
+ Options are encoded with delta encoding where each option number
410
+ is the sum of all previous deltas. Handles extended delta/length
411
+ encoding for values >= 13.
412
+
413
+ Args:
414
+ data: Complete message data.
415
+ start_offset: Offset where options start.
416
+
417
+ Returns:
418
+ Dictionary mapping option names to lists of decoded values.
419
+
420
+ Raises:
421
+ ValueError: If option encoding is invalid.
422
+
423
+ Example:
424
+ >>> analyzer = CoAPAnalyzer()
425
+ >>> # Parse message with Uri-Path option
426
+ >>> options = analyzer._parse_options(data, 5)
427
+ >>> options.get("Uri-Path", [])
428
+ ['temperature', 'sensor1']
429
+ """
430
+ options: dict[str, list[Any]] = {}
431
+ offset = start_offset
432
+ current_option_num = 0
433
+
434
+ while offset < len(data):
435
+ # Check for payload marker
436
+ if data[offset] == 0xFF:
437
+ break
438
+
439
+ if offset >= len(data):
440
+ break
441
+
442
+ option_byte = data[offset]
443
+ delta_base = (option_byte >> 4) & 0x0F
444
+ length_base = option_byte & 0x0F
445
+ offset += 1
446
+
447
+ # Check for payload marker in delta
448
+ if delta_base == 15:
449
+ offset -= 1 # Back up to marker
450
+ break
451
+
452
+ # Parse extended delta
453
+ try:
454
+ delta, delta_bytes = self.option_parser.parse_extended_value(
455
+ delta_base, data, offset
456
+ )
457
+ offset += delta_bytes
458
+ except ValueError as e:
459
+ raise ValueError(f"Failed to parse option delta: {e}") from e
460
+
461
+ # Parse extended length
462
+ try:
463
+ length, length_bytes = self.option_parser.parse_extended_value(
464
+ length_base, data, offset
465
+ )
466
+ offset += length_bytes
467
+ except ValueError as e:
468
+ raise ValueError(f"Failed to parse option length: {e}") from e
469
+
470
+ # Calculate actual option number
471
+ current_option_num += delta
472
+
473
+ # Extract option value
474
+ if len(data) < offset + length:
475
+ raise ValueError(
476
+ f"Insufficient data for option value: need {length} bytes at offset {offset}"
477
+ )
478
+
479
+ option_value = data[offset : offset + length]
480
+ offset += length
481
+
482
+ # Decode option value
483
+ decoded_value = self.option_parser.decode_value(current_option_num, option_value)
484
+
485
+ # Store option
486
+ option_name = OPTIONS.get(current_option_num, f"Option-{current_option_num}")
487
+
488
+ if option_name not in options:
489
+ options[option_name] = []
490
+
491
+ options[option_name].append(decoded_value)
492
+
493
+ return options
494
+
495
+ def _reconstruct_uri(self, options: dict[str, list[Any]]) -> str | None:
496
+ """Reconstruct URI from Uri-* options.
497
+
498
+ Combines Uri-Host, Uri-Port, Uri-Path, and Uri-Query options
499
+ into a complete URI string.
500
+
501
+ Args:
502
+ options: Parsed options dictionary.
503
+
504
+ Returns:
505
+ Reconstructed URI string, or None if no Uri options present.
506
+
507
+ Example:
508
+ >>> analyzer = CoAPAnalyzer()
509
+ >>> options = {
510
+ ... "Uri-Host": ["example.com"],
511
+ ... "Uri-Path": ["sensors", "temperature"],
512
+ ... "Uri-Query": ["format=json"],
513
+ ... }
514
+ >>> analyzer._reconstruct_uri(options)
515
+ 'coap://example.com/sensors/temperature?format=json'
516
+ """
517
+ if not any(key.startswith("Uri-") for key in options):
518
+ return None
519
+
520
+ # Build URI components
521
+ host = options.get("Uri-Host", [None])[0]
522
+ port = options.get("Uri-Port", [5683])[0] # Default CoAP port
523
+ path_segments = options.get("Uri-Path", [])
524
+ query_params = options.get("Uri-Query", [])
525
+
526
+ # Construct URI
527
+ uri_parts = []
528
+
529
+ if host:
530
+ scheme = "coap"
531
+ if isinstance(port, int) and port != 5683:
532
+ uri_parts.append(f"{scheme}://{host}:{port}")
533
+ else:
534
+ uri_parts.append(f"{scheme}://{host}")
535
+ else:
536
+ uri_parts.append("coap://")
537
+
538
+ # Add path
539
+ if path_segments:
540
+ path = "/" + "/".join(str(seg) for seg in path_segments)
541
+ uri_parts.append(path)
542
+
543
+ # Add query string
544
+ if query_params:
545
+ query = "&".join(str(param) for param in query_params)
546
+ uri_parts.append(f"?{query}")
547
+
548
+ return "".join(uri_parts)
549
+
550
+ def match_request_response(self) -> None:
551
+ """Match requests with responses by token and message ID.
552
+
553
+ Creates CoAPExchange objects linking requests with their responses.
554
+ Supports observe relationships where multiple responses map to one request.
555
+
556
+ Example:
557
+ >>> analyzer = CoAPAnalyzer()
558
+ >>> # Parse messages...
559
+ >>> analyzer.match_request_response()
560
+ >>> len(analyzer.exchanges)
561
+ 5
562
+ """
563
+ # Clear existing exchanges
564
+ self.exchanges = {}
565
+
566
+ for message in self.messages:
567
+ if message.is_request:
568
+ # Create new exchange for request
569
+ observe = "Observe" in message.options
570
+ exchange = CoAPExchange(request=message, observe=observe)
571
+ self.exchanges[message.token] = exchange
572
+ else:
573
+ # Match response to request by token
574
+ if message.token in self.exchanges:
575
+ exchange = self.exchanges[message.token]
576
+ exchange.responses.append(message)
577
+
578
+ # Mark complete if not observe or if ACK/RST
579
+ if not exchange.observe or message.msg_type in ("ACK", "RST"):
580
+ exchange.complete = True
581
+
582
+ def export_exchanges(self, output_path: Path) -> None:
583
+ """Export request-response exchanges as JSON.
584
+
585
+ Exports all matched exchanges including request, responses, timing,
586
+ and decoded options.
587
+
588
+ Args:
589
+ output_path: Path to output JSON file.
590
+
591
+ Example:
592
+ >>> analyzer = CoAPAnalyzer()
593
+ >>> # Parse and match messages...
594
+ >>> analyzer.export_exchanges(Path("coap_exchanges.json"))
595
+ """
596
+ export_data: dict[str, Any] = {
597
+ "summary": {
598
+ "total_messages": len(self.messages),
599
+ "total_exchanges": len(self.exchanges),
600
+ "complete_exchanges": sum(1 for ex in self.exchanges.values() if ex.complete),
601
+ },
602
+ "exchanges": [],
603
+ }
604
+
605
+ for token, exchange in self.exchanges.items():
606
+ request_data = self._message_to_dict(exchange.request)
607
+
608
+ responses_data = [self._message_to_dict(resp) for resp in exchange.responses]
609
+
610
+ exchange_entry = {
611
+ "token": token.hex() if token else "",
612
+ "observe": exchange.observe,
613
+ "complete": exchange.complete,
614
+ "request": request_data,
615
+ "responses": responses_data,
616
+ "response_count": len(exchange.responses),
617
+ }
618
+
619
+ export_data["exchanges"].append(exchange_entry)
620
+
621
+ # Write JSON
622
+ with output_path.open("w") as f:
623
+ json.dump(export_data, f, indent=2)
624
+
625
+ def _message_to_dict(self, message: CoAPMessage) -> dict[str, Any]:
626
+ """Convert CoAPMessage to dictionary for JSON export.
627
+
628
+ Args:
629
+ message: CoAP message to convert.
630
+
631
+ Returns:
632
+ Dictionary representation of message.
633
+ """
634
+ # Format options for readability
635
+ formatted_options: dict[str, list[Any]] = {}
636
+ for name, values in message.options.items():
637
+ formatted_values: list[Any] = []
638
+ for value in values:
639
+ if isinstance(value, bytes):
640
+ formatted_values.append(value.hex())
641
+ elif isinstance(value, int) and name == "Content-Format":
642
+ # Add content format name
643
+ format_name = CONTENT_FORMATS.get(value, "unknown")
644
+ formatted_values.append({"code": value, "format": format_name})
645
+ elif isinstance(value, int) and name in ("Block1", "Block2"):
646
+ # Decode block option
647
+ block_info = format_block_option(value)
648
+ formatted_values.append(block_info)
649
+ else:
650
+ formatted_values.append(value)
651
+ formatted_options[name] = formatted_values
652
+
653
+ return {
654
+ "timestamp": message.timestamp,
655
+ "type": message.msg_type,
656
+ "code": message.code,
657
+ "message_id": f"0x{message.message_id:04X}",
658
+ "token": message.token.hex() if message.token else "",
659
+ "uri": message.uri,
660
+ "options": formatted_options,
661
+ "payload_length": len(message.payload),
662
+ "payload": message.payload.hex()
663
+ if len(message.payload) <= 64
664
+ else f"{message.payload[:64].hex()}... ({len(message.payload)} bytes)",
665
+ }
666
+
667
+
668
+ __all__ = ["CoAPAnalyzer", "CoAPExchange", "CoAPMessage"]