oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,525 @@
1
+ """Modbus RTU/TCP protocol analyzer.
2
+
3
+ This module provides comprehensive Modbus protocol analysis supporting both
4
+ RTU (serial) and TCP (Ethernet) variants. Decodes all standard Modbus function
5
+ codes, validates CRC for RTU, tracks device states, and exports register maps.
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.protocols.industrial.modbus.analyzer import ModbusAnalyzer
9
+ >>> analyzer = ModbusAnalyzer()
10
+ >>> # Parse RTU frame
11
+ >>> frame = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xCD, 0xC5])
12
+ >>> message = analyzer.parse_rtu(frame, timestamp=0.0)
13
+ >>> print(f"{message.function_name}: {message.parsed_data}")
14
+ >>> # Parse TCP frame
15
+ >>> tcp_frame = bytes([0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, ...])
16
+ >>> message = analyzer.parse_tcp(tcp_frame, timestamp=0.0)
17
+
18
+ References:
19
+ Modbus Application Protocol V1.1b3:
20
+ https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
21
+
22
+ Modbus over Serial Line V1.02:
23
+ https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ from dataclasses import dataclass, field
30
+ from pathlib import Path
31
+ from typing import Any, ClassVar
32
+
33
+ from oscura.analyzers.protocols.industrial.modbus.crc import verify_crc
34
+ from oscura.analyzers.protocols.industrial.modbus.functions import (
35
+ parse_read_coils_request,
36
+ parse_read_coils_response,
37
+ parse_read_discrete_inputs_request,
38
+ parse_read_discrete_inputs_response,
39
+ parse_read_holding_registers_request,
40
+ parse_read_holding_registers_response,
41
+ parse_read_input_registers_request,
42
+ parse_read_input_registers_response,
43
+ parse_write_multiple_coils_request,
44
+ parse_write_multiple_coils_response,
45
+ parse_write_multiple_registers_request,
46
+ parse_write_multiple_registers_response,
47
+ parse_write_single_coil,
48
+ parse_write_single_register,
49
+ )
50
+
51
+
52
+ @dataclass
53
+ class ModbusMessage:
54
+ """Modbus message representation.
55
+
56
+ Attributes:
57
+ timestamp: Message timestamp in seconds.
58
+ variant: Protocol variant ("RTU" or "TCP").
59
+ is_request: True if request, False if response.
60
+ transaction_id: TCP transaction ID (TCP only).
61
+ unit_id: Slave address (RTU) or unit identifier (TCP).
62
+ function_code: Modbus function code.
63
+ function_name: Human-readable function name.
64
+ data: Raw function data bytes.
65
+ exception_code: Exception code if error response.
66
+ parsed_data: Parsed function-specific data.
67
+ crc_valid: CRC validation result (RTU only).
68
+ """
69
+
70
+ timestamp: float
71
+ variant: str # "RTU" or "TCP"
72
+ is_request: bool
73
+ transaction_id: int | None = None # TCP only
74
+ unit_id: int = 0
75
+ function_code: int = 0
76
+ function_name: str = ""
77
+ data: bytes = b""
78
+ exception_code: int | None = None
79
+ parsed_data: dict[str, Any] = field(default_factory=dict)
80
+ crc_valid: bool | None = None # RTU only
81
+
82
+
83
+ @dataclass
84
+ class ModbusDevice:
85
+ """Modbus device state information.
86
+
87
+ Tracks the state of a Modbus device including register and coil values
88
+ observed in communication.
89
+
90
+ Attributes:
91
+ unit_id: Device unit ID / slave address.
92
+ function_codes_seen: Set of function codes observed.
93
+ coils: Coil states (address -> value).
94
+ discrete_inputs: Discrete input states (address -> value).
95
+ holding_registers: Holding register values (address -> value).
96
+ input_registers: Input register values (address -> value).
97
+ """
98
+
99
+ unit_id: int
100
+ function_codes_seen: set[int] = field(default_factory=set)
101
+ coils: dict[int, bool] = field(default_factory=dict)
102
+ discrete_inputs: dict[int, bool] = field(default_factory=dict)
103
+ holding_registers: dict[int, int] = field(default_factory=dict)
104
+ input_registers: dict[int, int] = field(default_factory=dict)
105
+
106
+
107
+ class ModbusAnalyzer:
108
+ """Modbus protocol analyzer for RTU and TCP variants.
109
+
110
+ Provides comprehensive Modbus protocol analysis including frame parsing,
111
+ function code decoding, CRC validation, and device state tracking.
112
+
113
+ Attributes:
114
+ messages: List of parsed Modbus messages.
115
+ devices: Dictionary of device states by unit ID.
116
+
117
+ Example:
118
+ >>> analyzer = ModbusAnalyzer()
119
+ >>> # Parse RTU frame
120
+ >>> rtu_frame = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xCD, 0xC5])
121
+ >>> msg = analyzer.parse_rtu(rtu_frame)
122
+ >>> print(f"Function: {msg.function_name}, CRC Valid: {msg.crc_valid}")
123
+ >>> # Export device register map
124
+ >>> analyzer.export_register_map(Path("registers.json"))
125
+ """
126
+
127
+ # Standard Modbus function codes
128
+ FUNCTION_CODES: ClassVar[dict[int, str]] = {
129
+ 1: "Read Coils",
130
+ 2: "Read Discrete Inputs",
131
+ 3: "Read Holding Registers",
132
+ 4: "Read Input Registers",
133
+ 5: "Write Single Coil",
134
+ 6: "Write Single Register",
135
+ 15: "Write Multiple Coils",
136
+ 16: "Write Multiple Registers",
137
+ 23: "Read/Write Multiple Registers",
138
+ # Diagnostic and maintenance functions
139
+ 8: "Diagnostics",
140
+ 11: "Get Comm Event Counter",
141
+ 12: "Get Comm Event Log",
142
+ 17: "Report Server ID",
143
+ # File and queue operations
144
+ 20: "Read File Record",
145
+ 21: "Write File Record",
146
+ 22: "Mask Write Register",
147
+ 24: "Read FIFO Queue",
148
+ 43: "Encapsulated Interface Transport",
149
+ }
150
+
151
+ # Exception codes
152
+ EXCEPTION_CODES: ClassVar[dict[int, str]] = {
153
+ 1: "Illegal Function",
154
+ 2: "Illegal Data Address",
155
+ 3: "Illegal Data Value",
156
+ 4: "Server Device Failure",
157
+ 5: "Acknowledge",
158
+ 6: "Server Device Busy",
159
+ 8: "Memory Parity Error",
160
+ 10: "Gateway Path Unavailable",
161
+ 11: "Gateway Target Device Failed to Respond",
162
+ }
163
+
164
+ def __init__(self) -> None:
165
+ """Initialize Modbus analyzer."""
166
+ self.messages: list[ModbusMessage] = []
167
+ self.devices: dict[int, ModbusDevice] = {}
168
+
169
+ def parse_rtu(self, data: bytes, timestamp: float = 0.0) -> ModbusMessage:
170
+ """Parse Modbus RTU frame.
171
+
172
+ RTU Frame Format:
173
+ - Slave Address (1 byte)
174
+ - Function Code (1 byte)
175
+ - Data (N bytes, function-specific)
176
+ - CRC-16 (2 bytes, little-endian)
177
+
178
+ Args:
179
+ data: Complete RTU frame including CRC.
180
+ timestamp: Message timestamp in seconds.
181
+
182
+ Returns:
183
+ Parsed Modbus message.
184
+
185
+ Raises:
186
+ ValueError: If frame is invalid.
187
+
188
+ Example:
189
+ >>> analyzer = ModbusAnalyzer()
190
+ >>> frame = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xCD, 0xC5])
191
+ >>> msg = analyzer.parse_rtu(frame)
192
+ >>> assert msg.unit_id == 1
193
+ >>> assert msg.function_code == 3
194
+ >>> assert msg.crc_valid is True
195
+ """
196
+ if len(data) < 4: # Minimum: Address + FC + CRC
197
+ raise ValueError(f"RTU frame too short: {len(data)} bytes (minimum 4)")
198
+
199
+ # Verify CRC
200
+ crc_valid = verify_crc(data)
201
+
202
+ unit_id = data[0]
203
+ function_code = data[1]
204
+ function_data = data[2:-2]
205
+
206
+ # Check for exception response (high bit set in function code)
207
+ is_exception = bool(function_code & 0x80)
208
+ exception_code = None
209
+ parsed_data: dict[str, Any] = {}
210
+
211
+ if is_exception:
212
+ actual_fc = function_code & 0x7F
213
+ if len(function_data) > 0:
214
+ exception_code = function_data[0]
215
+ parsed_data = {
216
+ "exception": self.EXCEPTION_CODES.get(exception_code, "Unknown Exception")
217
+ }
218
+ else:
219
+ actual_fc = function_code
220
+ try:
221
+ # Parse function-specific data
222
+ parsed_data = self._parse_function(actual_fc, function_data, is_request=True)
223
+ except ValueError as e:
224
+ parsed_data = {"parse_error": str(e)}
225
+
226
+ message = ModbusMessage(
227
+ timestamp=timestamp,
228
+ variant="RTU",
229
+ is_request=True, # Determined by context in real usage
230
+ unit_id=unit_id,
231
+ function_code=actual_fc,
232
+ function_name=self.FUNCTION_CODES.get(actual_fc, f"Unknown (0x{actual_fc:02X})"),
233
+ data=function_data,
234
+ exception_code=exception_code,
235
+ parsed_data=parsed_data,
236
+ crc_valid=crc_valid,
237
+ )
238
+
239
+ self.messages.append(message)
240
+ self.update_device_state(message)
241
+
242
+ return message
243
+
244
+ def parse_tcp(self, data: bytes, timestamp: float = 0.0) -> ModbusMessage:
245
+ """Parse Modbus TCP frame.
246
+
247
+ TCP Frame Format (MBAP Header + PDU):
248
+ - Transaction ID (2 bytes, big-endian)
249
+ - Protocol ID (2 bytes, always 0x0000)
250
+ - Length (2 bytes, big-endian, remaining bytes)
251
+ - Unit ID (1 byte)
252
+ - Function Code (1 byte)
253
+ - Data (N bytes, function-specific)
254
+
255
+ Args:
256
+ data: Complete TCP frame including MBAP header.
257
+ timestamp: Message timestamp in seconds.
258
+
259
+ Returns:
260
+ Parsed Modbus message.
261
+
262
+ Raises:
263
+ ValueError: If frame is invalid.
264
+
265
+ Example:
266
+ >>> analyzer = ModbusAnalyzer()
267
+ >>> # Read Holding Registers request
268
+ >>> frame = bytes([0x00, 0x01, 0x00, 0x00, 0x00, 0x06,
269
+ ... 0x01, 0x03, 0x00, 0x00, 0x00, 0x0A])
270
+ >>> msg = analyzer.parse_tcp(frame)
271
+ >>> assert msg.transaction_id == 1
272
+ >>> assert msg.function_code == 3
273
+ """
274
+ if len(data) < 8: # Minimum: MBAP (7) + FC (1)
275
+ raise ValueError(f"TCP frame too short: {len(data)} bytes (minimum 8)")
276
+
277
+ # Parse MBAP header
278
+ transaction_id = int.from_bytes(data[0:2], "big")
279
+ protocol_id = int.from_bytes(data[2:4], "big")
280
+ length = int.from_bytes(data[4:6], "big")
281
+ unit_id = data[6]
282
+ function_code = data[7]
283
+ function_data = data[8:]
284
+
285
+ if protocol_id != 0:
286
+ raise ValueError(f"Invalid Modbus TCP protocol ID: {protocol_id} (expected 0)")
287
+
288
+ # Verify length field
289
+ expected_length = len(data) - 6 # Everything after protocol ID and length
290
+ if length != expected_length:
291
+ raise ValueError(f"Length mismatch: {length} != {expected_length}")
292
+
293
+ # Check for exception response
294
+ is_exception = bool(function_code & 0x80)
295
+ exception_code = None
296
+ parsed_data: dict[str, Any] = {}
297
+
298
+ if is_exception:
299
+ actual_fc = function_code & 0x7F
300
+ if len(function_data) > 0:
301
+ exception_code = function_data[0]
302
+ parsed_data = {
303
+ "exception": self.EXCEPTION_CODES.get(exception_code, "Unknown Exception")
304
+ }
305
+ else:
306
+ actual_fc = function_code
307
+ try:
308
+ parsed_data = self._parse_function(actual_fc, function_data, is_request=True)
309
+ except ValueError as e:
310
+ parsed_data = {"parse_error": str(e)}
311
+
312
+ message = ModbusMessage(
313
+ timestamp=timestamp,
314
+ variant="TCP",
315
+ is_request=True,
316
+ transaction_id=transaction_id,
317
+ unit_id=unit_id,
318
+ function_code=actual_fc,
319
+ function_name=self.FUNCTION_CODES.get(actual_fc, f"Unknown (0x{actual_fc:02X})"),
320
+ data=function_data,
321
+ exception_code=exception_code,
322
+ parsed_data=parsed_data,
323
+ )
324
+
325
+ self.messages.append(message)
326
+ self.update_device_state(message)
327
+
328
+ return message
329
+
330
+ def _parse_function(self, function_code: int, data: bytes, is_request: bool) -> dict[str, Any]:
331
+ """Parse function-specific data.
332
+
333
+ Args:
334
+ function_code: Modbus function code.
335
+ data: Function data bytes.
336
+ is_request: True if request, False if response.
337
+
338
+ Returns:
339
+ Parsed function data.
340
+
341
+ Raises:
342
+ ValueError: If parsing fails.
343
+ """
344
+ # Read operations (1-4)
345
+ if function_code in (1, 2, 3, 4):
346
+ return self._parse_read_function(function_code, data, is_request)
347
+
348
+ # Write operations (5, 6, 15, 16)
349
+ if function_code in (5, 6, 15, 16):
350
+ return self._parse_write_function(function_code, data, is_request)
351
+
352
+ # Unsupported function codes
353
+ return {"raw_data": data.hex()}
354
+
355
+ def _parse_read_function(
356
+ self, function_code: int, data: bytes, is_request: bool
357
+ ) -> dict[str, Any]:
358
+ """Parse read function codes (1-4).
359
+
360
+ Args:
361
+ function_code: Function code (1-4).
362
+ data: Function data bytes.
363
+ is_request: True if request, False if response.
364
+
365
+ Returns:
366
+ Parsed function data.
367
+ """
368
+ parsers = {
369
+ 1: (parse_read_coils_request, parse_read_coils_response),
370
+ 2: (parse_read_discrete_inputs_request, parse_read_discrete_inputs_response),
371
+ 3: (parse_read_holding_registers_request, parse_read_holding_registers_response),
372
+ 4: (parse_read_input_registers_request, parse_read_input_registers_response),
373
+ }
374
+
375
+ request_parser, response_parser = parsers[function_code]
376
+ return request_parser(data) if is_request else response_parser(data)
377
+
378
+ def _parse_write_function(
379
+ self, function_code: int, data: bytes, is_request: bool
380
+ ) -> dict[str, Any]:
381
+ """Parse write function codes (5, 6, 15, 16).
382
+
383
+ Args:
384
+ function_code: Function code (5, 6, 15, or 16).
385
+ data: Function data bytes.
386
+ is_request: True if request, False if response.
387
+
388
+ Returns:
389
+ Parsed function data.
390
+ """
391
+ # Single write operations (no request/response distinction)
392
+ if function_code == 5:
393
+ return parse_write_single_coil(data)
394
+ if function_code == 6:
395
+ return parse_write_single_register(data)
396
+
397
+ # Multiple write operations
398
+ parsers = {
399
+ 15: (parse_write_multiple_coils_request, parse_write_multiple_coils_response),
400
+ 16: (parse_write_multiple_registers_request, parse_write_multiple_registers_response),
401
+ }
402
+
403
+ request_parser, response_parser = parsers[function_code]
404
+ return request_parser(data) if is_request else response_parser(data)
405
+
406
+ def update_device_state(self, message: ModbusMessage) -> None:
407
+ """Update device state based on message.
408
+
409
+ Tracks coil and register values observed in Modbus communication.
410
+
411
+ Args:
412
+ message: Parsed Modbus message.
413
+ """
414
+ device = self._ensure_device_exists(message.unit_id)
415
+ device.function_codes_seen.add(message.function_code)
416
+
417
+ # Don't update state for exceptions
418
+ if message.exception_code is not None:
419
+ return
420
+
421
+ # Dispatch to function-specific handlers
422
+ self._update_state_by_function_code(device, message)
423
+
424
+ def _ensure_device_exists(self, unit_id: int) -> ModbusDevice:
425
+ """Ensure device exists in registry.
426
+
427
+ Args:
428
+ unit_id: Modbus unit ID.
429
+
430
+ Returns:
431
+ ModbusDevice instance.
432
+ """
433
+ if unit_id not in self.devices:
434
+ self.devices[unit_id] = ModbusDevice(unit_id=unit_id)
435
+ return self.devices[unit_id]
436
+
437
+ def _update_state_by_function_code(self, device: ModbusDevice, message: ModbusMessage) -> None:
438
+ """Dispatch state update based on function code.
439
+
440
+ Args:
441
+ device: Device to update.
442
+ message: Modbus message.
443
+ """
444
+ parsed = message.parsed_data
445
+ fc = message.function_code
446
+
447
+ # Coil operations
448
+ if fc == 5:
449
+ self._update_single_coil(device, parsed)
450
+
451
+ # Register operations
452
+ elif fc == 6:
453
+ self._update_single_register(device, parsed)
454
+ elif fc == 16:
455
+ self._update_multiple_registers(device, parsed)
456
+
457
+ def _update_single_coil(self, device: ModbusDevice, parsed: dict[str, Any]) -> None:
458
+ """Update single coil value.
459
+
460
+ Args:
461
+ device: Device to update.
462
+ parsed: Parsed message data.
463
+ """
464
+ if "output_address" in parsed and "coil_state" in parsed:
465
+ device.coils[parsed["output_address"]] = parsed["coil_state"]
466
+
467
+ def _update_single_register(self, device: ModbusDevice, parsed: dict[str, Any]) -> None:
468
+ """Update single register value.
469
+
470
+ Args:
471
+ device: Device to update.
472
+ parsed: Parsed message data.
473
+ """
474
+ if "register_address" in parsed and "register_value" in parsed:
475
+ device.holding_registers[parsed["register_address"]] = parsed["register_value"]
476
+
477
+ def _update_multiple_registers(self, device: ModbusDevice, parsed: dict[str, Any]) -> None:
478
+ """Update multiple register values.
479
+
480
+ Args:
481
+ device: Device to update.
482
+ parsed: Parsed message data.
483
+ """
484
+ if "starting_address" in parsed and "registers" in parsed:
485
+ start_addr = parsed["starting_address"]
486
+ for i, value in enumerate(parsed["registers"]):
487
+ device.holding_registers[start_addr + i] = value
488
+
489
+ def export_register_map(self, output_path: Path) -> None:
490
+ """Export register map for all devices as JSON.
491
+
492
+ Args:
493
+ output_path: Path to output JSON file.
494
+
495
+ Example:
496
+ >>> analyzer = ModbusAnalyzer()
497
+ >>> # ... parse messages ...
498
+ >>> analyzer.export_register_map(Path("modbus_registers.json"))
499
+ """
500
+ export_data = {
501
+ "devices": [
502
+ {
503
+ "unit_id": device.unit_id,
504
+ "function_codes": sorted(device.function_codes_seen),
505
+ "coils": {str(k): v for k, v in sorted(device.coils.items())},
506
+ "discrete_inputs": {
507
+ str(k): v for k, v in sorted(device.discrete_inputs.items())
508
+ },
509
+ "holding_registers": {
510
+ str(k): v for k, v in sorted(device.holding_registers.items())
511
+ },
512
+ "input_registers": {
513
+ str(k): v for k, v in sorted(device.input_registers.items())
514
+ },
515
+ }
516
+ for device in self.devices.values()
517
+ ],
518
+ "message_count": len(self.messages),
519
+ }
520
+
521
+ with output_path.open("w") as f:
522
+ json.dump(export_data, f, indent=2)
523
+
524
+
525
+ __all__ = ["ModbusAnalyzer", "ModbusDevice", "ModbusMessage"]
@@ -0,0 +1,79 @@
1
+ """Modbus RTU CRC-16 calculation.
2
+
3
+ This module provides CRC-16 calculation and validation for Modbus RTU frames.
4
+
5
+ The Modbus RTU CRC-16 uses polynomial 0xA001 (reversed 0x8005).
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.protocols.industrial.modbus.crc import calculate_crc
9
+ >>> data = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A])
10
+ >>> crc = calculate_crc(data)
11
+ >>> print(f"CRC: 0x{crc:04X}")
12
+
13
+ References:
14
+ Modbus over Serial Line Specification V1.02 (Section 6.2.2)
15
+ https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+
21
+ def calculate_crc(data: bytes) -> int:
22
+ """Calculate Modbus RTU CRC-16.
23
+
24
+ Uses the polynomial 0xA001 (bit-reversed representation of 0x8005).
25
+ This is the standard CRC-16-IBM/CRC-16-ANSI with reflected input/output.
26
+
27
+ Args:
28
+ data: Data bytes to calculate CRC for (excludes CRC itself).
29
+
30
+ Returns:
31
+ 16-bit CRC value as integer.
32
+
33
+ Example:
34
+ >>> data = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A])
35
+ >>> crc = calculate_crc(data)
36
+ >>> assert crc == 0xC5CD # Expected CRC for this data
37
+ """
38
+ crc = 0xFFFF
39
+
40
+ for byte in data:
41
+ crc ^= byte
42
+ for _ in range(8):
43
+ if crc & 0x0001:
44
+ crc = (crc >> 1) ^ 0xA001
45
+ else:
46
+ crc >>= 1
47
+
48
+ return crc
49
+
50
+
51
+ def verify_crc(data: bytes) -> bool:
52
+ """Verify Modbus RTU CRC-16.
53
+
54
+ Checks if the last 2 bytes of data contain the correct CRC for the
55
+ preceding bytes.
56
+
57
+ Args:
58
+ data: Complete frame including CRC (last 2 bytes, little-endian).
59
+
60
+ Returns:
61
+ True if CRC is valid, False otherwise.
62
+
63
+ Example:
64
+ >>> frame = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, 0xCD, 0xC5])
65
+ >>> assert verify_crc(frame) is True
66
+ """
67
+ if len(data) < 4: # Minimum: Address + FC + CRC
68
+ return False
69
+
70
+ # Calculate CRC for all bytes except last 2
71
+ calculated = calculate_crc(data[:-2])
72
+
73
+ # Extract CRC from last 2 bytes (little-endian)
74
+ received = int.from_bytes(data[-2:], "little")
75
+
76
+ return calculated == received
77
+
78
+
79
+ __all__ = ["calculate_crc", "verify_crc"]