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,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"]