# Capitolo 7: Output e Report

[← Interfaccia Grafica](06_Interfaccia_Grafica_GUI.md) | [Torna all'Indice](00_INDEX.md) | [Prossimo: Configurazione →](08_Configurazione_Personalizzazione.md)

---

## 7.1 Overview Output

Il test genera **tre tipi di output** per ogni esecuzione completa:

| Formato | File | Scopo | Strumento |
|---------|------|-------|-----------|
| **PDF** | `GRIFO_M_PBIT_YYYY-MM-DD_HH-MM-SS.pdf` | Report formale leggibile da umani | Visualizzazione e archiviazione |
| **CSV** | `GRIFO_M_PBIT_YYYY-MM-DD_HH-MM-SS.csv` | Dati tabulari per analisi | Excel, Python pandas, R |
| **JSON** | `GRIFO_M_PBIT_YYYY-MM-DD_HH-MM-SS.json` | Dati strutturati machine-readable | Parsing automatico, CI/CD |

**Cartelle Output**:
```
TestEnvironment/
├── pdf_reports/           # PDF generati
├── LOG/                   # CSV generati
└── json/                  # Template JSON (non output)
```

### 7.1.1 Naming Convention

**Pattern**: `GRIFO_M_PBIT_<timestamp>.<ext>`

**Timestamp Format**: `YYYY-MM-DD_HH-MM-SS` (ISO 8601 adapted)

**Esempi**:
```
GRIFO_M_PBIT_2026-02-05_15-24-38.pdf
GRIFO_M_PBIT_2026-02-05_15-24-38.csv
GRIFO_M_PBIT_2026-02-05_15-24-38.json
```

**Vantaggi**:
- ✅ Sorting alfabetico = sorting cronologico
- ✅ No spazi (shell-friendly)
- ✅ Timestamp leggibile da umani e machine
- ✅ Filename univoco (collisioni solo se <1s tra run)

## 7.2 Report PDF

### 7.2.1 Struttura PDF

**Modulo**: `TestEnvironment/env/leo_grifo_test_report.py`

**Funzione Principale**: `generate_pdf_report(output_folder, json_data)`

**Layout PDF** (3 pagine tipiche):

```
┌─────────────────────────────────────────────────────────────┐
│  PAGE 1: HEADER & RUN SUMMARY                               │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐    │
│  │  GRIFO M-PBIT TEST REPORT                           │    │
│  │  Date: 2026-02-05  Time: 15:24:38                   │    │
│  │  ================================================     │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
│  EXECUTIVE SUMMARY                                           │
│  ┌──────────────────────────────────────────┐               │
│  │  Total Runs:        10                   │               │
│  │  Passed:            8  (80.0%)           │               │
│  │  Failed:            2  (20.0%)           │               │
│  │  Total Duration:    8m 42s               │               │
│  │  Average Run Time:  52.2s                │               │
│  └──────────────────────────────────────────┘               │
│                                                              │
│  RUN DETAILS TABLE                                           │
│  ┌────┬──────┬──────┬─────────┬─────────┬──────┬────────┐  │
│  │Run │Status│ PBIT │B6 Status│B8 Checks│Serial│ Target │  │
│  │    │      │ Time │ (P/F/K) │(Pass/F) │(E/F/R│        │  │
│  ├────┼──────┼──────┼─────────┼─────────┼──────┼────────┤  │
│  │1/10│PASS  │ 2.2s │11/0/1   │173/0    │0/0/0 │2000m@3 │  │
│  │2/10│PASS  │ 0.7s │11/0/1   │173/0    │0/0/0 │  -     │  │
│  │3/10│FAIL  │ 0.1s │9/2/1    │  -      │0/3/3 │  -     │  │
│  │4/10│FAIL  │ 0.1s │9/2/1    │173/0    │0/3/0 │  -     │  │
│  │5/10│PASS  │ 2.0s │9/2/1    │173/0    │0/0/0 │  -     │  │
│  │6/10│PASS  │ 0.3s │11/0/1   │173/0    │0/2/2 │  -     │  │
│  │7/10│PASS  │ 0.0s │11/0/1   │173/0    │0/2/2 │2000m@3 │  │
│  │8/10│PASS  │ 2.9s │11/0/1   │173/0    │0/0/0 │  -     │  │
│  │9/10│PASS  │ 2.1s │11/0/1   │173/0    │0/0/0 │  -     │  │
│  │10/1│PASS  │ 0.8s │11/0/1   │173/0    │0/0/0 │  -     │  │
│  └────┴──────┴──────┴─────────┴─────────┴──────┴────────┘  │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  PAGE 2: AGGREGATED STATISTICS                              │
├─────────────────────────────────────────────────────────────┤
│  B6 STATUS STATISTICS (across all runs)                     │
│  ┌────────────────────────────────────────┬──────┬─────┐    │
│  │ Field Name                             │ P/F  │ Rate│    │
│  ├────────────────────────────────────────┼──────┼─────┤    │
│  │ processor_status                       │ 8/2  │ 80% │    │
│  │ transmitter_status                     │ 8/2  │ 80% │    │
│  │ receiver_status                        │10/0  │100% │    │
│  │ antenna_servo_status                   │10/0  │100% │    │
│  │ ... (remaining 8 fields)               │      │     │    │
│  └────────────────────────────────────────┴──────┴─────┘    │
│                                                              │
│  B8 FIELD STATISTICS (top failures only, max 30 rows)       │
│  ┌────────────────────────────────────────┬──────┬─────┐    │
│  │ Field Name                             │ P/F  │ Rate│    │
│  ├────────────────────────────────────────┼──────┼─────┤    │
│  │ radar_mode                             │ 9/1  │ 90% │    │
│  │ scan_sector_azimuth                    │10/0  │100% │    │
│  │ ... (filtered to show failures first)  │      │     │    │
│  └────────────────────────────────────────┴──────┴─────┘    │
│                                                              │
│  SERIAL TERMINAL STATISTICS                                  │
│  ┌────────────────────────────────────────┬────────────┐    │
│  │ Metric                                 │ Value      │    │
│  ├────────────────────────────────────────┼────────────┤    │
│  │ Total Errors (%%E)                     │      0     │    │
│  │ Total Fatal (%%F)                      │      7     │    │
│  │ Total Recycles (RECYCLE)               │      7     │    │
│  │ Average per Run                        │    0.7     │    │
│  └────────────────────────────────────────┴────────────┘    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  PAGE 3: FAILURES DETAIL                                    │
├─────────────────────────────────────────────────────────────┤
│  FAILED RUNS BREAKDOWN                                       │
│                                                              │
│  Run 3/10 - FAILED                                           │
│  ├─ B6 Status: 9/2/1 (processor_status, transmitter_status) │
│  ├─ B8 Checks: Skipped (PBIT incomplete)                    │
│  ├─ Serial: 0/3/3 (Fatal errors detected)                   │
│  ├─ Failure Reason:                                          │
│  │    - B6 Check Failed: processor_status [expected=1,      │
│  │                                         actual=0]         │
│  │    - B6 Check Failed: transmitter_status [expected=1,    │
│  │                                           actual=0]       │
│  │    - Terminal detected 3 FATAL (%%F) messages           │
│  │    - Terminal detected 3 RECYCLE events                  │
│  └─ Scenario: Random Component Failures (known config)      │
│                                                              │
│  Run 4/10 - FAILED                                           │
│  ├─ B6 Status: 9/2/1 (processor_status, transmitter_status) │
│  ├─ B8 Checks: 173/0 (all passed)                           │
│  ├─ Serial: 0/3/0 (Fatal errors detected)                   │
│  ├─ Failure Reason:                                          │
│  │    - B6 Check Failed: processor_status [expected=1,      │
│  │                                         actual=0]         │
│  │    - B6 Check Failed: transmitter_status [expected=1,    │
│  │                                           actual=0]       │
│  │    - Terminal detected 3 FATAL (%%F) messages           │
│  └─ Scenario: Random Component Failures (known config)      │
│                                                              │
│  KNOWN FAILURES                                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ The following failures were EXPECTED:                │    │
│  │ - B6 field 'bit_test_2_status' (known ICD mismatch) │    │
│  │ - All runs show this failure (marked as Known)       │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
```

### 7.2.2 Generazione PDF - Codice

```python
def generate_pdf_report(output_folder: str, json_data: Dict) -> str:
    """
    Generate PDF report from JSON data.
    
    Args:
        output_folder: Directory for PDF output
        json_data: Test results (parsed from JSON template)
    
    Returns:
        Full path to generated PDF file
    """
    from reportlab.lib import colors
    from reportlab.lib.pagesizes import letter, A4
    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
    from reportlab.lib.units import inch
    from reportlab.platypus import (
        SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
    )
    from reportlab.lib.enums import TA_CENTER, TA_LEFT
    
    # Extract metadata
    timestamp = json_data.get('test_metadata', {}).get('timestamp', 'unknown')
    total_runs = json_data.get('test_metadata', {}).get('total_runs', 0)
    passed = json_data.get('test_metadata', {}).get('runs_passed', 0)
    failed = json_data.get('test_metadata', {}).get('runs_failed', 0)
    duration = json_data.get('test_metadata', {}).get('total_duration', 0)
    
    # Create filename
    pdf_filename = f"GRIFO_M_PBIT_{timestamp}.pdf"
    pdf_path = os.path.join(output_folder, pdf_filename)
    
    # Create PDF document
    doc = SimpleDocTemplate(pdf_path, pagesize=A4,
                           rightMargin=36, leftMargin=36,
                           topMargin=36, bottomMargin=36)
    
    # Build content
    story = []
    styles = getSampleStyleSheet()
    
    # === PAGE 1: HEADER ===
    title_style = ParagraphStyle(
        'CustomTitle',
        parent=styles['Heading1'],
        fontSize=18,
        textColor=colors.HexColor('#1a5490'),
        spaceAfter=30,
        alignment=TA_CENTER
    )
    story.append(Paragraph("GRIFO M-PBIT TEST REPORT", title_style))
    story.append(Spacer(1, 0.2*inch))
    
    # Metadata
    script_version = json_data.get('test_metadata', {}).get('script_version', 'unknown')
    meta_data = [
      ['Date:', timestamp.split('_')[0]],
      ['Time:', timestamp.split('_')[1].replace('-', ':') if '_' in timestamp else ''],
      ['Script Version:', script_version],
      ['Total Runs:', str(total_runs)],
      ['Duration:', _format_duration(duration)]
    ]
    meta_table = Table(meta_data, colWidths=[1.5*inch, 3*inch])
    meta_table.setStyle(TableStyle([
        ('FONT', (0, 0), (-1, -1), 'Helvetica', 10),
        ('FONT', (0, 0), (0, -1), 'Helvetica-Bold', 10),
        ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
        ('ALIGN', (1, 0), (1, -1), 'LEFT'),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    story.append(meta_table)
    story.append(Spacer(1, 0.3*inch))
    
    # === EXECUTIVE SUMMARY ===
    story.append(Paragraph("EXECUTIVE SUMMARY", styles['Heading2']))
    story.append(Spacer(1, 0.1*inch))
    
    pass_rate = (passed / total_runs * 100) if total_runs > 0 else 0
    fail_rate = (failed / total_runs * 100) if total_runs > 0 else 0
    avg_time = duration / total_runs if total_runs > 0 else 0
    
    summary_data = [
        ['Total Runs:', str(total_runs)],
        ['Passed:', f"{passed}  ({pass_rate:.1f}%)"],
        ['Failed:', f"{failed}  ({fail_rate:.1f}%)"],
        ['Average Run Time:', f"{avg_time:.1f}s"]
    ]
    summary_table = Table(summary_data, colWidths=[2*inch, 3*inch])
    summary_table.setStyle(TableStyle([
        ('FONT', (0, 0), (-1, -1), 'Helvetica', 11),
        ('FONT', (0, 0), (0, -1), 'Helvetica-Bold', 11),
        ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#f0f0f0')),
        ('BOX', (0, 0), (-1, -1), 1, colors.black),
        ('INNERGRID', (0, 0), (-1, -1), 0.5, colors.grey),
        ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
        ('ALIGN', (1, 0), (1, -1), 'LEFT'),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
        ('LEFTPADDING', (0, 0), (-1, -1), 8),
        ('RIGHTPADDING', (0, 0), (-1, -1), 8),
        ('TOPPADDING', (0, 0), (-1, -1), 6),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
    ]))
    story.append(summary_table)
    story.append(Spacer(1, 0.4*inch))
    
    # === RUN DETAILS TABLE ===
    story.append(Paragraph("RUN DETAILS", styles['Heading2']))
    story.append(Spacer(1, 0.1*inch))
    
    # Build table data
    table_data = [
      ['Run', 'Status', 'PBIT\nTime', 'B6 Status\n(P/F/K)', 
       'B8 Checks\n(Pass/Fail/Known)', 'Serial\n(E/F/R)', 'Target']
    ]
    
    runs = json_data.get('runs', [])
    for r in runs:
        run_id = r.get('run_id', '')
        status = r.get('result', '')
        pbit_time = r.get('pbit_time', 0)
        b6_pass = r.get('b6_pass', 0)
        b6_fail = r.get('b6_fail', 0)
        b6_known = r.get('b6_known_fail', 0)
        b8_pass = r.get('b8_pass', 0)
        b8_fail = r.get('b8_fail', 0)
        serial_e = r.get('serial_errors', 0)
        serial_f = r.get('serial_fatal', 0)
        serial_r = r.get('serial_recycles', 0)
        target = r.get('target_info', '-')
        
      # Per-run B8 now includes known failures count
      table_data.append([
        f"{run_id}/{total_runs}",
        status,
        f"{pbit_time:.1f}s" if pbit_time else '-',
        f"{b6_pass}/{b6_fail}/{b6_known}",
        f"{b8_pass}/{b8_fail}/{r.get('b8_known_fail', 0)}" if (b8_pass or b8_fail or r.get('b8_known_fail', 0)) else '-',
        f"{serial_e}/{serial_f}/{serial_r}",
        target
      ])
    
    runs_table = Table(table_data, colWidths=[
        0.6*inch, 0.7*inch, 0.7*inch, 0.9*inch, 
        0.9*inch, 0.8*inch, 1.5*inch
    ])
    
    # Style: header row, alternating colors, borders
    table_style = [
        ('FONT', (0, 0), (-1, 0), 'Helvetica-Bold', 9),
        ('FONT', (0, 1), (-1, -1), 'Helvetica', 8),
        ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4a86e8')),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
        ('INNERGRID', (0, 0), (-1, -1), 0.5, colors.grey),
        ('BOX', (0, 0), (-1, -1), 1, colors.black),
    ]
    
    # Alternating row colors
    for i in range(1, len(table_data)):
        bg = colors.HexColor('#f9f9f9') if i % 2 == 0 else colors.white
        table_style.append(('BACKGROUND', (0, i), (-1, i), bg))
        
        # Color code status column
        status = table_data[i][1]
        if status.upper() == 'PASS':
            table_style.append(('TEXTCOLOR', (1, i), (1, i), colors.green))
            table_style.append(('FONT', (1, i), (1, i), 'Helvetica-Bold', 9))
        elif status.upper() == 'FAIL':
            table_style.append(('TEXTCOLOR', (1, i), (1, i), colors.red))
            table_style.append(('FONT', (1, i), (1, i), 'Helvetica-Bold', 9))
    
    runs_table.setStyle(TableStyle(table_style))
    story.append(runs_table)
    story.append(PageBreak())
    
    # === PAGE 2: STATISTICS ===
    story.append(Paragraph("AGGREGATED STATISTICS", styles['Heading2']))
    story.append(Spacer(1, 0.2*inch))
    
    # B6 Statistics
    story.append(Paragraph("B6 Status Statistics", styles['Heading3']))
    b6_stats = json_data.get('b6_statistics', {})
    if b6_stats:
        b6_data = [['Field Name', 'Pass/Fail', 'Success Rate']]
        for field, stats in b6_stats.items():
            passed_count = stats.get('passed', 0)
            failed_count = stats.get('failed', 0)
            total = passed_count + failed_count
            rate = (passed_count / total * 100) if total > 0 else 0
            b6_data.append([field, f"{passed_count}/{failed_count}", f"{rate:.0f}%"])
        
        b6_table = Table(b6_data, colWidths=[3.5*inch, 1*inch, 1*inch])
        b6_table.setStyle(TableStyle([
            ('FONT', (0, 0), (-1, 0), 'Helvetica-Bold', 9),
            ('FONT', (0, 1), (-1, -1), 'Helvetica', 8),
            ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#6aa84f')),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('ALIGN', (0, 0), (0, -1), 'LEFT'),
            ('ALIGN', (1, 0), (-1, -1), 'CENTER'),
            ('INNERGRID', (0, 0), (-1, -1), 0.5, colors.grey),
            ('BOX', (0, 0), (-1, -1), 1, colors.black),
        ]))
        story.append(b6_table)
    
    story.append(Spacer(1, 0.3*inch))
    
    # B8 Statistics (top 30 rows only)
    story.append(Paragraph("B8 Field Statistics (Top Failures)", styles['Heading3']))
    b8_stats = json_data.get('b8_statistics', {})
    if b8_stats:
        # Sort by failure count descending
        sorted_b8 = sorted(b8_stats.items(), 
                          key=lambda x: x[1].get('failed', 0), 
                          reverse=True)
        
        b8_data = [['Field Name', 'Pass/Fail', 'Success Rate']]
        for field, stats in sorted_b8[:30]:  # Max 30 rows
            passed_count = stats.get('passed', 0)
            failed_count = stats.get('failed', 0)
            total = passed_count + failed_count
            rate = (passed_count / total * 100) if total > 0 else 0
            b8_data.append([field, f"{passed_count}/{failed_count}", f"{rate:.0f}%"])
        
        b8_table = Table(b8_data, colWidths=[3.5*inch, 1*inch, 1*inch])
        b8_table.setStyle(TableStyle([
            ('FONT', (0, 0), (-1, 0), 'Helvetica-Bold', 9),
            ('FONT', (0, 1), (-1, -1), 'Helvetica', 8),
            ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#6aa84f')),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('ALIGN', (0, 0), (0, -1), 'LEFT'),
            ('ALIGN', (1, 0), (-1, -1), 'CENTER'),
            ('INNERGRID', (0, 0), (-1, -1), 0.5, colors.grey),
            ('BOX', (0, 0), (-1, -1), 1, colors.black),
        ]))
        story.append(b8_table)
    
    story.append(PageBreak())
    
    # === PAGE 3: FAILURES DETAIL ===
    story.append(Paragraph("FAILURES DETAIL", styles['Heading2']))
    story.append(Spacer(1, 0.2*inch))
    
    failed_runs = [r for r in runs if r.get('result', '').upper() == 'FAIL']
    if failed_runs:
        for r in failed_runs:
            run_id = r.get('run_id', '')
            story.append(Paragraph(f"Run {run_id}/{total_runs} - FAILED", 
                                  styles['Heading3']))
            
            # Failure details
            failures = r.get('failures', [])
            if failures:
                for f in failures:
                    story.append(Paragraph(f"• {f}", styles['BodyText']))
            
            story.append(Spacer(1, 0.2*inch))
    else:
        story.append(Paragraph("No failed runs.", styles['BodyText']))
    
    # Build PDF
    doc.build(story)
    
    logging.info(f"PDF report generated: {pdf_path}")
    return pdf_path
```

## 7.3 Export CSV

### 7.3.1 Struttura CSV

**File**: `TestEnvironment/LOG/GRIFO_M_PBIT_YYYY-MM-DD_HH-MM-SS.csv`

**Formato**: UTF-8, comma-separated, header row

**Colonne** (25 colonne totali):

```csv
run_id,result,pbit_time,b6_pass,b6_fail,b6_known_fail,b6_checked,b8_pass,b8_fail,b8_checked,serial_errors,serial_fatal,serial_recycles,target_detected,target_distance,target_cycle,run_duration,start_time,end_time,scenario_name,processor_status,transmitter_status,receiver_status,antenna_status,pbit_complete,bit_test_2_status
1,PASS,2.2,11,0,1,12,173,0,173,0,0,0,True,2000,3,65.3,15:24:38,15:25:43,Scenario 1,1,1,1,1,1,0
2,PASS,0.7,11,0,1,12,173,0,173,0,0,0,False,,,52.1,15:25:44,15:26:36,Scenario 1,1,1,1,1,1,0
3,FAIL,0.1,9,2,1,12,0,0,0,0,3,3,False,,,2.8,15:26:37,15:26:40,Random Failures,0,0,1,1,0,0
...
```

### 7.3.2 Generazione CSV - Codice

```python
def export_to_csv(output_folder: str, json_data: Dict) -> str:
    """
    Export test results to CSV for data analysis.
    
    Args:
        output_folder: Directory for CSV output (usually TestEnvironment/LOG/)
        json_data: Test results dictionary
    
    Returns:
        Full path to generated CSV file
    """
    import csv
    
    timestamp = json_data.get('test_metadata', {}).get('timestamp', 'unknown')
    csv_filename = f"GRIFO_M_PBIT_{timestamp}.csv"
    csv_path = os.path.join(output_folder, csv_filename)
    
    # Define CSV columns
    fieldnames = [
        'run_id', 'result', 'pbit_time',
        'b6_pass', 'b6_fail', 'b6_known_fail', 'b6_checked',
        'b8_pass', 'b8_fail', 'b8_checked',
        'serial_errors', 'serial_fatal', 'serial_recycles',
        'target_detected', 'target_distance', 'target_cycle',
        'run_duration', 'start_time', 'end_time',
        'scenario_name',
        # B6 individual fields (example subset)
        'processor_status', 'transmitter_status', 'receiver_status',
        'antenna_status', 'pbit_complete', 'bit_test_2_status'
    ]
    
    runs = json_data.get('runs', [])
    
    with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames, 
                               extrasaction='ignore')
        writer.writeheader()
        
        for r in runs:
            # Flatten nested data
            row = {
                'run_id': r.get('run_id', ''),
                'result': r.get('result', ''),
                'pbit_time': r.get('pbit_time', ''),
                'b6_pass': r.get('b6_pass', 0),
                'b6_fail': r.get('b6_fail', 0),
                'b6_known_fail': r.get('b6_known_fail', 0),
                'b6_checked': r.get('b6_checked', 0),
                'b8_pass': r.get('b8_pass', 0),
                'b8_fail': r.get('b8_fail', 0),
                'b8_checked': r.get('b8_checked', 0),
                'serial_errors': r.get('serial_errors', 0),
                'serial_fatal': r.get('serial_fatal', 0),
                'serial_recycles': r.get('serial_recycles', 0),
                'target_detected': r.get('target_detected', ''),
                'run_duration': r.get('run_duration', ''),
                'start_time': r.get('start_time', ''),
                'end_time': r.get('end_time', ''),
                'scenario_name': r.get('scenario_name', ''),
            }
            
            # Extract B6 field values
            b6_results = r.get('b6_results', {})
            row['processor_status'] = b6_results.get('processor_status', '')
            row['transmitter_status'] = b6_results.get('transmitter_status', '')
            row['receiver_status'] = b6_results.get('receiver_status', '')
            row['antenna_status'] = b6_results.get('antenna_servo_status', '')
            row['pbit_complete'] = b6_results.get('pbit_complete', '')
            row['bit_test_2_status'] = b6_results.get('bit_test_2_status', '')
            
            # Extract target info
            target = r.get('target_simulated', {})
            if target:
                row['target_distance'] = target.get('distance', '')
                row['target_cycle'] = target.get('appeared_after_cycles', '')
            
            writer.writerow(row)
    
    logging.info(f"CSV export completed: {csv_path}")
    return csv_path
```

### 7.3.3 Uso CSV per Analisi

**Pandas Example**:
```python
import pandas as pd

# Load CSV
df = pd.read_csv('GRIFO_M_PBIT_2026-02-05_15-24-38.csv')

# Calculate pass rate
pass_rate = (df['result'] == 'PASS').sum() / len(df)
print(f"Pass Rate: {pass_rate:.1%}")

# Average PBIT time by scenario
avg_pbit = df.groupby('scenario_name')['pbit_time'].mean()
print(avg_pbit)

# B6 field failure analysis
b6_fields = ['processor_status', 'transmitter_status', 'receiver_status']
for field in b6_fields:
    fail_count = (df[field] == 0).sum()
    print(f"{field}: {fail_count} failures")

# Serial message correlation
print(df[['serial_fatal', 'serial_recycles', 'result']].corr())
```

## 7.4 JSON Output

### 7.4.1 Schema JSON

**File**: `GRIFO_M_PBIT_YYYY-MM-DD_HH-MM-SS.json`

**Struttura Completa**:
```json
{
  "test_metadata": {
    "timestamp": "2026-02-05_15-24-38",
    "test_name": "GRIFO_M_PBIT",
    "total_runs": 10,
    "runs_passed": 8,
    "runs_failed": 2,
    "total_duration": 522.5,
    "hardware_mode": false,
    "simulation_mode": true,
    "python_version": "3.10.11",
    "script_version": "2.0"
  },
  
  "runs": [
    {
      "run_id": 1,
      "result": "PASS",
      "start_time": "15:24:38",
      "end_time": "15:25:43",
      "run_duration": 65.3,
      
      "pbit": {
        "pbit_time": 2.2,
        "pbit_available": 0.0,
        "pbit_complete": true
      },
      
      "b6_results": {
        "b6_pass": 11,
        "b6_fail": 0,
        "b6_known_fail": 1,
        "b6_checked": 12,
        "fields": {
          "processor_status": {"value": 1, "expected": 1, "result": "PASS"},
          "transmitter_status": {"value": 1, "expected": 1, "result": "PASS"},
          "receiver_status": {"value": 1, "expected": 1, "result": "PASS"},
          "antenna_servo_status": {"value": 1, "expected": 1, "result": "PASS"},
          "pbit_complete": {"value": 1, "expected": 1, "result": "PASS"},
          "bit_test_1_status": {"value": 1, "expected": 1, "result": "PASS"},
          "bit_test_2_status": {"value": 0, "expected": 1, "result": "KNOWN_FAIL"},
          "cooling_status": {"value": 1, "expected": 1, "result": "PASS"},
          "power_supply_status": {"value": 1, "expected": 1, "result": "PASS"},
          "memory_status": {"value": 1, "expected": 1, "result": "PASS"},
          "software_version": {"value": 123, "expected": 123, "result": "PASS"},
          "hardware_revision": {"value": 456, "expected": 456, "result": "PASS"}
        }
      },
      
      "b8_results": {
        "b8_pass": 173,
        "b8_fail": 0,
        "b8_checked": 173,
        "failures": []
      },
      
      "serial_results": {
        "serial_errors": 0,
        "serial_fatal": 0,
        "serial_recycles": 0,
        "messages": []
      },
      
      "target_test": {
        "target_detected": true,
        "target_simulated": {
          "distance": 2000,
          "appeared_after_cycles": 3,
          "found_at_iter": 3
        }
      },
      
      "scenario": {
        "name": "Scenario 1: All Systems Nominal",
        "description": "Baseline test with all components functioning correctly"
      },
      
      "failures": []
    },
    
    {
      "run_id": 3,
      "result": "FAIL",
      "start_time": "15:26:37",
      "end_time": "15:26:40",
      "run_duration": 2.8,
      
      "pbit": {
        "pbit_time": 0.1,
        "pbit_available": 0.0,
        "pbit_complete": false
      },
      
      "b6_results": {
        "b6_pass": 9,
        "b6_fail": 2,
        "b6_known_fail": 1,
        "b6_checked": 12,
        "fields": {
          "processor_status": {"value": 0, "expected": 1, "result": "FAIL"},
          "transmitter_status": {"value": 0, "expected": 1, "result": "FAIL"},
          "receiver_status": {"value": 1, "expected": 1, "result": "PASS"}
        }
      },
      
      "b8_results": {
        "b8_pass": 0,
        "b8_fail": 0,
        "b8_checked": 0,
        "failures": [],
        "skipped": true,
        "skip_reason": "PBIT incomplete"
      },
      
      "serial_results": {
        "serial_errors": 0,
        "serial_fatal": 3,
        "serial_recycles": 3,
        "messages": [
          {"type": "FATAL", "text": "%%F Processor failure detected"},
          {"type": "FATAL", "text": "%%F Transmitter offline"},
          {"type": "FATAL", "text": "%%F System RECYCLE initiated"}
        ]
      },
      
      "target_test": {
        "target_detected": false,
        "target_simulated": null,
        "skipped": true,
        "skip_reason": "Run failed before target test"
      },
      
      "scenario": {
        "name": "Random Component Failures",
        "description": "Random selection of component failures (30% probability)"
      },
      
      "failures": [
        "B6 Check Failed: processor_status [expected=1, actual=0]",
        "B6 Check Failed: transmitter_status [expected=1, actual=0]",
        "Terminal detected 3 FATAL (%%F) messages",
        "Terminal detected 3 RECYCLE events"
      ]
    }
  ],
  
  "b6_statistics": {
    "processor_status": {"passed": 8, "failed": 2, "success_rate": 0.8},
    "transmitter_status": {"passed": 8, "failed": 2, "success_rate": 0.8},
    "receiver_status": {"passed": 10, "failed": 0, "success_rate": 1.0},
    "antenna_servo_status": {"passed": 10, "failed": 0, "success_rate": 1.0}
  },
  
  "b8_statistics": {
    "radar_mode": {"passed": 9, "failed": 1, "success_rate": 0.9}
  },
  
  "serial_statistics": {
    "total_errors": 0,
    "total_fatal": 7,
    "total_recycles": 7,
    "average_per_run": 0.7
  },
  
  "target_statistics": {
    "runs_with_target_test": 2,
    "targets_detected": 2,
    "detection_rate": 1.0,
    "average_cycle_to_detection": 3.0
  }
}
```

### 7.4.2 Parsing JSON

**Python Example**:
```python
import json

# Load JSON
with open('GRIFO_M_PBIT_2026-02-05_15-24-38.json', 'r') as f:
    data = json.load(f)

# Access metadata
print(f"Total Runs: {data['test_metadata']['total_runs']}")
print(f"Pass Rate: {data['test_metadata']['runs_passed']/data['test_metadata']['total_runs']:.1%}")

# Iterate runs
for run in data['runs']:
    print(f"Run {run['run_id']}: {run['result']}")
    
    # B6 failures
    for field, info in run['b6_results']['fields'].items():
        if info['result'] == 'FAIL':
            print(f"  B6 FAIL: {field} = {info['value']} (expected {info['expected']})")
    
    # Serial issues
    serial = run['serial_results']
    if serial['serial_fatal'] > 0:
        print(f"  {serial['serial_fatal']} FATAL messages")
```

## 7.5 Template JSON (Input)

### 7.5.1 Scopo Template

**Cartella**: `TestEnvironment/json/`

**File**: `grifo_test_template.json`

**Uso**: Define expected structure and default values

```json
{
  "test_metadata": {
    "timestamp": "",
    "test_name": "GRIFO_M_PBIT",
    "total_runs": 0,
    "runs_passed": 0,
    "runs_failed": 0,
    "total_duration": 0.0,
    "hardware_mode": false,
    "simulation_mode": false
  },
  "runs": [],
  "b6_statistics": {},
  "b8_statistics": {},
  "serial_statistics": {},
  "target_statistics": {}
}
```

**Loading Template**:
```python
def load_test_template() -> Dict:
    """Load JSON template and initialize with defaults"""
    template_path = os.path.join(
        os.path.dirname(__file__),
        '../json/grifo_test_template.json'
    )
    
    with open(template_path, 'r') as f:
        template = json.load(f)
    
    # Initialize timestamp
    template['test_metadata']['timestamp'] = datetime.now().strftime(
        '%Y-%m-%d_%H-%M-%S'
    )
    
    return template
```

## 7.6 Best Practices Output

### 7.6.1 Checklist Generazione Report

✅ **Always**:
- Genera tutti e 3 i formati (PDF, CSV, JSON)
- Usa timestamp identico per tutti i file
- Salva in cartelle corrette (`pdf_reports/`, `LOG/`)
- Log success/failure con path completo

✅ **Error Handling**:
```python
try:
    pdf_path = generate_pdf_report(output_folder, json_data)
    csv_path = export_to_csv(log_folder, json_data)
    json_path = save_json_output(output_folder, json_data)
    
    print(f"\n✅ Reports generated:")
    print(f"   PDF:  {pdf_path}")
    print(f"   CSV:  {csv_path}")
    print(f"   JSON: {json_path}")

except Exception as e:
    logging.error(f"Failed to generate reports: {e}")
    # Continue execution (reports are not critical for test validity)
```

### 7.6.2 Validazione Output

**Check PDF**:
```python
import os

def validate_pdf_output(pdf_path: str) -> bool:
    """Verify PDF was created and is not empty"""
    if not os.path.exists(pdf_path):
        logging.error(f"PDF not found: {pdf_path}")
        return False
    
    size = os.path.getsize(pdf_path)
    if size < 1024:  # Less than 1KB
        logging.warning(f"PDF suspiciously small: {size} bytes")
        return False
    
    logging.info(f"PDF validated: {size} bytes")
    return True
```

**Check CSV Row Count**:
```python
def validate_csv_output(csv_path: str, expected_runs: int) -> bool:
    """Verify CSV has expected number of rows"""
    import csv
    
    with open(csv_path, 'r') as f:
        reader = csv.reader(f)
        rows = sum(1 for _ in reader) - 1  # Exclude header
    
    if rows != expected_runs:
        logging.warning(f"CSV row count mismatch: {rows} != {expected_runs}")
        return False
    
    logging.info(f"CSV validated: {rows} runs")
    return True
```

---

[← Interfaccia Grafica](06_Interfaccia_Grafica_GUI.md) | [Torna all'Indice](00_INDEX.md) | [Prossimo: Configurazione →](08_Configurazione_Personalizzazione.md)
