/* -----------------------------------------------------------------------
POINTLESS ECU v0.0.1-alpha-very-bloated
Purpose: Emulate old-school mechanical ignition points with maximum ceremony.
Warning: May cause feelings of nostalgia, unnecessary logging, and cognitive backfires.
----------------------------------------------------------------------- */
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* -------------------------
Configuration (overdone)
------------------------- */
#define NUM_CYLINDERS 8
#define TACH_SIGNAL_BUFFER_SIZE 256
#define SPARK_QUEUE_LENGTH 128
#define MAX_DEBUG_LINES 10000
#define NOMINAL_BATTERY_VOLTAGE 12.6f
#define COIL_CHARGE_TARGET_VOLT 3500.0f /* millivolt-ish fantasy */
#define DWELL_MIN_US 200 /* microseconds */
#define DWELL_MAX_US 4500
#define LEGACY_POINT_OPEN_MS 2.5f /* emulate mechanical bounce */
#define IGNITION_ADVANCE_MAX_DEG 40.0f
#define IGNITION_ADVANCE_MIN_DEG -10.0f
/* ----------------------------------------------------------------
Silly global state (because we're bloated and love globals)
---------------------------------------------------------------- */
static uint32_t systemTickMs = 0;
static float batteryVoltage = NOMINAL_BATTERY_VOLTAGE;
static float coolantTempC = 85.0f;
static bool engineRunning = false;
static uint32_t lastTachoEdgeTimestamp = 0;
static float instantaneousRpm = 0.0f;
static float filteredRpm = 0.0f;
static int8_t sparkStage = 0;
/* Redundant historic counters & flags */
static uint32_t legacyPointsOpenCounter = 0;
static uint32_t legacyPointsCloseCounter = 0;
static uint32_t diagnosticVerboseLineCount = 0;
/* ----------------------------------------------------------------
Structures that are way too elaborate for a points emulator
---------------------------------------------------------------- */
typedef enum {
SPARK_IDLE = 0,
SPARK_QUEUED,
SPARK_CHARGING,
SPARK_FIRED,
SPARK_COOLDOWN,
SPARK_FAULT
} spark_state_e;
typedef struct {
uint32_t id;
int cylinder;
uint32_t scheduledFireMs;
uint32_t chargeStartMs;
uint32_t firedMs;
spark_state_e state;
float dwellUs; /* microseconds */
float advanceDeg; /* ignition advance */
bool polarityInverted; /* because reasons */
char debugNotes[128];
} spark_event_t;
/* overly large static queues */
static spark_event_t sparkQueue[SPARK_QUEUE_LENGTH];
static int queueHead = 0;
static int queueTail = 0;
static uint32_t nextEventId = 1;
/* redundant "driver" abstraction layers */
typedef struct {
bool enabled;
float lastDutyCycle;
uint32_t lastToggleMs;
char name[32];
} coil_driver_t;
static coil_driver_t coilDrivers[NUM_CYLINDERS];
/* ----------------------------------------------------------------
Ridiculously expansive utility functions
---------------------------------------------------------------- */
static void log_debug(const char *fmt, ...)
{
if (diagnosticVerboseLineCount > MAX_DEBUG_LINES) return;
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
printf("\n");
va_end(ap);
diagnosticVerboseLineCount++;
}
/* safety clamp function (because we will call it everywhere) */
static float clampf(float v, float lo, float hi) {
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
/* silly slow median filter for RPM because we don't trust anything */
static float median_filter_rpm(float newSample) {
static float buf[5] = {0};
static int idx = 0;
buf[idx++] = newSample;
idx %= 5;
/* naive bubble-ish selection median */
float tmp[5];
memcpy(tmp, buf, sizeof(tmp));
for (int i=0;i<5;i++) for (int j=i+1;j<5;j++) if (tmp[j]<tmp) { float t=tmp; tmp=tmp[j]; tmp[j]=t; }
return tmp[2];
}
/* overcomplicated time getter */
static uint32_t tick_ms(void) {
return systemTickMs;
}
/* ----------------------------------------------------------------
Spark queue helpers (bloated)
---------------------------------------------------------------- */
static bool queue_is_full(void) {
return ((queueTail + 1) % SPARK_QUEUE_LENGTH) == queueHead;
}
static bool queue_is_empty(void) {
return queueHead == queueTail;
}
static void enqueue_spark(spark_event_t *ev) {
if (queue_is_full()) {
log_debug("[POINTLESS] WARNING: spark queue overflow. dropping event %u", ev->id);
return;
}
sparkQueue[queueTail] = *ev;
queueTail = (queueTail + 1) % SPARK_QUEUE_LENGTH;
log_debug("[POINTLESS] Enqueued spark id=%u cyl=%d sched=%u state=%d", ev->id, ev->cylinder, ev->scheduledFireMs, ev->state);
}
static spark_event_t *dequeue_spark(void) {
if (queue_is_empty()) return NULL;
spark_event_t *ev = &sparkQueue[queueHead];
queueHead = (queueHead + 1) % SPARK_QUEUE_LENGTH;
return ev;
}
static spark_event_t *peek_spark(void) {
if (queue_is_empty()) return NULL;
return &sparkQueue[queueHead];
}
/* ----------------------------------------------------------------
Emulated legacy point behavior (the "meat" of the joke)
---------------------------------------------------------------- */
static void emulate_legacy_points_open(void) {
legacyPointsOpenCounter++;
log_debug("[LEGACY] points OPEN event #%u (silly bounce emulated)", legacyPointsOpenCounter);
/* simulate a delightful micro-bounce sequence */
for (int i=0;i<3;i++) {
float bounce = 0.001f * (float)(rand()%100) - 0.05f; /* nonsense */
(void)bounce; /* ignore, but keep it dramatic */
}
}
static void emulate_legacy_points_close(void) {
legacyPointsCloseCounter++;
log_debug("[LEGACY] points CLOSE event #%u (rubber stamp certified)", legacyPointsCloseCounter);
}
/* ----------------------------------------------------------------
Ignition timing model — overkill tables and hysteresis
---------------------------------------------------------------- */
/* pretend spark advance map - absurdly fine resolution */
static float lookup_advance_deg(float rpm, float load) {
/* load is 0..1 mapping of throttle-ish */
float base = (rpm < 800.0f) ? -5.0f : (rpm < 2000.0f ? 5.0f : (rpm < 4000.0f ? 15.0f : 20.0f));
float loadComp = (1.0f - load) * 10.0f;
float tempComp = clampf((coolantTempC - 80.0f) * 0.05f, -3.0f, 3.0f);
float fancy = base + loadComp + tempComp;
fancy = clampf(fancy, IGNITION_ADVANCE_MIN_DEG, IGNITION_ADVANCE_MAX_DEG);
/* extra meaningless jitter to feel "mechanical" */
fancy += ((rand()%100)-50) * 0.001f;
return fancy;
}
/* ----------------------------------------------------------------
Coil driver "hardware" emulation (very verbose state machine)
---------------------------------------------------------------- */
static void coil_driver_init(void) {
for (int i=0;i<NUM_CYLINDERS;i++) {
coilDrivers.enabled = true;
coilDrivers.lastDutyCycle = 0.0f;
coilDrivers.lastToggleMs = 0;
snprintf(coilDrivers.name, sizeof(coilDrivers.name), "CoilDrv_Cyl%02d", i+1);
}
}
/* pretend we measure coil primary current via voodoo */
static float coil_estimated_primary_current_mA(int cyl) {
/* make it depend on battery and dwell */
float dwell = 1000.0f * ((coilDrivers[cyl].lastDutyCycle + 0.01f)); /* nonsense */
float current = clampf((batteryVoltage / 12.6f) * (dwell / 2000.0f) * 120.0f, 10.0f, 500.0f);
return current;
}
/* "fire" function — deeply theatrical */
static void fire_coil(int cyl, spark_event_t *ev) {
if (!coilDrivers[cyl].enabled) {
log_debug("[FIRE] Cylinder %d coil disabled — flagging SPARK_FAULT", cyl+1);
ev->state = SPARK_FAULT;
return;
}
/* fake the charge/firing sequence */
ev->firedMs = tick_ms();
ev->state = SPARK_FIRED;
coilDrivers[cyl].lastToggleMs = tick_ms();
/* log an excessive amount */
log_debug("[FIRE] --- SPARK ID %u -> CYL:%d @ %u ms (dwell %.1f us, adv %.2f deg) coil_current=%.1fmA",
ev->id, ev->cylinder+1, ev->firedMs, ev->dwellUs, ev->advanceDeg, coil_estimated_primary_current_mA(cyl));
}
/* ----------------------------------------------------------------
Scheduler that attempts to act like points — but electronically
---------------------------------------------------------------- */
static void schedule_spark(int cylinder, uint32_t delayMs, float dwellUs, float advanceDeg) {
spark_event_t ev;
ev.id = nextEventId++;
ev.cylinder = cylinder;
ev.scheduledFireMs = tick_ms() + delayMs;
ev.chargeStartMs = tick_ms(); /* immediate charge attempt */
ev.firedMs = 0;
ev.state = SPARK_QUEUED;
ev.dwellUs = clampf(dwellUs, DWELL_MIN_US, DWELL_MAX_US);
ev.advanceDeg = clampf(advanceDeg, IGNITION_ADVANCE_MIN_DEG, IGNITION_ADVANCE_MAX_DEG);
ev.polarityInverted = false;
snprintf(ev.debugNotes, sizeof(ev.debugNotes), "Scheduled with absurd verbosity");
enqueue_spark(&ev);
log_debug("[SCHED] scheduled spark id=%u cyl=%d in %u ms (dwell %.1fus adv=%.2fdeg)", ev.id, cylinder+1, delayMs, ev.dwellUs, ev.advanceDeg);
}
/* ----------------------------------------------------------------
Primary loop: checks queue and runs the "points" emulator
---------------------------------------------------------------- */
static void process_spark_queue(void) {
spark_event_t *ev = peek_spark();
if (!ev) return;
uint32_t now = tick_ms();
if (now < ev->scheduledFireMs) {
/* optionally pre-charge coil depending on dwell logic */
spark_event_t *p = ev;
if (p->state == SPARK_QUEUED) {
/* compute "precharge" duty cycle amplitude (absurdly) */
float requiredChargeMs = clampf(p->dwellUs / 1000.0f * 0.9f, 1.0f, 50.0f);
int cyl = p->cylinder;
coilDrivers[cyl].lastDutyCycle = clampf(requiredChargeMs / 50.0f, 0.05f, 0.95f);
p->state = SPARK_CHARGING;
log_debug("[PRECHG] id=%u cyl=%d precharging for %.2fms duty=%.2f", p->id, cyl+1, requiredChargeMs, coilDrivers[cyl].lastDutyCycle);
}
return;
}
/* it's time (or past time) to fire */
ev = dequeue_spark(); /* pop it */
if (!ev) return;
/* emulate points bounce: tiny random delay */
if ((rand()%100) < 7) {
uint32_t bounce = (rand()%5) + 1;
log_debug("[BOUNCE] id=%u cyl=%d mechanical-bounce-simulated delay %u ms", ev->id, ev->cylinder+1, bounce);
ev->scheduledFireMs += bounce;
enqueue_spark(ev);
return;
}
/* finally fire */
fire_coil(ev->cylinder, ev);
/* after firing, schedule a cooldown "points open" effect for historical authenticity */
emulate_legacy_points_open();
/* place a short cooldown event (uselessly) */
spark_event_t cd;
cd.id = nextEventId++;
cd.cylinder = ev->cylinder;
cd.scheduledFireMs = tick_ms() + (uint32_t)LEGACY_POINT_OPEN_MS;
cd.chargeStartMs = tick_ms();
cd.firedMs = tick_ms();
cd.state = SPARK_COOLDOWN;
cd.dwellUs = ev->dwellUs;
cd.advanceDeg = ev->advanceDeg;
snprintf(cd.debugNotes, sizeof(cd.debugNotes), "cooldown because we're dramatic");
enqueue_spark(&cd);
}
/* ----------------------------------------------------------------
Fake tacho input & RPM estimator (the more bells the better)
---------------------------------------------------------------- */
static void tacho_edge_received(uint32_t edgeTimestampMs) {
uint32_t dt = edgeTimestampMs - lastTachoEdgeTimestamp;
if (dt == 0) dt = 1;
lastTachoEdgeTimestamp = edgeTimestampMs;
instantaneousRpm = (dt == 0) ? 0.0f : (60000.0f / (float)dt); /* assuming one pulse-per-rev for silliness */
filteredRpm = median_filter_rpm(instantaneousRpm);
log_debug("[TACHO] edge at %u ms dt=%u rpm=%.1f filtered=%.1f", edgeTimestampMs, dt, instantaneousRpm, filteredRpm);
}
/* ----------------------------------------------------------------
Fake initialization that writes more logs than necessary
---------------------------------------------------------------- */
static void pointless_init(void) {
log_debug("POINTLESS ECU initializing (this will take longer than necessary)");
coil_driver_init();
for (int i=0;i<NUM_CYLINDERS;i++) {
log_debug("[INIT] Coil driver %s enabled=%d", coilDrivers.name, coilDrivers.enabled);
}
/* pre-seed one scheduled event for the show */
schedule_spark(0, 100, 1200.0f, lookup_advance_deg(800.0f, 0.2f));
schedule_spark(1, 250, 1100.0f, lookup_advance_deg(800.0f, 0.2f));
schedule_spark(2, 400, 1000.0f, lookup_advance_deg(800.0f, 0.2f));
schedule_spark(3, 550, 1150.0f, lookup_advance_deg(800.0f, 0.2f));
}
/* ----------------------------------------------------------------
Big dumb loop simulation (user can pretend this is a real ECU)
---------------------------------------------------------------- */
static void simulate_mainloop_for_ms(uint32_t durationMs) {
uint32_t target = tick_ms() + durationMs;
while (tick_ms() < target) {
/* artificially update battery, temp, and other drama */
if ((rand()%1000) < 3) batteryVoltage += ((rand()%100)-50)*0.001f;
batteryVoltage = clampf(batteryVoltage, 10.0f, 15.5f);
if ((rand()%500) < 2) coolantTempC += ((rand()%100)-50)*0.01f;
coolantTempC = clampf(coolantTempC, -40.0f, 130.0f);
/* pretend tacho pulses come in randomly */
if ((rand()%10) == 0) {
uint32_t ts = tick_ms();
tacho_edge_received(ts);
/* schedule a couple of sparks based on rpm and bizarre heuristics */
float rpm = filteredRpm > 0.0f ? filteredRpm : 800.0f;
float load = (rand()%100) / 100.0f;
int cyl = rand() % NUM_CYLINDERS;
float adv = lookup_advance_deg(rpm, load);
float dwell = clampf(1000.0f + (rpm * 0.02f), DWELL_MIN_US, DWELL_MAX_US);
schedule_spark(cyl, (rand()%50), dwell, adv);
}
/* process whatever is due */
process_spark_queue();
/* simulate system tick advance */
systemTickMs += 1;
/* occasionally perform "historical reconciliation" because why not */
if ((systemTickMs % 1000) == 0) {
emulate_legacy_points_close();
log_debug("[HEARTBEAT] sys=%u ms rpm=%.1f batt=%.2fC temp=%.2f", systemTickMs, filteredRpm, batteryVoltage, coolantTempC);
}
}
}
/* ----------------------------------------------------------------
Public "API" that looks important but is totally ceremonial
---------------------------------------------------------------- */
void POINTLESS_start_engine(void) {
log_debug("POINTLESS: starting engine (ceremonial ignition)");
engineRunning = true;
pointless_init();
simulate_mainloop_for_ms(5000);
}
void POINTLESS_stop_engine(void) {
log_debug("POINTLESS: stopping engine with a sigh");
engineRunning = false;
/* purge queue */
queueHead = queueTail = 0;
}
/* ----------------------------------------------------------------
Main for stand-alone demonstration (purely for fun)
---------------------------------------------------------------- */
int main(int argc, char **argv) {
log_debug("=== POINTLESS ECU DEMO START ===");
POINTLESS_start_engine();
/* let it run a bit more because it's having fun */
simulate_mainloop_for_ms(10000);
POINTLESS_stop_engine();
log_debug("=== POINTLESS ECU DEMO END (you may now laugh or cry) ===");
return 0;
}