/***
Mazda MX-5 Instrument Cluster Controller
Copyright (c) 2026 Alastair Aitchison, Playful Technology
Uses Arduino Nano / MCP2515 CAN controller / Serial MP3 player
*/
// INCLUDES
#include <SPI.h>
// For audio interface
#include <SoftwareSerial.h>
// See https://github.com/autowp/arduino-mcp2515
#include <mcp2515.h>
// DEFINES
// If the crystal on your MCP2515 says "8.000", use MCP_8MHZ.
// If it says "16.000", change this to MCP_16MHZ.
#define MCP_CLOCK MCP_8MHZ
// CONSTANTS
// Fuel resistor selector relay (SPDT: selects either 10 or 100 ohms)
const byte RELAY_FUEL_SELECT = 5;
// Set false if the relay's energized contact is wired to the 100 ohm resistor.
const bool FUEL_10_OHM_WHEN_RELAY_ON = true;
// MCP2515 CS pin
const byte CS_PIN = 10;
// Turn indicator relays
const byte RELAY_LEFT = 7;
const byte RELAY_RIGHT = 6;
// Ignition relay and switch inputs
const byte RELAY_IGNITION = 4;
const byte IGNITION_PIN = 2;
const byte HORN_PIN = 3;
// YX5200/Catalex serial MP3 player
// Arduino RX connects to the player's TX; Arduino TX connects to its RX.
const byte MP3_RX_PIN = A1;
const byte MP3_TX_PIN = A0;
// Most inexpensive relay modules are ACTIVE LOW.
// Change to false if yours activates when the Arduino pin is HIGH.
const bool RELAY_ACTIVE_LOW = true;
// Measured on this cluster: 0x212 byte 4 warning/indicator masks.
// Other bits remain deliberately unnamed until bench-tested.
const uint8_t CAN212_ABS_WARN = 0x10;
const uint8_t CAN212_ABS_FLASH = 0x20;
const uint8_t CAN212_GENERAL_WARN = 0x40;
const unsigned long CAN201_PERIOD_MS = 10; // 100 Hz
const unsigned long CAN_AUX_SLOT_MS = 33; // three IDs interleaved at ~10 Hz each
// One complete flash per second: 500 ms on, then 500 ms off.
const unsigned long INDICATOR_TOGGLE_MS = 500;
const unsigned long SWITCH_DEBOUNCE_MS = 30;
// Everything the cluster should currently display. CAN frame builders read
// only this state; test/application code changes it without knowing CAN IDs.
struct ClusterState {
uint16_t rpm;
uint16_t speedMph;
int16_t coolantC;
uint8_t odometerCounter;
bool batteryWarning;
bool generalWarning;
bool absWarning;
bool absFlashing;
bool leftIndicator;
bool rightIndicator;
bool fuelEmpty;
};
ClusterState clusterState = {
0, // rpm
0, // speedMph
90, // coolantC
0, // odometerCounter
false, // batteryWarning
false, // generalWarning
false, // absWarning
false, // absFlashing
false, // leftIndicator
false, // rightIndicator
false // fuelEmpty
};
// Pre-encoded 0x201 speed value, recalculated only when speed changes.
uint16_t speedCanRaw = 10000;
// Global MCP2515 object
MCP2515 mcp2515(CS_PIN);
// YX5200 serial interface (9600 baud).
SoftwareSerial mp3Serial(MP3_RX_PIN, MP3_TX_PIN);
struct DebouncedSwitch {
bool rawOn;
bool stableOn;
unsigned long rawChangedAt;
};
DebouncedSwitch hornSwitch = { false, false, 0 };
DebouncedSwitch ignitionSwitch = { false, false, 0 };
// ---
// YX5200 / CATALEX SERIAL MP3 PLAYER
// ---
void initialiseMp3Player() {
// YX5200 boards can take a few seconds to index the card after power-up.
delay(2000);
mp3Serial.listen();
// Select the TF/microSD card explicitly (device 2).
sendMp3Command(0x09, 2);
// Set an audible volume (valid range 0-30).
sendMp3Command(0x06, 24);
}
void sendMp3Command(uint8_t command, uint16_t argument) {
// Catalex/YX5200 eight-byte command frame, with feedback disabled.
const uint8_t frame[8] = {
0x7E,
0xFF,
0x06,
command,
0x00,
highByte(argument),
lowByte(argument),
0xEF
};
mp3Serial.write(frame, sizeof(frame));
}
// ====================
// CAN ID 0x201
// RPM + speed
// ====================
void send201() {
uint16_t rpmRaw = clusterState.rpm * 4UL;
uint8_t data[8];
data[0] = highByte(rpmRaw);
data[1] = lowByte(rpmRaw);
// Unknown / engine-related fields
data[2] = 0x00;
data[3] = 0x00;
data[4] = highByte(speedCanRaw);
data[5] = lowByte(speedCanRaw);
// Accelerator position
data[6] = 0x00;
// Observed as FF in normal NC traffic
data[7] = 0xFF;
sendFrame(0x201, data);
}
void setSpeedMph(uint16_t speedMph) {
clusterState.speedMph = speedMph;
// 1 mph = 1.609344 km/h. Work in integer hundredths of km/h.
uint32_t speedKphTimes100 =
((uint32_t)speedMph * 160934UL + 500UL) / 1000UL;
speedCanRaw = (uint16_t)(10000UL + speedKphTimes100);
}
// ---
// CAN ID 0x212
// ABS / brake / DSC / TCS
// ---
void send212() {
uint8_t byte4 = 0;
if (clusterState.generalWarning)
byte4 |= CAN212_GENERAL_WARN;
if (clusterState.absWarning)
byte4 |= CAN212_ABS_WARN;
if (clusterState.absFlashing)
byte4 |= CAN212_ABS_FLASH;
uint8_t data[8] = {
0x00,
0x00,
0x00,
0x00,
byte4,
0x00,
0x00,
0x00
};
sendFrame(0x212, data);
}
// ====================
// CAN ID 0x240
// engine data / coolant
// ====================
void send240() {
// Coolant = degC + 40
int raw = clusterState.coolantC + 40;
raw = constrain(raw, 0, 255);
uint8_t data[8] = {
0x40, // calculated engine load
(uint8_t)raw, // coolant
0x00,
0x00,
65, // ~25 C intake temperature
0x00,
0x00,
0x00
};
sendFrame(0x240, data);
}
// ---
// CAN ID 0x420
// Coolant + PCM warning lamps
// ---
void send420() {
uint8_t temp = (uint8_t) constrain(clusterState.coolantC + 40, 0, 255);
uint8_t byte6 = 0;
if (clusterState.batteryWarning) {
byte6 |= 0x40;
}
uint8_t data[8] = {
temp, // byte 0 - coolant temperature
clusterState.odometerCounter,
0x00,
0x00,
0x01, // byte 4 - oil pressure "OK"
0x00,
byte6,
0x00
};
sendFrame(0x420, data);
}
// ---
// Relay helpers
// ---
void setRelay(byte pin, bool on) {
if (RELAY_ACTIVE_LOW)
digitalWrite(pin, on ? LOW : HIGH);
else
digitalWrite(pin, on ? HIGH : LOW);
}
void selectFuelResistance(bool tenOhms) {
setRelay(RELAY_FUEL_SELECT,
tenOhms == FUEL_10_OHM_WHEN_RELAY_ON);
}
void allRelaysOff() {
setRelay(RELAY_FUEL_SELECT, false);
setRelay(RELAY_LEFT, false);
setRelay(RELAY_RIGHT, false);
setRelay(RELAY_IGNITION, false);
}
void applyClusterOutputs() {
static bool indicatorOutputOn = true;
static uint8_t previousRequest = 0;
static unsigned long lastToggle = 0;
static int8_t previousLeftOutput = -1;
static int8_t previousRightOutput = -1;
static int8_t previousFuelEmpty = -1;
uint8_t request = (clusterState.leftIndicator ? 0x01 : 0x00) |
(clusterState.rightIndicator ? 0x02 : 0x00);
unsigned long now = millis();
// Start each newly selected direction with its lamps illuminated.
if (request != previousRequest) {
previousRequest = request;
indicatorOutputOn = request != 0;
lastToggle = now;
} else if (request != 0 && now - lastToggle >= INDICATOR_TOGGLE_MS) {
indicatorOutputOn = !indicatorOutputOn;
lastToggle = now;
}
bool leftOutput = clusterState.leftIndicator && indicatorOutputOn;
bool rightOutput = clusterState.rightIndicator && indicatorOutputOn;
if (leftOutput != previousLeftOutput) {
setRelay(RELAY_LEFT, leftOutput);
previousLeftOutput = leftOutput;
}
if (rightOutput != previousRightOutput) {
setRelay(RELAY_RIGHT, rightOutput);
previousRightOutput = rightOutput;
}
if (clusterState.fuelEmpty != previousFuelEmpty) {
selectFuelResistance(!clusterState.fuelEmpty);
previousFuelEmpty = clusterState.fuelEmpty;
}
}
// CAN transmit helper
void sendFrame(uint16_t id, const uint8_t data[8]) {
// Build up the CAN frame
struct can_frame frame;
frame.can_id = id;
frame.can_dlc = 8;
for (byte i = 0; i < 8; i++)
frame.data[i] = data[i];
MCP2515::ERROR result = mcp2515.sendMessage(&frame);
static unsigned long lastErrorPrint = 0;
if (result == MCP2515::ERROR_OK)
return;
uint8_t errorFlags = mcp2515.getErrorFlags();
uint8_t txErrorCount = mcp2515.errorCountTX();
uint8_t rxErrorCount = mcp2515.errorCountRX();
// The autowp library returns ERROR_FAILTX if it sees the MCP2515's
// arbitration-lost bit immediately after requesting transmission. Losing
// arbitration is normal on a busy CAN bus; with one-shot mode disabled the
// controller retains the frame and retries it automatically. Do not report
// that healthy case as a fault.
bool cleanArbitrationLoss =
result == MCP2515::ERROR_FAILTX &&
errorFlags == 0 && txErrorCount == 0 && rxErrorCount == 0;
if (cleanArbitrationLoss)
return;
if (millis() - lastErrorPrint > 1000) {
Serial.print(F("CAN TX error"));
Serial.print((int)result);
if (result == MCP2515::ERROR_ALLTXBUSY)
Serial.print(F(" (all TX buffers busy)"));
else if (result == MCP2515::ERROR_FAILTX)
Serial.print(F(" (controller reported TX failure)"));
Serial.print(F(", ID=0x"));
Serial.print(id, HEX);
Serial.print(F(", EFLG=0x"));
Serial.print(errorFlags, HEX);
Serial.print(F(", TEC="));
Serial.print(txErrorCount);
Serial.print(F(", REC="));
Serial.println(rxErrorCount);
lastErrorPrint = millis();
}
}
// This tester does not consume CAN traffic from the cluster, but the MCP2515
// still stores received frames. Drain them regularly so its two RX buffers do
// not fill during long tests (EFLG 0x40/0x80 = RX buffer overflow).
void discardReceivedFrames() {
struct can_frame receivedFrame;
while (mcp2515.readMessage(&receivedFrame) == MCP2515::ERROR_OK) {
// Intentionally discarded.
}
if (mcp2515.getErrorFlags() & 0xC0) {
mcp2515.clearRXnOVRFlags();
}
}
// Maintain factory-ish transmission rates:
//
// 0x201 = 100 Hz
// 0x240, 0x420 and 0x212 = approximately 10 Hz each
unsigned long last201Frame = 0;
unsigned long lastAuxFrame = 0;
uint8_t nextAuxFrame = 0;
void serviceClusterCan() {
applyClusterOutputs();
discardReceivedFrames();
unsigned long now = millis();
// 0x201 at 100 Hz.
if (now - last201Frame >= CAN201_PERIOD_MS) {
last201Frame = now;
send201();
}
// Interleave the three slower frames. Sending all three immediately
// after 0x201 can fill all three MCP2515 TX buffers and drop the fourth
// frame with ERROR_ALLTXBUSY. Each ID is sent approximately every 99 ms.
if (now - lastAuxFrame >= CAN_AUX_SLOT_MS) {
lastAuxFrame = now;
switch (nextAuxFrame) {
case 0:
send240();
break;
case 1:
send420();
break;
default:
send212();
break;
}
nextAuxFrame = (nextAuxFrame + 1) % 3;
}
}
// ====================
// SERIAL COMMANDS (115200 baud, newline terminated)
// ====================
void printCommandHelp() {
Serial.println(F("Commands:"));
Serial.println(F(" rpm <0-8000>"));
Serial.println(F(" speed <0-150> (mph)"));
Serial.println(F(" coolant <-40-215> (degrees C)"));
Serial.println(F(" indicator left|right|hazard|off"));
Serial.println(F(" indicator left|right|hazard on|off"));
Serial.println(F(" warning battery|general|abs|absflash on|off"));
Serial.println(F(" warning off"));
Serial.println(F(" fuel full|empty"));
Serial.println(F(" status"));
}
int8_t parseOnOff(const char *word) {
if (word != NULL && strcmp(word, "on") == 0)
return 1;
if (word != NULL && strcmp(word, "off") == 0)
return 0;
return -1;
}
bool parseNumber(const char *word, long minimum, long maximum, long &value) {
if (word == NULL || *word == '\0')
return false;
char *end;
value = strtol(word, &end, 10);
return *end == '\0' && value >= minimum && value <= maximum;
}
void printClusterStatus() {
Serial.print(F("RPM="));
Serial.print(clusterState.rpm);
Serial.print(F(" speed_mph="));
Serial.print(clusterState.speedMph);
Serial.print(F(" coolant_c="));
Serial.print(clusterState.coolantC);
Serial.print(F(" indicators="));
if (clusterState.leftIndicator && clusterState.rightIndicator)
Serial.print(F("hazard"));
else if (clusterState.leftIndicator)
Serial.print(F("left"));
else if (clusterState.rightIndicator)
Serial.print(F("right"));
else
Serial.print(F("off"));
Serial.print(F(" warnings="));
if (!clusterState.batteryWarning && !clusterState.generalWarning &&
!clusterState.absWarning && !clusterState.absFlashing) {
Serial.print(F("off"));
} else {
if (clusterState.batteryWarning) Serial.print(F("battery"));
if (clusterState.generalWarning) Serial.print(F("general"));
if (clusterState.absWarning) Serial.print(F("abs"));
if (clusterState.absFlashing) Serial.print(F("absflash"));
}
Serial.print(F(" fuel="));
Serial.println(clusterState.fuelEmpty ? F("empty") : F("full"));
}
void handleSerialCommand(char *line) {
// Commands are case-insensitive.
for (char *p = line; *p != '\0'; ++p) {
if (*p >= 'A' && *p <= 'Z')
*p += 'a' - 'A';
}
char *command = strtok(line, " \t");
if (command == NULL)
return;
if (strcmp(command, "help") == 0 || strcmp(command, "?") == 0) {
printCommandHelp();
return;
}
if (strcmp(command, "status") == 0) {
printClusterStatus();
return;
}
if (strcmp(command, "rpm") == 0 || strcmp(command, "speed") == 0 ||
strcmp(command, "coolant") == 0) {
long value;
long minimum = strcmp(command, "coolant") == 0 ? -40 : 0;
long maximum = strcmp(command, "rpm") == 0 ? 8000 :
strcmp(command, "speed") == 0 ? 150 : 215;
if (!parseNumber(strtok(NULL, " \t"), minimum, maximum, value)) {
Serial.println(F("ERR: value out of range"));
return;
}
if (strcmp(command, "rpm") == 0)
clusterState.rpm = (uint16_t)value;
else if (strcmp(command, "speed") == 0)
setSpeedMph((uint16_t)value);
else
clusterState.coolantC = (int16_t)value;
Serial.println(F("OK"));
return;
}
if (strcmp(command, "indicator") == 0) {
char *target = strtok(NULL, " \t");
char *settingWord = strtok(NULL, " \t");
if (target != NULL && strcmp(target, "off") == 0 && settingWord == NULL) {
clusterState.leftIndicator = false;
clusterState.rightIndicator = false;
} else {
int8_t setting = settingWord == NULL ? 1 : parseOnOff(settingWord);
if (target == NULL || setting < 0) {
Serial.println(F("ERR: use indicator left|right|hazard|off"));
return;
}
if (strcmp(target, "left") == 0) {
clusterState.leftIndicator = setting;
if (setting) clusterState.rightIndicator = false;
} else if (strcmp(target, "right") == 0) {
clusterState.rightIndicator = setting;
if (setting) clusterState.leftIndicator = false;
} else if (strcmp(target, "hazard") == 0 || strcmp(target, "hazards") == 0) {
clusterState.leftIndicator = setting;
clusterState.rightIndicator = setting;
} else {
Serial.println(F("ERR: unknown indicator"));
return;
}
}
applyClusterOutputs();
Serial.println(F("OK"));
return;
}
if (strcmp(command, "warning") == 0) {
char *target = strtok(NULL, " \t");
if (target != NULL && strcmp(target, "off") == 0) {
clearWarnings();
Serial.println(F("OK"));
return;
}
int8_t setting = parseOnOff(strtok(NULL, " \t"));
if (target == NULL || setting < 0) {
Serial.println(F("ERR: use warning <name> on|off"));
return;
}
if (strcmp(target, "battery") == 0)
clusterState.batteryWarning = setting;
else if (strcmp(target, "general") == 0)
clusterState.generalWarning = setting;
else if (strcmp(target, "abs") == 0)
clusterState.absWarning = setting;
else if (strcmp(target, "absflash") == 0)
clusterState.absFlashing = setting;
else {
Serial.println(F("ERR: unknown warning"));
return;
}
Serial.println(F("OK"));
return;
}
if (strcmp(command, "fuel") == 0) {
char *setting = strtok(NULL, " \t");
if (setting != NULL && strcmp(setting, "empty") == 0)
clusterState.fuelEmpty = true;
else if (setting != NULL && strcmp(setting, "full") == 0)
clusterState.fuelEmpty = false;
else {
Serial.println(F("ERR: use fuel full|empty"));
return;
}
applyClusterOutputs();
Serial.println(F("OK"));
return;
}
Serial.println(F("ERR: unknown command; enter help"));
}
void serviceSerialCommands() {
static char line[64];
static byte length = 0;
while (Serial.available() > 0) {
char character = Serial.read();
if (character == '\r')
continue;
if (character == '\n') {
line[length] = '\0';
handleSerialCommand(line);
length = 0;
} else if (length < sizeof(line) - 1) {
line[length++] = character;
} else {
length = 0;
Serial.println(F("ERR: command too long"));
}
}
}
void clearWarnings() {
clusterState.batteryWarning = false;
clusterState.generalWarning = false;
clusterState.absWarning = false;
clusterState.absFlashing = false;
}
// ====================================================================
// WARNING LIGHTS
// ====================================================================
void testWarningLights() {
Serial.println(F("=== WARNING LIGHT TEST ==="));
clearWarnings();
clusterState.rpm = 0;
setSpeedMph(0);
clusterState.coolantC = 0;
// ---
// BATTERY / CHARGING
// ---
Serial.println(F("Battery / charging warning"));
clusterState.batteryWarning = true;
holdState(1000);
clusterState.batteryWarning = false;
holdState(1000);
// ---
// GENERAL WARNING
// ---
Serial.println(F("General warning"));
clusterState.generalWarning = true;
holdState(1000);
clusterState.generalWarning = false;
holdState(1000);
// ---
// ABS
// ---
Serial.println(F("ABS warning"));
clusterState.absWarning = true;
holdState(1000);
clusterState.absWarning = false;
holdState(1000);
Serial.println(F("ABS activity / flashing request"));
clusterState.absFlashing = true;
holdState(1000);
clusterState.absFlashing = false;
holdState(1000);
clearWarnings();
}
// Keep CAN traffic going while waiting.
// This is important: don't simply delay() for several seconds.
void holdState(unsigned long duration) {
unsigned long start = millis();
while (millis() - start < duration) {
serviceSystem();
delay(1);
}
}
// ====================================================================
// FUEL GAUGE
// ====================================================================
void testFuel() {
Serial.println();
Serial.println(F("=== FUEL GAUGE: FULL / EMPTY ==="));
// The MX-5 sender is approximately 10 ohms at full and 100 ohms at empty.
// Long holds are intentional because the cluster heavily damps the gauge
Serial.println(F("Fuel: FULL (~10 ohm)"));
clusterState.fuelEmpty = false;
clusterState.rpm = 900;
setSpeedMph(0);
clusterState.coolantC = 90;
holdState(30000);
Serial.println(F("Fuel: EMPTY (~100 ohm)"));
clusterState.fuelEmpty = true;
holdState(30000);
// Leave the gauge at empty after the test.
clusterState.fuelEmpty = true;
}
// ====================================================================
// SPEEDOMETER
// ====================================================================
void testSpeedometer() {
Serial.println();
Serial.println(F("=== SPEEDOMETER ==="));
// Sweep the complete UK 150 mph cluster scale.
clusterState.rpm = 900;
clusterState.coolantC = 90;
for (int speed = 0; speed <= 150; speed += 1) {
setSpeedMph(speed);
holdState(20);
}
holdState(1000);
for (int speed = 150; speed >= 0; speed -= 1) {
setSpeedMph(speed);
holdState(20);
}
holdState(1000);
}
// ====================================================================
// TACHOMETER
// ====================================================================
void testTachometer() {
Serial.println();
Serial.println(F("=== TACHOMETER ==="));
setSpeedMph(0);
clusterState.coolantC = 90;
for (int rpm = 0; rpm <= 7200; rpm += 60) {
clusterState.rpm = rpm;
holdState(20);
}
holdState(1000);
for (int rpm = 7200; rpm >= 0; rpm -= 60) {
clusterState.rpm = rpm;
holdState(20);
}
holdState(1000);
}
// ====================================================================
// COOLANT TEMPERATURE
// ====================================================================
void testTemperature() {
Serial.println();
Serial.println(F("=== COOLANT TEMPERATURE ==="));
Serial.println(F("Coolant: cold"));
clusterState.rpm = 0;
setSpeedMph(0);
clusterState.coolantC = 45;
holdState(1000);
Serial.println(F("Coolant: warming"));
for (int temp = 45; temp <= 115; temp += 2) {
clusterState.coolantC = temp;
holdState(150);
}
Serial.println(F("Coolant: HOT"));
holdState(1000);
Serial.println(F("Coolant: cooling"));
for (int temp = 115; temp >= 45; temp -= 2) {
clusterState.coolantC = temp;
holdState(150);
}
holdState(1000);
}
// ==================================================
// TURN INDICATORS
// ==================================================
void testIndicators() {
Serial.println();
Serial.println(F("=== LEFT INDICATOR ==="));
clusterState.rpm = 900;
setSpeedMph(0);
clusterState.coolantC = 90;
clusterState.leftIndicator = true;
holdState(10000);
clusterState.leftIndicator = false;
holdState(1000);
Serial.println(F("=== RIGHT INDICATOR ==="));
clusterState.rightIndicator = true;
holdState(10000);
clusterState.rightIndicator = false;
holdState(1000);
}
void startIgnition() {
// Command 0x03 plays a track by its SD-card index; index 1 is the first.
sendMp3Command(0x03, 1);
}
void soundHorn() {
// Track index 3 is the horn sound effect.
sendMp3Command(0x03, 3);
}
bool updateDebouncedSwitch(byte pin, DebouncedSwitch &input) {
// Inputs use INPUT_PULLUP, so a closed switch reads LOW.
bool rawOn = digitalRead(pin) == LOW;
unsigned long now = millis();
if (rawOn != input.rawOn) {
input.rawOn = rawOn;
input.rawChangedAt = now;
}
if (input.stableOn != input.rawOn &&
now - input.rawChangedAt >= SWITCH_DEBOUNCE_MS) {
input.stableOn = input.rawOn;
return true;
}
return false;
}
void serviceControlInputs() {
if (updateDebouncedSwitch(IGNITION_PIN, ignitionSwitch)) {
setRelay(RELAY_IGNITION, ignitionSwitch.stableOn);
// Only the OFF-to-ON edge starts the ignition sound.
if (ignitionSwitch.stableOn)
startIgnition();
}
if (updateDebouncedSwitch(HORN_PIN, hornSwitch) && hornSwitch.stableOn) {
// Play once on the press edge, not continuously while held.
soundHorn();
}
}
void serviceSystem() {
serviceControlInputs();
serviceSerialCommands();
serviceClusterCan();
}
void runSelfTests() {
Serial.println();
Serial.println(F("========================"));
Serial.println(F("STARTING CLUSTER TEST"));
Serial.println(F("========================"));
testWarningLights();
testFuel();
testTachometer();
testSpeedometer();
testTemperature();
testIndicators();
Serial.println();
Serial.println(F("TEST COMPLETE"));
}
// ====================================================================
// SETUP
// ====================================================================
void setup() {
// Initialise serial interface
Serial.begin(115200);
Serial.println(__FILE__ __DATE__);
printCommandHelp();
// Initialise MP3 serial interface
mp3Serial.begin(9600);
initialiseMp3Player();
// Initialise inputs
pinMode(HORN_PIN, INPUT_PULLUP);
pinMode(IGNITION_PIN, INPUT_PULLUP);
// Initialise relay outputs
const byte relayPins[] = {
RELAY_FUEL_SELECT,
RELAY_LEFT,
RELAY_RIGHT,
RELAY_IGNITION
};
for (byte i = 0; i < sizeof(relayPins); i++) {
pinMode(relayPins[i], OUTPUT);
}
allRelaysOff();
// Initialise CAN interface
SPI.begin();
mcp2515.reset();
MCP2515::ERROR result = mcp2515.setBitrate(CAN_500KBPS, MCP_CLOCK);
Serial.print(F("CAN bitrate setup: "));
Serial.println((int)result);
result = mcp2515.setNormalMode();
Serial.print(F("CAN normal mode: "));
Serial.println((int)result);
delay(1000);
// Apply the initial full-tank and indicators-off state.
applyClusterOutputs();
// Start from a sensible baseline
holdState(2000);
}
// ====================================================================
// MAIN LOOP
// ====================================================================
void loop() {
serviceSystem();
delay(1);
}