oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,153 @@
1
+ """Zigbee security and encryption support.
2
+
3
+ This module provides utilities for handling Zigbee security frames
4
+ including network key management and frame decryption support.
5
+
6
+ Note: This module provides structure parsing only. Actual AES-CCM
7
+ decryption requires cryptography libraries and valid network keys.
8
+
9
+ References:
10
+ Zigbee Specification Section 4.5 (Security)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+
18
+ def parse_security_header(data: bytes) -> dict[str, Any]:
19
+ """Parse Zigbee security header from NWK auxiliary header.
20
+
21
+ Args:
22
+ data: Security header bytes.
23
+
24
+ Returns:
25
+ Parsed security header fields.
26
+
27
+ Example:
28
+ >>> header = parse_security_header(security_data)
29
+ >>> print(header['security_level'])
30
+ 5
31
+ """
32
+ if len(data) < 5:
33
+ return {"error": "Insufficient data for security header"}
34
+
35
+ security_control = data[0]
36
+ frame_counter = int.from_bytes(data[1:5], "little")
37
+
38
+ security_level = security_control & 0x07
39
+ key_identifier = (security_control >> 3) & 0x03
40
+ extended_nonce = bool(security_control & 0x20)
41
+
42
+ result: dict[str, Any] = {
43
+ "security_control": security_control,
44
+ "security_level": security_level,
45
+ "key_identifier": key_identifier,
46
+ "extended_nonce": extended_nonce,
47
+ "frame_counter": frame_counter,
48
+ }
49
+
50
+ offset = 5
51
+
52
+ # Extended nonce includes source address
53
+ if extended_nonce:
54
+ if len(data) < offset + 8:
55
+ result["error"] = "Insufficient data for extended nonce"
56
+ return result
57
+ source_address = int.from_bytes(data[offset : offset + 8], "little")
58
+ result["source_address"] = source_address
59
+ offset += 8
60
+
61
+ # Key sequence number (if key identifier indicates it)
62
+ if key_identifier == 0x01: # Network key
63
+ if len(data) < offset + 1:
64
+ result["error"] = "Insufficient data for key sequence"
65
+ return result
66
+ result["key_sequence_number"] = data[offset]
67
+ offset += 1
68
+
69
+ result["header_length"] = offset
70
+ return result
71
+
72
+
73
+ def is_frame_encrypted(nwk_frame_control: int) -> bool:
74
+ """Check if NWK frame is encrypted.
75
+
76
+ Args:
77
+ nwk_frame_control: NWK frame control field (2 bytes as int).
78
+
79
+ Returns:
80
+ True if frame is encrypted, False otherwise.
81
+
82
+ Example:
83
+ >>> frame_control = 0x0208 # Security enabled
84
+ >>> is_frame_encrypted(frame_control)
85
+ True
86
+ """
87
+ # Security bit is bit 1 of the high byte (bit 9 overall)
88
+ return bool((nwk_frame_control >> 9) & 0x01)
89
+
90
+
91
+ def get_security_level_name(level: int) -> str:
92
+ """Get human-readable security level name.
93
+
94
+ Args:
95
+ level: Security level (0-7).
96
+
97
+ Returns:
98
+ Security level name.
99
+
100
+ Example:
101
+ >>> get_security_level_name(5)
102
+ 'ENC-MIC-32'
103
+ """
104
+ levels = {
105
+ 0x00: "None",
106
+ 0x01: "MIC-32",
107
+ 0x02: "MIC-64",
108
+ 0x03: "MIC-128",
109
+ 0x04: "ENC",
110
+ 0x05: "ENC-MIC-32",
111
+ 0x06: "ENC-MIC-64",
112
+ 0x07: "ENC-MIC-128",
113
+ }
114
+ return levels.get(level, f"Unknown ({level})")
115
+
116
+
117
+ def decrypt_frame(
118
+ encrypted_payload: bytes,
119
+ network_key: bytes,
120
+ nonce: bytes,
121
+ security_level: int,
122
+ ) -> bytes | None:
123
+ """Decrypt Zigbee encrypted frame.
124
+
125
+ Note: This is a placeholder implementation. Actual decryption
126
+ requires AES-CCM implementation with proper nonce construction.
127
+
128
+ Args:
129
+ encrypted_payload: Encrypted payload with MIC.
130
+ network_key: 128-bit AES network key.
131
+ nonce: 13-byte nonce.
132
+ security_level: Security level (determines MIC size).
133
+
134
+ Returns:
135
+ Decrypted payload or None if decryption not available.
136
+
137
+ Example:
138
+ >>> # Requires cryptography library
139
+ >>> decrypted = decrypt_frame(payload, key, nonce, 5)
140
+ """
141
+ # Placeholder - real implementation would use AES-CCM
142
+ # from cryptography.hazmat.primitives.ciphers.aead import AESCCM
143
+ # cipher = AESCCM(network_key)
144
+ # return cipher.decrypt(nonce, encrypted_payload, None)
145
+ return None # Not implemented without cryptography dependencies
146
+
147
+
148
+ __all__ = [
149
+ "decrypt_frame",
150
+ "get_security_level_name",
151
+ "is_frame_encrypted",
152
+ "parse_security_header",
153
+ ]
@@ -0,0 +1,349 @@
1
+ """Zigbee Cluster Library (ZCL) definitions and parsers.
2
+
3
+ This module contains standard ZCL cluster definitions and frame parsers
4
+ for common clusters including On/Off, Level Control, Temperature, etc.
5
+
6
+ References:
7
+ Zigbee Cluster Library Specification (CSA-IOT)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ # Standard ZCL cluster IDs (subset of most common clusters)
15
+ ZCL_CLUSTERS: dict[int, str] = {
16
+ 0x0000: "Basic",
17
+ 0x0001: "Power Configuration",
18
+ 0x0003: "Identify",
19
+ 0x0004: "Groups",
20
+ 0x0005: "Scenes",
21
+ 0x0006: "On/Off",
22
+ 0x0008: "Level Control",
23
+ 0x0009: "Alarms",
24
+ 0x000A: "Time",
25
+ 0x0020: "Poll Control",
26
+ 0x0201: "Thermostat",
27
+ 0x0300: "Color Control",
28
+ 0x0400: "Illuminance Measurement",
29
+ 0x0402: "Temperature Measurement",
30
+ 0x0403: "Pressure Measurement",
31
+ 0x0404: "Flow Measurement",
32
+ 0x0405: "Relative Humidity Measurement",
33
+ 0x0406: "Occupancy Sensing",
34
+ 0x0500: "IAS Zone",
35
+ 0x0702: "Metering",
36
+ 0x0B04: "Electrical Measurement",
37
+ }
38
+
39
+ # ZCL frame control field bits
40
+ ZCL_FRAME_TYPE_MASK = 0x03
41
+ ZCL_FRAME_TYPE_GLOBAL = 0x00
42
+ ZCL_FRAME_TYPE_CLUSTER = 0x01
43
+
44
+ ZCL_MANUFACTURER_SPECIFIC = 0x04
45
+ ZCL_DIRECTION_SERVER_TO_CLIENT = 0x08
46
+ ZCL_DISABLE_DEFAULT_RESPONSE = 0x10
47
+
48
+ # Global ZCL commands
49
+ ZCL_GLOBAL_COMMANDS = {
50
+ 0x00: "Read Attributes",
51
+ 0x01: "Read Attributes Response",
52
+ 0x02: "Write Attributes",
53
+ 0x03: "Write Attributes Undivided",
54
+ 0x04: "Write Attributes Response",
55
+ 0x05: "Write Attributes No Response",
56
+ 0x06: "Configure Reporting",
57
+ 0x07: "Configure Reporting Response",
58
+ 0x08: "Read Reporting Configuration",
59
+ 0x09: "Read Reporting Configuration Response",
60
+ 0x0A: "Report Attributes",
61
+ 0x0B: "Default Response",
62
+ 0x0C: "Discover Attributes",
63
+ 0x0D: "Discover Attributes Response",
64
+ 0x0E: "Read Attributes Structured",
65
+ 0x0F: "Write Attributes Structured",
66
+ 0x10: "Write Attributes Structured Response",
67
+ 0x11: "Discover Commands Received",
68
+ 0x12: "Discover Commands Received Response",
69
+ 0x13: "Discover Commands Generated",
70
+ 0x14: "Discover Commands Generated Response",
71
+ 0x15: "Discover Attributes Extended",
72
+ 0x16: "Discover Attributes Extended Response",
73
+ }
74
+
75
+ # On/Off cluster (0x0006) commands
76
+ ZCL_ONOFF_COMMANDS = {
77
+ 0x00: "Off",
78
+ 0x01: "On",
79
+ 0x02: "Toggle",
80
+ 0x40: "Off With Effect",
81
+ 0x41: "On With Recall Global Scene",
82
+ 0x42: "On With Timed Off",
83
+ }
84
+
85
+ # Level Control cluster (0x0008) commands
86
+ ZCL_LEVEL_CONTROL_COMMANDS = {
87
+ 0x00: "Move to Level",
88
+ 0x01: "Move",
89
+ 0x02: "Step",
90
+ 0x03: "Stop",
91
+ 0x04: "Move to Level (with On/Off)",
92
+ 0x05: "Move (with On/Off)",
93
+ 0x06: "Step (with On/Off)",
94
+ 0x07: "Stop (with On/Off)",
95
+ }
96
+
97
+ # Color Control cluster (0x0300) commands
98
+ ZCL_COLOR_CONTROL_COMMANDS = {
99
+ 0x00: "Move to Hue",
100
+ 0x01: "Move Hue",
101
+ 0x02: "Step Hue",
102
+ 0x03: "Move to Saturation",
103
+ 0x04: "Move Saturation",
104
+ 0x05: "Step Saturation",
105
+ 0x06: "Move to Hue and Saturation",
106
+ 0x07: "Move to Color",
107
+ 0x08: "Move Color",
108
+ 0x09: "Step Color",
109
+ 0x0A: "Move to Color Temperature",
110
+ }
111
+
112
+
113
+ def parse_zcl_frame(cluster_id: int, data: bytes) -> dict[str, Any]:
114
+ """Parse ZCL frame for specific cluster.
115
+
116
+ Args:
117
+ cluster_id: ZCL cluster ID (e.g., 0x0006 for On/Off).
118
+ data: ZCL frame payload.
119
+
120
+ Returns:
121
+ Parsed ZCL frame with cluster-specific details.
122
+
123
+ Example:
124
+ >>> data = bytes([0x01, 0x00, 0x01]) # On/Off cluster, On command
125
+ >>> result = parse_zcl_frame(0x0006, data)
126
+ >>> print(result['command_name'])
127
+ On
128
+ """
129
+ if len(data) < 3:
130
+ return {
131
+ "error": "Insufficient ZCL data",
132
+ "cluster_id": cluster_id,
133
+ "cluster_name": ZCL_CLUSTERS.get(cluster_id, "Unknown"),
134
+ }
135
+
136
+ frame_control = data[0]
137
+ transaction_seq = data[1]
138
+ command_id = data[2]
139
+
140
+ frame_type = frame_control & ZCL_FRAME_TYPE_MASK
141
+ manufacturer_specific = bool(frame_control & ZCL_MANUFACTURER_SPECIFIC)
142
+ direction = (
143
+ "server_to_client"
144
+ if (frame_control & ZCL_DIRECTION_SERVER_TO_CLIENT)
145
+ else "client_to_server"
146
+ )
147
+ disable_default_response = bool(frame_control & ZCL_DISABLE_DEFAULT_RESPONSE)
148
+
149
+ result: dict[str, Any] = {
150
+ "cluster_id": cluster_id,
151
+ "cluster_name": ZCL_CLUSTERS.get(cluster_id, f"Unknown (0x{cluster_id:04X})"),
152
+ "frame_control": frame_control,
153
+ "frame_type": "global" if frame_type == ZCL_FRAME_TYPE_GLOBAL else "cluster_specific",
154
+ "manufacturer_specific": manufacturer_specific,
155
+ "direction": direction,
156
+ "disable_default_response": disable_default_response,
157
+ "transaction_sequence": transaction_seq,
158
+ "command_id": command_id,
159
+ }
160
+
161
+ # Parse manufacturer code if present
162
+ offset = 3
163
+ if manufacturer_specific:
164
+ if len(data) >= 5:
165
+ mfr_code = int.from_bytes(data[3:5], "little")
166
+ result["manufacturer_code"] = mfr_code
167
+ offset = 5
168
+ else:
169
+ result["error"] = "Insufficient data for manufacturer code"
170
+ return result
171
+
172
+ payload = data[offset:]
173
+ result["payload"] = payload
174
+
175
+ # Parse cluster-specific commands
176
+ if frame_type == ZCL_FRAME_TYPE_GLOBAL:
177
+ result["command_name"] = ZCL_GLOBAL_COMMANDS.get(
178
+ command_id, f"Unknown Global (0x{command_id:02X})"
179
+ )
180
+ if command_id == 0x00: # Read Attributes
181
+ result["details"] = _parse_read_attributes(payload)
182
+ elif command_id == 0x01: # Read Attributes Response
183
+ result["details"] = _parse_read_attributes_response(payload)
184
+ elif command_id == 0x0A: # Report Attributes
185
+ result["details"] = _parse_report_attributes(payload)
186
+ else:
187
+ # Cluster-specific command
188
+ if cluster_id == 0x0006: # On/Off
189
+ result["command_name"] = ZCL_ONOFF_COMMANDS.get(
190
+ command_id, f"Unknown (0x{command_id:02X})"
191
+ )
192
+ if command_id in [0x00, 0x01, 0x02]: # Off, On, Toggle
193
+ result["details"] = {"simple_command": True}
194
+ elif cluster_id == 0x0008: # Level Control
195
+ result["command_name"] = ZCL_LEVEL_CONTROL_COMMANDS.get(
196
+ command_id, f"Unknown (0x{command_id:02X})"
197
+ )
198
+ if command_id == 0x00 and len(payload) >= 2: # Move to Level
199
+ result["details"] = {
200
+ "level": payload[0],
201
+ "transition_time": int.from_bytes(payload[1:3], "little")
202
+ if len(payload) >= 3
203
+ else None,
204
+ }
205
+ elif cluster_id == 0x0300: # Color Control
206
+ result["command_name"] = ZCL_COLOR_CONTROL_COMMANDS.get(
207
+ command_id, f"Unknown (0x{command_id:02X})"
208
+ )
209
+ else:
210
+ result["command_name"] = f"Cluster Command 0x{command_id:02X}"
211
+
212
+ return result
213
+
214
+
215
+ def _parse_read_attributes(payload: bytes) -> dict[str, Any]:
216
+ """Parse Read Attributes command payload.
217
+
218
+ Args:
219
+ payload: ZCL payload after command ID.
220
+
221
+ Returns:
222
+ Parsed attribute IDs.
223
+ """
224
+ if len(payload) < 2:
225
+ return {"error": "Insufficient data"}
226
+
227
+ attribute_ids = []
228
+ offset = 0
229
+ while offset + 2 <= len(payload):
230
+ attr_id = int.from_bytes(payload[offset : offset + 2], "little")
231
+ attribute_ids.append(attr_id)
232
+ offset += 2
233
+
234
+ return {"attribute_ids": attribute_ids}
235
+
236
+
237
+ def _parse_read_attributes_response(payload: bytes) -> dict[str, Any]:
238
+ """Parse Read Attributes Response payload.
239
+
240
+ Args:
241
+ payload: ZCL payload after command ID.
242
+
243
+ Returns:
244
+ Parsed attribute records.
245
+ """
246
+ if len(payload) < 3:
247
+ return {"error": "Insufficient data"}
248
+
249
+ attributes = []
250
+ offset = 0
251
+ while offset + 3 <= len(payload):
252
+ attr_id = int.from_bytes(payload[offset : offset + 2], "little")
253
+ status = payload[offset + 2]
254
+ offset += 3
255
+
256
+ attr_record: dict[str, Any] = {
257
+ "attribute_id": attr_id,
258
+ "status": status,
259
+ }
260
+
261
+ if status == 0x00: # Success
262
+ if offset >= len(payload):
263
+ break
264
+ data_type = payload[offset]
265
+ offset += 1
266
+
267
+ # Parse value based on data type (simplified)
268
+ if data_type == 0x10: # Boolean
269
+ if offset < len(payload):
270
+ attr_record["value"] = bool(payload[offset])
271
+ offset += 1
272
+ elif data_type == 0x20: # Uint8
273
+ if offset < len(payload):
274
+ attr_record["value"] = payload[offset]
275
+ offset += 1
276
+ elif data_type == 0x21: # Uint16
277
+ if offset + 2 <= len(payload):
278
+ attr_record["value"] = int.from_bytes(payload[offset : offset + 2], "little")
279
+ offset += 2
280
+ elif data_type == 0x29: # Int16
281
+ if offset + 2 <= len(payload):
282
+ attr_record["value"] = int.from_bytes(
283
+ payload[offset : offset + 2], "little", signed=True
284
+ )
285
+ offset += 2
286
+ else:
287
+ attr_record["data_type"] = data_type
288
+ # Skip unknown data type
289
+ break
290
+
291
+ attributes.append(attr_record)
292
+
293
+ return {"attributes": attributes}
294
+
295
+
296
+ def _parse_report_attributes(payload: bytes) -> dict[str, Any]:
297
+ """Parse Report Attributes payload.
298
+
299
+ Args:
300
+ payload: ZCL payload after command ID.
301
+
302
+ Returns:
303
+ Parsed attribute reports.
304
+ """
305
+ # Similar to Read Attributes Response but without status field
306
+ if len(payload) < 3:
307
+ return {"error": "Insufficient data"}
308
+
309
+ attributes = []
310
+ offset = 0
311
+ while offset + 3 <= len(payload):
312
+ attr_id = int.from_bytes(payload[offset : offset + 2], "little")
313
+ data_type = payload[offset + 2]
314
+ offset += 3
315
+
316
+ attr_record: dict[str, Any] = {
317
+ "attribute_id": attr_id,
318
+ "data_type": data_type,
319
+ }
320
+
321
+ # Parse value based on data type (simplified)
322
+ if data_type == 0x10: # Boolean
323
+ if offset < len(payload):
324
+ attr_record["value"] = bool(payload[offset])
325
+ offset += 1
326
+ elif data_type == 0x20: # Uint8
327
+ if offset < len(payload):
328
+ attr_record["value"] = payload[offset]
329
+ offset += 1
330
+ elif data_type == 0x21: # Uint16
331
+ if offset + 2 <= len(payload):
332
+ attr_record["value"] = int.from_bytes(payload[offset : offset + 2], "little")
333
+ offset += 2
334
+ elif data_type == 0x29: # Int16
335
+ if offset + 2 <= len(payload):
336
+ attr_record["value"] = int.from_bytes(
337
+ payload[offset : offset + 2], "little", signed=True
338
+ )
339
+ offset += 2
340
+ else:
341
+ # Skip unknown data type
342
+ break
343
+
344
+ attributes.append(attr_record)
345
+
346
+ return {"attributes": attributes}
347
+
348
+
349
+ __all__ = ["ZCL_CLUSTERS", "parse_zcl_frame"]
oscura/jupyter/display.py CHANGED
@@ -15,9 +15,15 @@ Example:
15
15
  from typing import Any
