├── .hgignore ├── README.md ├── kawaduino.h └── kawaduino.ino /.hgignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomnz/kawaduino/02d1278806635b166deaaf3b6f9edfbf58e7ca02/.hgignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kawaduino # 2 | 3 | This is an Arduino project that uses an external IC to query the onboard ECU of a Kawasaki motorcycle. Various data can be read back from the bike, including RPM, speed, throttle position, and sensor data. 4 | 5 | You can see a video of an early prototype here: https://www.youtube.com/watch?v=ie-Pfxzt-yQ 6 | 7 | ### Application 8 | 9 | This particular script is designed to power a string of AdaFruit NeoPixel LEDs like [these ones](http://www.adafruit.com/products/1461). In its current state, the script has two modes - one that is based on RPM (animates color), and one based on speed (animates moving dots). 10 | 11 | ### Details 12 | 13 | A few miscellaneous points about the script and hardware: 14 | 15 | * If the link with the ECU is broken for any reason (e.g. switching off the Arduino, and back on again), it will not accept a new connection for 10 seconds or so - in this time the LEDs will not do anything... Just give it a minute 16 | * You'll notice when you switch modes that it blips an animation over the LEDs for a split second. The reason for this is convoluted, but interesting... The rate at which the ECU can be queried maxes out at around twice per second - so if you were to update the LEDs after each query, then the animation would be extremely choppy. However, the querying process is fairly sparse - there are a lot of "waits" in order to meet the requirements of the communication protocol. This gives an opportunity to update the LEDs during the communication downtime. I originally tried to achieve this using interrupts to call out to the animation function (enabling/disabling the interrupts in each wait period), but occasionally the timing would be thrown off by an LED update running slightly too long, and the communication would be cut off unpredictably. Instead of relying on the hardware to do timing, I decided to implement my own "wait" function that would just cycle the LEDs continuously until the required time had passed - but this requires knowing how long each animation cycle takes in order to not overshoot the timing. Hence, whenever switching modes (or turning on the Arduino), the animation runs a bunch of cycles to determine the average refresh time. This is what causes the "blip". 17 | * Because we are animating often, but updating the variables from the ECU only occasionally, some work is needed to "smooth" out the changes in raw data... This means the animations look much nicer, but at the cost of introducing some lag time between receiving a value from the ECU, and the LEDs reflecting that value 18 | 19 | ### Hardware 20 | 21 | There are several types of chips that can facilitate communication with the ECU. One example of a chip that works is the [MC33660 SOIC](http://www.digikey.com/product-detail/en/MC33660EFR2/MC33660EFR2CT-ND/5215177). I'd recommend picking up an SOIC to DIP adapter off eBay to make working with the chip easier. Be sure to check the data sheet for the chip that you choose - many will benefit from external caps/resistors to provide protection from voltage spikes. 22 | 23 | Obviously you will also need an LED string. I'd recommend a button to switch modes - and possibly a switch to disable the whole system when you need to. A rough overview of my entire installation: 24 | 25 | * I have a 12V -> 5V DC converter that ultimately powers both the LEDs and the Arduino 26 | * A relay switches power to the converter on/off when the bike is powered on, meaning no dead battery! 27 | * A physical switch under the tail of the bike can break the relay on signal - allowing me to kill power with the switch 28 | * A physical button under the tail lets me switch between the modes 29 | * The IC and various caps/resistors are soldered onto a prototyping shield, and wrapped up in a makeshift container with the Arduino 30 | * Wires with pins soldered on the end run to the ECU port (I was unable to source the male plug) 31 | * Two LED strings are snaked through the internals of the bike, lighting up the engine and wheels -------------------------------------------------------------------------------- /kawaduino.h: -------------------------------------------------------------------------------- 1 | class Color { 2 | public: 3 | Color() {}; 4 | Color(int r, int g, int b) : r(r), g(g), b(b) {}; 5 | int r, g, b; 6 | uint32_t toUint32() { return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; }; 7 | }; 8 | -------------------------------------------------------------------------------- /kawaduino.ino: -------------------------------------------------------------------------------- 1 | /* 2 | KAWADUINO 3 | 4 | Reads data from a Kawasaki ECU Diagnostic Port, and updates an LED string based on RPMs 5 | read from the ECU. This code is adapted from code originally written by Greebo on page 2 6 | of the following thread: 7 | 8 | http://ecuhacking.activeboard.com/t56234221/kds-protocol/ 9 | 10 | KDS Packet format is: 11 | 12 | 0x8? - Start Addressed Packet, ? = 1, one byte packet. ? = 0, packet size below. 13 | 0x?? - Target Address (ECU = 0x11) 14 | 0x?? - Source Address (GiPro = 0xF1) 15 | 0x?? - Single byte command/response if first byte is 0x?1, otherwise number of bytes (n). 16 | .... - (n) bytes of command/response data 17 | 0x?? - Checksum = sum of all previous bytes & 0xFF 18 | 19 | Commands/Responses: 20 | 0x81 - Start Communication Request 21 | 0xC1 0xEA 0x8F - Start Communication Accepted 22 | 0x10 0x80 - Start Diagnostic Session Request 23 | 0x50 0x80 - Start Diagnostic Session accepted 24 | 0x21 0x?? - Request register 0x?? value 25 | 0x61 0x?? 0x## ... - Register response for register 0x??, value(s) 0x## 26 | 0x7F 0x21 0x## - Negative response for register, error code 0x## 27 | 28 | 2013 Z1000SX (Ninja 1000) 29 | Registers (byte responses are: a, b, c...): 30 | 00 (4 bytes): ? 31 | 01 (1 byte): ? 32 | 02 (1 byte): ? 33 | 04 (2 bytes): Throttle Position Sensor: 0% = 0x00 0xD8, 100% = 0x03 0x7F /// TODO: VERIFY 34 | 05 (2 bytes): Air Pressure = ?? 35 | 06 (1 byte): Engine Coolant Temperature = (a - 48) / 1.6 36 | 07 (1 bytes): Intake Air Temperature 37 | 08 (2 bytes): Abs Pressure(?) 38 | 09 (2 bytes): Engine RPM = (a * 100) + b ... 39 | 0A (1 byte): ? 40 | 0B (1 byte): Gear Position = x 41 | 0C (2 bytes): Speed = (a << 8 + b) / 2 42 | 20 (4 bytes): ? 43 | 27 (1 byte): ? 44 | 28 (1 byte): ? 45 | 29 (1 byte): ? 46 | 2A (1 byte): ? 47 | 2E (1 byte): ? 48 | 31 (1 byte): ? 49 | 32 (1 byte): ? 50 | 33 (1 byte): ? 51 | 3C (1 byte): ? 52 | 3D (1 byte): ? 53 | 3E (1 byte): ? 54 | 3F (1 byte): ? 55 | 40 (4 bytes): ? 56 | 44 (4 bytes): ? 57 | 54 (2 bytes): ? 58 | 56 (1 byte): ? 59 | 5B (1 byte): ? 60 | 5C (1 byte): ? 61 | 5D (1 byte): ? 62 | 5E (1 byte): ? 63 | 5F (1 byte): ? 64 | 60 (4 bytes): ? 65 | 61 (1 byte): ? 66 | 62 (2 bytes): ? 67 | 63 (1 byte): ? 68 | 64 (1 byte): ? 69 | 65 (1 byte): ? 70 | 66 (1 byte): ? 71 | 67 (1 byte): ? 72 | 68 (1 byte): ? 73 | 6E (1 byte): ? 74 | 6F (1 byte): ? 75 | 80 (4 bytes): ? 76 | 9B (1 byte): ? 77 | A0 (4 bytes): ? 78 | B4 (1 byte): ? 79 | 80 | From ISO14230-2: 81 | Time (ms) Sequence 82 | 5-20 Inter byte time in tester request 83 | 0-20 Inter byte timing in ECU response 84 | 25-50 Time between end of tester request and start of ECU response or between ECU responses 85 | 25-5000 Extended mode for "rspPending" 86 | 55-5000 Time between end of ECU response and start of new tester request, or time between end of tester 87 | request and start of new request if ECU doesn't respond 88 | 89 | */ 90 | 91 | #include 92 | #include 93 | #include "kawaduino.h" 94 | 95 | #define K_OUT 1 // K Output Line - TX on Arduino 96 | #define K_IN 0 // K Input Line - RX on Arduino 97 | #define SERIAL_ON 3 98 | 99 | // Animation settings 100 | #define REFRESH_MICROS 30000 101 | // Mode 1 102 | #define MAX_RPM 7000 103 | #define MIN_RPM 1200 104 | #define MIN_COL 180 105 | #define MAX_COL 255 106 | #define MIN_BRIGHT 25 107 | #define MAX_BRIGHT 255 108 | // Mode 2 109 | #define MAX_MPH2 160 110 | #define MPH_DOTS 3 111 | #define MPH_DOT_SIZE 3 112 | #define MIN_MPH2 4 113 | #define BACKGROUND_MAX 0.4 114 | #define BACKGROUND_INCREASE 0.1 115 | #define BACKGROUND_DECREASE 0.7 116 | #define BACKGROUND_COLOR 180 117 | #define PROGRESS_MPH2_MULT 1 118 | #define DAMPED_MPH2_FACTOR 10 119 | 120 | // LED settings 121 | #define N_PIXELS 60 122 | #define LED_PIN 6 123 | #define DIAG_LED1 4 124 | #define DIAG_LED2 5 125 | #define BOARD_LED 13 126 | 127 | // Modes 128 | #define N_MODES 2 129 | #define MODE_ADDR 0 130 | #define BTN_PIN 7 131 | 132 | // Startup 133 | #define AVG_CYCLES 50 134 | 135 | // Timings 136 | #define MAXSENDTIME 2000 // 2 second timeout on KDS comms. 137 | const uint32_t ISORequestByteDelay = 10; 138 | const uint32_t ISORequestDelay = 40; // Time between requests. 139 | 140 | // Addresses 141 | const uint8_t ECUaddr = 0x11; 142 | const uint8_t myAddr = 0xF2; 143 | 144 | const uint8_t validRegs[] = { 0x00, 0x01, 0x02, 0x04, 0x05, 0x06, 0x07, 0x08, 145 | 0x09, 0x0A, 0x0B, 0x0C, 0x20, 0x27, 0x28, 0x29, 0x2A, 0x2E, 0x31, 0x32, 146 | 0x33, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x44, 0x54, 0x56, 0x5B, 0x5C, 0x5D, 147 | 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x6E, 148 | 0x6F, 0x80, 0x9B, 0xA0, 0xB4 }; 149 | 150 | const uint8_t numValidRegs = (uint8_t)(sizeof(validRegs)); 151 | 152 | 153 | Adafruit_NeoPixel 154 | strip = Adafruit_NeoPixel(N_PIXELS, LED_PIN, NEO_GRB + NEO_KHZ800); 155 | 156 | 157 | bool ECUconnected = false; 158 | 159 | // Animation variables 160 | unsigned long lastFrameTime = 0; 161 | // Mode 1 162 | uint32_t rpms = 0; 163 | uint32_t dampedRpms = 0; 164 | // Mode 2 165 | uint32_t mph2 = 0; 166 | float dampedMph2 = 0; 167 | float backgroundLevel = 0; 168 | float progress = 0; 169 | 170 | // Modes 171 | uint8_t mode = 0; 172 | boolean btnPressed = false; 173 | 174 | void setup() { 175 | // Setup pins 176 | pinMode(K_OUT, OUTPUT); 177 | pinMode(K_IN, INPUT); 178 | #ifdef SERIAL_ON 179 | pinMode(SERIAL_ON, OUTPUT); 180 | #endif 181 | #ifdef DIAG_LED1 182 | pinMode(DIAG_LED1, OUTPUT); 183 | #endif 184 | #ifdef DIAG_LED2 185 | pinMode(DIAG_LED2, OUTPUT); 186 | #endif 187 | pinMode(BOARD_LED, OUTPUT); 188 | digitalWrite(BOARD_LED, LOW); 189 | 190 | // Show startup routine 191 | strip.begin(); 192 | 193 | startupLeds(); 194 | 195 | // Read mode 196 | mode = EEPROM.read(MODE_ADDR); 197 | 198 | lastFrameTime = micros(); 199 | 200 | // Determine duration of updateLeds() 201 | determineAverage(); 202 | 203 | strip.clear(); 204 | strip.show(); 205 | } 206 | 207 | void loop() { 208 | // Init blink 209 | #ifdef SERIAL_ON 210 | digitalWrite(SERIAL_ON, HIGH); 211 | #endif 212 | digitalWrite(BOARD_LED, HIGH); 213 | delay(200); 214 | digitalWrite(BOARD_LED, LOW); 215 | delay(200); 216 | digitalWrite(BOARD_LED, HIGH); 217 | delay(200); 218 | digitalWrite(BOARD_LED, LOW); 219 | 220 | uint8_t cmdSize; 221 | uint8_t cmdBuf[6]; 222 | uint8_t respSize; 223 | uint8_t respBuf[12]; 224 | uint8_t ect; 225 | 226 | if (!ECUconnected) { 227 | // Start KDS comms 228 | ECUconnected = initPulse(); 229 | 230 | if (ECUconnected) { 231 | // Show we're connected 232 | digitalWrite(BOARD_LED, HIGH); 233 | } else { 234 | } 235 | } 236 | 237 | // Endless loop. 238 | boolean diag1On = true; 239 | while (ECUconnected) { 240 | doButton(); 241 | 242 | // Send register requests 243 | cmdSize = 2; // each request is a 2 byte packet. 244 | cmdBuf[0] = 0x21; // Register request cmd 245 | // Response to a register request is either: 246 | // 0x61 - Register read OK 247 | // 0x?? - Register requested 248 | // 0x?? - Value byte 1 249 | // ... - (if more than 1 byte value - remainder of values) 250 | // ___ or: 251 | // 0x7F - Error response 252 | // 0x21 - command (0x21 = Read register) 253 | // 0x?? - error code (0x10 = General Reject: The service is rejected 254 | // but the server does not specify the reason of the rejection 255 | 256 | // Grab RPMs 257 | if (mode == 1) { 258 | for (uint8_t i = 0; i < 5; i++) respBuf[i] = 0; 259 | 260 | // Request RPM is register: 0x09 261 | cmdBuf[1] = 0x09; 262 | respSize = sendRequest(cmdBuf, respBuf, cmdSize, 12); 263 | if (respSize == 4) { 264 | // Formula for RPMs from response 265 | rpms = respBuf[2] * 100 + respBuf[3]; 266 | 267 | // Conform RPMs 268 | rpms = max(min(rpms, MAX_RPM), MIN_RPM); 269 | 270 | #ifdef DIAG_LED1 271 | // Diagnostic blink to show update rate 272 | if (diag1On) { 273 | digitalWrite(DIAG_LED1, HIGH); 274 | } 275 | else { 276 | digitalWrite(DIAG_LED1, LOW); 277 | } 278 | diag1On = !diag1On; 279 | #endif 280 | } 281 | else if (respSize == 0) { 282 | ECUconnected = false; 283 | break; 284 | } 285 | delayLeds(ISORequestDelay, true); 286 | } 287 | 288 | // Gram Speed 289 | if (mode == 2) { 290 | for (uint8_t i = 0; i < 5; i++) respBuf[i] = 0; 291 | 292 | // Request Speed is register: 0x0C 293 | cmdBuf[1] = 0x0C; 294 | respSize = sendRequest(cmdBuf, respBuf, cmdSize, 12); 295 | if (respSize == 4) { 296 | // NOTE: Actual MPH is this value halved, but we want to 297 | // keep full available resolution 298 | mph2 = (respBuf[2] << 8) + respBuf[3]; 299 | 300 | #ifdef DIAG_LED1 301 | // Diagnostic blink to show update rate 302 | if (diag1On) { 303 | digitalWrite(DIAG_LED1, HIGH); 304 | } 305 | else { 306 | digitalWrite(DIAG_LED1, LOW); 307 | } 308 | diag1On = !diag1On; 309 | #endif 310 | } 311 | else if (respSize == 0) { 312 | ECUconnected = false; 313 | break; 314 | } 315 | delayLeds(ISORequestDelay, true); 316 | } 317 | 318 | } 319 | 320 | // Housekeeping 321 | digitalWrite(BOARD_LED, LOW); 322 | #ifdef SERIAL_ON 323 | digitalWrite(SERIAL_ON, LOW); 324 | #endif 325 | strip.clear(); 326 | strip.show(); 327 | 328 | delay(5000); 329 | } 330 | 331 | 332 | unsigned long avg = 0; 333 | #ifdef DIAG_LED2 334 | boolean diag2On = false; 335 | #endif 336 | 337 | // One-time function to measure average runtime of 338 | // updateLeds() - called at startup 339 | void determineAverage() { 340 | boolean oldECUconnected = ECUconnected; 341 | uint32_t oldRpms = rpms; 342 | uint32_t oldMph2 = mph2; 343 | ECUconnected = true; 344 | unsigned long start = micros(); 345 | 346 | for (int i = 0; i < AVG_CYCLES; i++) { 347 | // Update these values each time to make sure the function 348 | // is working as hard as possible 349 | rpms = map(i, 0, AVG_CYCLES, MIN_RPM, MAX_RPM); 350 | dampedRpms = rpms; 351 | mph2 = map(i, 0, AVG_CYCLES, 0, MAX_MPH2); 352 | dampedMph2 = mph2; 353 | updateLeds(); 354 | } 355 | avg = (micros() - start) / AVG_CYCLES; 356 | 357 | rpms = oldRpms; 358 | dampedRpms = oldRpms; 359 | mph2 = oldMph2; 360 | dampedMph2 = oldMph2; 361 | ECUconnected = oldECUconnected; 362 | } 363 | 364 | 365 | boolean doButton() { 366 | #ifdef BTN_PIN 367 | // Check the button 368 | if (digitalRead(BTN_PIN) == HIGH) { 369 | if (!btnPressed) { 370 | // This is the first time we're seeing the press 371 | btnPressed = true; 372 | 373 | // Increment the mode 374 | mode++; 375 | if (mode > N_MODES) { 376 | mode = 1; 377 | } 378 | 379 | // Save it 380 | EEPROM.write(MODE_ADDR, mode); 381 | 382 | // Calculate the average again 383 | determineAverage(); 384 | 385 | // Reset 386 | resetLeds(); 387 | 388 | return true; 389 | } 390 | } else { 391 | btnPressed = false; 392 | } 393 | #endif 394 | return false; 395 | } 396 | 397 | // Custom delay routine that updates LEDs while idle 398 | void delayLeds(unsigned long ms, boolean safe) { 399 | unsigned long last = micros(); 400 | unsigned long lastUpdate = 0; 401 | unsigned long first = last; 402 | 403 | // Run as long as we haven't exceeded given ms 404 | while ((last - first) < ms * 1000) { 405 | unsigned long curr = micros(); 406 | 407 | // Refresh the lights if we go over a given interval, and we'll have time 408 | // Note that this conservatively will NOT run updateLeds if it doesn't look 409 | // like there will be enough time to complete 410 | boolean changed = false; 411 | if (curr - lastUpdate > REFRESH_MICROS && ((curr - first) + avg*4 < ms * 1000)) { 412 | if (!safe) { 413 | changed = doButton(); 414 | } 415 | updateLeds(); 416 | 417 | last = micros(); 418 | lastUpdate = last; 419 | if (!changed) { 420 | if (avg == 0) { 421 | avg = last - curr; 422 | } else { 423 | avg = (avg * 15 + (last - curr)) >> 4; 424 | } 425 | } 426 | } else { 427 | last = curr; 428 | } 429 | } 430 | } 431 | 432 | 433 | void resetLeds() { 434 | strip.clear(); 435 | strip.setBrightness(255); 436 | strip.show(); 437 | backgroundLevel = 0; 438 | } 439 | 440 | 441 | // Show the next frame on the LEDs 442 | void updateLeds() { 443 | unsigned long currTime = micros(); 444 | unsigned long frameTime = currTime - lastFrameTime; 445 | lastFrameTime = currTime; 446 | 447 | if (!ECUconnected) { 448 | return; 449 | } 450 | 451 | #ifdef DIAG_LED2 452 | // Diagnostic blink 453 | if (diag2On) { 454 | digitalWrite(DIAG_LED2, HIGH); 455 | } 456 | else { 457 | digitalWrite(DIAG_LED2, LOW); 458 | } 459 | 460 | diag2On = !diag2On; 461 | #endif 462 | 463 | switch(mode) { 464 | case 1: 465 | doMode1(frameTime); 466 | break; 467 | case 2: 468 | doMode2(frameTime); 469 | break; 470 | } 471 | } 472 | 473 | 474 | // Show frame for mode 1 475 | void doMode1(unsigned long frameTime) { 476 | // Uncomment for test RPMs 477 | //rpms += 100; 478 | //if (rpms > MAX_RPM) { 479 | // rpms = 0; 480 | //} 481 | 482 | // Update rpms 483 | dampedRpms = (dampedRpms * 7 + rpms) >> 3; 484 | 485 | // Set brightness 486 | strip.setBrightness(map(dampedRpms, MIN_RPM, MAX_RPM, MIN_BRIGHT, MAX_BRIGHT)); 487 | 488 | // Grab color for RPM 489 | Color col = wheel(map(dampedRpms, MIN_RPM, MAX_RPM, MIN_COL, MAX_COL)); 490 | 491 | // Display 492 | strip.clear(); 493 | for (uint8_t k = 0; k < N_PIXELS; k++) { 494 | strip.setPixelColor(k, col.r, col.g, col.b); 495 | } 496 | 497 | // Random pixel for visual refresh representation 498 | //strip.setPixelColor(random(N_PIXELS), 255, 255, 255); 499 | 500 | strip.show(); 501 | } 502 | 503 | // Show frame for mode 2 504 | void doMode2(unsigned long frameTime) { 505 | strip.clear(); 506 | strip.setBrightness(255); 507 | float frameSecs = (float)frameTime / 1000000; 508 | 509 | // Update mph 510 | dampedMph2 = (dampedMph2 * DAMPED_MPH2_FACTOR + mph2) / (DAMPED_MPH2_FACTOR + 1); 511 | 512 | // Update the background 513 | if (mph2 > MIN_MPH2) { 514 | backgroundLevel -= (float)BACKGROUND_DECREASE * frameSecs; 515 | } else { 516 | backgroundLevel += (float)BACKGROUND_INCREASE * frameSecs; 517 | } 518 | backgroundLevel = max(0, min(backgroundLevel, BACKGROUND_MAX)); 519 | 520 | // Show background 521 | if (backgroundLevel > 0) { 522 | Color bgBase = wheel(BACKGROUND_COLOR); 523 | uint32_t bg = Color((float)bgBase.r * backgroundLevel, (float)bgBase.g * backgroundLevel, (float)bgBase.b * backgroundLevel).toUint32(); 524 | 525 | for (int i = 0; i < N_PIXELS; i++) { 526 | strip.setPixelColor(i, bg); 527 | } 528 | } 529 | 530 | // Increase progress 531 | progress += dampedMph2 * (float)PROGRESS_MPH2_MULT * frameSecs; 532 | while (progress > N_PIXELS - 1) { 533 | progress -= N_PIXELS; 534 | } 535 | 536 | // Show mph points 537 | if (dampedMph2 > MIN_MPH2) { 538 | // Get the color for the dots 539 | uint32_t col = wheel(map(dampedMph2, 0, MAX_MPH2, MIN_COL, MAX_COL)).toUint32(); 540 | for (int p = 0; p < MPH_DOTS; p++) { 541 | // Get the dot position 542 | int pos = (int)progress + N_PIXELS * p / MPH_DOTS; 543 | if (pos > N_PIXELS - 1) { 544 | pos -= N_PIXELS; 545 | } 546 | 547 | for (int i = 0; i < MPH_DOT_SIZE; i++) { 548 | int n = pos + i; 549 | if (n > N_PIXELS - 1) { 550 | n -= N_PIXELS; 551 | } 552 | 553 | strip.setPixelColor(n, col); 554 | } 555 | } 556 | } 557 | 558 | strip.show(); 559 | } 560 | 561 | 562 | // Run startup routine on LEDs (purely cosmetic!) 563 | void startupLeds() { 564 | strip.clear(); 565 | strip.setBrightness(255); 566 | 567 | // Show 568 | for (uint8_t i = 0; i < N_PIXELS; i++) { 569 | strip.setPixelColor(i, wheel(i * 255 / N_PIXELS).toUint32()); 570 | strip.show(); 571 | delay(1000 / N_PIXELS); 572 | } 573 | 574 | // Hide 575 | for (uint8_t i = 0; i < N_PIXELS; i++) { 576 | strip.setPixelColor(i, 0); 577 | strip.show(); 578 | delay(1000 / N_PIXELS); 579 | } 580 | 581 | strip.clear(); 582 | strip.show(); 583 | } 584 | 585 | 586 | // Initialize connection to ECU 587 | bool initPulse() { 588 | uint8_t rLen; 589 | uint8_t req[2]; 590 | uint8_t resp[3]; 591 | 592 | Serial.end(); 593 | 594 | // This is the ISO 14230-2 "Fast Init" sequence. 595 | digitalWrite(K_OUT, HIGH); 596 | delay(300); 597 | digitalWrite(K_OUT, LOW); 598 | delay(25); 599 | digitalWrite(K_OUT, HIGH); 600 | delay(25); 601 | 602 | Serial.begin(10400); 603 | 604 | // Start Communication is a single byte "0x81" packet. 605 | req[0] = 0x81; 606 | rLen = sendRequest(req, resp, 1, 3); 607 | 608 | delay(ISORequestDelay); 609 | // Response should be 3 bytes: 0xC1 0xEA 0x8F 610 | if ((rLen == 3) && (resp[0] == 0xC1) && (resp[1] == 0xEA) && (resp[2] == 0x8F)) { 611 | // Success, so send the Start Diag frame 612 | // 2 bytes: 0x10 0x80 613 | req[0] = 0x10; 614 | req[1] = 0x80; 615 | rLen = sendRequest(req, resp, 2, 3); 616 | 617 | // OK Response should be 2 bytes: 0x50 0x80 618 | if ((rLen == 2) && (resp[0] == 0x50) && (resp[1] == 0x80)) { 619 | return true; 620 | } 621 | } 622 | // Otherwise, we failed to init. 623 | return false; 624 | } 625 | 626 | 627 | // Send a request to the ECU and wait for the response 628 | // request = buffer to send 629 | // response = buffer to hold the response 630 | // reqLen = length of request 631 | // maxLen = maximum size of response buffer 632 | // 633 | // Returns: number of bytes of response returned. 634 | uint8_t sendRequest(const uint8_t *request, uint8_t *response, uint8_t reqLen, uint8_t maxLen) { 635 | uint8_t buf[16], rbuf[16]; 636 | uint8_t bytesToSend; 637 | uint8_t bytesSent = 0; 638 | uint8_t bytesToRcv = 0; 639 | uint8_t bytesRcvd = 0; 640 | uint8_t rCnt = 0; 641 | uint8_t c, z; 642 | bool forMe = false; 643 | char radioBuf[32]; 644 | uint32_t startTime; 645 | 646 | for (uint8_t i = 0; i < 16; i++) { 647 | buf[i] = 0; 648 | } 649 | 650 | // Zero the response buffer up to maxLen 651 | for (uint8_t i = 0; i < maxLen; i++) { 652 | response[i] = 0; 653 | } 654 | 655 | // Form the request: 656 | if (reqLen == 1) { 657 | buf[0] = 0x81; 658 | } else { 659 | buf[0] = 0x80; 660 | } 661 | buf[1] = ECUaddr; 662 | buf[2] = myAddr; 663 | 664 | if (reqLen == 1) { 665 | buf[3] = request[0]; 666 | buf[4] = calcChecksum(buf, 4); 667 | bytesToSend = 5; 668 | } else { 669 | buf[3] = reqLen; 670 | for (z = 0; z < reqLen; z++) { 671 | buf[4 + z] = request[z]; 672 | } 673 | buf[4 + z] = calcChecksum(buf, 4 + z); 674 | bytesToSend = 5 + z; 675 | } 676 | 677 | // Now send the command... 678 | for (uint8_t i = 0; i < bytesToSend; i++) { 679 | bytesSent += Serial.write(buf[i]); 680 | delay(ISORequestByteDelay); 681 | } 682 | 683 | // Wait required time for response. 684 | delayLeds(ISORequestDelay, false); 685 | 686 | startTime = millis(); 687 | 688 | // Wait for and deal with the reply 689 | while ((bytesRcvd <= maxLen) && ((millis() - startTime) < MAXSENDTIME)) { 690 | if (Serial.available()) { 691 | c = Serial.read(); 692 | startTime = millis(); // reset the timer on each byte received 693 | 694 | delayLeds(ISORequestByteDelay, true); 695 | 696 | rbuf[rCnt] = c; 697 | switch (rCnt) { 698 | case 0: 699 | // should be an addr packet either 0x80 or 0x81 700 | if (c == 0x81) { 701 | bytesToRcv = 1; 702 | } else if (c == 0x80) { 703 | bytesToRcv = 0; 704 | } 705 | rCnt++; 706 | break; 707 | case 1: 708 | // should be the target address 709 | if (c == myAddr) { 710 | forMe = true; 711 | } 712 | rCnt++; 713 | break; 714 | case 2: 715 | // should be the sender address 716 | if (c == ECUaddr) { 717 | forMe = true; 718 | } else if (c == myAddr) { 719 | forMe = false; // ignore the packet if it came from us! 720 | } 721 | rCnt++; 722 | break; 723 | case 3: 724 | // should be the number of bytes, or the response if its a single byte packet. 725 | if (bytesToRcv == 1) { 726 | bytesRcvd++; 727 | if (forMe) { 728 | response[0] = c; // single byte response so store it. 729 | } 730 | } else { 731 | bytesToRcv = c; // number of bytes of data in the packet. 732 | } 733 | rCnt++; 734 | break; 735 | default: 736 | if (bytesToRcv == bytesRcvd) { 737 | // must be at the checksum... 738 | if (forMe) { 739 | // Only check the checksum if it was for us - don't care otherwise! 740 | if (calcChecksum(rbuf, rCnt) == rbuf[rCnt]) { 741 | // Checksum OK. 742 | return(bytesRcvd); 743 | } else { 744 | // Checksum Error. 745 | return(0); 746 | } 747 | } 748 | // Reset the counters 749 | rCnt = 0; 750 | bytesRcvd = 0; 751 | 752 | // ISO 14230 specifies a delay between ECU responses. 753 | delayLeds(ISORequestDelay, true); 754 | } else { 755 | // must be data, so put it in the response buffer 756 | // rCnt must be >= 4 to be here. 757 | if (forMe) { 758 | response[bytesRcvd] = c; 759 | } 760 | bytesRcvd++; 761 | rCnt++; 762 | } 763 | break; 764 | } 765 | } 766 | } 767 | 768 | return false; 769 | } 770 | 771 | // Checksum is simply the sum of all data bytes modulo 0xFF 772 | // (same as being truncated to one byte) 773 | uint8_t calcChecksum(uint8_t *data, uint8_t len) { 774 | uint8_t crc = 0; 775 | 776 | for (uint8_t i = 0; i < len; i++) { 777 | crc = crc + data[i]; 778 | } 779 | return crc; 780 | } 781 | 782 | 783 | // Input a value 0 to 255 to get a color value. 784 | // The colors are a transition r - g - b - back to r. 785 | Color wheel(byte wheelPos) { 786 | if(wheelPos < 85) { 787 | return Color(255 - wheelPos * 3, 0, wheelPos * 3); 788 | } else if(wheelPos < 170) { 789 | wheelPos -= 85; 790 | return Color(0, wheelPos * 3, 255 - wheelPos * 3); 791 | } else { 792 | wheelPos -= 170; 793 | return Color(wheelPos * 3, 255 - wheelPos * 3, 0); 794 | } 795 | } 796 | 797 | --------------------------------------------------------------------------------