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,801 @@
1
+ """LoRaWAN protocol decoder with MAC layer parsing and payload decryption.
2
+
3
+ This module provides comprehensive LoRaWAN MAC frame decoding including:
4
+ - MAC header (MHDR) parsing
5
+ - Frame control (FCtrl) field parsing
6
+ - MAC command parsing from FOpts field
7
+ - Payload decryption using AES-128 CTR mode
8
+ - Message Integrity Code (MIC) verification
9
+
10
+ References:
11
+ LoRaWAN Specification 1.0.3: https://lora-alliance.org/resource_hub/lorawan-specification-v1-0-3/
12
+ Section 4 - MAC Message Formats
13
+ Section 4.3 - MAC Frame Payload Encryption
14
+ Section 4.4 - Message Integrity Code (MIC)
15
+
16
+ Example:
17
+ >>> from oscura.iot.lorawan import LoRaWANDecoder, LoRaWANKeys
18
+ >>> keys = LoRaWANKeys(
19
+ ... app_skey=bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"),
20
+ ... nwk_skey=bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"),
21
+ ... )
22
+ >>> decoder = LoRaWANDecoder(keys=keys)
23
+ >>> frame_bytes = bytes.fromhex("40...")
24
+ >>> frame = decoder.decode_frame(frame_bytes, timestamp=0.0)
25
+ >>> print(f"MType: {frame.mtype}, DevAddr: 0x{frame.dev_addr:08X}")
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass, field
31
+ from typing import Any, ClassVar, Literal
32
+
33
+ from oscura.iot.lorawan.mac_commands import parse_mac_commands
34
+
35
+
36
+ @dataclass
37
+ class LoRaWANKeys:
38
+ """LoRaWAN encryption keys.
39
+
40
+ Attributes:
41
+ app_skey: Application session key (16 bytes) for encrypting application data.
42
+ nwk_skey: Network session key (16 bytes) for MIC calculation.
43
+ app_key: Application key (16 bytes) for Join-accept decryption.
44
+
45
+ Example:
46
+ >>> keys = LoRaWANKeys(
47
+ ... app_skey=bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"),
48
+ ... nwk_skey=bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"),
49
+ ... )
50
+ """
51
+
52
+ app_skey: bytes | None = None
53
+ nwk_skey: bytes | None = None
54
+ app_key: bytes | None = None
55
+
56
+ def __post_init__(self) -> None:
57
+ """Validate key lengths."""
58
+ if self.app_skey is not None and len(self.app_skey) != 16:
59
+ msg = f"AppSKey must be 16 bytes, got {len(self.app_skey)}"
60
+ raise ValueError(msg)
61
+ if self.nwk_skey is not None and len(self.nwk_skey) != 16:
62
+ msg = f"NwkSKey must be 16 bytes, got {len(self.nwk_skey)}"
63
+ raise ValueError(msg)
64
+ if self.app_key is not None and len(self.app_key) != 16:
65
+ msg = f"AppKey must be 16 bytes, got {len(self.app_key)}"
66
+ raise ValueError(msg)
67
+
68
+
69
+ @dataclass
70
+ class LoRaWANFrame:
71
+ """LoRaWAN MAC frame representation.
72
+
73
+ Attributes:
74
+ timestamp: Frame timestamp in seconds.
75
+ mtype: Message type string (e.g., "Unconfirmed Data Up").
76
+ dev_addr: Device address (4 bytes, optional).
77
+ fctrl: Frame control flags dictionary.
78
+ fcnt: Frame counter.
79
+ fopts: Frame options (MAC commands).
80
+ fport: Application port number.
81
+ frm_payload: Encrypted or plaintext payload.
82
+ mic: Message Integrity Code (32-bit).
83
+ decrypted_payload: Decrypted payload if keys available.
84
+ parsed_mac_commands: Parsed MAC commands from FOpts.
85
+ mic_valid: Whether MIC verification passed (None if not checked).
86
+ errors: List of parsing/validation errors.
87
+
88
+ Example:
89
+ >>> frame = LoRaWANFrame(
90
+ ... timestamp=0.0,
91
+ ... mtype="Unconfirmed Data Up",
92
+ ... dev_addr=0x01020304,
93
+ ... fcnt=1,
94
+ ... )
95
+ """
96
+
97
+ timestamp: float
98
+ mtype: str
99
+ dev_addr: int | None = None
100
+ fctrl: dict[str, bool | int] | None = None
101
+ fcnt: int | None = None
102
+ fopts: bytes = b""
103
+ fport: int | None = None
104
+ frm_payload: bytes = b""
105
+ mic: int | None = None
106
+ decrypted_payload: bytes | None = None
107
+ parsed_mac_commands: list[dict[str, Any]] = field(default_factory=list)
108
+ mic_valid: bool | None = None
109
+ errors: list[str] = field(default_factory=list)
110
+
111
+
112
+ class LoRaWANDecoder:
113
+ """LoRaWAN protocol decoder with payload decryption.
114
+
115
+ Supports all LoRaWAN message types and provides MAC command parsing
116
+ and optional payload decryption when session keys are provided.
117
+
118
+ Attributes:
119
+ MTYPES: Message type lookup table.
120
+ MAJOR_VERSIONS: LoRaWAN version lookup table.
121
+
122
+ Example:
123
+ >>> decoder = LoRaWANDecoder()
124
+ >>> frame = decoder.decode_frame(raw_bytes, timestamp=0.0)
125
+ >>> print(f"MType: {frame.mtype}")
126
+ """
127
+
128
+ # Message types (MType field in MHDR)
129
+ MTYPES: ClassVar[dict[int, str]] = {
130
+ 0x00: "Join-request",
131
+ 0x01: "Join-accept",
132
+ 0x02: "Unconfirmed Data Up",
133
+ 0x03: "Unconfirmed Data Down",
134
+ 0x04: "Confirmed Data Up",
135
+ 0x05: "Confirmed Data Down",
136
+ 0x06: "RFU",
137
+ 0x07: "Proprietary",
138
+ }
139
+
140
+ # Major version
141
+ MAJOR_VERSIONS: ClassVar[dict[int, str]] = {
142
+ 0x00: "LoRaWAN R1",
143
+ }
144
+
145
+ def __init__(self, keys: LoRaWANKeys | None = None) -> None:
146
+ """Initialize LoRaWAN decoder with optional encryption keys.
147
+
148
+ Args:
149
+ keys: LoRaWAN encryption keys for payload decryption and MIC verification.
150
+
151
+ Example:
152
+ >>> keys = LoRaWANKeys(app_skey=bytes(16), nwk_skey=bytes(16))
153
+ >>> decoder = LoRaWANDecoder(keys=keys)
154
+ """
155
+ self.keys = keys or LoRaWANKeys()
156
+ self.frames: list[LoRaWANFrame] = []
157
+
158
+ def set_keys(self, keys: LoRaWANKeys) -> None:
159
+ """Set encryption keys for payload decryption.
160
+
161
+ Args:
162
+ keys: LoRaWAN encryption keys.
163
+
164
+ Example:
165
+ >>> decoder.set_keys(LoRaWANKeys(app_skey=bytes(16)))
166
+ """
167
+ self.keys = keys
168
+
169
+ def decode_frame(self, data: bytes, timestamp: float = 0.0) -> LoRaWANFrame:
170
+ """Decode LoRaWAN MAC frame.
171
+
172
+ Frame Format:
173
+ MHDR (1 byte) | MACPayload (variable) | MIC (4 bytes)
174
+
175
+ Data Frame MACPayload:
176
+ FHDR | FPort (optional) | FRMPayload (optional)
177
+
178
+ FHDR Format:
179
+ DevAddr (4 bytes) | FCtrl (1 byte) | FCnt (2 bytes) | FOpts (0-15 bytes)
180
+
181
+ Args:
182
+ data: Raw frame bytes.
183
+ timestamp: Frame timestamp in seconds.
184
+
185
+ Returns:
186
+ Decoded LoRaWAN frame.
187
+
188
+ Raises:
189
+ ValueError: If frame is too short or malformed.
190
+
191
+ Example:
192
+ >>> frame = decoder.decode_frame(bytes.fromhex("40..."), timestamp=1.0)
193
+ >>> print(f"DevAddr: 0x{frame.dev_addr:08X}")
194
+ """
195
+ errors: list[str] = []
196
+
197
+ # Minimum frame: MHDR (1) + MIC (4) = 5 bytes
198
+ if len(data) < 5:
199
+ msg = f"Frame too short: {len(data)} bytes (minimum 5)"
200
+ raise ValueError(msg)
201
+
202
+ # Parse frame components
203
+ mtype, mac_payload, mic = self._extract_frame_components(data)
204
+
205
+ # Route to specific decoder based on message type
206
+ return self._route_frame_decoder(mtype, mac_payload, mic, timestamp, data, errors)
207
+
208
+ def _extract_frame_components(self, data: bytes) -> tuple[str, bytes, int]:
209
+ """Extract MHDR, MACPayload, and MIC from frame.
210
+
211
+ Args:
212
+ data: Raw frame bytes.
213
+
214
+ Returns:
215
+ Tuple of (mtype, mac_payload, mic).
216
+ """
217
+ # Parse MHDR (MAC Header)
218
+ mhdr = data[0]
219
+ mtype_val, rfu, major = self._parse_mhdr(mhdr)
220
+ mtype = self.MTYPES.get(mtype_val, f"Unknown_0x{mtype_val:02X}")
221
+
222
+ # Extract MIC (last 4 bytes)
223
+ mic = int.from_bytes(data[-4:], "little")
224
+
225
+ # Extract MACPayload (between MHDR and MIC)
226
+ mac_payload = data[1:-4]
227
+
228
+ return mtype, mac_payload, mic
229
+
230
+ def _route_frame_decoder(
231
+ self,
232
+ mtype: str,
233
+ mac_payload: bytes,
234
+ mic: int,
235
+ timestamp: float,
236
+ full_frame: bytes,
237
+ errors: list[str],
238
+ ) -> LoRaWANFrame:
239
+ """Route frame to appropriate decoder based on message type.
240
+
241
+ Args:
242
+ mtype: Message type string.
243
+ mac_payload: MACPayload bytes.
244
+ mic: Message Integrity Code.
245
+ timestamp: Frame timestamp.
246
+ full_frame: Complete frame including MHDR and MIC.
247
+ errors: Error list.
248
+
249
+ Returns:
250
+ Decoded LoRaWAN frame.
251
+ """
252
+ # Data frames (uplink/downlink)
253
+ if mtype in (
254
+ "Unconfirmed Data Up",
255
+ "Unconfirmed Data Down",
256
+ "Confirmed Data Up",
257
+ "Confirmed Data Down",
258
+ ):
259
+ return self._decode_data_frame(mtype, mac_payload, mic, timestamp, full_frame, errors)
260
+
261
+ # Join frames
262
+ if mtype == "Join-request":
263
+ return self._decode_join_request(mac_payload, mic, timestamp, errors)
264
+ if mtype == "Join-accept":
265
+ return self._decode_join_accept(mac_payload, mic, timestamp, errors)
266
+
267
+ # Unknown or proprietary frame
268
+ frame = LoRaWANFrame(
269
+ timestamp=timestamp,
270
+ mtype=mtype,
271
+ frm_payload=mac_payload,
272
+ mic=mic,
273
+ errors=errors,
274
+ )
275
+ return frame
276
+
277
+ def _parse_mhdr(self, mhdr: int) -> tuple[int, int, int]:
278
+ """Parse MAC header (MHDR) into MType, RFU, Major.
279
+
280
+ MHDR format (1 byte):
281
+ Bits 7-5: MType (message type)
282
+ Bits 4-2: RFU (reserved for future use)
283
+ Bits 1-0: Major (LoRaWAN version)
284
+
285
+ Args:
286
+ mhdr: MHDR byte value.
287
+
288
+ Returns:
289
+ Tuple of (mtype, rfu, major).
290
+
291
+ Example:
292
+ >>> mtype, rfu, major = decoder._parse_mhdr(0x40)
293
+ >>> mtype
294
+ 2
295
+ """
296
+ mtype = (mhdr >> 5) & 0x07
297
+ rfu = (mhdr >> 2) & 0x07
298
+ major = mhdr & 0x03
299
+ return mtype, rfu, major
300
+
301
+ def _parse_fctrl(self, fctrl: int, direction: Literal["up", "down"]) -> dict[str, bool | int]:
302
+ """Parse frame control byte.
303
+
304
+ FCtrl format (1 byte):
305
+ Uplink:
306
+ Bit 7: ADR (Adaptive Data Rate)
307
+ Bit 6: ADRACKReq
308
+ Bit 5: ACK
309
+ Bit 4: ClassB
310
+ Bits 3-0: FOptsLen
311
+ Downlink:
312
+ Bit 7: ADR
313
+ Bit 6: RFU
314
+ Bit 5: ACK
315
+ Bit 4: FPending
316
+ Bits 3-0: FOptsLen
317
+
318
+ Args:
319
+ fctrl: FCtrl byte value.
320
+ direction: "up" for uplink, "down" for downlink.
321
+
322
+ Returns:
323
+ Dictionary of frame control flags.
324
+
325
+ Example:
326
+ >>> flags = decoder._parse_fctrl(0x80, "up")
327
+ >>> flags["adr"]
328
+ True
329
+ """
330
+ result: dict[str, bool | int] = {
331
+ "adr": bool(fctrl & 0x80),
332
+ "ack": bool(fctrl & 0x20),
333
+ "fopts_len": fctrl & 0x0F,
334
+ }
335
+
336
+ if direction == "up":
337
+ result["adr_ack_req"] = bool(fctrl & 0x40)
338
+ result["class_b"] = bool(fctrl & 0x10)
339
+ else: # downlink
340
+ result["fpending"] = bool(fctrl & 0x10)
341
+
342
+ return result
343
+
344
+ def _decode_data_frame(
345
+ self,
346
+ mtype: str,
347
+ mac_payload: bytes,
348
+ mic: int,
349
+ timestamp: float,
350
+ full_frame: bytes,
351
+ errors: list[str],
352
+ ) -> LoRaWANFrame:
353
+ """Decode data frame (unconfirmed or confirmed).
354
+
355
+ Args:
356
+ mtype: Message type string.
357
+ mac_payload: MACPayload bytes (FHDR | FPort | FRMPayload).
358
+ mic: Message Integrity Code.
359
+ timestamp: Frame timestamp.
360
+ full_frame: Complete frame including MHDR and MIC.
361
+ errors: Error list to append to.
362
+
363
+ Returns:
364
+ Decoded LoRaWAN frame.
365
+
366
+ Example:
367
+ >>> decoder = LoRaWANDecoder()
368
+ >>> frame = decoder._decode_data_frame(
369
+ ... "Unconfirmed Data Up", b"\\x01\\x02\\x03\\x04...", 0x12345678, 0.0, b"...", []
370
+ ... )
371
+ """
372
+ if len(mac_payload) < 7: # Minimum FHDR length
373
+ errors.append("MACPayload too short for data frame")
374
+ return LoRaWANFrame(
375
+ timestamp=timestamp,
376
+ mtype=mtype,
377
+ mic=mic,
378
+ errors=errors,
379
+ )
380
+
381
+ direction: Literal["up", "down"] = "up" if "Up" in mtype else "down"
382
+
383
+ # Parse all frame components
384
+ frame_data = self._parse_data_frame_components(
385
+ mac_payload, mtype, full_frame, mic, direction, errors
386
+ )
387
+
388
+ # Create and store frame
389
+ frame = LoRaWANFrame(
390
+ timestamp=timestamp,
391
+ mtype=mtype,
392
+ dev_addr=frame_data["dev_addr"],
393
+ fctrl=frame_data["fctrl"],
394
+ fcnt=frame_data["fcnt"],
395
+ fopts=frame_data["fopts"],
396
+ fport=frame_data["fport"],
397
+ frm_payload=frame_data["frm_payload"],
398
+ mic=mic,
399
+ decrypted_payload=frame_data["decrypted_payload"],
400
+ parsed_mac_commands=frame_data["parsed_mac_commands"],
401
+ mic_valid=frame_data["mic_valid"],
402
+ errors=errors,
403
+ )
404
+
405
+ self.frames.append(frame)
406
+ return frame
407
+
408
+ def _parse_data_frame_components(
409
+ self,
410
+ mac_payload: bytes,
411
+ mtype: str,
412
+ full_frame: bytes,
413
+ mic: int,
414
+ direction: Literal["up", "down"],
415
+ errors: list[str],
416
+ ) -> dict[str, Any]:
417
+ """Parse all components of data frame.
418
+
419
+ Args:
420
+ mac_payload: MACPayload bytes.
421
+ mtype: Message type string.
422
+ full_frame: Complete frame.
423
+ mic: Message Integrity Code.
424
+ direction: Frame direction.
425
+ errors: Error list.
426
+
427
+ Returns:
428
+ Dictionary of parsed frame components.
429
+ """
430
+ # Parse frame header
431
+ dev_addr, fctrl, fcnt, fopts = self._parse_fhdr(mac_payload, mtype, errors)
432
+
433
+ # Extract port and payload
434
+ fport, frm_payload = self._extract_port_and_payload(mac_payload, fopts)
435
+
436
+ # Parse MAC commands
437
+ parsed_mac_commands = self._parse_fopts_mac_commands(fopts, direction, errors)
438
+
439
+ # Decrypt payload
440
+ decrypted_payload = self._decrypt_frm_payload(
441
+ fport, frm_payload, dev_addr, fcnt, direction, errors
442
+ )
443
+
444
+ # Verify MIC
445
+ mic_valid = self._verify_frame_mic(full_frame, mic, dev_addr, fcnt, direction, errors)
446
+
447
+ return {
448
+ "dev_addr": dev_addr,
449
+ "fctrl": fctrl,
450
+ "fcnt": fcnt,
451
+ "fopts": fopts,
452
+ "fport": fport,
453
+ "frm_payload": frm_payload,
454
+ "decrypted_payload": decrypted_payload,
455
+ "parsed_mac_commands": parsed_mac_commands,
456
+ "mic_valid": mic_valid,
457
+ }
458
+
459
+ def _parse_fhdr(
460
+ self,
461
+ mac_payload: bytes,
462
+ mtype: str,
463
+ errors: list[str],
464
+ ) -> tuple[int, dict[str, bool | int], int, bytes]:
465
+ """Parse frame header (FHDR) fields.
466
+
467
+ Args:
468
+ mac_payload: MACPayload bytes.
469
+ mtype: Message type string.
470
+ errors: Error list to append to.
471
+
472
+ Returns:
473
+ Tuple of (DevAddr, FCtrl, FCnt, FOpts).
474
+ """
475
+ dev_addr = int.from_bytes(mac_payload[0:4], "little")
476
+ fctrl_byte = mac_payload[4]
477
+ fcnt = int.from_bytes(mac_payload[5:7], "little")
478
+
479
+ direction: Literal["up", "down"] = "up" if "Up" in mtype else "down"
480
+ fctrl = self._parse_fctrl(fctrl_byte, direction)
481
+
482
+ fopts_len = fctrl["fopts_len"]
483
+ if fopts_len > 15:
484
+ errors.append(f"Invalid FOpts length: {fopts_len}")
485
+ fopts_len = 0
486
+
487
+ fopts = mac_payload[7 : 7 + fopts_len] if fopts_len > 0 else b""
488
+ return dev_addr, fctrl, fcnt, fopts
489
+
490
+ def _extract_port_and_payload(
491
+ self, mac_payload: bytes, fopts: bytes
492
+ ) -> tuple[int | None, bytes]:
493
+ """Extract FPort and FRMPayload from MAC payload.
494
+
495
+ Args:
496
+ mac_payload: MACPayload bytes.
497
+ fopts: FOpts bytes (for calculating offset).
498
+
499
+ Returns:
500
+ Tuple of (FPort, FRMPayload). FPort is None if not present.
501
+ """
502
+ offset = 7 + len(fopts)
503
+ fport = None
504
+ frm_payload = b""
505
+
506
+ if offset < len(mac_payload):
507
+ fport = mac_payload[offset]
508
+ frm_payload = mac_payload[offset + 1 :] if offset + 1 < len(mac_payload) else b""
509
+
510
+ return fport, frm_payload
511
+
512
+ def _parse_fopts_mac_commands(
513
+ self,
514
+ fopts: bytes,
515
+ direction: Literal["up", "down"],
516
+ errors: list[str],
517
+ ) -> list[dict[str, Any]]:
518
+ """Parse MAC commands from FOpts field.
519
+
520
+ Args:
521
+ fopts: FOpts bytes.
522
+ direction: Message direction ("up" or "down").
523
+ errors: Error list to append to.
524
+
525
+ Returns:
526
+ List of parsed MAC command dictionaries.
527
+ """
528
+ if not fopts:
529
+ return []
530
+
531
+ try:
532
+ return parse_mac_commands(fopts, direction)
533
+ except Exception as exc:
534
+ errors.append(f"Failed to parse MAC commands: {exc}")
535
+ return []
536
+
537
+ def _decrypt_frm_payload(
538
+ self,
539
+ fport: int | None,
540
+ frm_payload: bytes,
541
+ dev_addr: int,
542
+ fcnt: int,
543
+ direction: Literal["up", "down"],
544
+ errors: list[str],
545
+ ) -> bytes | None:
546
+ """Decrypt FRMPayload using AES-128 CTR mode.
547
+
548
+ Args:
549
+ fport: FPort value.
550
+ frm_payload: Encrypted FRMPayload.
551
+ dev_addr: Device address.
552
+ fcnt: Frame counter.
553
+ direction: Message direction.
554
+ errors: Error list to append to.
555
+
556
+ Returns:
557
+ Decrypted payload bytes, or None if decryption not performed.
558
+ """
559
+ if fport is None or not frm_payload or not self.keys.app_skey:
560
+ return None
561
+
562
+ try:
563
+ from oscura.iot.lorawan.crypto import decrypt_payload
564
+
565
+ # Use AppSKey for FPort != 0, NwkSKey for FPort == 0
566
+ key = self.keys.nwk_skey if fport == 0 else self.keys.app_skey
567
+ if key:
568
+ return decrypt_payload(frm_payload, key, dev_addr, fcnt, direction)
569
+ except ImportError:
570
+ errors.append("PyCryptodome not available for decryption")
571
+ except Exception as exc:
572
+ errors.append(f"Decryption failed: {exc}")
573
+
574
+ return None
575
+
576
+ def _verify_frame_mic(
577
+ self,
578
+ full_frame: bytes,
579
+ mic: int,
580
+ dev_addr: int,
581
+ fcnt: int,
582
+ direction: Literal["up", "down"],
583
+ errors: list[str],
584
+ ) -> bool | None:
585
+ """Verify Message Integrity Code (MIC).
586
+
587
+ Args:
588
+ full_frame: Complete frame including MHDR and MIC.
589
+ mic: Received MIC value.
590
+ dev_addr: Device address.
591
+ fcnt: Frame counter.
592
+ direction: Message direction.
593
+ errors: Error list to append to.
594
+
595
+ Returns:
596
+ True if MIC valid, False if invalid, None if not checked.
597
+ """
598
+ if not self.keys.nwk_skey:
599
+ return None
600
+
601
+ try:
602
+ from oscura.iot.lorawan.crypto import verify_mic
603
+
604
+ # MIC is computed over MHDR | FHDR | FPort | FRMPayload
605
+ mic_data = full_frame[:-4]
606
+ mic_valid = verify_mic(mic_data, mic, self.keys.nwk_skey, dev_addr, fcnt, direction)
607
+ if not mic_valid:
608
+ errors.append("MIC verification failed")
609
+ return mic_valid
610
+ except ImportError:
611
+ return None # Crypto not available
612
+ except Exception as exc:
613
+ errors.append(f"MIC verification error: {exc}")
614
+ return None
615
+
616
+ def _decode_join_request(
617
+ self,
618
+ mac_payload: bytes,
619
+ mic: int,
620
+ timestamp: float,
621
+ errors: list[str],
622
+ ) -> LoRaWANFrame:
623
+ """Decode Join-request frame.
624
+
625
+ Join-request format:
626
+ AppEUI (8 bytes) | DevEUI (8 bytes) | DevNonce (2 bytes)
627
+
628
+ Args:
629
+ mac_payload: Join-request payload.
630
+ mic: Message Integrity Code.
631
+ timestamp: Frame timestamp.
632
+ errors: Error list.
633
+
634
+ Returns:
635
+ Decoded frame.
636
+ """
637
+ if len(mac_payload) < 18:
638
+ errors.append(f"Join-request too short: {len(mac_payload)} bytes")
639
+
640
+ frame = LoRaWANFrame(
641
+ timestamp=timestamp,
642
+ mtype="Join-request",
643
+ frm_payload=mac_payload,
644
+ mic=mic,
645
+ errors=errors,
646
+ )
647
+
648
+ self.frames.append(frame)
649
+ return frame
650
+
651
+ def _decode_join_accept(
652
+ self,
653
+ mac_payload: bytes,
654
+ mic: int,
655
+ timestamp: float,
656
+ errors: list[str],
657
+ ) -> LoRaWANFrame:
658
+ """Decode Join-accept frame.
659
+
660
+ Join-accept is encrypted with AppKey and includes:
661
+ AppNonce (3 bytes) | NetID (3 bytes) | DevAddr (4 bytes) |
662
+ DLSettings (1 byte) | RxDelay (1 byte) | CFList (optional, 16 bytes)
663
+
664
+ Args:
665
+ mac_payload: Encrypted Join-accept payload.
666
+ mic: Message Integrity Code.
667
+ timestamp: Frame timestamp.
668
+ errors: Error list.
669
+
670
+ Returns:
671
+ Decoded frame.
672
+ """
673
+ # Join-accept decryption requires AppKey
674
+ frame = LoRaWANFrame(
675
+ timestamp=timestamp,
676
+ mtype="Join-accept",
677
+ frm_payload=mac_payload,
678
+ mic=mic,
679
+ errors=errors,
680
+ )
681
+
682
+ self.frames.append(frame)
683
+ return frame
684
+
685
+ def export_json(self) -> list[dict[str, Any]]:
686
+ """Export decoded frames as JSON-serializable list.
687
+
688
+ Returns:
689
+ List of frame dictionaries.
690
+
691
+ Example:
692
+ >>> frames_json = decoder.export_json()
693
+ >>> import json
694
+ >>> print(json.dumps(frames_json, indent=2))
695
+ """
696
+ result = []
697
+ for frame in self.frames:
698
+ frame_dict: dict[str, Any] = {
699
+ "timestamp": frame.timestamp,
700
+ "mtype": frame.mtype,
701
+ }
702
+
703
+ if frame.dev_addr is not None:
704
+ frame_dict["dev_addr"] = f"0x{frame.dev_addr:08X}"
705
+
706
+ if frame.fctrl:
707
+ frame_dict["fctrl"] = frame.fctrl
708
+
709
+ if frame.fcnt is not None:
710
+ frame_dict["fcnt"] = frame.fcnt
711
+
712
+ if frame.fopts:
713
+ frame_dict["fopts"] = frame.fopts.hex()
714
+
715
+ if frame.fport is not None:
716
+ frame_dict["fport"] = frame.fport
717
+
718
+ if frame.frm_payload:
719
+ frame_dict["frm_payload"] = frame.frm_payload.hex()
720
+
721
+ if frame.decrypted_payload:
722
+ frame_dict["decrypted_payload"] = frame.decrypted_payload.hex()
723
+
724
+ if frame.mic is not None:
725
+ frame_dict["mic"] = f"0x{frame.mic:08X}"
726
+
727
+ if frame.mic_valid is not None:
728
+ frame_dict["mic_valid"] = frame.mic_valid
729
+
730
+ if frame.parsed_mac_commands:
731
+ frame_dict["mac_commands"] = frame.parsed_mac_commands
732
+
733
+ if frame.errors:
734
+ frame_dict["errors"] = frame.errors
735
+
736
+ result.append(frame_dict)
737
+
738
+ return result
739
+
740
+ def export_csv_rows(self) -> list[dict[str, str]]:
741
+ """Export decoded frames as CSV rows.
742
+
743
+ Returns:
744
+ List of dictionaries suitable for CSV export.
745
+
746
+ Example:
747
+ >>> import csv
748
+ >>> rows = decoder.export_csv_rows()
749
+ >>> with open("frames.csv", "w") as f:
750
+ ... writer = csv.DictWriter(f, fieldnames=rows[0].keys())
751
+ ... writer.writeheader()
752
+ ... writer.writerows(rows)
753
+ """
754
+ rows = []
755
+ for frame in self.frames:
756
+ row = {
757
+ "timestamp": str(frame.timestamp),
758
+ "mtype": frame.mtype,
759
+ "dev_addr": f"0x{frame.dev_addr:08X}" if frame.dev_addr else "",
760
+ "fcnt": str(frame.fcnt) if frame.fcnt is not None else "",
761
+ "fport": str(frame.fport) if frame.fport is not None else "",
762
+ "payload_hex": frame.frm_payload.hex(),
763
+ "decrypted_hex": frame.decrypted_payload.hex() if frame.decrypted_payload else "",
764
+ "mic": f"0x{frame.mic:08X}" if frame.mic is not None else "",
765
+ "mic_valid": str(frame.mic_valid) if frame.mic_valid is not None else "",
766
+ "errors": "; ".join(frame.errors),
767
+ }
768
+ rows.append(row)
769
+
770
+ return rows
771
+
772
+
773
+ def decode_lorawan_frame(
774
+ data: bytes,
775
+ timestamp: float = 0.0,
776
+ keys: LoRaWANKeys | None = None,
777
+ ) -> LoRaWANFrame:
778
+ """Convenience function to decode a single LoRaWAN frame.
779
+
780
+ Args:
781
+ data: Raw frame bytes.
782
+ timestamp: Frame timestamp in seconds.
783
+ keys: Optional encryption keys for decryption and MIC verification.
784
+
785
+ Returns:
786
+ Decoded LoRaWAN frame.
787
+
788
+ Example:
789
+ >>> frame = decode_lorawan_frame(bytes.fromhex("40..."))
790
+ >>> print(f"MType: {frame.mtype}")
791
+ """
792
+ decoder = LoRaWANDecoder(keys=keys)
793
+ return decoder.decode_frame(data, timestamp=timestamp)
794
+
795
+
796
+ __all__ = [
797
+ "LoRaWANDecoder",
798
+ "LoRaWANFrame",
799
+ "LoRaWANKeys",
800
+ "decode_lorawan_frame",
801
+ ]