|
| 1 | +// ======================================================= |
| 2 | +// Travis Digital Dash – Haltech CAN → TS Dash Serial Bridge |
| 3 | +// Arduino Mega 2560 + MCP2515 (16MHz) @ 500kbps |
| 4 | +// |
| 5 | +// Serial side emulates the INI requirements: |
| 6 | +// queryCommand = "Q" -> returns "speeduino-travis" |
| 7 | +// versionInfo = "S" -> returns VERSION string |
| 8 | +// ochGetCommand = "r" -> returns 87-byte output block |
| 9 | +// |
| 10 | +// INI OutputChannels (selected): |
| 11 | +// rpm U16 @ 0 |
| 12 | +// oilanalograw U08 @ 3 (°C + 40) |
| 13 | +// mapraw U16 @ 4 (kPa absolute) |
| 14 | +// coolantanalograw U08 @ 7 (°C + 40) |
| 15 | +// oilPressure U08 @ 10 (PSI) |
| 16 | +// fuelPressure U08 @ 11 (PSI) |
| 17 | +// fuellevel U08 @ 15 (%) |
| 18 | +// vss U16 @ 19 (kph) |
| 19 | +// leftTurn U08 @ 40 (0/1) |
| 20 | +// rightTurn U08 @ 41 (0/1) |
| 21 | +// cel U08 @ 42 (0/1) |
| 22 | +// highBeam U08 @ 43 (0/1) |
| 23 | +// handbrake U08 @ 44 (0/1) |
| 24 | +// ======================================================= |
| 25 | + |
| 26 | +#include <Arduino.h> |
| 27 | +#include <SPI.h> |
| 28 | +#include <mcp_can.h> |
| 29 | + |
| 30 | +// ===================== IDENTITY (INI MATCH) ===================== |
| 31 | +static const char SIGNATURE[] = "speeduino-travis"; |
| 32 | +static const char VERSION[] = "Travis Digital Dash v1.0"; |
| 33 | + |
| 34 | +// ===================== SERIAL ===================== |
| 35 | +static constexpr uint32_t BAUD_RATE = 115200; |
| 36 | + |
| 37 | +// ===================== CAN (MCP2515) ===================== |
| 38 | +#define CAN_CS_PIN 53 |
| 39 | +#define CAN_INT_PIN 2 |
| 40 | +#define CAN_CLOCK MCP_16MHZ |
| 41 | +#define CAN_SPEED CAN_500KBPS |
| 42 | + |
| 43 | +MCP_CAN CAN(CAN_CS_PIN); |
| 44 | + |
| 45 | +// ======================================================= |
| 46 | +// HALTECH CAN MAPPING (DEFAULTS / YOU MAY NEED TO ADJUST) |
| 47 | +// ------------------------------------------------------- |
| 48 | +// Haltech CAN layouts can vary by ECU config and stream. |
| 49 | +// These defaults are *common* patterns, but if any channel |
| 50 | +// shows wrong, sniff the bus and update IDs/bytes/scales. |
| 51 | +// ======================================================= |
| 52 | + |
| 53 | +// --------- Frame IDs (11-bit) ---------- |
| 54 | +static constexpr uint16_t ID_RPM_MAP = 0x360; // rpm + map |
| 55 | +static constexpr uint16_t ID_TEMPS = 0x361; // clt + oil temp (example) |
| 56 | +static constexpr uint16_t ID_PRESSURES = 0x362; // oil psi + fuel psi (example) |
| 57 | +static constexpr uint16_t ID_SPEED_FUEL = 0x363; // speed kph + fuel % (example) |
| 58 | +static constexpr uint16_t ID_INDICATORS = 0x364; // turn/cel/high/handbrake (example) |
| 59 | + |
| 60 | +// --------- Byte layouts / scaling ---------- |
| 61 | +// RPM: U16 little-endian, units rpm |
| 62 | +// MAP: U16 little-endian, units kPa absolute |
| 63 | +// CLT/OIL: int16 or u16? (varies). We convert to °C then encode as U08 (°C + 40). |
| 64 | +// Pressures: usually kPa or PSI; we output PSI already scaled in firmware (U08). |
| 65 | +// Speed: U16 kph |
| 66 | +// Fuel: U08 percent |
| 67 | +// |
| 68 | +// If your Haltech stream uses different scaling (e.g. 0.1 kPa, 0.1°C), adjust here: |
| 69 | +static constexpr float TEMP_SCALE = 1.0f; // e.g. 0.1f if temp is in 0.1°C |
| 70 | +static constexpr float MAP_SCALE = 1.0f; // e.g. 0.1f if map is in 0.1 kPa |
| 71 | +static constexpr float RPM_SCALE = 1.0f; // e.g. 0.5f if half-RPM, etc. |
| 72 | +static constexpr float PSI_SCALE = 1.0f; // e.g. 0.145038 if kPa->psi, etc. |
| 73 | + |
| 74 | +// ===================== OUTPUT BLOCK ===================== |
| 75 | +static constexpr uint8_t OCH_BLOCK_SIZE = 87; // ini: ochBlockSize = 87 |
| 76 | +static uint8_t och[OCH_BLOCK_SIZE]; |
| 77 | + |
| 78 | +// ===================== LIVE VALUES (decoded from CAN) ===================== |
| 79 | +static volatile uint16_t g_rpm = 0; |
| 80 | +static volatile uint16_t g_map_kpa = 100; // default ~atmosphere |
| 81 | +static volatile int16_t g_clt_c = 20; |
| 82 | +static volatile int16_t g_oil_c = 20; |
| 83 | +static volatile uint8_t g_oil_psi = 0; |
| 84 | +static volatile uint8_t g_fuel_psi = 0; |
| 85 | +static volatile uint16_t g_speed_kph = 0; |
| 86 | +static volatile uint8_t g_fuel_pct = 0; |
| 87 | + |
| 88 | +static volatile uint8_t g_leftTurn = 0; |
| 89 | +static volatile uint8_t g_rightTurn = 0; |
| 90 | +static volatile uint8_t g_cel = 0; |
| 91 | +static volatile uint8_t g_highBeam = 0; |
| 92 | +static volatile uint8_t g_handbrake = 0; |
| 93 | + |
| 94 | +// Optional: basic timeout protection |
| 95 | +static uint32_t lastCanRxMs = 0; |
| 96 | + |
| 97 | +// ===================== SMALL HELPERS ===================== |
| 98 | +static inline uint16_t u16le(const uint8_t *b) { |
| 99 | + return (uint16_t)b[0] | ((uint16_t)b[1] << 8); |
| 100 | +} |
| 101 | +static inline int16_t s16le(const uint8_t *b) { |
| 102 | + return (int16_t)((uint16_t)b[0] | ((uint16_t)b[1] << 8)); |
| 103 | +} |
| 104 | + |
| 105 | +static inline uint8_t clampU8(int v) { |
| 106 | + if (v < 0) return 0; |
| 107 | + if (v > 255) return 255; |
| 108 | + return (uint8_t)v; |
| 109 | +} |
| 110 | + |
| 111 | +static inline uint8_t tempC_to_iniRaw(int16_t tempC) { |
| 112 | + // INI expects U08 = (°C + 40) |
| 113 | + return clampU8((int)tempC + 40); |
| 114 | +} |
| 115 | + |
| 116 | +static void writeU16LE(uint8_t *dst, uint16_t v) { |
| 117 | + dst[0] = (uint8_t)(v & 0xFF); |
| 118 | + dst[1] = (uint8_t)(v >> 8); |
| 119 | +} |
| 120 | + |
| 121 | +// ===================== CAN RECEIVE ===================== |
| 122 | +static void handleCanFrame(uint16_t id, const uint8_t *buf, uint8_t len) { |
| 123 | + (void)len; |
| 124 | + lastCanRxMs = millis(); |
| 125 | + |
| 126 | + switch (id) { |
| 127 | + case ID_RPM_MAP: { |
| 128 | + // [0..1]=RPM, [2..3]=MAP (kPa abs) |
| 129 | + uint16_t rpmRaw = u16le(&buf[0]); |
| 130 | + uint16_t mapRaw = u16le(&buf[2]); |
| 131 | + |
| 132 | + uint32_t rpm = (uint32_t)(rpmRaw * RPM_SCALE); |
| 133 | + uint32_t map = (uint32_t)(mapRaw * MAP_SCALE); |
| 134 | + |
| 135 | + if (rpm > 30000) rpm = 30000; |
| 136 | + if (map > 4000) map = 4000; |
| 137 | + |
| 138 | + g_rpm = (uint16_t)rpm; |
| 139 | + g_map_kpa = (uint16_t)map; |
| 140 | + } break; |
| 141 | + |
| 142 | + case ID_TEMPS: { |
| 143 | + // Example: [0..1]=CLT, [2..3]=OilTemp (in °C * TEMP_SCALE) |
| 144 | + int16_t cltRaw = s16le(&buf[0]); |
| 145 | + int16_t oilRaw = s16le(&buf[2]); |
| 146 | + |
| 147 | + int16_t cltC = (int16_t)(cltRaw * TEMP_SCALE); |
| 148 | + int16_t oilC = (int16_t)(oilRaw * TEMP_SCALE); |
| 149 | + |
| 150 | + // sanity |
| 151 | + if (cltC < -40) cltC = -40; |
| 152 | + if (cltC > 215) cltC = 215; |
| 153 | + if (oilC < -40) oilC = -40; |
| 154 | + if (oilC > 215) oilC = 215; |
| 155 | + |
| 156 | + g_clt_c = cltC; |
| 157 | + g_oil_c = oilC; |
| 158 | + } break; |
| 159 | + |
| 160 | + case ID_PRESSURES: { |
| 161 | + // Example: [0]=oil psi, [1]=fuel psi (already PSI) |
| 162 | + // If yours is kPa, change PSI_SCALE and conversion. |
| 163 | + uint16_t oilRaw = buf[0]; |
| 164 | + uint16_t fuelRaw = buf[1]; |
| 165 | + |
| 166 | + uint16_t oilPsi = (uint16_t)(oilRaw * PSI_SCALE); |
| 167 | + uint16_t fuelPsi = (uint16_t)(fuelRaw * PSI_SCALE); |
| 168 | + |
| 169 | + if (oilPsi > 255) oilPsi = 255; |
| 170 | + if (fuelPsi > 255) fuelPsi = 255; |
| 171 | + |
| 172 | + g_oil_psi = (uint8_t)oilPsi; |
| 173 | + g_fuel_psi = (uint8_t)fuelPsi; |
| 174 | + } break; |
| 175 | + |
| 176 | + case ID_SPEED_FUEL: { |
| 177 | + // Example: [0..1]=speed kph, [2]=fuel % |
| 178 | + g_speed_kph = u16le(&buf[0]); |
| 179 | + g_fuel_pct = buf[2]; |
| 180 | + if (g_fuel_pct > 100) g_fuel_pct = 100; |
| 181 | + } break; |
| 182 | + |
| 183 | + case ID_INDICATORS: { |
| 184 | + // Example: packed bits in buf[0] |
| 185 | + // bit0=left, bit1=right, bit2=cel, bit3=high, bit4=handbrake |
| 186 | + uint8_t bits = buf[0]; |
| 187 | + g_leftTurn = (bits & (1 << 0)) ? 1 : 0; |
| 188 | + g_rightTurn = (bits & (1 << 1)) ? 1 : 0; |
| 189 | + g_cel = (bits & (1 << 2)) ? 1 : 0; |
| 190 | + g_highBeam = (bits & (1 << 3)) ? 1 : 0; |
| 191 | + g_handbrake = (bits & (1 << 4)) ? 1 : 0; |
| 192 | + } break; |
| 193 | + |
| 194 | + default: |
| 195 | + break; |
| 196 | + } |
| 197 | +} |
| 198 | + |
| 199 | +// ===================== BUILD THE 87-BYTE OCH BLOCK ===================== |
| 200 | +static void buildOchBlock() { |
| 201 | + // Clear everything (anything not defined in your INI stays 0) |
| 202 | + memset(och, 0, sizeof(och)); |
| 203 | + |
| 204 | + // Match INI offsets exactly :contentReference[oaicite:4]{index=4} |
| 205 | + // rpm: U16 @ 0 |
| 206 | + writeU16LE(&och[0], g_rpm); |
| 207 | + |
| 208 | + // oilanalograw: U08 @ 3 (°C + 40) |
| 209 | + och[3] = tempC_to_iniRaw(g_oil_c); |
| 210 | + |
| 211 | + // mapraw: U16 @ 4 (kPa abs) |
| 212 | + writeU16LE(&och[4], g_map_kpa); |
| 213 | + |
| 214 | + // coolantanalograw: U08 @ 7 (°C + 40) |
| 215 | + och[7] = tempC_to_iniRaw(g_clt_c); |
| 216 | + |
| 217 | + // oilPressure: U08 @ 10 (PSI) |
| 218 | + och[10] = g_oil_psi; |
| 219 | + |
| 220 | + // fuelPressure: U08 @ 11 (PSI) |
| 221 | + och[11] = g_fuel_psi; |
| 222 | + |
| 223 | + // fuellevel: U08 @ 15 (%) |
| 224 | + och[15] = g_fuel_pct; |
| 225 | + |
| 226 | + // vss: U16 @ 19 (kph) |
| 227 | + writeU16LE(&och[19], g_speed_kph); |
| 228 | + |
| 229 | + // indicators: U08 @ 40..44 |
| 230 | + och[40] = g_leftTurn; |
| 231 | + och[41] = g_rightTurn; |
| 232 | + och[42] = g_cel; |
| 233 | + och[43] = g_highBeam; |
| 234 | + och[44] = g_handbrake; |
| 235 | +} |
| 236 | + |
| 237 | +// ===================== TS SERIAL COMMAND HANDLING ===================== |
| 238 | +// TS will send: |
| 239 | +// 'Q' -> expects signature string :contentReference[oaicite:5]{index=5} |
| 240 | +// 'S' -> expects version string :contentReference[oaicite:6]{index=6} |
| 241 | +// 'r' -> expects ochBlockSize bytes :contentReference[oaicite:7]{index=7} |
| 242 | +// |
| 243 | +// Important: Do NOT print debug text while TS is connected. |
| 244 | +static void handleSerialByte(uint8_t c) { |
| 245 | + if (c == 'Q') { |
| 246 | + Serial.print(SIGNATURE); |
| 247 | + return; |
| 248 | + } |
| 249 | + if (c == 'S') { |
| 250 | + Serial.print(VERSION); |
| 251 | + return; |
| 252 | + } |
| 253 | + if (c == 'r') { |
| 254 | + buildOchBlock(); |
| 255 | + Serial.write(och, OCH_BLOCK_SIZE); |
| 256 | + return; |
| 257 | + } |
| 258 | + // ignore anything else (keeps TS happy) |
| 259 | +} |
| 260 | + |
| 261 | +// ===================== SETUP ===================== |
| 262 | +void setup() { |
| 263 | + Serial.begin(BAUD_RATE); |
| 264 | + |
| 265 | + pinMode(CAN_INT_PIN, INPUT); |
| 266 | + |
| 267 | + if (CAN.begin(MCP_ANY, CAN_SPEED, CAN_CLOCK) != CAN_OK) { |
| 268 | + // No Serial prints here if you want TS to connect cleanly later. |
| 269 | + // If you need debug, temporarily uncomment this: |
| 270 | + // Serial.println("CAN INIT FAILED"); |
| 271 | + while (1) {;} |
| 272 | + } |
| 273 | + |
| 274 | + CAN.setMode(MCP_NORMAL); |
| 275 | + lastCanRxMs = millis(); |
| 276 | +} |
| 277 | + |
| 278 | +// ===================== LOOP ===================== |
| 279 | +void loop() { |
| 280 | + // --- CAN service --- |
| 281 | + if (!digitalRead(CAN_INT_PIN)) { |
| 282 | + long unsigned int rxId32; |
| 283 | + unsigned char len = 0; |
| 284 | + unsigned char buf[8]; |
| 285 | + |
| 286 | + if (CAN.readMsgBuf(&rxId32, &len, buf) == CAN_OK) { |
| 287 | + uint16_t id = (uint16_t)(rxId32 & 0x7FF); // 11-bit |
| 288 | + handleCanFrame(id, buf, len); |
| 289 | + } |
| 290 | + } |
| 291 | + |
| 292 | + // --- Optional: CAN timeout fallback (prevent stale numbers) --- |
| 293 | + if (millis() - lastCanRxMs > 1000) { |
| 294 | + g_rpm = 0; |
| 295 | + g_speed_kph = 0; |
| 296 | + // keep temps/map last-known; you can zero them too if you prefer |
| 297 | + } |
| 298 | + |
| 299 | + // --- Serial service --- |
| 300 | + while (Serial.available()) { |
| 301 | + handleSerialByte((uint8_t)Serial.read()); |
| 302 | + } |
| 303 | +} |
0 commit comments