haltech-mock 0.1.1
A Python library and CLI tool for simulating Haltech ECU CAN bus data
Requires Python: >=3.11
Keywords: automotive, can, ecu, haltech, simulator

# haltech-mock

[![PyPI version](https://badge.fury.io/py/haltech-mock.svg)](https://badge.fury.io/py/haltech-mock)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![CI](https://github.com/YeeP79/haltech-mock/actions/workflows/ci.yml/badge.svg)](https://github.com/YeeP79/haltech-mock/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/YeeP79/haltech-mock/graph/badge.svg)](https://codecov.io/gh/YeeP79/haltech-mock)

A Python library and CLI tool for simulating Haltech ECU CAN bus data. Perfect for developing and testing dashboards, data loggers, and other automotive tools that interface with Haltech engine management systems—without requiring a running engine.

## Features

- **Full V2.35 Protocol Support** — 38 frames, 167 channels including RPM, temperatures, pressures, lambda, and status flags
- **PD16 Power Distribution Module** — Simulate the 16-output PDM with fault injection, current simulation, and 5 built-in scenarios
- **Physics-Based Simulation** — Realistic engine behavior with RPM response, temperature modeling, and vehicle dynamics
- **9 Built-in ECU Scenarios** — Idle, acceleration, warmup, track laps, and warning conditions (overheating, low oil, redline, battery)
- **Custom JSON Scenarios** — Define your own scenarios with transitions, variations, and multi-stage sequences
- **Generic Sensor Support** — 10 user-configurable generic sensor channels for custom data
- **Multiple Output Modes** — Virtual CAN (vcan), SocketCAN interfaces, or stdout in candump format
- **Flexible Python API** — Use as a library in your own projects
- **Rich CLI** — Beautiful terminal output with channel/frame inspection tools

## Installation

```bash
pip install haltech-mock
```

### From Source

```bash
git clone https://github.com/YeeP79/haltech-mock.git
cd haltech-mock
pip install -e ".[dev]"
```

### Linux vcan Setup (Optional)

To use virtual CAN on Linux:

```bash
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0
```

## Quick Start

### CLI Usage

```bash
# Run with physics simulation (outputs to stdout)
haltech-mock run --physics

# Run idle scenario for 30 seconds
haltech-mock scenario idle --duration 30

# Run a warning scenario to test dashboard alerts
haltech-mock scenario overheat --duration 60

# Run a track lap scenario
haltech-mock scenario track

# Run a custom JSON scenario
haltech-mock scenario my_scenario.json

# List all available channels
haltech-mock channels

# List all CAN frames
haltech-mock frames

# Decode a raw CAN frame
haltech-mock decode 0x360 0DAC03E800000000
```

### Python API

```python
from haltech_mock import HaltechSimulator
from haltech_mock.interfaces.stdout import StdoutInterface

# Create simulator with stdout output
interface = StdoutInterface()
sim = HaltechSimulator(interface=interface)

# Set channel values directly
sim.set_channel("RPM", 3500)
sim.set_channel("Coolant Temperature", 85, units="C")  # Auto-converts to Kelvin
sim.set_channel("Oil Pressure", 45, units="psi")       # Auto-converts to kPa

# Use generic sensors for custom data
sim.set_channel("Generic Sensor 1", 42.5)   # Your custom sensor
sim.set_channel("Generic Sensor 2", 100.0)  # Another custom value

# Send all frames once
sim.send_all_frames()

# Or run continuously
sim.start()  # Blocks until Ctrl+C
```

### Physics-Based Simulation

```python
from haltech_mock import HaltechSimulator, create_physics_simulator
from haltech_mock.interfaces.stdout import StdoutInterface

interface = StdoutInterface()
sim = HaltechSimulator(interface=interface)

# Create physics engine with "sport" preset
physics = create_physics_simulator(sim, performance="sport")

# Control the simulated vehicle
physics.set_throttle(0.75)  # 75% throttle
physics.set_gear(3)

# Run simulation loop
import time
while True:
    physics.update(0.02)  # 50Hz update
    sim.send_all_frames()
    time.sleep(0.02)
```

### Scenario Playback

```python
import asyncio
from haltech_mock import HaltechSimulator, ScenarioRunner, create_idle_scenario
from haltech_mock.interfaces.stdout import StdoutInterface

interface = StdoutInterface()
sim = HaltechSimulator(interface=interface)
runner = ScenarioRunner(sim)

# Run built-in idle scenario
scenario = create_idle_scenario(duration=60.0)

async def main():
    await sim.start_async()
    await runner.run(scenario)
    sim.stop()

asyncio.run(main())
```

### PD16 Power Distribution Module

The PD16 simulator supports the full bidirectional protocol with 16 outputs and 8 inputs.

**CLI:**
```bash
# Run PD16 simulator (device A)
haltech-mock pd16 run --device A

# Run a PD16 scenario
haltech-mock pd16 scenario lights

# List available PD16 scenarios
haltech-mock pd16 scenarios

# Show PD16 output/input information
haltech-mock pd16 outputs
```

**Python API:**
```python
from haltech_mock.simulators.pd16 import PD16Simulator
from haltech_mock.protocols.pd16 import DeviceID
from haltech_mock.interfaces.stdout import StdoutInterface

interface = StdoutInterface()
sim = PD16Simulator(interface=interface, device_id=DeviceID.A)

# Set output states
sim.set_output_25a(0, duty_cycle=75.0)  # Fan motor at 75%
sim.set_output_8a(0, duty_cycle=100.0)  # Headlights full on

# Inject a fault for testing
from haltech_mock.models.pd16.enums import MuxType, PinState
sim.inject_fault(MuxType.OUTPUT_8A, 5, PinState.SHORT_CIRCUIT)

# Start simulator (transmits status frames at protocol rates)
sim.start()  # Blocks until Ctrl+C
```

**Async with scenarios:**
```python
import asyncio
from haltech_mock.simulators.pd16 import PD16Simulator
from haltech_mock.scenarios.pd16 import PD16ScenarioRunner, create_lights_scenario

sim = PD16Simulator(interface=interface)
runner = PD16ScenarioRunner(sim)
scenario = create_lights_scenario()

async def main():
    await sim.start_async()
    await runner.run(scenario)
    sim.stop()

asyncio.run(main())
```

## Built-in Scenarios

| Scenario | Description | Default Duration |
|----------|-------------|------------------|
| `idle` | Engine idling with realistic RPM and temperature variations | 30s |
| `accel` | Smooth acceleration from low to high RPM | 5s |
| `warmup` | Cold start with high idle, rich mixture, gradual temperature rise | 120s |
| `track` | Single track lap with straights, braking zones, and corners | 90s |
| `track-session` | Multi-lap track session (loops until stopped) | 90s/lap |
| `overheat` | Engine overheating with fan activation and warning lights | 30s |
| `low-oil` | Oil pressure dropping with warning light activation | 20s |
| `redline` | Hitting the rev limiter with limiting flags active | 15s |
| `battery` | Alternator failure with dropping voltage and warning light | 20s |

```bash
# Run any built-in scenario
haltech-mock scenario idle
haltech-mock scenario overheat --duration 60
haltech-mock scenario track --loop
```

### PD16 Scenarios

The PD16 simulator includes 5 built-in scenarios for testing power distribution:

| Scenario | Description | Outputs Used |
|----------|-------------|--------------|
| `lights` | Parking, low beam, high beam, turn signals | 8A: 0-4 |
| `cooling` | Temperature-based fan control (0% → 30% → 80%) | 25A: 0 |
| `fuel-pump` | Priming sequence and steady operation | 25A: 1 |
| `fault` | Cycle through fault conditions (overcurrent, short, open) | 8A: 5 |
| `startup` | Boot sequence with output self-test | Multiple |

```bash
# Run PD16 scenarios
haltech-mock pd16 scenario lights
haltech-mock pd16 scenario cooling --loop
haltech-mock pd16 scenario fault --device B
```

## Custom JSON Scenarios

Create your own scenarios using JSON files. This is perfect for:
- Testing specific dashboard behaviors
- Simulating custom warning conditions
- Replaying recorded data patterns
- Creating test sequences for your application

### JSON Scenario Format

```json
{
  "name": "My Custom Scenario",
  "description": "A custom test scenario",
  "loop": false,
  "stages": [
    {
      "name": "Stage 1",
      "duration": 10.0,
      "channels": {
        "RPM": 2500,
        "Coolant Temperature": {"value": 358.15},
        "Oil Pressure": {
          "range": [300, 400],
          "variation": "noise"
        },
        "Generic Sensor 1": {"value": 50.0}
      },
      "transitions": {
        "Throttle Position": {"from": 0, "to": 50, "curve": "ease_in"}
      }
    }
  ]
}
```

### Channel Value Options

Channels can be specified in several ways:

```json
{
  "channels": {
    "RPM": 3500,

    "Coolant Temperature": {"value": 358.15},

    "Oil Pressure": {
      "range": [300, 400],
      "variation": "sine",
      "period": 2.0
    },

    "Wideband Lambda 1": {
      "range": [0.95, 1.05],
      "variation": "random_walk"
    }
  }
}
```

| Property | Description |
|----------|-------------|
| `value` | Static value |
| `range` | `[min, max]` for variations |
| `variation` | `"none"`, `"sine"`, `"noise"`, or `"random_walk"` |
| `period` | Period in seconds for sine variation |
| `units` | Unit conversion: `"C"`, `"F"`, `"psi"`, `"bar"` |

### Transitions

Transitions smoothly change values over the stage duration:

```json
{
  "transitions": {
    "RPM": {
      "from": 1000,
      "to": 7000,
      "curve": "ease_in"
    }
  }
}
```

Available curves: `"linear"`, `"exponential"`, `"ease_in"`, `"ease_out"`, `"ease_in_out"`

### Complete Example: Custom Warning Test

Save as `warning_test.json`:

```json
{
  "name": "Dashboard Warning Test",
  "description": "Test all warning lights and conditions",
  "stages": [
    {
      "name": "Normal Operation",
      "duration": 5.0,
      "channels": {
        "RPM": {"range": [800, 900], "variation": "sine", "period": 2.0},
        "Coolant Temperature": {"value": 358.15},
        "Oil Pressure": {"value": 350},
        "Battery Voltage": {"value": 14.2},
        "Check Engine Light": {"value": 0},
        "Oil Pressure Light": {"value": 0},
        "Battery Light": {"value": 0}
      }
    },
    {
      "name": "Check Engine Warning",
      "duration": 5.0,
      "channels": {
        "RPM": {"value": 850},
        "Check Engine Light": {"value": 1},
        "Oil Pressure Light": {"value": 0},
        "Battery Light": {"value": 0}
      }
    },
    {
      "name": "Oil Pressure Warning",
      "duration": 5.0,
      "channels": {
        "RPM": {"value": 850},
        "Oil Pressure": {"value": 80},
        "Check Engine Light": {"value": 0},
        "Oil Pressure Light": {"value": 1},
        "Battery Light": {"value": 0}
      }
    },
    {
      "name": "Battery Warning",
      "duration": 5.0,
      "channels": {
        "RPM": {"value": 850},
        "Battery Voltage": {"value": 11.5},
        "Check Engine Light": {"value": 0},
        "Oil Pressure Light": {"value": 0},
        "Battery Light": {"value": 1}
      }
    },
    {
      "name": "All Warnings",
      "duration": 5.0,
      "channels": {
        "RPM": {"value": 850},
        "Coolant Temperature": {"value": 393.15},
        "Oil Pressure": {"value": 80},
        "Battery Voltage": {"value": 11.5},
        "Check Engine Light": {"value": 1},
        "Oil Pressure Light": {"value": 1},
        "Battery Light": {"value": 1},
        "Thermo Fan 1": {"value": 1},
        "Thermo Fan 2": {"value": 1}
      }
    }
  ]
}
```

Run it:
```bash
haltech-mock scenario warning_test.json --loop
```

## Generic Sensors

Haltech ECUs support 10 generic sensor channels that can be configured for any purpose. These are perfect for:
- Custom sensors not covered by standard channels
- Testing dashboard elements with arbitrary data
- Simulating auxiliary inputs

### Using Generic Sensors

```python
from haltech_mock import HaltechSimulator
from haltech_mock.interfaces.stdout import StdoutInterface

sim = HaltechSimulator(interface=StdoutInterface())

# Set generic sensor values
sim.set_channel("Generic Sensor 1", 42.5)    # Boost controller setpoint
sim.set_channel("Generic Sensor 2", 100.0)   # Nitrous bottle pressure
sim.set_channel("Generic Sensor 3", 75.0)    # Intercooler water temp
sim.set_channel("Generic Sensor 4", 1.0)     # Custom switch state
# ... up to Generic Sensor 10

sim.send_all_frames()
```

### In JSON Scenarios

```json
{
  "name": "Custom Sensors Demo",
  "stages": [
    {
      "name": "Sensor Test",
      "duration": 30.0,
      "channels": {
        "RPM": 3000,
        "Generic Sensor 1": {"range": [0, 100], "variation": "sine", "period": 5.0},
        "Generic Sensor 2": {"value": 50.0},
        "Generic Sensor 3": {"range": [40, 60], "variation": "noise"}
      }
    }
  ]
}
```

## CLI Reference

### `haltech-mock run`

Run the simulator with manual or physics-based control.

```bash
haltech-mock run [OPTIONS]

Options:
  -i, --interface TEXT     CAN interface (stdout, null, vcan0, etc.)
  -p, --physics            Enable physics-based simulation
  --performance TEXT       Physics preset: street, sport, race
  -t, --throttle FLOAT     Initial throttle position (0-100)
  -g, --gear INTEGER       Initial gear (0=neutral, 1-6)
  --rpm FLOAT              Initial RPM (manual mode only)
  -d, --duration FLOAT     Run duration in seconds
  --no-timestamp           Disable timestamps in output
```

### `haltech-mock scenario`

Run a predefined or custom scenario.

```bash
haltech-mock scenario NAME [OPTIONS]

Arguments:
  NAME                     Scenario name or path to JSON file
                          Built-in: idle, accel, warmup, track, track-session,
                                   overheat, low-oil, redline, battery

Options:
  -i, --interface TEXT     CAN interface
  -d, --duration FLOAT     Override scenario duration
  -l, --loop               Loop the scenario
  --no-timestamp           Disable timestamps in output
```

### `haltech-mock channels`

List all available channels in the protocol.

```bash
haltech-mock channels [FILTER]

Arguments:
  FILTER                   Filter channels by name (optional)

Examples:
  haltech-mock channels              # List all channels
  haltech-mock channels temp         # Filter by "temp"
  haltech-mock channels generic      # Show generic sensors
```

### `haltech-mock frames`

List all CAN frames or show details for a specific frame.

```bash
haltech-mock frames [FRAME_ID]

Arguments:
  FRAME_ID                 Show details for specific frame (e.g., 0x360)
```

### `haltech-mock decode`

Decode a CAN frame and display channel values.

```bash
haltech-mock decode FRAME_ID DATA

Arguments:
  FRAME_ID                 CAN frame ID (e.g., 0x360)
  DATA                     Hex data string (e.g., 0DAC03E800000000)
```

### `haltech-mock pd16`

PD16 Power Distribution Module simulator commands.

#### `haltech-mock pd16 run`

Run the PD16 simulator.

```bash
haltech-mock pd16 run [OPTIONS]

Options:
  -i, --interface TEXT     CAN interface (stdout, null, vcan0, etc.)
  -d, --device TEXT        Device ID: A, B, C, or D (default: A)
  --duration FLOAT         Run duration in seconds
  --no-timestamp           Disable timestamps in output
```

#### `haltech-mock pd16 scenario`

Run a PD16 scenario.

```bash
haltech-mock pd16 scenario NAME [OPTIONS]

Arguments:
  NAME                     Scenario name (lights, cooling, fuel-pump, fault, startup)

Options:
  -i, --interface TEXT     CAN interface
  -d, --device TEXT        Device ID
  -l, --loop               Loop the scenario
  --no-timestamp           Disable timestamps in output
```

#### `haltech-mock pd16 scenarios`

List available PD16 scenarios with descriptions.

#### `haltech-mock pd16 outputs`

Show PD16 output and input information (types, counts, index ranges).

## Protocol Support

Based on Haltech CAN Broadcast Protocol V2.35:

| Frame Range | Description | Rate |
|-------------|-------------|------|
| 0x360-0x362 | Engine Core (RPM, pressures, injection) | 50 Hz |
| 0x363-0x370 | Sensors (lambda, knock, wheel speeds) | 20 Hz |
| 0x371-0x376 | Fuel, battery, EGT, ambient | 10 Hz |
| 0x3E0-0x3EF | Temperatures, status flags, generic sensors | 5-50 Hz |
| 0x470-0x472 | Wideband, pedal, cruise control | 20-50 Hz |

### Supported Channels (167 Total)

- **Engine**: RPM, Throttle Position, Engine Demand, Manifold Pressure
- **Temperatures**: Coolant, Oil, Air, Fuel, Ambient, Gearbox, Diff, EGT 1-12
- **Pressures**: Oil, Fuel, Coolant, Manifold, Barometric, Brake, NOS 1-4
- **Lambda**: Wideband 1-12, Target Lambda, Bank averages
- **Drivetrain**: Vehicle Speed, Gear, Wheel Speeds (FL/FR/RL/RR), Driveshaft RPM
- **Electrical**: Battery Voltage, Injector Duty Cycle (4 stages)
- **Status Flags**: 23 boolean indicators (Check Engine, Oil Warning, Fans, etc.)
- **Generic Sensors**: 10 user-configurable channels
- **And more**: Knock, Cam angles, Turbo speed, G-forces, Cruise control, etc.

### PD16 Protocol Support

The PD16 Power Distribution Module uses multiplexed CAN messages with bidirectional communication.

**Device IDs** (up to 4 PD16 units on one CAN bus):

| Device | Base CAN ID | Frame Range |
|--------|-------------|-------------|
| A | 0x6D0 | 0x6D0-0x6D7 |
| B | 0x6D8 | 0x6D8-0x6DF |
| C | 0x6E0 | 0x6E0-0x6E7 |
| D | 0x6E8 | 0x6E8-0x6EF |

**Outputs:**
- 4 × 25A High Current outputs (index 0-3) — for fans, fuel pumps, high-draw loads
- 10 × 8A High Side outputs (index 0-9) — for lights, relays, low-draw loads
- 2 × Half-Bridge outputs (index 0-1) — for DC motor control

**Inputs:**
- 4 × SPI (Speed/Pulse) inputs — for wheel speed sensors, etc.
- 4 × AVI (Analog Voltage) inputs — for temperature sensors, pressure sensors

**Transmission Rates:**

| Frame Type | Direction | Rate |
|------------|-----------|------|
| Input Status | PD16→ECU | 20 Hz |
| Output Status | PD16→ECU | 5 Hz |
| Device Status | PD16→ECU | 2 Hz |
| Diagnostics | PD16→ECU | 2 Hz |

## Output Formats

### stdout (candump format)

```
(1234567890.123456) stdout 360#0DAC03E800000000
(1234567890.143456) stdout 361#00C8012C00000000
```

### SocketCAN

Connect directly to `vcan0`, `can0`, or any SocketCAN interface on Linux.

## Development

```bash
# Clone and install
git clone https://github.com/YeeP79/haltech-mock.git
cd haltech-mock
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Run tests
pytest

# Run linter
ruff check src/ tests/

# Run type checker
mypy src/
```

## Contributing

Contributions are welcome! Please ensure:

1. All tests pass (`pytest`)
2. Code is formatted (`ruff format src/ tests/`)
3. No lint errors (`ruff check src/ tests/`)
4. Type checks pass (`mypy src/`)
5. New features include tests

## License

MIT License - see [LICENSE](LICENSE) for details.

## Acknowledgments

- Protocol specification based on Haltech CAN Broadcast Protocol V2.35
- Built for the automotive tuning and dashboard development community