16
16
 
17
17
  try:
18
- from IPython.display import HTML, SVG, display
18
+ from IPython.display import HTML, SVG
19
+ from IPython.display import display as ipython_display
19
20
 
20
21
  IPYTHON_AVAILABLE = True
22
+
23
+ def display(*args: Any, **kwargs: Any) -> None:
24
+ """Wrapper for IPython display."""
25
+ ipython_display(*args, **kwargs) # type: ignore[no-untyped-call]
26
+
21
27
  except ImportError:
22
28
  IPYTHON_AVAILABLE = False
23
29
 
@@ -33,7 +39,7 @@ except ImportError:
33
39
  def __init__(self, data: str) -> None:
34
40
  self.data = data
35
41
 
36
- def display(*args: Any, **kwargs: Any) -> None: # type: ignore[no-redef,misc]
42
+ def display(*args: Any, **kwargs: Any) -> None:
37
43
  """Fallback display when IPython not available."""
38
44
  for arg in args:
39
45
  print(arg)
@@ -58,56 +64,130 @@ class TraceDisplay:
58
64
  def _repr_html_(self) -> str:
59
65
  """Generate HTML representation for Jupyter."""
60
66
  trace = self.trace
67
+ rows: list[tuple[str, str]] = []
68
+
69
+ # Collect trace information
70
+ self._add_basic_rows(trace, rows)
71
+ self._add_metadata_rows(trace, rows)
72
+ self._add_duration_row(trace, rows)
73
+ self._add_statistics_rows(trace, rows)
61
74
 
