oscura 0.3.0__py3-none-any.whl → 0.5.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.
- oscura/__init__.py +1 -7
- oscura/acquisition/__init__.py +147 -0
- oscura/acquisition/file.py +255 -0
- oscura/acquisition/hardware.py +186 -0
- oscura/acquisition/saleae.py +340 -0
- oscura/acquisition/socketcan.py +315 -0
- oscura/acquisition/streaming.py +38 -0
- oscura/acquisition/synthetic.py +229 -0
- oscura/acquisition/visa.py +376 -0
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/digital/__init__.py +48 -0
- oscura/analyzers/digital/clock.py +9 -1
- oscura/analyzers/digital/edges.py +1 -1
- oscura/analyzers/digital/extraction.py +195 -0
- oscura/analyzers/digital/ic_database.py +498 -0
- oscura/analyzers/digital/timing.py +41 -11
- oscura/analyzers/digital/timing_paths.py +339 -0
- oscura/analyzers/digital/vintage.py +377 -0
- oscura/analyzers/digital/vintage_result.py +148 -0
- oscura/analyzers/protocols/__init__.py +22 -1
- oscura/analyzers/protocols/parallel_bus.py +449 -0
- oscura/analyzers/side_channel/__init__.py +52 -0
- oscura/analyzers/side_channel/power.py +690 -0
- oscura/analyzers/side_channel/timing.py +369 -0
- oscura/analyzers/signal_integrity/sparams.py +1 -1
- oscura/automotive/__init__.py +4 -2
- oscura/automotive/can/patterns.py +3 -1
- oscura/automotive/can/session.py +277 -78
- oscura/automotive/can/state_machine.py +5 -2
- oscura/builders/__init__.py +9 -11
- oscura/builders/signal_builder.py +99 -191
- oscura/core/exceptions.py +5 -1
- oscura/export/__init__.py +12 -0
- oscura/export/wavedrom.py +430 -0
- oscura/exporters/json_export.py +47 -0
- oscura/exporters/vintage_logic_csv.py +247 -0
- oscura/loaders/__init__.py +1 -0
- oscura/loaders/chipwhisperer.py +393 -0
- oscura/loaders/touchstone.py +1 -1
- oscura/reporting/__init__.py +7 -0
- oscura/reporting/vintage_logic_report.py +523 -0
- oscura/session/session.py +54 -46
- oscura/sessions/__init__.py +70 -0
- oscura/sessions/base.py +323 -0
- oscura/sessions/blackbox.py +640 -0
- oscura/sessions/generic.py +189 -0
- oscura/utils/autodetect.py +5 -1
- oscura/visualization/digital_advanced.py +718 -0
- oscura/visualization/figure_manager.py +156 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
- oscura/automotive/dtc/data.json +0 -2763
- oscura/schemas/bus_configuration.json +0 -322
- oscura/schemas/device_mapping.json +0 -182
- oscura/schemas/packet_format.json +0 -418
- oscura/schemas/protocol_definition.json +0 -363
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""Vintage logic analysis report generation.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive reporting capabilities for vintage logic
|
|
4
|
+
analysis results, including HTML, PDF, and Markdown formats.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from oscura.reporting.vintage_logic_report import generate_vintage_logic_report
|
|
8
|
+
>>> report = generate_vintage_logic_report(result, traces, output_dir="./output")
|
|
9
|
+
>>> report.save_html("analysis.html")
|
|
10
|
+
>>> report.save_markdown("analysis.md")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from oscura.analyzers.digital.vintage_result import VintageLogicAnalysisResult
|
|
22
|
+
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ReportMetadata:
|
|
27
|
+
"""Report metadata.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
title: Report title.
|
|
31
|
+
author: Report author.
|
|
32
|
+
timestamp: Report generation timestamp.
|
|
33
|
+
version: Report format version.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
title: str
|
|
37
|
+
author: str = "Oscura Vintage Logic Analyzer"
|
|
38
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
39
|
+
version: str = "1.0"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class VintageLogicReport:
|
|
44
|
+
"""Vintage logic analysis report.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
result: Analysis result object.
|
|
48
|
+
plots: Dictionary mapping plot names to file paths.
|
|
49
|
+
metadata: Report metadata.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
result: VintageLogicAnalysisResult
|
|
53
|
+
plots: dict[str, Path]
|
|
54
|
+
metadata: ReportMetadata
|
|
55
|
+
|
|
56
|
+
def save_html(self, path: str | Path, **options: Any) -> Path:
|
|
57
|
+
"""Generate comprehensive HTML report.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: Output HTML file path.
|
|
61
|
+
**options: Additional options (currently unused, reserved for future).
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Path to saved HTML file.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> report.save_html("analysis.html")
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
path = Path(path)
|
|
71
|
+
|
|
72
|
+
# Generate HTML content
|
|
73
|
+
html = _generate_html_report(self)
|
|
74
|
+
|
|
75
|
+
# Write to file
|
|
76
|
+
path.write_text(html, encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
return path
|
|
79
|
+
|
|
80
|
+
def save_markdown(self, path: str | Path) -> Path:
|
|
81
|
+
"""Generate markdown summary.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
path: Output markdown file path.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Path to saved markdown file.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> report.save_markdown("analysis.md")
|
|
91
|
+
"""
|
|
92
|
+
path = Path(path)
|
|
93
|
+
|
|
94
|
+
# Generate markdown content
|
|
95
|
+
md = _generate_markdown_report(self)
|
|
96
|
+
|
|
97
|
+
# Write to file
|
|
98
|
+
path.write_text(md, encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
return path
|
|
101
|
+
|
|
102
|
+
def save_pdf(self, path: str | Path, **options: Any) -> Path:
|
|
103
|
+
"""Generate PDF report.
|
|
104
|
+
|
|
105
|
+
Currently generates HTML first, then suggests using browser print
|
|
106
|
+
or external tools for PDF conversion.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
path: Output PDF file path.
|
|
110
|
+
**options: Additional options.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Path to saved PDF file (or HTML file with instructions).
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> report.save_pdf("analysis.pdf")
|
|
117
|
+
"""
|
|
118
|
+
path = Path(path)
|
|
119
|
+
|
|
120
|
+
# For now, save as HTML with instructions
|
|
121
|
+
html_path = path.with_suffix(".html")
|
|
122
|
+
self.save_html(html_path)
|
|
123
|
+
|
|
124
|
+
# Add note about PDF conversion
|
|
125
|
+
print(f"HTML report generated: {html_path}")
|
|
126
|
+
print("To convert to PDF:")
|
|
127
|
+
print(" 1. Open in browser and use Print > Save as PDF")
|
|
128
|
+
print(" 2. Use weasyprint: weasyprint {html_path} {path}")
|
|
129
|
+
print(" 3. Use wkhtmltopdf: wkhtmltopdf {html_path} {path}")
|
|
130
|
+
|
|
131
|
+
return html_path
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def generate_vintage_logic_report(
|
|
135
|
+
result: VintageLogicAnalysisResult,
|
|
136
|
+
traces: dict[str, WaveformTrace | DigitalTrace],
|
|
137
|
+
*,
|
|
138
|
+
title: str | None = None,
|
|
139
|
+
include_plots: list[str] | None = None,
|
|
140
|
+
output_dir: Path | None = None,
|
|
141
|
+
) -> VintageLogicReport:
|
|
142
|
+
"""Generate complete vintage logic report.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
result: Analysis result from analyze_vintage_logic().
|
|
146
|
+
traces: Original signal traces.
|
|
147
|
+
title: Report title. Defaults to auto-generated title.
|
|
148
|
+
include_plots: List of plot types to include (None = all).
|
|
149
|
+
output_dir: Directory for saving plot files. If None, uses temp directory.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
VintageLogicReport object ready for export.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> from oscura.analyzers.digital.vintage import analyze_vintage_logic
|
|
156
|
+
>>> result = analyze_vintage_logic(traces)
|
|
157
|
+
>>> report = generate_vintage_logic_report(result, traces, output_dir="./output")
|
|
158
|
+
>>> report.save_html("report.html")
|
|
159
|
+
"""
|
|
160
|
+
from pathlib import Path
|
|
161
|
+
from tempfile import mkdtemp
|
|
162
|
+
|
|
163
|
+
from oscura.visualization.digital_advanced import generate_all_vintage_logic_plots
|
|
164
|
+
|
|
165
|
+
# Use temp directory if no output directory specified
|
|
166
|
+
if output_dir is None:
|
|
167
|
+
output_dir = Path(mkdtemp(prefix="oscura_vintage_"))
|
|
168
|
+
else:
|
|
169
|
+
output_dir = Path(output_dir)
|
|
170
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
|
|
172
|
+
# Generate default title if not provided
|
|
173
|
+
if title is None:
|
|
174
|
+
title = f"Vintage Logic Analysis - {result.detected_family}"
|
|
175
|
+
|
|
176
|
+
# Create metadata
|
|
177
|
+
metadata = ReportMetadata(title=title)
|
|
178
|
+
|
|
179
|
+
# Generate all plots
|
|
180
|
+
plot_objects = generate_all_vintage_logic_plots(
|
|
181
|
+
result,
|
|
182
|
+
traces,
|
|
183
|
+
output_dir=output_dir,
|
|
184
|
+
save_formats=["png"],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Get plot paths from output directory
|
|
188
|
+
plot_paths: dict[str, Path] = {}
|
|
189
|
+
for plot_name in plot_objects:
|
|
190
|
+
plot_path = output_dir / f"{plot_name}.png"
|
|
191
|
+
if plot_path.exists():
|
|
192
|
+
plot_paths[plot_name] = plot_path
|
|
193
|
+
|
|
194
|
+
# Filter plots if specified
|
|
195
|
+
if include_plots:
|
|
196
|
+
plot_paths = {k: v for k, v in plot_paths.items() if k in include_plots}
|
|
197
|
+
|
|
198
|
+
return VintageLogicReport(
|
|
199
|
+
result=result,
|
|
200
|
+
plots=plot_paths,
|
|
201
|
+
metadata=metadata,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _generate_html_report(report: VintageLogicReport) -> str:
|
|
206
|
+
"""Generate HTML report content."""
|
|
207
|
+
result = report.result
|
|
208
|
+
|
|
209
|
+
# Embed images as base64
|
|
210
|
+
from base64 import b64encode
|
|
211
|
+
|
|
212
|
+
plot_html = []
|
|
213
|
+
for plot_name, plot_path in report.plots.items():
|
|
214
|
+
if plot_path.exists():
|
|
215
|
+
with plot_path.open("rb") as f:
|
|
216
|
+
img_data = b64encode(f.read()).decode("utf-8")
|
|
217
|
+
plot_html.append(
|
|
218
|
+
f'<div class="plot"><h3>{plot_name.replace("_", " ").title()}</h3>'
|
|
219
|
+
f'<img src="data:image/png;base64,{img_data}" alt="{plot_name}" /></div>'
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
plots_section = "\n".join(plot_html)
|
|
223
|
+
|
|
224
|
+
# Generate IC table
|
|
225
|
+
ic_rows = []
|
|
226
|
+
for ic in result.identified_ics:
|
|
227
|
+
validation_status = (
|
|
228
|
+
"PASS" if all(v.get("passes", True) for v in ic.validation.values()) else "FAIL"
|
|
229
|
+
)
|
|
230
|
+
status_class = "pass" if validation_status == "PASS" else "fail"
|
|
231
|
+
|
|
232
|
+
ic_rows.append(
|
|
233
|
+
f"<tr>"
|
|
234
|
+
f"<td>{ic.ic_name}</td>"
|
|
235
|
+
f"<td>{ic.confidence * 100:.1f}%</td>"
|
|
236
|
+
f"<td>{ic.family}</td>"
|
|
237
|
+
f'<td class="{status_class}">{validation_status}</td>'
|
|
238
|
+
f"</tr>"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
ic_table = (
|
|
242
|
+
"<table><tr><th>IC</th><th>Confidence</th><th>Family</th><th>Validation</th></tr>\n"
|
|
243
|
+
+ "\n".join(ic_rows)
|
|
244
|
+
+ "</table>"
|
|
245
|
+
if ic_rows
|
|
246
|
+
else "<p>No ICs identified</p>"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Generate BOM table
|
|
250
|
+
bom_rows = []
|
|
251
|
+
for entry in result.bom:
|
|
252
|
+
bom_rows.append(
|
|
253
|
+
f"<tr>"
|
|
254
|
+
f"<td>{entry.part_number}</td>"
|
|
255
|
+
f"<td>{entry.description}</td>"
|
|
256
|
+
f"<td>{entry.quantity}</td>"
|
|
257
|
+
f"<td>{entry.category}</td>"
|
|
258
|
+
f"<td>{entry.notes or ''}</td>"
|
|
259
|
+
f"</tr>"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
bom_table = (
|
|
263
|
+
"<table><tr><th>Part Number</th><th>Description</th><th>Qty</th><th>Category</th><th>Notes</th></tr>\n"
|
|
264
|
+
+ "\n".join(bom_rows)
|
|
265
|
+
+ "</table>"
|
|
266
|
+
if bom_rows
|
|
267
|
+
else "<p>No BOM entries</p>"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Generate timing measurements table
|
|
271
|
+
timing_rows = []
|
|
272
|
+
for param, value in result.timing_measurements.items():
|
|
273
|
+
timing_rows.append(f"<tr><td>{param}</td><td>{value * 1e9:.2f} ns</td></tr>")
|
|
274
|
+
|
|
275
|
+
timing_table = (
|
|
276
|
+
"<table><tr><th>Parameter</th><th>Value</th></tr>\n" + "\n".join(timing_rows) + "</table>"
|
|
277
|
+
if timing_rows
|
|
278
|
+
else "<p>No timing measurements</p>"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Generate warnings
|
|
282
|
+
warnings_html = ""
|
|
283
|
+
if result.warnings:
|
|
284
|
+
warnings_list = "\n".join(f"<li>{w}</li>" for w in result.warnings)
|
|
285
|
+
warnings_html = f'<div class="warnings"><h2>Warnings</h2><ul>{warnings_list}</ul></div>'
|
|
286
|
+
|
|
287
|
+
html = f"""<!DOCTYPE html>
|
|
288
|
+
<html lang="en">
|
|
289
|
+
<head>
|
|
290
|
+
<meta charset="UTF-8">
|
|
291
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
292
|
+
<title>{report.metadata.title}</title>
|
|
293
|
+
<style>
|
|
294
|
+
body {{
|
|
295
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
296
|
+
line-height: 1.6;
|
|
297
|
+
color: #333;
|
|
298
|
+
max-width: 1200px;
|
|
299
|
+
margin: 0 auto;
|
|
300
|
+
padding: 20px;
|
|
301
|
+
background: #f5f5f5;
|
|
302
|
+
}}
|
|
303
|
+
.container {{
|
|
304
|
+
background: white;
|
|
305
|
+
padding: 30px;
|
|
306
|
+
border-radius: 8px;
|
|
307
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
308
|
+
}}
|
|
309
|
+
h1 {{
|
|
310
|
+
color: #2c3e50;
|
|
311
|
+
border-bottom: 3px solid #3498db;
|
|
312
|
+
padding-bottom: 10px;
|
|
313
|
+
}}
|
|
314
|
+
h2 {{
|
|
315
|
+
color: #34495e;
|
|
316
|
+
margin-top: 30px;
|
|
317
|
+
border-bottom: 2px solid #ecf0f1;
|
|
318
|
+
padding-bottom: 5px;
|
|
319
|
+
}}
|
|
320
|
+
h3 {{
|
|
321
|
+
color: #7f8c8d;
|
|
322
|
+
}}
|
|
323
|
+
table {{
|
|
324
|
+
width: 100%;
|
|
325
|
+
border-collapse: collapse;
|
|
326
|
+
margin: 20px 0;
|
|
327
|
+
}}
|
|
328
|
+
th, td {{
|
|
329
|
+
padding: 12px;
|
|
330
|
+
text-align: left;
|
|
331
|
+
border-bottom: 1px solid #ddd;
|
|
332
|
+
}}
|
|
333
|
+
th {{
|
|
334
|
+
background-color: #3498db;
|
|
335
|
+
color: white;
|
|
336
|
+
font-weight: bold;
|
|
337
|
+
}}
|
|
338
|
+
tr:hover {{
|
|
339
|
+
background-color: #f5f5f5;
|
|
340
|
+
}}
|
|
341
|
+
.plot {{
|
|
342
|
+
margin: 30px 0;
|
|
343
|
+
text-align: center;
|
|
344
|
+
}}
|
|
345
|
+
.plot img {{
|
|
346
|
+
max-width: 100%;
|
|
347
|
+
height: auto;
|
|
348
|
+
border: 1px solid #ddd;
|
|
349
|
+
border-radius: 4px;
|
|
350
|
+
padding: 5px;
|
|
351
|
+
}}
|
|
352
|
+
.summary-box {{
|
|
353
|
+
background: #ecf0f1;
|
|
354
|
+
padding: 20px;
|
|
355
|
+
border-radius: 4px;
|
|
356
|
+
margin: 20px 0;
|
|
357
|
+
}}
|
|
358
|
+
.summary-box strong {{
|
|
359
|
+
color: #2c3e50;
|
|
360
|
+
}}
|
|
361
|
+
.pass {{
|
|
362
|
+
color: #27ae60;
|
|
363
|
+
font-weight: bold;
|
|
364
|
+
}}
|
|
365
|
+
.fail {{
|
|
366
|
+
color: #e74c3c;
|
|
367
|
+
font-weight: bold;
|
|
368
|
+
}}
|
|
369
|
+
.warnings {{
|
|
370
|
+
background: #fff3cd;
|
|
371
|
+
border-left: 4px solid #ffc107;
|
|
372
|
+
padding: 15px;
|
|
373
|
+
margin: 20px 0;
|
|
374
|
+
}}
|
|
375
|
+
.warnings h2 {{
|
|
376
|
+
color: #856404;
|
|
377
|
+
margin-top: 0;
|
|
378
|
+
}}
|
|
379
|
+
.warnings ul {{
|
|
380
|
+
margin: 10px 0;
|
|
381
|
+
}}
|
|
382
|
+
.warnings li {{
|
|
383
|
+
color: #856404;
|
|
384
|
+
}}
|
|
385
|
+
.metadata {{
|
|
386
|
+
font-size: 0.9em;
|
|
387
|
+
color: #7f8c8d;
|
|
388
|
+
margin-bottom: 20px;
|
|
389
|
+
}}
|
|
390
|
+
</style>
|
|
391
|
+
</head>
|
|
392
|
+
<body>
|
|
393
|
+
<div class="container">
|
|
394
|
+
<h1>{report.metadata.title}</h1>
|
|
395
|
+
|
|
396
|
+
<div class="metadata">
|
|
397
|
+
<p>Generated: {report.metadata.timestamp.strftime("%Y-%m-%d %H:%M:%S")}</p>
|
|
398
|
+
<p>Analysis Date: {result.timestamp.strftime("%Y-%m-%d %H:%M:%S")}</p>
|
|
399
|
+
<p>Analysis Duration: {result.analysis_duration:.2f} seconds</p>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<div class="summary-box">
|
|
403
|
+
<h2>Summary</h2>
|
|
404
|
+
<p><strong>Detected Logic Family:</strong> {result.detected_family} ({result.family_confidence * 100:.1f}% confidence)</p>
|
|
405
|
+
<p><strong>ICs Identified:</strong> {len(result.identified_ics)}</p>
|
|
406
|
+
<p><strong>Timing Measurements:</strong> {len(result.timing_measurements)}</p>
|
|
407
|
+
<p><strong>Open-Collector Detected:</strong> {"Yes" if result.open_collector_detected else "No"}</p>
|
|
408
|
+
{f"<p><strong>Asymmetry Ratio:</strong> {result.asymmetry_ratio:.2f}</p>" if result.open_collector_detected else ""}
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
{warnings_html}
|
|
412
|
+
|
|
413
|
+
<h2>IC Identification</h2>
|
|
414
|
+
{ic_table}
|
|
415
|
+
|
|
416
|
+
<h2>Timing Measurements</h2>
|
|
417
|
+
{timing_table}
|
|
418
|
+
|
|
419
|
+
<h2>Bill of Materials</h2>
|
|
420
|
+
{bom_table}
|
|
421
|
+
|
|
422
|
+
<h2>Visualizations</h2>
|
|
423
|
+
{plots_section}
|
|
424
|
+
</div>
|
|
425
|
+
</body>
|
|
426
|
+
</html>"""
|
|
427
|
+
|
|
428
|
+
return html
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _generate_markdown_report(report: VintageLogicReport) -> str:
|
|
432
|
+
"""Generate markdown report content."""
|
|
433
|
+
result = report.result
|
|
434
|
+
|
|
435
|
+
# Generate IC table
|
|
436
|
+
ic_rows = []
|
|
437
|
+
for ic in result.identified_ics:
|
|
438
|
+
validation_status = (
|
|
439
|
+
"PASS" if all(v.get("passes", True) for v in ic.validation.values()) else "FAIL"
|
|
440
|
+
)
|
|
441
|
+
ic_rows.append(
|
|
442
|
+
f"| {ic.ic_name} | {ic.confidence * 100:.1f}% | {ic.family} | {validation_status} |"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
ic_table = (
|
|
446
|
+
"| IC | Confidence | Family | Validation |\n|---|---|---|---|\n" + "\n".join(ic_rows)
|
|
447
|
+
if ic_rows
|
|
448
|
+
else "*No ICs identified*"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Generate BOM table
|
|
452
|
+
bom_rows = []
|
|
453
|
+
for entry in result.bom:
|
|
454
|
+
bom_rows.append(
|
|
455
|
+
f"| {entry.part_number} | {entry.description} | {entry.quantity} | {entry.category} | {entry.notes or ''} |"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
bom_table = (
|
|
459
|
+
"| Part Number | Description | Qty | Category | Notes |\n|---|---|---|---|---|\n"
|
|
460
|
+
+ "\n".join(bom_rows)
|
|
461
|
+
if bom_rows
|
|
462
|
+
else "*No BOM entries*"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Generate warnings
|
|
466
|
+
warnings_md = ""
|
|
467
|
+
if result.warnings:
|
|
468
|
+
warnings_list = "\n".join(f"- {w}" for w in result.warnings)
|
|
469
|
+
warnings_md = f"\n## Warnings\n\n{warnings_list}\n"
|
|
470
|
+
|
|
471
|
+
md = f"""# {report.metadata.title}
|
|
472
|
+
|
|
473
|
+
**Generated:** {report.metadata.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
|
|
474
|
+
**Analysis Date:** {result.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
|
|
475
|
+
**Analysis Duration:** {result.analysis_duration:.2f} seconds
|
|
476
|
+
|
|
477
|
+
## Summary
|
|
478
|
+
|
|
479
|
+
- **Detected Logic Family:** {result.detected_family} ({result.family_confidence * 100:.1f}% confidence)
|
|
480
|
+
- **ICs Identified:** {len(result.identified_ics)}
|
|
481
|
+
- **Timing Measurements:** {len(result.timing_measurements)}
|
|
482
|
+
- **Open-Collector Detected:** {"Yes" if result.open_collector_detected else "No"}
|
|
483
|
+
{"- **Asymmetry Ratio:** " + f"{result.asymmetry_ratio:.2f}" if result.open_collector_detected else ""}
|
|
484
|
+
|
|
485
|
+
{warnings_md}
|
|
486
|
+
|
|
487
|
+
## IC Identification
|
|
488
|
+
|
|
489
|
+
{ic_table}
|
|
490
|
+
|
|
491
|
+
## Timing Measurements
|
|
492
|
+
|
|
493
|
+
| Parameter | Value |
|
|
494
|
+
|---|---|
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
for param, value in result.timing_measurements.items():
|
|
498
|
+
md += f"| {param} | {value * 1e9:.2f} ns |\n"
|
|
499
|
+
|
|
500
|
+
if not result.timing_measurements:
|
|
501
|
+
md += "*No timing measurements*\n"
|
|
502
|
+
|
|
503
|
+
md += f"""
|
|
504
|
+
## Bill of Materials
|
|
505
|
+
|
|
506
|
+
{bom_table}
|
|
507
|
+
|
|
508
|
+
## Visualizations
|
|
509
|
+
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
for plot_name, plot_path in report.plots.items():
|
|
513
|
+
md += f"### {plot_name.replace('_', ' ').title()}\n\n"
|
|
514
|
+
md += f"\n\n"
|
|
515
|
+
|
|
516
|
+
return md
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
__all__ = [
|
|
520
|
+
"ReportMetadata",
|
|
521
|
+
"VintageLogicReport",
|
|
522
|
+
"generate_vintage_logic_report",
|
|
523
|
+
]
|
oscura/session/session.py
CHANGED
|
@@ -18,7 +18,6 @@ import gzip
|
|
|
18
18
|
import hashlib
|
|
19
19
|
import hmac
|
|
20
20
|
import pickle
|
|
21
|
-
import warnings
|
|
22
21
|
from dataclasses import dataclass, field
|
|
23
22
|
from datetime import datetime
|
|
24
23
|
from pathlib import Path
|
|
@@ -421,19 +420,21 @@ class Session:
|
|
|
421
420
|
return "\n".join(lines)
|
|
422
421
|
|
|
423
422
|
|
|
424
|
-
def load_session(path: str | Path
|
|
425
|
-
"""Load session from file with
|
|
423
|
+
def load_session(path: str | Path) -> Session:
|
|
424
|
+
"""Load session from file with HMAC signature verification.
|
|
425
|
+
|
|
426
|
+
Session files must be in the current OSC1 format with HMAC signature.
|
|
427
|
+
Legacy session files without signatures are not supported.
|
|
426
428
|
|
|
427
429
|
Args:
|
|
428
430
|
path: Path to session file (.tks).
|
|
429
|
-
verify_signature: Verify HMAC signature (default: True). Set to False
|
|
430
|
-
only when loading legacy session files without signatures.
|
|
431
431
|
|
|
432
432
|
Returns:
|
|
433
433
|
Loaded Session object.
|
|
434
434
|
|
|
435
435
|
Raises:
|
|
436
|
-
SecurityError: If signature verification fails.
|
|
436
|
+
SecurityError: If signature verification fails or file is not in OSC1 format.
|
|
437
|
+
gzip.BadGzipFile: If file is neither valid gzip nor uncompressed session.
|
|
437
438
|
|
|
438
439
|
Example:
|
|
439
440
|
>>> session = load_session('debug_session.tks')
|
|
@@ -444,61 +445,68 @@ def load_session(path: str | Path, *, verify_signature: bool = True) -> Session:
|
|
|
444
445
|
trusted sources. Loading a malicious .tks file could execute arbitrary
|
|
445
446
|
code. Never load session files from untrusted or unknown sources.
|
|
446
447
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
For secure data exchange, consider exporting to JSON or HDF5 formats
|
|
451
|
-
instead of using pickle-based session files.
|
|
448
|
+
All session files must include HMAC signatures for integrity verification.
|
|
449
|
+
For secure data exchange with untrusted parties, consider exporting to
|
|
450
|
+
JSON or HDF5 formats instead of using pickle-based session files.
|
|
452
451
|
"""
|
|
453
452
|
path = Path(path)
|
|
454
453
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
454
|
+
def _load_with_verification(f: Any) -> dict[str, Any]:
|
|
455
|
+
"""Load and verify session file with HMAC signature.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
f: File object (gzip or regular).
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Deserialized session dictionary.
|
|
462
|
+
|
|
463
|
+
Raises:
|
|
464
|
+
SecurityError: If magic bytes or signature verification fails.
|
|
465
|
+
"""
|
|
466
|
+
# Read magic bytes
|
|
458
467
|
magic = f.read(len(_SESSION_MAGIC))
|
|
459
468
|
|
|
460
|
-
if magic
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
expected = hmac.new(_SECURITY_KEY, serialized, hashlib.sha256).digest()
|
|
468
|
-
if not hmac.compare_digest(signature, expected):
|
|
469
|
-
raise SecurityError(
|
|
470
|
-
"Session file signature verification failed",
|
|
471
|
-
file_path=str(path),
|
|
472
|
-
check_type="HMAC signature",
|
|
473
|
-
details="File may be corrupted or tampered with",
|
|
474
|
-
)
|
|
469
|
+
if magic != _SESSION_MAGIC:
|
|
470
|
+
raise SecurityError(
|
|
471
|
+
"This is a legacy session file. Please re-save with current version.",
|
|
472
|
+
file_path=str(path),
|
|
473
|
+
check_type="Session format",
|
|
474
|
+
details="Expected OSC1 format with HMAC signature",
|
|
475
|
+
)
|
|
475
476
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
"
|
|
483
|
-
|
|
484
|
-
|
|
477
|
+
# Read signature and payload
|
|
478
|
+
signature = f.read(_SESSION_SIGNATURE_SIZE)
|
|
479
|
+
serialized = f.read()
|
|
480
|
+
|
|
481
|
+
if not signature or not serialized:
|
|
482
|
+
raise SecurityError(
|
|
483
|
+
"This is a legacy session file. Please re-save with current version.",
|
|
484
|
+
file_path=str(path),
|
|
485
|
+
check_type="Session format",
|
|
486
|
+
details="File is incomplete or corrupted",
|
|
485
487
|
)
|
|
486
488
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
489
|
+
# Verify HMAC signature
|
|
490
|
+
expected = hmac.new(_SECURITY_KEY, serialized, hashlib.sha256).digest()
|
|
491
|
+
if not hmac.compare_digest(signature, expected):
|
|
492
|
+
raise SecurityError(
|
|
493
|
+
"Session file signature verification failed",
|
|
494
|
+
file_path=str(path),
|
|
495
|
+
check_type="HMAC signature",
|
|
496
|
+
details="File may be corrupted or tampered with",
|
|
497
|
+
)
|
|
490
498
|
|
|
499
|
+
# Deserialize verified data
|
|
500
|
+
data = cast("dict[str, Any]", pickle.loads(serialized))
|
|
491
501
|
return data
|
|
492
502
|
|
|
493
|
-
# Try loading (compressed
|
|
503
|
+
# Try loading (compressed first, then uncompressed)
|
|
494
504
|
try:
|
|
495
|
-
# Try gzip compressed first
|
|
496
505
|
with gzip.open(path, "rb") as f:
|
|
497
|
-
data = _load_with_verification(f
|
|
506
|
+
data = _load_with_verification(f)
|
|
498
507
|
except gzip.BadGzipFile:
|
|
499
|
-
# Fall back to uncompressed
|
|
500
508
|
with open(path, "rb") as f: # type: ignore[assignment]
|
|
501
|
-
data = _load_with_verification(f
|
|
509
|
+
data = _load_with_verification(f)
|
|
502
510
|
|
|
503
511
|
session = Session._from_dict(data)
|
|
504
512
|
session._file_path = path
|