# Capitolo 9: Integrazione Hardware

[← Configurazione](08_Configurazione_Personalizzazione.md) | [Torna all'Indice](00_INDEX.md) | [Prossimo: Best Practices →](10_Best_Practices.md)

---

## 9.1 Overview Integrazione Hardware

Il test environment interagisce con **tre tipi di hardware** per controllo e monitoraggio GRIFO:

| Hardware | Protocollo | Modulo | Scopo |
|----------|-----------|--------|-------|
| **1553 Bus** | MIL-STD-1553 | `leo_grifo_1553.py` | Command & data acquisition |
| **Serial Terminal** | RS-232 (UART) | `leo_grifo_terminal.py` | Status messages & diagnostics |
| **Power Control** | GPIO/Relay | `leo_grifo_io_box.py` | Power ON/OFF cycling |

**Architettura Hardware**:
```
┌─────────────────────────────────────────────────────────────┐
│                   Test PC (Windows)                          │
│  ┌────────────────────────────────────────────────────┐     │
│  │  GRIFO_M_PBIT.py (Test Script)                    │     │
│  └───┬────────────────────┬────────────────────┬──────┘     │
│      │                    │                    │            │
│      │                    │                    │            │
│   ┌──▼──────────┐   ┌────▼─────────┐   ┌─────▼───────┐    │
│   │ 1553 Driver │   │Serial Driver │   │ IO Box API  │    │
│   │(SWIG/C++)  │   │ (pyserial)   │   │ (Ethernet)  │    │
│   └──┬──────────┘   └────┬─────────┘   └─────┬───────┘    │
└──────┼─────────────────┬─┼────────────────────┼────────────┘
       │                 │ │                    │
       │ MIL-STD-1553    │ │ RS-232             │ Ethernet
       │ (Bus interface) │ │ (Serial cable)     │ (TCP/IP)
       │                 │ │                    │
┌──────▼─────────────────┼─▼────────────────────▼────────────┐
│              GRIFO Radar System                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  1553 RT (Remote Terminal)                          │   │
│  │  - Address: 0x1A                                    │   │
│  │  - Subaddresses: B6 (status), B8 (telemetry)       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Serial Debug Port                                  │   │
│  │  - Baud: 115200 / Parity: None / Stop: 1           │   │
│  │  - Protocol: ASCII messages (%%E, %%F, RECYCLE)    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Power Input                                        │   │
│  │  - Controlled by external relay (BrainBox)         │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│              BrainBox (IO Controller)                        │
│  - Ethernet interface (192.168.1.100)                       │
│  - Relay outputs for power control                          │
│  - Digital I/O for status indicators                        │
└─────────────────────────────────────────────────────────────┘
```

## 9.2 MIL-STD-1553 Bus Interface

### 9.2.1 Cos'è MIL-STD-1553?

**MIL-STD-1553** è un bus di comunicazione **dual-redundant** usato in sistemi militari/aerospaziali per:
- **Command/Response**: Bus Controller (BC) invia comandi, Remote Terminal (RT) risponde
- **Deterministic**: Timing predicibile, priorità definite
- **Robust**: Dual bus (A/B) per ridondanza

**Topologia**:
```
    Bus Controller (BC)          ← Test PC con 1553 card
           |
    ┌──────┴──────┐
    │             │
  Bus A         Bus B    ← Redundant buses
    │             │
    └──────┬──────┘
           |
    Remote Terminal (RT)         ← GRIFO radar (address 0x1A)
```

### 9.2.2 SWIG Bindings (C++ → Python)

L'interfaccia 1553 è implementata in **C++** e wrappata per Python via **SWIG** (Simplified Wrapper and Interface Generator).

**Files Chiave**:
```
PlatformSimulator/bin/
├── interpreter.py           # SWIG-generated Python module
├── _interpreter.pyd         # C++ compiled DLL (Windows)
└── [source C++ files]       # Native implementation
```

**Python Interface**:
```python
# File: PlatformSimulator/bin/interpreter.py (SWIG-generated)

class Bus1553Interface:
    """
    Native 1553 bus interface (SWIG wrapper).
    DO NOT modify - auto-generated.
    """
    
    def __init__(self, bus_config: str):
        """Initialize 1553 bus controller"""
        pass
    
    def write_command(self, rt_address: int, subaddress: int, 
                     data: List[int]) -> bool:
        """
        Send command to RT.
        
        Args:
            rt_address: RT address (0-31)
            subaddress: Subaddress (0-31)
            data: List of 16-bit words
        
        Returns:
            True if ACK received
        """
        pass
    
    def read_response(self, rt_address: int, subaddress: int, 
                     word_count: int) -> List[int]:
        """
        Read data from RT.
        
        Args:
            rt_address: RT address (0-31)
            subaddress: Subaddress (0-31)
            word_count: Number of 16-bit words to read
        
        Returns:
            List of received words
        """
        pass
    
    def check_status(self, rt_address: int) -> int:
        """
        Check RT status word.
        
        Returns:
            Status word (16-bit)
            Bit meanings per MIL-STD-1553:
              Bit 0: Terminal Flag
              Bit 1: Dynamic Bus Control
              Bit 2: Subsystem Flag
              Bit 3: Busy
              Bit 4: Service Request
              Bit 5-7: Reserved
              Bit 8: Broadcast Command Received
              Bit 9: Message Error
              Bit 10: Instrumentation
              Bit 11-15: Reserved
        """
        pass
```

### 9.2.3 GrifoInstrumentInterface Wrapper

**File**: `TestEnvironment/env/leo_grifo_1553.py`

**Scopo**: Abstraction layer che nasconde complessità 1553 e fornisce API high-level.

```python
class GrifoInstrumentInterface(TestCommonInterface):
    """
    High-level wrapper for GRIFO 1553 interface.
    Implements check/get/set pattern.
    """
    
    def __init__(self, bus_config: str = "default"):
        """
        Initialize 1553 interface.
        
        Args:
            bus_config: Bus configuration string (hardware-specific)
        """
        from PlatformSimulator.bin import interpreter
        
        self.bus = interpreter.Bus1553Interface(bus_config)
        self.rt_address = 0x1A  # GRIFO RT address
        
        # Field definitions (address mapping)
        self.b6_fields = {
            'processor_status': 0x00,
            'transmitter_status': 0x01,
            'receiver_status': 0x02,
            'antenna_servo_status': 0x03,
            'pbit_complete': 0x04,
            'bit_test_1_status': 0x05,
            'bit_test_2_status': 0x06,
            'cooling_status': 0x07,
            'power_supply_status': 0x08,
            'memory_status': 0x09,
            'software_version': 0x0A,
            'hardware_revision': 0x0B
        }
        
        self.b8_subaddress = 0x08  # B8 telemetry subaddress
        self.b8_word_count = 185   # B8 has 185 fields
    
    def check(self, field_name: str, expected_value: int, 
             timeout: float = 5.0) -> bool:
        """
        Check if field matches expected value.
        
        Args:
            field_name: B6 field name
            expected_value: Expected value
            timeout: Max time to wait (seconds)
        
        Returns:
            True if match, False otherwise
        """
        start = time.time()
        
        while time.time() - start < timeout:
            actual = self.get(field_name)
            if actual == expected_value:
                return True
            time.sleep(0.1)
        
        return False
    
    def get(self, field_name: str) -> int:
        """
        Read B6 field value.
        
        Args:
            field_name: B6 field name (e.g., 'processor_status')
        
        Returns:
            Field value (integer)
        
        Raises:
            ValueError: If field_name unknown
            RuntimeError: If 1553 communication fails
        """
        if field_name not in self.b6_fields:
            raise ValueError(f"Unknown B6 field: {field_name}")
        
        subaddress = 0x06  # B6 subaddress
        word_offset = self.b6_fields[field_name]
        
        try:
            # Read entire B6 block (more efficient than per-field)
            data = self.bus.read_response(self.rt_address, subaddress, 12)
            
            # Extract specific field
            value = data[word_offset]
            return value
        
        except Exception as e:
            raise RuntimeError(f"1553 read failed for {field_name}: {e}")
    
    def set(self, field_name: str, value: int) -> bool:
        """
        Write command to GRIFO (limited fields writable).
        
        Args:
            field_name: Command name (e.g., 'SILENCE', 'STANDBY')
            value: Command parameter
        
        Returns:
            True if ACK received
        """
        command_map = {
            'SILENCE': (0x10, 0x01),      # (subaddr, command_code)
            'STANDBY': (0x10, 0x02),
            'REMOVE_STANDBY': (0x10, 0x03),
            'SCAN_SECTOR_AZ': (0x11, None),  # value = azimuth
            'SCAN_SECTOR_EL': (0x12, None),  # value = elevation
        }
        
        if field_name not in command_map:
            raise ValueError(f"Unknown command: {field_name}")
        
        subaddr, cmd_code = command_map[field_name]
        
        # Build command message
        if cmd_code is not None:
            data = [cmd_code]
        else:
            data = [value]
        
        try:
            ack = self.bus.write_command(self.rt_address, subaddr, data)
            return ack
        
        except Exception as e:
            logging.error(f"1553 write failed for {field_name}: {e}")
            return False
    
    def read_b8_telemetry(self) -> Dict[str, int]:
        """
        Read all B8 telemetry fields.
        
        Returns:
            Dict mapping field names to values
        """
        try:
            data = self.bus.read_response(self.rt_address, 
                                         self.b8_subaddress, 
                                         self.b8_word_count)
            
            # Map to field names (simplified - real implementation uses ICD)
            b8_data = {}
            for i, value in enumerate(data):
                b8_data[f'b8_field_{i:03d}'] = value
            
            return b8_data
        
        except Exception as e:
            raise RuntimeError(f"B8 read failed: {e}")
```

### 9.2.4 Troubleshooting 1553

**Problema**: `RuntimeError: 1553 read failed`

**Possibili Cause**:
1. **Hardware non connesso**: Verifica cavo 1553 collegato correttamente
2. **RT non powered**: GRIFO deve essere acceso
3. **Wrong RT address**: Verifica `rt_address = 0x1A`
4. **Bus conflict**: Solo un BC deve essere attivo sul bus
5. **Driver issue**: Reinstalla driver 1553 card

**Debug**:
```python
# Test 1553 connection
def test_1553_connection():
    """Verify 1553 bus is operational"""
    try:
        from PlatformSimulator.bin import interpreter
        bus = interpreter.Bus1553Interface("default")
        
        # Try to read RT status
        status = bus.check_status(0x1A)
        print(f"RT Status: 0x{status:04X}")
        
        if status & 0x0008:  # Bit 3 = Busy
            print("RT is BUSY")
        if status & 0x0200:  # Bit 9 = Message Error
            print("RT reports MESSAGE ERROR")
        
        return True
    
    except Exception as e:
        print(f"1553 connection FAILED: {e}")
        return False
```

## 9.3 Serial Terminal Interface

### 9.3.1 Protocol Serial GRIFO

**Configurazione**:
- **Baud Rate**: 115200
- **Data Bits**: 8
- **Parity**: None
- **Stop Bits**: 1
- **Flow Control**: None (hardware/software disabled)

**Formato Messaggi**:
```
Standard message: [TIMESTAMP] MESSAGE_TEXT\r\n
Error message:    [TIMESTAMP] %%E ERROR_DESCRIPTION\r\n
Fatal message:    [TIMESTAMP] %%F FATAL_ERROR\r\n
Recycle event:    [TIMESTAMP] RECYCLE: REASON\r\n
```

**Esempi**:
```
[00:02:15.324] System initialization complete
[00:02:18.891] %%E Receiver calibration drift detected
[00:02:19.003] %%F Processor watchdog timeout
[00:02:19.105] RECYCLE: Automatic recovery initiated
[00:02:22.456] Power-on self-test started
[00:02:25.789] BIT completed successfully
```

### 9.3.2 Implementazione Serial Interface

**File**: `TestEnvironment/env/leo_grifo_terminal.py`

```python
import serial
import threading
import queue
import re
import logging

class GrifoSerialTerminal(TestCommonInterface):
    """
    Serial terminal interface for GRIFO diagnostics.
    Monitors ASCII messages and tracks errors/recycles.
    """
    
    def __init__(self, port: str = 'COM3', baudrate: int = 115200):
        """
        Initialize serial connection.
        
        Args:
            port: Serial port (e.g., 'COM3', '/dev/ttyUSB0')
            baudrate: Baud rate (default 115200)
        """
        self.port = port
        self.baudrate = baudrate
        self.serial = None
        self.running = False
        self.thread = None
        
        # Message buffer (thread-safe)
        self.message_queue = queue.Queue()
        
        # Statistics
        self.stats = {
            'total_messages': 0,
            'error_count': 0,      # %%E
            'fatal_count': 0,      # %%F
            'recycle_count': 0,    # RECYCLE
            'last_error': None,
            'last_fatal': None,
            'last_recycle': None
        }
        
        # Message patterns
        self.pattern_error = re.compile(r'%%E\s+(.+)')
        self.pattern_fatal = re.compile(r'%%F\s+(.+)')
        self.pattern_recycle = re.compile(r'RECYCLE:\s*(.+)')
    
    def connect(self) -> bool:
        """
        Open serial connection and start monitoring thread.
        
        Returns:
            True if connected successfully
        """
        try:
            self.serial = serial.Serial(
                port=self.port,
                baudrate=self.baudrate,
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                timeout=0.1,
                xonxoff=False,
                rtscts=False,
                dsrdtr=False
            )
            
            # Clear buffers
            self.serial.reset_input_buffer()
            self.serial.reset_output_buffer()
            
            # Start monitoring thread
            self.running = True
            self.thread = threading.Thread(target=self._monitor_loop, 
                                          daemon=True)
            self.thread.start()
            
            logging.info(f"Serial connected: {self.port} @ {self.baudrate}")
            return True
        
        except serial.SerialException as e:
            logging.error(f"Serial connection failed: {e}")
            return False
    
    def disconnect(self):
        """Stop monitoring and close serial port"""
        self.running = False
        if self.thread:
            self.thread.join(timeout=2.0)
        if self.serial and self.serial.is_open:
            self.serial.close()
        logging.info("Serial disconnected")
    
    def _monitor_loop(self):
        """Background thread: read and parse messages"""
        buffer = ""
        
        while self.running:
            try:
                if self.serial.in_waiting > 0:
                    chunk = self.serial.read(self.serial.in_waiting)
                    buffer += chunk.decode('ascii', errors='ignore')
                    
                    # Process complete lines
                    while '\n' in buffer:
                        line, buffer = buffer.split('\n', 1)
                        line = line.strip()
                        
                        if line:
                            self._process_message(line)
                
                else:
                    time.sleep(0.01)  # 10ms
            
            except Exception as e:
                logging.error(f"Serial monitor error: {e}")
                time.sleep(0.1)
    
    def _process_message(self, message: str):
        """Parse and categorize message"""
        self.stats['total_messages'] += 1
        
        # Check for error patterns
        if '%%E' in message:
            match = self.pattern_error.search(message)
            error_text = match.group(1) if match else message
            self.stats['error_count'] += 1
            self.stats['last_error'] = error_text
            logging.warning(f"Serial ERROR: {error_text}")
        
        elif '%%F' in message:
            match = self.pattern_fatal.search(message)
            fatal_text = match.group(1) if match else message
            self.stats['fatal_count'] += 1
            self.stats['last_fatal'] = fatal_text
            logging.error(f"Serial FATAL: {fatal_text}")
        
        elif 'RECYCLE' in message:
            match = self.pattern_recycle.search(message)
            recycle_reason = match.group(1) if match else message
            self.stats['recycle_count'] += 1
            self.stats['last_recycle'] = recycle_reason
            logging.warning(f"Serial RECYCLE: {recycle_reason}")
        
        else:
            # Normal message
            logging.debug(f"Serial: {message}")
        
        # Store in queue for external access
        self.message_queue.put({
            'timestamp': time.time(),
            'message': message,
            'type': self._classify_message(message)
        })
    
    def _classify_message(self, message: str) -> str:
        """Determine message type"""
        if '%%E' in message:
            return 'ERROR'
        elif '%%F' in message:
            return 'FATAL'
        elif 'RECYCLE' in message:
            return 'RECYCLE'
        else:
            return 'INFO'
    
    def get_statistics(self) -> Dict:
        """
        Get current statistics.
        
        Returns:
            Dict with counts and last messages
        """
        return self.stats.copy()
    
    def reset_statistics(self):
        """Clear statistics counters"""
        self.stats['error_count'] = 0
        self.stats['fatal_count'] = 0
        self.stats['recycle_count'] = 0
        self.stats['last_error'] = None
        self.stats['last_fatal'] = None
        self.stats['last_recycle'] = None
    
    def check(self, condition: str, expected: Any, 
             timeout: float = 5.0) -> bool:
        """
        Check for specific message or condition.
        
        Args:
            condition: 'no_fatal', 'no_error', 'no_recycle', or message pattern
            expected: Expected value or None
            timeout: Not used (returns current state)
        
        Returns:
            True if condition met
        """
        if condition == 'no_fatal':
            return self.stats['fatal_count'] == 0
        elif condition == 'no_error':
            return self.stats['error_count'] == 0
        elif condition == 'no_recycle':
            return self.stats['recycle_count'] == 0
        else:
            # Check for specific message in recent buffer
            # (simplified implementation)
            return True
```

### 9.3.3 Uso Serial Terminal nel Test

```python
# Initialize
theTerminal = GrifoSerialTerminal(port='COM3')
theTerminal.connect()

# Reset stats before run
theTerminal.reset_statistics()

# ... execute test ...

# Check for issues
stats = theTerminal.get_statistics()
if stats['fatal_count'] > 0:
    logging.error(f"Run FAILED: {stats['fatal_count']} FATAL messages")
    run_result = 'FAIL'

if stats['recycle_count'] > 0:
    logging.warning(f"{stats['recycle_count']} RECYCLE events detected")

# Cleanup
theTerminal.disconnect()
```

### 9.3.4 Troubleshooting Serial

**Problema**: `Serial connection failed`

**Possibili Cause**:
1. **Wrong port**: Verifica COM port in Device Manager (Windows)
2. **Port in use**: Chiudi altri programmi (PuTTY, TeraTerm)
3. **Permission denied**: (Linux) aggiungi user a `dialout` group
4. **Hardware disconnected**: Verifica cavo USB/RS-232

**Debug**:
```python
import serial.tools.list_ports

def list_serial_ports():
    """List available serial ports"""
    ports = serial.tools.list_ports.comports()
    print("Available serial ports:")
    for port in ports:
        print(f"  {port.device}: {port.description}")
    return [p.device for p in ports]

# Usage
available = list_serial_ports()
if 'COM3' in available:
    print("COM3 is available")
```

## 9.4 Power Control (BrainBox)

### 9.4.1 BrainBox Interface

**Hardware**: BrainBox IO controller (Ethernet-based relay controller)

**File**: `TestEnvironment/env/leo_grifo_io_box.py`

```python
import socket
import logging

class BrainBoxInterface:
    """
    Interface to BrainBox IO controller for power control.
    Controls relay outputs via Ethernet (TCP/IP).
    """
    
    def __init__(self, ip: str = '192.168.1.100', port: int = 5000):
        """
        Initialize BrainBox connection.
        
        Args:
            ip: BrainBox IP address
            port: TCP port (default 5000)
        """
        self.ip = ip
        self.port = port
        self.socket = None
        
        # Relay mapping
        self.relays = {
            'GRIFO_POWER': 1,      # Relay 1 = GRIFO main power
            'COOLING_FAN': 2,      # Relay 2 = Cooling system
            'INDICATOR_LED': 3     # Relay 3 = Status LED
        }
    
    def connect(self) -> bool:
        """Establish TCP connection to BrainBox"""
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(5.0)
            self.socket.connect((self.ip, self.port))
            logging.info(f"BrainBox connected: {self.ip}:{self.port}")
            return True
        
        except socket.error as e:
            logging.error(f"BrainBox connection failed: {e}")
            return False
    
    def disconnect(self):
        """Close connection"""
        if self.socket:
            self.socket.close()
            self.socket = None
    
    def set_relay(self, relay_id: int, state: bool) -> bool:
        """
        Set relay state.
        
        Args:
            relay_id: Relay number (1-8)
            state: True=ON, False=OFF
        
        Returns:
            True if successful
        """
        try:
            # BrainBox protocol: "REL <id> <state>\r\n"
            cmd = f"REL {relay_id} {'ON' if state else 'OFF'}\r\n"
            self.socket.sendall(cmd.encode('ascii'))
            
            # Wait for ACK
            response = self.socket.recv(64).decode('ascii').strip()
            if response == 'OK':
                logging.debug(f"Relay {relay_id} set to {state}")
                return True
            else:
                logging.error(f"Relay command failed: {response}")
                return False
        
        except Exception as e:
            logging.error(f"Relay control error: {e}")
            return False
    
    def power_grifo_on(self) -> bool:
        """Turn GRIFO power ON"""
        relay = self.relays['GRIFO_POWER']
        success = self.set_relay(relay, True)
        if success:
            logging.info("GRIFO power ON")
            time.sleep(1.0)  # Power stabilization delay
        return success
    
    def power_grifo_off(self) -> bool:
        """Turn GRIFO power OFF"""
        relay = self.relays['GRIFO_POWER']
        success = self.set_relay(relay, False)
        if success:
            logging.info("GRIFO power OFF")
            time.sleep(0.5)
        return success


# Global singleton
theBrainBox = BrainBoxInterface()
```

### 9.4.2 Power Cycling Logic

```python
def power_cycle_grifo():
    """
    Complete power cycle: OFF → wait → ON → wait for boot
    """
    logging.info("Starting power cycle...")
    
    # Turn OFF
    if not theBrainBox.power_grifo_off():
        raise RuntimeError("Failed to power OFF GRIFO")
    
    # Wait for capacitors to discharge
    time.sleep(2.0)
    
    # Turn ON
    if not theBrainBox.power_grifo_on():
        raise RuntimeError("Failed to power ON GRIFO")
    
    # Wait for boot (GRIFO takes ~5s to boot)
    logging.info("Waiting for GRIFO boot...")
    time.sleep(5.0)
    
    # Verify 1553 is responsive
    max_retries = 10
    for attempt in range(max_retries):
        try:
            status = theGrifo1553.bus.check_status(0x1A)
            if status != 0xFFFF:  # 0xFFFF = no response
                logging.info("GRIFO boot complete, 1553 responsive")
                return True
        except Exception:
            pass
        
        time.sleep(1.0)
    
    raise RuntimeError("GRIFO failed to boot after power cycle")
```

## 9.5 Hardware Setup Checklist

### 9.5.1 Physical Connections

✅ **Before Test**:
- [ ] **1553 Bus**: Cable collegato a PC 1553 card e GRIFO RT
- [ ] **Serial**: USB/RS-232 adapter collegato a GRIFO debug port
- [ ] **Power**: BrainBox relay connesso a GRIFO power input
- [ ] **Network**: BrainBox su stessa subnet del test PC (192.168.1.x)
- [ ] **Safety**: Emergency stop button accessibile

### 9.5.2 Software Prerequisites

✅ **Drivers**:
- [ ] 1553 card driver installato (verifica in Device Manager)
- [ ] Serial driver (CH340, FTDI, o built-in)
- [ ] Python `pyserial` package: `pip install pyserial`

✅ **Configuration**:
- [ ] Verifica COM port in `leo_grifo_terminal.py`
- [ ] Verifica BrainBox IP in `leo_grifo_io_box.py`
- [ ] Verifica RT address (0x1A) in `leo_grifo_1553.py`

### 9.5.3 Pre-Test Verification

```python
def verify_hardware_connectivity():
    """Run before starting test to ensure all hardware is ready"""
    
    checks_passed = 0
    checks_total = 3
    
    # Check 1: 1553 Bus
    print("1. Testing 1553 connection...")
    try:
        from TestEnvironment.env.leo_grifo_1553 import theGrifo1553
        status = theGrifo1553.bus.check_status(0x1A)
        print(f"   ✓ 1553 OK (RT status: 0x{status:04X})")
        checks_passed += 1
    except Exception as e:
        print(f"   ✗ 1553 FAILED: {e}")
    
    # Check 2: Serial Terminal
    print("2. Testing serial connection...")
    try:
        from TestEnvironment.env.leo_grifo_terminal import theTerminal
        if theTerminal.connect():
            print(f"   ✓ Serial OK ({theTerminal.port})")
            theTerminal.disconnect()
            checks_passed += 1
        else:
            print("   ✗ Serial FAILED")
    except Exception as e:
        print(f"   ✗ Serial FAILED: {e}")
    
    # Check 3: BrainBox
    print("3. Testing BrainBox connection...")
    try:
        from TestEnvironment.env.leo_grifo_io_box import theBrainBox
        if theBrainBox.connect():
            print(f"   ✓ BrainBox OK ({theBrainBox.ip})")
            theBrainBox.disconnect()
            checks_passed += 1
        else:
            print("   ✗ BrainBox FAILED")
    except Exception as e:
        print(f"   ✗ BrainBox FAILED: {e}")
    
    # Summary
    print(f"\nHardware Check: {checks_passed}/{checks_total} passed")
    
    if checks_passed == checks_total:
        print("✓ All hardware ready for test")
        return True
    else:
        print("✗ Hardware issues detected - resolve before testing")
        return False
```

## 9.6 Safety Considerations

### 9.6.1 Hardware Safety

⚠️ **WARNINGS**:
- **High Voltage**: GRIFO operates at potentially dangerous voltages
- **Power Cycling**: Allow 2+ seconds between OFF and ON
- **Emergency Stop**: Ensure accessible kill switch
- **Cooling**: Verify cooling fan operational before extended tests

### 9.6.2 Software Safety Limits

```python
# File: GRIFO_M_PBIT.py

# Safety limits
MAX_CONSECUTIVE_FAILURES = 5  # Abort if 5 failures in a row
MAX_POWER_CYCLES_PER_HOUR = 20  # Prevent excessive cycling
MIN_TIME_BETWEEN_CYCLES = 10.0  # Seconds

def check_safety_limits(consecutive_failures: int,
                       power_cycles_last_hour: int) -> bool:
    """
    Verify test is within safety limits.
    
    Returns:
        True if safe to continue, False to abort
    """
    if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
        logging.error(f"ABORT: {consecutive_failures} consecutive failures")
        return False
    
    if power_cycles_last_hour >= MAX_POWER_CYCLES_PER_HOUR:
        logging.error(f"ABORT: Too many power cycles ({power_cycles_last_hour}/hour)")
        return False
    
    return True
```

---

[← Configurazione](08_Configurazione_Personalizzazione.md) | [Torna all'Indice](00_INDEX.md) | [Prossimo: Best Practices →](10_Best_Practices.md)