62
- # Build info rows
63
- rows = []
75
+ # Build HTML table
76
+ return self._build_html_table(rows)
77
+
78
+ def _add_basic_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
79
+ """Add basic trace information rows.
64
80
 
81
+ Args:
82
+ trace: Trace object.
83
+ rows: List of (label, value) tuples.
84
+ """
65
85
  if hasattr(trace, "data"):
66
86
  rows.append(("Samples", f"{len(trace.data):,}"))
67
87
 
68
- if hasattr(trace, "metadata"):
69
- meta = trace.metadata
70
- if hasattr(meta, "sample_rate") and meta.sample_rate:
71
- rate = meta.sample_rate
72
- if rate >= 1e9:
73
- rate_str = f"{rate / 1e9:.3f} GSa/s"
74
- elif rate >= 1e6:
75
- rate_str = f"{rate / 1e6:.3f} MSa/s"
76
- else:
77
- rate_str = f"{rate / 1e3:.3f} kSa/s"
78
- rows.append(("Sample Rate", rate_str))
79
-
80
- if hasattr(meta, "channel_name") and meta.channel_name:
81
- rows.append(("Channel", meta.channel_name))
82
-
83
- if hasattr(meta, "source_file") and meta.source_file:
84
- rows.append(("Source", meta.source_file))
85
-
86
- # Calculate duration if possible
87
- if hasattr(trace, "data") and hasattr(trace, "metadata"):
88
- if hasattr(trace.metadata, "sample_rate") and trace.metadata.sample_rate:
89
- duration = len(trace.data) / trace.metadata.sample_rate
90
- if duration >= 1:
91
- dur_str = f"{duration:.3f} s"
92
- elif duration >= 1e-3:
93
- dur_str = f"{duration * 1e3:.3f} ms"
94
- elif duration >= 1e-6:
95
- dur_str = f"{duration * 1e6:.3f} us"
96
- else:
97
- dur_str = f"{duration * 1e9:.3f} ns"
98
- rows.append(("Duration", dur_str))
99
-
100
- # Data statistics
101
- if hasattr(trace, "data"):
102
- import numpy as np
88
+ def _add_metadata_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
89
+ """Add metadata information rows.
103
90
 
104
- data = trace.data
105
- rows.append(("Min", f"{np.min(data):.4g}"))
106
- rows.append(("Max", f"{np.max(data):.4g}"))
107
- rows.append(("Mean", f"{np.mean(data):.4g}"))
108
- rows.append(("Std Dev", f"{np.std(data):.4g}"))
91
+ Args:
92
+ trace: Trace object.
93
+ rows: List of (label, value) tuples.
94
+ """
95
+ if not hasattr(trace, "metadata"):
96
+ return
109
97
 
