Note: This package was renamed from
haltech-mocktodevdash-haltech-mockin v0.2.0. The CLI commandhaltech-mockstill works for backwards compatibility.
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.
- 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
pip install devdash-haltech-mockgit clone https://github.com/devdash-project/haltech-mock.git
cd haltech-mock
pip install -e ".[dev]"To use virtual CAN on Linux:
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0# 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 0DAC03E800000000from 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+Cfrom 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)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())The PD16 simulator supports the full bidirectional protocol with 16 outputs and 8 inputs.
CLI:
# 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 outputsPython API:
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+CAsync with scenarios:
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())| 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 |
# Run any built-in scenario
haltech-mock scenario idle
haltech-mock scenario overheat --duration 60
haltech-mock scenario track --loopThe 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 |
# Run PD16 scenarios
haltech-mock pd16 scenario lights
haltech-mock pd16 scenario cooling --loop
haltech-mock pd16 scenario fault --device BCreate 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
{
"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"}
}
}
]
}Channels can be specified in several ways:
{
"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 smoothly change values over the stage duration:
{
"transitions": {
"RPM": {
"from": 1000,
"to": 7000,
"curve": "ease_in"
}
}
}Available curves: "linear", "exponential", "ease_in", "ease_out", "ease_in_out"
Save as warning_test.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:
haltech-mock scenario warning_test.json --loopHaltech 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
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(){
"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"}
}
}
]
}Run the simulator with manual or physics-based control.
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 outputRun a predefined or custom scenario.
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 outputList all available channels in the protocol.
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 sensorsList all CAN frames or show details for a specific frame.
haltech-mock frames [FRAME_ID]
Arguments:
FRAME_ID Show details for specific frame (e.g., 0x360)Decode a CAN frame and display channel values.
haltech-mock decode FRAME_ID DATA
Arguments:
FRAME_ID CAN frame ID (e.g., 0x360)
DATA Hex data string (e.g., 0DAC03E800000000)PD16 Power Distribution Module simulator commands.
Run the PD16 simulator.
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 outputRun a PD16 scenario.
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 outputList available PD16 scenarios with descriptions.
Show PD16 output and input information (types, counts, index ranges).
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 |
- 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.
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 |
(1234567890.123456) stdout 360#0DAC03E800000000
(1234567890.143456) stdout 361#00C8012C00000000
Connect directly to vcan0, can0, or any SocketCAN interface on Linux.
# Clone and install
git clone https://github.com/devdash-project/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/Contributions are welcome! Please ensure:
- All tests pass (
pytest) - Code is formatted (
ruff format src/ tests/) - No lint errors (
ruff check src/ tests/) - Type checks pass (
mypy src/) - New features include tests
MIT License - see LICENSE for details.
- Protocol specification based on Haltech CAN Broadcast Protocol V2.35
- Built for the automotive tuning and dashboard development community