GM ECU Simulator

hjtrbo
Posts: 353
Joined: Tue Jul 06, 2021 8:57 am
cars: VF2 R8 LSA
FG XR6T
HJ Ute w/RB25DET

Re: GM ECU Simulator

Post by hjtrbo »

Added can broadcast support. Roll your own or import a dbc file. Tidying some loose ends. Starting to have a play with getting a Ford ecu going. Different learning curve - mode A1 rapid packet protocol.
You do not have the required permissions to view the files attached to this post.
User avatar
Tre-Cool
Posts: 533
Joined: Tue Oct 16, 2012 2:17 am
cars: VY SS UTE, VX Drag Car
Location: Perth

Re: GM ECU Simulator

Post by Tre-Cool »

I initially had some success using the ecu simulator function for can broadcast but, ended up getting claude to write me a new program to playback my existing captured files from my ixxat cable. (also had it write me a new can capture program for the cable too).
Can-Replay-Capture.png
This lets me connect the TouchGFX simulator & present real world CAN data rates so i could tweak the refresh rates on the RPM data.
Simulatr.png
Been working on another project for Efilive Cax generation, that i've wanted done for ages.. Now Im doing it with zero code experiance/knowledge. Will post seperate thread on that, once i test everything functions correctly.
You do not have the required permissions to view the files attached to this post.
jxx
Posts: 222
Joined: Tue Oct 25, 2011 9:47 am
cars: To many
Location: Vic

Re: GM ECU Simulator

Post by jxx »

Wish i'd seen this before I went on a mission to make similar thing for ALDL.
This new cheat mode for coding is both ridiculous and amazing.

I'll remove this if it's unwelcome, not trying to hijack the thread.

I hadn't looked at claude so thought i'd give it a test.
Just threw my aldl sim code at it and told it i wanted to port to an arduino uno so i could connect to it with with software to read a static output, then changed my mind to make it more realistic.
I want to be cruising down the highway at 110 km/h on a hot aussie summer day with the ac going......... nice thought for this crappy cold june weather.

lets just say i now have a device that could inject the aldl datastream into a vehicle to test dash etc or just plug it into a pc and connect software to it to test da.
Outputting the ALDL single wire wasn't even my idea for this, claude just decided it was mentioned in the source code and added it.

Code: Select all

/*
 * aldl_ecu_sim_uno.ino
 * -------------------------------------------------------------------------
 * A minimal ALDL ECU responder for the Arduino Uno (ATmega328P).
 *
 * Ported down from the `aldl-ecu-simulator` Rust crate (v0.1.3). This build
 * simulates a car CRUISING on the highway at a steady 110 km/h:
 *
 *   ~1950 rpm (top gear, TCC locked), coolant 90 C, A/C running (request +
 *   clutch), high-speed fan on for a 38 C Australian summer day, intake air
 *   ~45 C, light cruise load (MAP ~47 kPa, TPS ~12%), closed-loop fuelling,
 *   battery 14.2 V. No faults, MIL off.
 *
 * Unlike a static dump, the live channels gently FLUCTUATE to look like a
 * real car holding a steady speed: RPM wanders a few revs, road speed drifts
 * around 110, MAP/throttle nibble up and down, the O2 sensor sweeps across
 * stoich in closed loop, and the temps/battery drift a touch. All movement is
 * a bounded integer random-walk (plus a triangle sweep for O2) applied to the
 * RAW datastream bytes -- no floating-point runs on the AVR.
 *
 * The baseline frame was generated by the real Rust encoder and decodes
 * cleanly through the crate's protocol layer; the wander only nudges
 * individual bytes within realistic bands and the checksum is recomputed
 * every frame.
 *
 *   Mode 1 (live data)  sub-id 0x00 -> 56-byte "full Petrol" frame
 *                       sub-id 0x06 ->  8-byte "Message 6" frame
 *                       sub-id 0x07 -> 10-byte "Message 6 Dyno" frame
 *   Mode 4 / Mode 8     acknowledged / ignored (nothing to mutate)
 *
 * WIRING
 *   ALDL runs on the hardware UART (pin 0 = RX, pin 1 = TX) at 8192 baud.
 *   For PC software (e.g. TunerPro DA), just use the USB cable -- the Uno's
 *   USB port IS the serial port; no adapter, diode, or resistor needed.
 *   For a hardware scan tool on a real 1-wire ALDL bus, bridge pins 0/1 with
 *   the passive diode+resistor adapter from the crate's USAGE.md.
 *
 *   NOTE: pins 0/1 are shared with the USB-serial chip, so if anything is
 *   wired to them, disconnect it while flashing the board.
 * -------------------------------------------------------------------------
 */