110
- # Build HTML table
98
+ meta = trace.metadata
99
+
100
+ # Sample rate
101
+ if hasattr(meta, "sample_rate") and meta.sample_rate:
102
+ rate_str = self._format_sample_rate(meta.sample_rate)
103
+ rows.append(("Sample Rate", rate_str))
104
+
105
+ # Channel name
106
+ if hasattr(meta, "channel_name") and meta.channel_name:
107
+ rows.append(("Channel", meta.channel_name))
108
+
109
+ # Source file
110
+ if hasattr(meta, "source_file") and meta.source_file:
111
+ rows.append(("Source", meta.source_file))
112
+
113
+ def _format_sample_rate(self, rate: float) -> str:
114
+ """Format sample rate with appropriate units.
115
+
116
+ Args:
117
+ rate: Sample rate in Hz.
118
+
119
+ Returns:
120
+ Formatted string with units.
121
+ """
122
+ if rate >= 1e9:
123
+ return f"{rate / 1e9:.3f} GSa/s"
124
+ elif rate >= 1e6:
125
+ return f"{rate / 1e6:.3f} MSa/s"
126
+ else:
127
+ return f"{rate / 1e3:.3f} kSa/s"
128
+
129
+ def _add_duration_row(self, trace: Any, rows: list[tuple[str, str]]) -> None:
130
+ """Add duration information row.
131
+
132
+ Args:
133
+ trace: Trace object.
134
+ rows: List of (label, value) tuples.
135
+ """
136
+ if not (hasattr(trace, "data") and hasattr(trace, "metadata")):
137
+ return
138
+
139
+ if not (hasattr(trace.metadata, "sample_rate") and trace.metadata.sample_rate):
140
+ return
141
+
142
+ duration = len(trace.data) / trace.metadata.sample_rate
143
+ dur_str = self._format_duration(duration)
144
+ rows.append(("Duration", dur_str))
145
+
146
+ def _format_duration(self, duration: float) -> str:
147
+ """Format duration with appropriate units.
148
+
149
+ Args:
150
+ duration: Duration in seconds.
151
+
152
+ Returns:
153
+ Formatted string with units.
154
+ """
155
+ if duration >= 1:
156
+ return f"{duration:.3f} s"
157
+ elif duration >= 1e-3:
158
+ return f"{duration * 1e3:.3f} ms"
159
+ elif duration >= 1e-6:
160
+ return f"{duration * 1e6:.3f} us"
161
+ else:
162
+ return f"{duration * 1e9:.3f} ns"
163
+
164
+ def _add_statistics_rows(self, trace: Any, rows: list[tuple[str, str]]) -> None:
165
+ """Add data statistics rows.
166
+
167
+ Args:
168
+ trace: Trace object.
169
+ rows: List of (label, value) tuples.
170
+ """
171
+ if not hasattr(trace, "data"):
172
+ return
173
+
174
+ import numpy as np
175
+
176
+ data = trace.data
177
+ rows.append(("Min", f"{np.min(data):.4g}"))
178
+ rows.append(("Max", f"{np.max(data):.4g}"))
179
+ rows.append(("Mean", f"{np.mean(data):.4g}"))
180
+ rows.append(("Std Dev", f"{np.std(data):.4g}"))
181
+
182
+ def _build_html_table(self, rows: list[tuple[str, str]]) -> str:
183
+ """Build HTML table from rows.
184
+
185
+ Args:
186
+ rows: List of (label, value) tuples.
187
+
188
+ Returns:
189
+ HTML string.
190
+ """
111
191
  html = f"""
112
192
  <div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px; max-width: 400px;">
113
193
  <h4 style="margin: 0 0 10px 0; color: #333;">{self.title}</h4>