oscura 0.0.1__py3-none-any.whl → 0.1.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 (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,639 @@
1
+ """High-level reverse engineering workflow for unknown signals.
2
+
3
+ This module provides a complete workflow for reverse engineering unknown
4
+ digital signals from initial capture to protocol understanding.
5
+
6
+ Example:
7
+ >>> import oscura as tk
8
+ >>> trace = tk.load("unknown_capture.wfm")
9
+ >>> result = tk.workflows.reverse_engineer_signal(trace)
10
+ >>> print(result.protocol_spec)
11
+ >>> print(f"Detected baud rate: {result.baud_rate}")
12
+ >>> print(f"Frames decoded: {len(result.frames)}")
13
+
14
+ The workflow includes:
15
+ 1. Signal characterization (voltage levels, signal type)
16
+ 2. Clock recovery / baud rate detection
17
+ 3. Bit stream extraction
18
+ 4. Frame boundary detection
19
+ 5. Sync pattern identification
20
+ 6. Field structure inference
21
+ 7. Checksum analysis
22
+ 8. Protocol specification generation
23
+
24
+ References:
25
+ - sigrok Protocol Analysis
26
+ - UART: TIA-232-F
27
+ - I2C: NXP UM10204
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from dataclasses import dataclass, field
33
+ from typing import TYPE_CHECKING, Any
34
+
35
+ import numpy as np
36
+
37
+ if TYPE_CHECKING:
38
+ from oscura.core.types import WaveformTrace
39
+
40
+
41
+ @dataclass
42
+ class InferredFrame:
43
+ """An inferred protocol frame.
44
+
45
+ Attributes:
46
+ start_bit: Starting bit index.
47
+ end_bit: Ending bit index.
48
+ raw_bits: Raw bit string.
49
+ raw_bytes: Raw bytes.
50
+ fields: Identified field mapping.
51
+ checksum_valid: Whether checksum validated (None if unknown).
52
+ """
53
+
54
+ start_bit: int
55
+ end_bit: int
56
+ raw_bits: str
57
+ raw_bytes: bytes
58
+ fields: dict[str, bytes] = field(default_factory=dict)
59
+ checksum_valid: bool | None = None
60
+
61
+
62
+ @dataclass
63
+ class FieldSpec:
64
+ """Specification for an inferred field.
65
+
66
+ Attributes:
67
+ name: Field name.
68
+ offset: Byte offset in frame.
69
+ size: Size in bytes (or expression for variable).
70
+ field_type: Data type (uint8, bytes, checksum, etc.).
71
+ value: Example or constant value.
72
+ """
73
+
74
+ name: str
75
+ offset: int
76
+ size: int | str
77
+ field_type: str
78
+ value: Any = None
79
+
80
+
81
+ @dataclass
82
+ class ProtocolSpec:
83
+ """Inferred protocol specification.
84
+
85
+ Attributes:
86
+ name: Protocol name.
87
+ baud_rate: Detected baud rate.
88
+ frame_format: Frame format (e.g., "8N1").
89
+ sync_pattern: Detected sync pattern (hex string).
90
+ frame_length: Frame length in bytes (or None if variable).
91
+ fields: List of field specifications.
92
+ checksum_type: Detected checksum type (or None).
93
+ checksum_position: Position of checksum in frame.
94
+ confidence: Overall confidence score (0-1).
95
+ """
96
+
97
+ name: str
98
+ baud_rate: float
99
+ frame_format: str
100
+ sync_pattern: str
101
+ frame_length: int | None
102
+ fields: list[FieldSpec]
103
+ checksum_type: str | None
104
+ checksum_position: int | None
105
+ confidence: float
106
+
107
+
108
+ @dataclass
109
+ class ReverseEngineeringResult:
110
+ """Complete results from reverse engineering workflow.
111
+
112
+ Attributes:
113
+ protocol_spec: Inferred protocol specification.
114
+ frames: List of decoded frames.
115
+ baud_rate: Detected baud rate.
116
+ bit_stream: Extracted bit stream.
117
+ byte_stream: Extracted byte stream.
118
+ sync_positions: Positions where sync patterns found.
119
+ characterization: Signal characterization results.
120
+ confidence: Overall analysis confidence (0-1).
121
+ warnings: List of analysis warnings.
122
+ """
123
+
124
+ protocol_spec: ProtocolSpec
125
+ frames: list[InferredFrame]
126
+ baud_rate: float
127
+ bit_stream: str
128
+ byte_stream: bytes
129
+ sync_positions: list[int]
130
+ characterization: dict[str, Any]
131
+ confidence: float
132
+ warnings: list[str]
133
+
134
+
135
+ def reverse_engineer_signal(
136
+ trace: WaveformTrace,
137
+ *,
138
+ expected_baud_rates: list[int] | None = None,
139
+ min_frames: int = 3,
140
+ max_frame_length: int = 256,
141
+ checksum_types: list[str] | None = None,
142
+ ) -> ReverseEngineeringResult:
143
+ """Complete reverse engineering workflow for unknown signals.
144
+
145
+ Analyzes an unknown digital signal to infer protocol parameters,
146
+ frame structure, and decode messages.
147
+
148
+ Args:
149
+ trace: Input waveform trace.
150
+ expected_baud_rates: List of expected baud rates to try.
151
+ Default: [9600, 19200, 38400, 57600, 115200].
152
+ min_frames: Minimum frames required for analysis (default 3).
153
+ max_frame_length: Maximum expected frame length in bytes.
154
+ checksum_types: Checksum types to try.
155
+ Default: ["xor", "sum8", "crc8", "crc16"].
156
+
157
+ Returns:
158
+ ReverseEngineeringResult with protocol specification and decoded frames.
159
+
160
+ Example:
161
+ >>> trace = tk.load("unknown_capture.wfm")
162
+ >>> result = tk.workflows.reverse_engineer_signal(trace)
163
+ >>> print(f"Baud rate: {result.baud_rate}")
164
+ >>> print(f"Sync pattern: {result.protocol_spec.sync_pattern}")
165
+ >>> print(f"Frame length: {result.protocol_spec.frame_length} bytes")
166
+ >>> for frame in result.frames[:5]:
167
+ ... print(f" {frame.raw_bytes.hex()}")
168
+ """
169
+ if expected_baud_rates is None:
170
+ expected_baud_rates = [9600, 19200, 38400, 57600, 115200, 230400, 460800]
171
+
172
+ if checksum_types is None:
173
+ checksum_types = ["xor", "sum8", "crc8", "crc16"]
174
+
175
+ warnings: list[str] = []
176
+ data = trace.data
177
+ sample_rate = trace.metadata.sample_rate
178
+
179
+ # ========== Step 1: Signal Characterization ==========
180
+ characterization = _characterize_signal(data, sample_rate)
181
+
182
+ # ========== Step 2: Clock Recovery ==========
183
+ baud_rate, baud_confidence = _detect_baud_rate(
184
+ data, sample_rate, expected_baud_rates, characterization["threshold"]
185
+ )
186
+ if baud_confidence < 0.7:
187
+ warnings.append(f"Low baud rate confidence: {baud_confidence:.2f}")
188
+
189
+ # ========== Step 3: Bit Stream Extraction ==========
190
+ bit_stream = _extract_bit_stream(data, sample_rate, baud_rate, characterization["threshold"])
191
+
192
+ if len(bit_stream) < 100:
193
+ warnings.append("Short bit stream extracted")
194
+
195
+ # ========== Step 4: Byte Extraction ==========
196
+ byte_positions, byte_stream = _extract_bytes(bit_stream)
197
+
198
+ if len(byte_stream) < 10:
199
+ warnings.append("Few bytes extracted")
200
+
201
+ # ========== Step 5: Sync Pattern Detection ==========
202
+ sync_pattern, sync_positions, sync_confidence = _detect_sync_pattern(
203
+ byte_stream, max_frame_length
204
+ )
205
+
206
+ # ========== Step 6: Frame Extraction ==========
207
+ frames = _extract_frames(bit_stream, byte_stream, byte_positions, sync_positions, sync_pattern)
208
+
209
+ if len(frames) < min_frames:
210
+ warnings.append(f"Only {len(frames)} frames found (minimum {min_frames})")
211
+
212
+ # ========== Step 7: Field Analysis ==========
213
+ field_specs = _infer_fields(frames, sync_pattern)
214
+
215
+ # ========== Step 8: Checksum Analysis ==========
216
+ checksum_type, checksum_pos, checksum_confidence = _detect_checksum(frames, checksum_types)
217
+
218
+ if checksum_type:
219
+ # Validate frames with checksum
220
+ for frame in frames:
221
+ frame.checksum_valid = _verify_checksum(frame.raw_bytes, checksum_type, checksum_pos)
222
+
223
+ # ========== Build Protocol Specification ==========
224
+ frame_lengths = [len(f.raw_bytes) for f in frames]
225
+ frame_length = int(np.median(frame_lengths)) if len(set(frame_lengths)) == 1 else None
226
+
227
+ # Calculate overall confidence
228
+ overall_confidence = (
229
+ baud_confidence * 0.3
230
+ + sync_confidence * 0.3
231
+ + checksum_confidence * 0.2
232
+ + min(len(frames) / 10, 1.0) * 0.2
233
+ )
234
+
235
+ protocol_spec = ProtocolSpec(
236
+ name="Unknown Protocol (Inferred)",
237
+ baud_rate=baud_rate,
238
+ frame_format="8N1", # Most common
239
+ sync_pattern=sync_pattern.hex() if sync_pattern else "",
240
+ frame_length=frame_length,
241
+ fields=field_specs,
242
+ checksum_type=checksum_type,
243
+ checksum_position=checksum_pos,
244
+ confidence=overall_confidence,
245
+ )
246
+
247
+ return ReverseEngineeringResult(
248
+ protocol_spec=protocol_spec,
249
+ frames=frames,
250
+ baud_rate=baud_rate,
251
+ bit_stream=bit_stream,
252
+ byte_stream=byte_stream,
253
+ sync_positions=sync_positions,
254
+ characterization=characterization,
255
+ confidence=overall_confidence,
256
+ warnings=warnings,
257
+ )
258
+
259
+
260
+ def _characterize_signal(
261
+ data: np.ndarray[Any, np.dtype[np.float64]], sample_rate: float
262
+ ) -> dict[str, Any]:
263
+ """Characterize signal voltage levels and type."""
264
+ high_level = float(np.percentile(data, 95))
265
+ low_level = float(np.percentile(data, 5))
266
+ threshold = (high_level + low_level) / 2
267
+ swing = high_level - low_level
268
+
269
+ # Detect if inverted (idle low vs idle high)
270
+ is_inverted = np.mean(data) > threshold
271
+
272
+ # Detect signal type
273
+ signal_type = "digital" # Default assumption
274
+ if swing < 0.5:
275
+ signal_type = "low_swing"
276
+ elif swing > 10:
277
+ signal_type = "high_swing"
278
+
279
+ return {
280
+ "high_level": high_level,
281
+ "low_level": low_level,
282
+ "threshold": threshold,
283
+ "swing": swing,
284
+ "is_inverted": is_inverted,
285
+ "signal_type": signal_type,
286
+ "sample_rate": sample_rate,
287
+ }
288
+
289
+
290
+ def _detect_baud_rate(
291
+ data: np.ndarray[Any, np.dtype[np.float64]],
292
+ sample_rate: float,
293
+ expected_rates: list[int],
294
+ threshold: float,
295
+ ) -> tuple[float, float]:
296
+ """Detect baud rate from edge timing."""
297
+ # Convert to digital
298
+ digital = data > threshold
299
+ edges = np.where(np.diff(digital.astype(int)) != 0)[0]
300
+
301
+ if len(edges) < 20:
302
+ # Default to most common baud rate if not enough edges
303
+ return 115200, 0.3
304
+
305
+ # Measure edge-to-edge intervals
306
+ intervals = np.diff(edges)
307
+ intervals = intervals[intervals > 5] # Filter very short glitches
308
+
309
+ if len(intervals) < 10:
310
+ return 115200, 0.3
311
+
312
+ # Find minimum interval (single bit period)
313
+ min_interval = float(np.percentile(intervals, 5))
314
+
315
+ # Estimate baud rate
316
+ estimated_baud = sample_rate / min_interval
317
+
318
+ # Find closest standard baud rate
319
+ closest_baud = min(expected_rates, key=lambda x: abs(x - estimated_baud))
320
+
321
+ # Calculate confidence based on how close we are
322
+ error_percent = abs(estimated_baud - closest_baud) / closest_baud
323
+ confidence = max(0.0, 1.0 - error_percent * 5)
324
+
325
+ return float(closest_baud), confidence
326
+
327
+
328
+ def _extract_bit_stream(
329
+ data: np.ndarray[Any, np.dtype[np.float64]],
330
+ sample_rate: float,
331
+ baud_rate: float,
332
+ threshold: float,
333
+ ) -> str:
334
+ """Extract bit stream by sampling at bit centers."""
335
+ samples_per_bit = int(sample_rate / baud_rate)
336
+ n_bits = len(data) // samples_per_bit
337
+
338
+ bits: list[str] = []
339
+ for i in range(n_bits):
340
+ sample_idx = i * samples_per_bit + samples_per_bit // 2
341
+ if sample_idx < len(data):
342
+ bit = "1" if data[sample_idx] > threshold else "0"
343
+ bits.append(bit)
344
+
345
+ return "".join(bits)
346
+
347
+
348
+ def _extract_bytes(bit_stream: str) -> tuple[list[int], bytes]:
349
+ """Extract bytes from bit stream (8N1 format)."""
350
+ byte_positions: list[int] = []
351
+ byte_values: list[int] = []
352
+
353
+ bit_pos = 0
354
+ while bit_pos < len(bit_stream) - 10:
355
+ # Look for start bit (0)
356
+ if bit_stream[bit_pos] == "0":
357
+ # Extract 8 data bits (LSB first for UART)
358
+ byte_bits = bit_stream[bit_pos + 1 : bit_pos + 9]
359
+ if len(byte_bits) == 8:
360
+ byte_val = sum(int(byte_bits[i]) << i for i in range(8))
361
+ byte_positions.append(bit_pos)
362
+ byte_values.append(byte_val)
363
+ bit_pos += 10 # Skip to next potential start
364
+ else:
365
+ bit_pos += 1
366
+
367
+ return byte_positions, bytes(byte_values)
368
+
369
+
370
+ def _detect_sync_pattern(
371
+ byte_stream: bytes,
372
+ max_frame_length: int,
373
+ ) -> tuple[bytes, list[int], float]:
374
+ """Detect sync pattern by finding repeated byte sequences."""
375
+ if len(byte_stream) < 20:
376
+ return b"", [], 0.0
377
+
378
+ list(byte_stream)
379
+
380
+ # Try common sync patterns first
381
+ common_patterns = [
382
+ bytes([0xAA, 0x55]),
383
+ bytes([0x55, 0xAA]),
384
+ bytes([0x7E]), # HDLC
385
+ bytes([0xA5]),
386
+ bytes([0x5A]),
387
+ bytes([0xFF, 0x00]),
388
+ ]
389
+
390
+ best_pattern = b""
391
+ best_positions: list[int] = []
392
+ best_confidence = 0.0
393
+
394
+ for pattern in common_patterns:
395
+ positions = []
396
+ for i in range(len(byte_stream) - len(pattern)):
397
+ if byte_stream[i : i + len(pattern)] == pattern:
398
+ positions.append(i)
399
+
400
+ if len(positions) >= 3:
401
+ # Check for regular spacing
402
+ spacings = np.diff(positions)
403
+ if len(spacings) > 0:
404
+ median_spacing = np.median(spacings)
405
+ regularity = 1.0 - np.std(spacings) / (median_spacing + 1)
406
+
407
+ confidence = min(len(positions) / 10, 1.0) * max(regularity, 0)
408
+
409
+ if confidence > best_confidence:
410
+ best_pattern = pattern
411
+ best_positions = positions
412
+ best_confidence = confidence
413
+
414
+ # If no common pattern found, try to find repeating sequences
415
+ if best_confidence < 0.5:
416
+ for pattern_len in range(1, 4):
417
+ for start in range(min(50, len(byte_stream) - pattern_len)):
418
+ pattern = byte_stream[start : start + pattern_len]
419
+ positions = []
420
+ for i in range(len(byte_stream) - pattern_len):
421
+ if byte_stream[i : i + pattern_len] == pattern:
422
+ positions.append(i)
423
+
424
+ if len(positions) >= 5:
425
+ spacings = np.diff(positions)
426
+ if len(spacings) > 0 and np.std(spacings) / (np.median(spacings) + 1) < 0.2:
427
+ confidence = min(len(positions) / 10, 1.0)
428
+ if confidence > best_confidence:
429
+ best_pattern = pattern
430
+ best_positions = positions
431
+ best_confidence = confidence
432
+
433
+ return best_pattern, best_positions, best_confidence
434
+
435
+
436
+ def _extract_frames(
437
+ bit_stream: str,
438
+ byte_stream: bytes,
439
+ byte_positions: list[int],
440
+ sync_positions: list[int],
441
+ sync_pattern: bytes,
442
+ ) -> list[InferredFrame]:
443
+ """Extract frames based on sync positions."""
444
+ frames: list[InferredFrame] = []
445
+
446
+ if not sync_positions or len(sync_positions) < 2:
447
+ return frames
448
+
449
+ # Calculate frame length from sync spacing
450
+ spacings = np.diff(sync_positions)
451
+ frame_length = int(np.median(spacings))
452
+
453
+ for _i, sync_pos in enumerate(sync_positions[:-1]):
454
+ end_pos = min(sync_pos + frame_length, len(byte_stream))
455
+ frame_bytes = byte_stream[sync_pos:end_pos]
456
+
457
+ if len(frame_bytes) < 3:
458
+ continue
459
+
460
+ # Get bit positions
461
+ start_bit = byte_positions[sync_pos] if sync_pos < len(byte_positions) else sync_pos * 10
462
+ end_bit = byte_positions[end_pos - 1] if end_pos - 1 < len(byte_positions) else end_pos * 10
463
+
464
+ frames.append(
465
+ InferredFrame(
466
+ start_bit=start_bit,
467
+ end_bit=end_bit,
468
+ raw_bits=bit_stream[start_bit:end_bit] if end_bit <= len(bit_stream) else "",
469
+ raw_bytes=frame_bytes,
470
+ )
471
+ )
472
+
473
+ return frames
474
+
475
+
476
+ def _infer_fields(frames: list[InferredFrame], sync_pattern: bytes) -> list[FieldSpec]:
477
+ """Infer field structure from frames."""
478
+ if not frames:
479
+ return []
480
+
481
+ fields: list[FieldSpec] = []
482
+ sync_len = len(sync_pattern)
483
+
484
+ # Sync field
485
+ if sync_len > 0:
486
+ fields.append(
487
+ FieldSpec(
488
+ name="sync",
489
+ offset=0,
490
+ size=sync_len,
491
+ field_type="constant",
492
+ value=sync_pattern.hex(),
493
+ )
494
+ )
495
+
496
+ # Analyze remaining bytes
497
+ frame_lengths = [len(f.raw_bytes) for f in frames]
498
+ min_len = min(frame_lengths)
499
+
500
+ # Check for length field (common at offset 2 or after sync)
501
+ length_offset = sync_len
502
+ if min_len > length_offset:
503
+ length_values = [
504
+ f.raw_bytes[length_offset] for f in frames if len(f.raw_bytes) > length_offset
505
+ ]
506
+ frame_lens = [len(f.raw_bytes) for f in frames]
507
+
508
+ # Check if value correlates with frame length
509
+ if len(length_values) > 2:
510
+ correlation = (
511
+ np.corrcoef(length_values, frame_lens)[0, 1] if len(set(length_values)) > 1 else 0
512
+ )
513
+ if correlation > 0.8 or (
514
+ len(set(length_values)) == 1 and length_values[0] == frame_lens[0]
515
+ ):
516
+ fields.append(
517
+ FieldSpec(
518
+ name="length",
519
+ offset=length_offset,
520
+ size=1,
521
+ field_type="uint8",
522
+ )
523
+ )
524
+ length_offset += 1
525
+
526
+ # Assume remaining bytes are data + checksum
527
+ if min_len > length_offset + 1:
528
+ fields.append(
529
+ FieldSpec(
530
+ name="data",
531
+ offset=length_offset,
532
+ size="length - sync_len - 2", # Variable
533
+ field_type="bytes",
534
+ )
535
+ )
536
+
537
+ # Last byte likely checksum
538
+ fields.append(
539
+ FieldSpec(
540
+ name="checksum",
541
+ offset=-1,
542
+ size=1,
543
+ field_type="checksum",
544
+ )
545
+ )
546
+
547
+ return fields
548
+
549
+
550
+ def _detect_checksum(
551
+ frames: list[InferredFrame],
552
+ checksum_types: list[str],
553
+ ) -> tuple[str | None, int | None, float]:
554
+ """Detect checksum type by trying different algorithms."""
555
+ if len(frames) < 3:
556
+ return None, None, 0.0
557
+
558
+ best_type: str | None = None
559
+ best_pos: int | None = None
560
+ best_matches = 0
561
+
562
+ for chk_type in checksum_types:
563
+ # Assume checksum is last byte
564
+ pos = -1
565
+ matches = 0
566
+
567
+ for frame in frames:
568
+ if len(frame.raw_bytes) < 3:
569
+ continue
570
+
571
+ data = frame.raw_bytes[:-1] # All but last byte
572
+ expected = frame.raw_bytes[-1]
573
+
574
+ calculated = _calculate_checksum(data, chk_type)
575
+ if calculated == expected:
576
+ matches += 1
577
+
578
+ if matches > best_matches:
579
+ best_matches = matches
580
+ best_type = chk_type
581
+ best_pos = pos
582
+
583
+ confidence = best_matches / len(frames) if frames else 0
584
+ if confidence < 0.5:
585
+ return None, None, confidence
586
+
587
+ return best_type, best_pos, confidence
588
+
589
+
590
+ def _calculate_checksum(data: bytes, checksum_type: str) -> int:
591
+ """Calculate checksum using specified algorithm."""
592
+ if checksum_type == "xor":
593
+ result = 0
594
+ for b in data:
595
+ result ^= b
596
+ return result
597
+
598
+ elif checksum_type == "sum8":
599
+ return sum(data) & 0xFF
600
+
601
+ elif checksum_type == "crc8":
602
+ # Simple CRC-8
603
+ crc = 0
604
+ for b in data:
605
+ crc ^= b
606
+ for _ in range(8):
607
+ if crc & 0x80:
608
+ crc = ((crc << 1) ^ 0x07) & 0xFF
609
+ else:
610
+ crc = (crc << 1) & 0xFF
611
+ return crc
612
+
613
+ elif checksum_type == "crc16":
614
+ # CRC-16-CCITT (return low byte only for single-byte comparison)
615
+ crc = 0xFFFF
616
+ for b in data:
617
+ crc ^= b << 8
618
+ for _ in range(8):
619
+ if crc & 0x8000:
620
+ crc = ((crc << 1) ^ 0x1021) & 0xFFFF
621
+ else:
622
+ crc = (crc << 1) & 0xFFFF
623
+ return crc & 0xFF # Return low byte
624
+
625
+ return 0
626
+
627
+
628
+ def _verify_checksum(data: bytes, checksum_type: str, checksum_pos: int | None) -> bool:
629
+ """Verify checksum in frame."""
630
+ if checksum_pos == -1 or checksum_pos is None:
631
+ # Checksum is last byte
632
+ frame_data = data[:-1]
633
+ expected = data[-1]
634
+ else:
635
+ frame_data = data[:checksum_pos] + data[checksum_pos + 1 :]
636
+ expected = data[checksum_pos]
637
+
638
+ calculated = _calculate_checksum(frame_data, checksum_type)
639
+ return calculated == expected