#include <Arduino.h>
#include <avr/pgmspace.h>

/* ====================== CONFIG ========================================== */

static const uint32_t ALDL_BAUD = 8192;          // Commodore ALDL ECUs
static const int8_t   ALIVE_LED_PIN = LED_BUILTIN;
static const uint16_t FRAME_TIMEOUT_MS = 40;     // inter-byte resync timeout
static const uint16_t ANIM_TICK_MS = 90;         // how often the values wander

/* ====================== PROTOCOL CONSTANTS ============================== */

static const uint8_t HEADER_TOOL_REQUEST = 0xF4;
static const uint8_t HEADER_ECM_RESPONSE = 0xF0;
static const uint8_t MSG_ID_LIVE_DATA    = 0x57;
static const uint8_t MSG_ID_DIAGNOSTIC   = 0x62;
static const uint8_t MODE_1_LIVE_DATA    = 0x01;
static const uint8_t MODE_4_DIAGNOSTIC   = 0x04;
static const uint8_t MODE_8_CHATTER      = 0x08;
static const uint8_t MODE_10_FIELD_SVC   = 0x0A;

static const uint8_t SUB_FULL = 0x00;
static const uint8_t SUB_MSG6 = 0x06;
static const uint8_t SUB_DYNO = 0x07;

/* ====================== PRE-BAKED CRUISE BASELINE ======================
 * Data sections only (no [F0 57 01] prefix, no checksum). Generated by the
 * real crate encoder at 110 km/h. The animated channels are seeded from
 * these bytes at boot; every other byte ships unchanged.
 */
const uint8_t CRUISE_FULL[] PROGMEM = {
  0x07, 0x9E, 0x80, 0x00, 0x4C, 0xAD, 0x78, 0x71, 0x63, 0x00, 0xF6, 0x36,
  0x2F, 0x1F, 0x66, 0x08, 0x00, 0xEC, 0x93, 0x81, 0x03, 0x0C, 0x93, 0x00,
  0x8E, 0x03, 0xBA, 0x21, 0x00, 0x64, 0x00, 0x00, 0x55, 0x6E, 0x00, 0x00,
  0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8F, 0x0C, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6D,
};
const uint8_t CRUISE_MSG6[] PROGMEM = {
  0x07, 0x9E, 0x00, 0x55, 0x63, 0x93, 0x93, 0x00,
};
const uint8_t CRUISE_DYNO[] PROGMEM = {
  0x07, 0x9E, 0x00, 0x55, 0x63, 0x93, 0x93, 0x1F, 0x8F, 0xAD,
};

/* ---- data-section byte offsets of the animated channels (full Petrol) --- */
enum {
  OFF_RPM_HI = 0x00, OFF_RPM_LO = 0x01,
  OFF_CLT    = 0x05,
  OFF_MAT    = 0x07,
  OFF_MAP1   = 0x08,
  OFF_TPS    = 0x0D,
  OFF_O2     = 0x0E,
  OFF_BPW_HI = 0x10, OFF_BPW_LO = 0x11,
  OFF_WB     = 0x12,
  OFF_BATT   = 0x18,
  OFF_SPK_HI = 0x1F, OFF_SPK_LO = 0x20,
  OFF_SPEED  = 0x21,
  OFF_MAP2   = 0x25,
};

/* ====================== LIVE (WANDERING) STATE =========================
 * Current raw value of each animated channel, plus its allowed band. Seeded
 * from the baseline at boot, then nudged each tick within [lo, hi].
 */
struct Chan { int16_t v, lo, hi; };
static Chan rpm, mapr, spk, bpw, o2, tps, clt, mat, batt, wb;
static int8_t  speedOff = 0;       // road speed wanders +/-1 around baseline
static int8_t  o2dir = 1;          // O2 sweep direction
static int8_t  slowDiv = 0;        // divides the tick rate for slow channels
static uint32_t lastTick = 0;

static const int16_t O2_LO = 25, O2_HI = 190, O2_STEP = 11; // ~100..850 mV sweep

static int16_t clampi(int16_t x, int16_t lo, int16_t hi) {
  if (x < lo) return lo; if (x > hi) return hi; return x;
}
static void seed(Chan &c, int16_t center, int16_t half) {
  c.v = center; c.lo = center - half; c.hi = center + half;
}
// small signed random walk in [-step, +step]
static void walk(Chan &c, int8_t step) {
  c.v = clampi(c.v + (int16_t)(random(2 * step + 1) - step), c.lo, c.hi);
}

static uint16_t base16(const uint8_t *p, uint8_t hiOff) {
  return ((uint16_t)pgm_read_byte(&p[hiOff]) << 8) | pgm_read_byte(&p[hiOff + 1]);
}

static void animInit() {
  seed(rpm,  base16(CRUISE_FULL, OFF_RPM_HI), 35);   // +/-35 rpm
  seed(mapr, pgm_read_byte(&CRUISE_FULL[OFF_MAP1]),  7); // +/-~2.5 kPa
  seed(spk,  base16(CRUISE_FULL, OFF_SPK_HI),  2);
  seed(bpw,  base16(CRUISE_FULL, OFF_BPW_HI),  4);
  seed(tps,  pgm_read_byte(&CRUISE_FULL[OFF_TPS]),  2);
  seed(clt,  pgm_read_byte(&CRUISE_FULL[OFF_CLT]),  2);  // +/-~1.5 C
  seed(mat,  pgm_read_byte(&CRUISE_FULL[OFF_MAT]),  2);
  seed(batt, pgm_read_byte(&CRUISE_FULL[OFF_BATT]), 1);  // +/-0.1 V
  seed(wb,   pgm_read_byte(&CRUISE_FULL[OFF_WB]),   3);
  o2.v = O2_LO; o2.lo = O2_LO; o2.hi = O2_HI;
}

static void animTick() {
  walk(rpm, 6);
  walk(mapr, 2);
  walk(spk, 1);
  walk(bpw, 1);
  walk(tps, 1);
  walk(wb, 1);
  // road speed: drift across 109..111 (offset -1..+1 from baseline 110)
  speedOff = (int8_t)clampi(speedOff + (random(3) - 1), -1, 1);
  // O2 closed-loop sweep (triangle, fast enough to look alive)
  o2.v += (int16_t)O2_STEP * o2dir;
  if (o2.v >= o2.hi) { o2.v = o2.hi; o2dir = -1; }
  if (o2.v <= o2.lo) { o2.v = o2.lo; o2dir = 1; }
  // slow drifters: only every 8th tick
  if (++slowDiv >= 8) {
    slowDiv = 0;
    walk(clt, 1);
    walk(mat, 1);
    walk(batt, 1);
  }
}

/* ====================== FRAME BUILD / SEND ============================== */

static uint8_t aldlChecksum(const uint8_t *frame, uint8_t len) {
  uint8_t sum = 0;
  for (uint8_t i = 0; i < len; i++) sum += frame[i];
  return (uint8_t)(~sum + 1);
}

static inline void put16(uint8_t *d, uint8_t off, uint16_t v) {
  d[off] = (uint8_t)(v >> 8); d[off + 1] = (uint8_t)v;
}

// Copy the baseline data section from PROGMEM and overlay the current
// wandering values. Returns the payload length.
static uint8_t buildBody(uint8_t subId, uint8_t *d) {
  if (subId == SUB_FULL) {
    memcpy_P(d, CRUISE_FULL, sizeof(CRUISE_FULL));
    put16(d, OFF_RPM_HI, (uint16_t)rpm.v);
    put16(d, OFF_BPW_HI, (uint16_t)bpw.v);
    put16(d, OFF_SPK_HI, (uint16_t)spk.v);
    d[OFF_CLT]  = (uint8_t)clt.v;
    d[OFF_MAT]  = (uint8_t)mat.v;
    d[OFF_MAP1] = (uint8_t)mapr.v;
    d[OFF_MAP2] = (uint8_t)mapr.v;
    d[OFF_TPS]  = (uint8_t)tps.v;
    d[OFF_O2]   = (uint8_t)o2.v;
    d[OFF_WB]   = (uint8_t)wb.v;
    d[OFF_BATT] = (uint8_t)batt.v;
    d[OFF_SPEED] = (uint8_t)(pgm_read_byte(&CRUISE_FULL[OFF_SPEED]) + speedOff);
    return sizeof(CRUISE_FULL);
  }
  if (subId == SUB_MSG6) {
    memcpy_P(d, CRUISE_MSG6, sizeof(CRUISE_MSG6));
    put16(d, 0, (uint16_t)rpm.v);   // RPM
    put16(d, 2, (uint16_t)spk.v);   // spark
    d[4] = (uint8_t)mapr.v;         // MAP
    d[6] = (uint8_t)wb.v;           // wideband AFR
    return sizeof(CRUISE_MSG6);
  }
  if (subId == SUB_DYNO) {
    memcpy_P(d, CRUISE_DYNO, sizeof(CRUISE_DYNO));
    put16(d, 0, (uint16_t)rpm.v);
    put16(d, 2, (uint16_t)spk.v);
    d[4] = (uint8_t)mapr.v;
    d[6] = (uint8_t)wb.v;
    d[7] = (uint8_t)tps.v;
    return sizeof(CRUISE_DYNO);
  }
  return 0;
}

static void sendLiveData(uint8_t subId) {
  uint8_t frame[3 + 56 + 1];
  frame[0] = HEADER_ECM_RESPONSE;
  frame[1] = MSG_ID_LIVE_DATA;
  frame[2] = MODE_1_LIVE_DATA;
  uint8_t n = buildBody(subId, &frame[3]);
  if (n == 0) return;             // unsupported sub-id: stay silent
  uint8_t total = 3 + n;
  frame[total] = aldlChecksum(frame, total);
  total += 1;
  Serial.write(frame, total);
  Serial.flush();
  while (Serial.available()) Serial.read(); // drop our own 1-wire echo
}

static void sendDiagAck(uint8_t ackByte) {
  uint8_t frame[5] = { HEADER_ECM_RESPONSE, MSG_ID_DIAGNOSTIC, MODE_4_DIAGNOSTIC, ackByte, 0 };
  frame[4] = aldlChecksum(frame, 4);
  Serial.write(frame, 5);
  Serial.flush();
  while (Serial.available()) Serial.read();
}

/* ====================== FRAME READER ===================================
 * Resync to a header, read the fixed request length implied by the mode,
 * validate the checksum, dispatch. Mode 8 chatter is 4 bytes (F4 56 08 AE,
 * no sub-id) -- reading it as 5 would swallow the next poll's 0xF4.
 */
static uint8_t expectedLenForMode(uint8_t mode) {
  switch (mode) {
    case MODE_1_LIVE_DATA:  return 5;
    case MODE_4_DIAGNOSTIC: return 15;
    case MODE_8_CHATTER:    return 4;
    case MODE_10_FIELD_SVC: return 7;
    default:                return 0;
  }
}

static bool readBytes(uint8_t *buf, uint8_t n) {
  uint8_t got = 0;
  uint32_t last = millis();
  while (got < n) {
    if (Serial.available()) { buf[got++] = Serial.read(); last = millis(); }
    else if (millis() - last > FRAME_TIMEOUT_MS) return false;
  }
  return true;
}

static void handleRequest(const uint8_t *frame) {
  uint8_t mode  = frame[2];
  uint8_t subId = frame[3];
  if (mode == MODE_1_LIVE_DATA) { sendLiveData(subId); return; }
  if (mode == MODE_4_DIAGNOSTIC) { sendDiagAck(0x00); return; }
  // chatter / others: ignore
}

static void serviceAldl() {
  if (!Serial.available()) return;
  int b = Serial.peek();
  if (b != HEADER_TOOL_REQUEST && b != HEADER_ECM_RESPONSE) { Serial.read(); return; }
  if (b == HEADER_ECM_RESPONSE) { Serial.read(); return; }

  uint8_t head[3];
  if (!readBytes(head, 3)) return;
  uint8_t expected = expectedLenForMode(head[2]);
  if (expected == 0) return;

  uint8_t frame[15];
  frame[0] = head[0]; frame[1] = head[1]; frame[2] = head[2];
  if (!readBytes(&frame[3], expected - 3)) return;

  uint8_t sum = 0;
  for (uint8_t i = 0; i < expected; i++) sum += frame[i];
  if (sum != 0) return;           // bad checksum: resync

  handleRequest(frame);
}

/* ====================== ARDUINO ENTRY POINTS =========================== */

void setup() {
  Serial.begin(ALDL_BAUD);
  if (ALIVE_LED_PIN >= 0) { pinMode(ALIVE_LED_PIN, OUTPUT); digitalWrite(ALIVE_LED_PIN, HIGH); }
  randomSeed(((uint32_t)analogRead(A0) << 16) ^ micros());
  animInit();
  lastTick = millis();
}

void loop() {
  serviceAldl();
  uint32_t now = millis();
  if (now - lastTick >= ANIM_TICK_MS) { lastTick = now; animTick(); }
}
User avatar
morfi
Posts: 32
Joined: Wed Jan 22, 2025 7:17 pm
cars: Insignia 2014

Re: GM ECU Simulator

Post by morfi »

Awesome tool. Indeed what a time to be alive w/ the "cheat mode for coding". I've managed to unblock so many pet projects that were lingering on a shelf due to lack of time/motivation!
Image
Image
Image