Progress Thread Nicholase "lights out" build- TKX install

Unfortunately this will be mostly at a stand still for reasons out of my control.
I learned that the system uses a seperate transmission control harness. It's supposed to be included in the kit. Unfortunately they didn't include the transmission control harness.


It's been 3 weeks and they won't respond to my inquiries on why it wasn't included. I did order one from their website out of pocket but they won't reply if it's the right one. It's not in stock so hopefully whenever they ship it to me it's correct.

There are a few things I can do while I wait. I'll try and cover everything as I go.


Today I got started. Hopefully I'll get my missing transmission harness from ProM soon. Still haven't heard anything on it. They are aware, so I'll just trust it will ship soon. I lost the summer anyway, so I may as well get it all prepped.

Well, it's officially over a month since I first contacted ProM about the missing transmission harness. Other than "[sic]Nice,I'll look into it" I haven't heard anything from them. I'm assuming Nice meant Nick.

I did eat the cost and ordered one to speed up the process. I asked for clarification on if I ordered the correct one (not specific in the info listed on the website) and when it might ship. No response.

Not cool.
 
  • Angry
Reactions: gkomo
I got the widebands sensors in and all the wires under the car secured. I welded in the L&R bungs equal distance from the rear most cylinder port. I did this back when the engine was out.

eafcbc9f-2fe8-4433-8e34-f6affd236d4f.webp



The ProM EMS doesn't simply have wideband plugs on the engine harness. You use a third party wideband controller. This needs to be decided when you order so they can set up the PCM to match whatever brand wideband controller you're using. They sell Innovate and the instructions are all geared to that. I read quite a few bad reviews of Innovate controller and sensor failures with ProM so I decided to avoid that. Zeitronix seems to be the brand that works well from all my research, so that's what I went with.

To test my gauge I hooked the wideband controller to my computer with a serial cable adapter and downloaded the Zeitronix software. I ran power and ground temporarily to the controller plug. The gauge connects to the controller with a telephone cable. All looks good there.

fb0f9fd5-5f48-4ed2-a6aa-23eb0bbbf64d.webp



The wideband controller needs to be wired into the ProM wideband connector. Wiring schematics aren't included for Zeitronix. But by comparing the ProM instructions, Innovate instructions, and the Zeitronix instructions it can be figured out. So here's the proper wiring if anyone ever stumbles upon this looking for it.

ProM Red ---> ZT4 Red
ProM Black ---->ZT4 Black
ProM Purple---->ZT4 White (Left/Drivers side)
ProM Yellow---->ZT4 Grey (Right/Passenger side)

Here's the two harnesses that have to be spliced together.

c687c0da-9885-496d-ae04-4732abeceedd.webp



Staggered soldered splices with heat shrink covering the splices. Then covered in woven loom. I also made a little mounting bracket for the controller. It's just a peice of aluminum bar. I'll double side tape the plastic controller box to it and then secure it to the carpet with some push pins. No reason for more holes in the floor pan for this little guy.

23b8b8cb-4489-4b2e-906d-feccd0427ef9.webp


Then I mounted everything up nice and neat. The OBDII connector mounts to the seat support brace. The little square box is a VSS buffer. I thought keeping the wideband controller next to the PCM was clean and keeps all the stuff together. Hard to notice in the picture, but I put a little peice of (R)ed heat shrink on the (R)ight widband lead. All the (R)ight side wiring ends have one. It's important to keep L&R correct to the PCM.

37915f70-49f4-4dc1-8530-4194b394e03f.webp


bdeb2b9f-0b7a-47f7-9eb5-19505ef0d737.webp


It fits nicely under the seat. I did a seat test fit before I mounted it becasue it is pretty snug left to right. I also had to remove the long seat track spring that pulls the seat forward automatically when you pull the slider handle. It was touching the PCM case. But the seat does slide through its full range.

74d74a36-62a1-47a3-bd06-97e335ef7665.webp
 
Last edited:
Next I moved to the crank trigger setup. As I mentioned earlier the new EMS doesnt use the TFI. And by using the optional crank trigger it eliminates need for OEM quality PIP sensors. This was a big selling point for me. No more worry about TFI issues or looking for unobatnium PIP's.

The distributor is no longer a combination crank / cam sensor. It's soley a cam sensor now. Some modifications need to be done. So i ordered this gem from Ebay. I'm not cutting or selling any of my original stuff until I get this thing running.
023722e0-6644-4939-b7a9-ebab0f4a9256.webp



All I want from it is this OEM shudder wheel.
2bb392fa-589c-4bdc-bc3a-7a86876be872.webp


It goes from this:
96855857-dc12-4aee-8162-bb43adfaa394.webp


To this:
de0d1214-5836-4bb8-a9a9-3e96e4d34ee5.webp


All the teeth except the narrow signature tooth are removed because they are no longer needed. It only needs one tooth to determine cam position. That's why it doesn't need such high quality PIP sensors. It's just catching the one tooth as it goes by. The trigger wheel mounted on the crank shaft has 36 teeth so it has much better resolution and measures the crank position much more accurately as well as taking any timing chain slop out of the equation.

Then I mounted it up in my real distro.
6d2de6f7-0004-4c58-a89c-ddb6ad010370.webp
 
  • Like
Reactions: 85GT4V and Noobz347
Question:

What if you left 2 teeth 180* out or 4 teeth 90* out. Does the software allow you to specify?
It doesn't allow. For better resolution?

I believe it just needs the one tooth to identify TDC on the compression stroke. All the critical info, engine speed and exact crank position used for both ignition and injector firing comes from the crank sensor and PCM logic. The crank sensor trigger wheel has a signature tooth as well. But because the crank and cam spin at a different ratio (2:1) it needs to see the one tooth on the cam sensor to know what stroke it's on. That's all the info it needs from that senor. One tick. So more teeth wouldn't improve anything. It's not really doing any calculations. Just letting the PCM know when it sees the signature tooth on the crank sensor and sig tooth on the cam sensor at the same time it's compression stroke #1 cyl.

That's how I understand it anyway.

It is important to have the distributor phased correctly. Meaning the signature tooth is lined up with the rotor pointer tip and both are directly in the center of the hall sensor in the distributor. Then #1 plug wire has to be directly over the rotor tip.
 
Last edited:
  • Like
Reactions: Noobz347
Today I installed the new powerpipe and MAF. If you've been following along I did an engine build over the winter and ran it occasionally this summer (untuned) just to prove the engine work was good and there were no issues there. If I had a problem after the engine management install I didn't want to be wondering if the issue was mechanical or EMS related. So I installed my old calibrated MAF temporarily.

When ordering the EMS you have to either send in your old MAF to be flowed and calibrated to your new PCM or buy one from them to be set up with everything. That's what I did so that I would still have my old MAF to get up and running temporarily until the EMS shipped.

Here's the new Anderson power pipe and a few other things that will need to go on. I've already mocked this up when the engine was out and welded a bung in the powerpipe for the PCV inlet. I had it powdercoated crinkle black to match the valve covers and upper intake.

c1008956-166a-4b21-b204-e2cdf67dc31b.webp


This is the old plastic UPR powerpipe that was using. It was temporary but the concept is the same I'll be using permanently.
500741a1-93ed-41b8-b8d5-396fed17a87b.webp


A word on bypass valves. I'll be running between 7-10psi. So I need to run a bypass valve. The bypass valve protects the impeller and housing. It's open when is sees engine vacuum at idle or steady cruise and allows air to flow between the inlet and discharge pipe. Once it loses vacuum it closes and boost will build. It's there so when you snap the throttle closed at full boost (like a gear shift) all that pressure in the discharge pipe spikes (becasue the throttle blade snapped shut and quicky stopped the air flow). That pressure spike can damage the supercharger.

I'll be using some new hose for the bypass valve. 25mm air hose - pressure rated
248df1cd-82a4-4cee-84b7-ccc0430fe207.webp



Swapped out the air filter from my old calibrated MAF to the new one that's set up for my EMS.
d76f205a-e8c9-49fc-bcf5-2d223965b703.webp


Then some mock up. Heres some pics of the complete assembly. You can see how the bypass valve runs from the blower inlet after MAF to the discharge pipe which allows some air to bypass the S/C housing. That's what prevents the pressure spike when the throttle closes at full boost. It doesn't make any noise like a blow off valve.
2555e949-164f-4ce8-a49a-e8bb46229924.webp


Here a shot that shows the oil seperator on the PCV inlet. I was running a dedicated catch can on my old set up for that line but never saw any oil in it. So this time I'm just going to use a small oil seperator with a clear body so I can keep an eye on it. I got it fom JEGS for 10 bucks. That line goes to the valve cover.
78a7f8c9-f5bc-40da-97bc-199f199918e3.webp


Here it is all installed
0bc27395-a801-4819-8d1f-35961ebca53c.webp


9824db83-57fd-482b-a49b-cc7f157828d5.webp


I have a check valve for my PCV valve so boost pressure in the intake can't pressurize the crankcase. So any blowby gasses under boost will come out the valve cover nipple, through the oil seperator and into the blower inlet. To assist in ventilation I'm using a slick catch can from UPR. It operates like a stock closed PCV system in most conditions. However when under boost when crankcase pressure builds a check valve opens and it vents to atmosphere. It's mounted to the strut tower.
16b48c28-a3e8-4c20-ae42-1910ba1780d7.webp


Under the little air filter is a perforated screen and some other devices to catch any oil vapor. It's a really nice set up and takes care of alot of common PCV issues with forced induction MAF setups. More info on it here begining at post #251: https://stangnet.com/mustang-forums...prom-engine-management-install.928171/page-13
80a412d3-44ae-493b-b261-7ff107863a8e.webp


Then I moved on to mount the MAF. I noticed when running the new harness the female contacts in the MAF connector were loose and at odd angles. It won't plug on like that. So I'll hold off until a later date to tackle it. Not sure what's up with that, but I'll order a new MAF pigtail just incase the best fix it to just replace it.
0b936095-dd12-415d-8752-eaaeb95200ce.webp
 
Last edited:
Sorting out the the MAF connector. The contact pin are loose in the connector and spin around. I tugged lightly on the wires coming out of the connector and the black wire pulled right out. So something isnt right in there. Also, another part of the problem is the latch isn't split like the factory. It has a one peice clasp and wont lock right.
9604e7d3-1a9f-4c36-b5f2-76fce39cfffe.webp


I ordered a MAF pigtail connector and it is the same way, though the connectors are secure inside it. But it wont fully plug on. So I tried my original MAF connector from the original Ford wiring harness and it plugged on fine.

The contact pin terminals are the same on the ProM harness connector as the factory ford wiring. So I de-pinned my factory connector and installed it on the ProM harmess.
f963128d-f467-482e-84ff-8ab6134a4c35.webp


The original split latch connector pushes on to the MAF fine and latches correctly.
c14f982d-bc3e-4712-a801-76df0a10618c.webp
 
I'm sure I could have made the latch work one way or another. Most of the issue seemed to be the contacts in the connector loose and turning at odd angles in there and not lining up.
 
  • Like
Reactions: gkomo
Today I finished up the install. It was just a few punch list things left, a zip tie here and there. That sort of stuff. Then I put the intake back on and all the duct work. I have the battery on a charger and next time I get a chance I'll give it all one last look over and go through the initial start up procedure. Hopefully the missing transmission harness won't interfere. But I'd really like to get it idling and I dont think the trans harness should cause an issue for that.

The install wasn't bad at all actually. Other than the wonky MAF connector it was pretty straight forward plug and play.

I'm happy with how the new powerpipe came out and the inner fender smooth panel. Giving the engine bay a coat of paint really made it look a lot nicer. The new harness looks nice as well.

e8ea5d17-5a0f-4983-aaca-38b4ae56cfc6.webp


3feca41a-fa2a-4e95-9a97-be613961ae05.webp


Wish me luck on the start up!
 
Little update on ProM. To the best of my knowledge, the company was originally owned and operated by Chris Richards. He sold it but is still on as a consultant type role in the tech department. He's the guy that sends out the link for the tuning software and writes the calibration (tune) file. Chris has been very helpful as I'll point out below. The new owners in the sales end of the company have been unresponsive.
.........

After a good once over I started the initial pre start check list. It involves several steps. I just wanted to show how the software works for anyone that might be interested. It's pretty cool and when I was doing my research I never really came across any in depth info on what it looked like, how it all worked or what you would be required to do. You don't get the full instructions until your order ships. So until you have it in hand it's a bit of a mystery.

First step is to connect the laptop and verify an online communication link with the PCM. Then set the TPS.

The left side of the screen are all the various sensors and options of things you can adjust or look at. To work on one of them you click on it and drag it into the desktop window. So in this case I drag TPS volts into the window and a guage pops up showing the voltage. Key on, set to under .95. Way better than back probing the old way lol.

b9e5489c-cf57-43f6-8c1e-8215a19a8d86.webp


Then the base timing needs to be set. With the crank trigger option it's done while the engine is cranking (not running). So I disable the injectors so it can't start. Save the the change and write it to the PCM.

1d097578-78cd-4e78-bcdf-6989d5e00cb2.webp


Here's where I ran into a problem. I set up my timing light and remote starter relay bypass button. Turn key to run and crank over. No timing light flash. Hmmm Ut oh. After a little digging I determine there is no spark. I have 12v on both sides of the coil and no test light flash while cranking. I pulled DTC's.
f1bcd232-9e3a-4419-8be6-3984606a2e1e.webp


I checked and re checked my distributor set up and crank trigger install. That was all good. So i reached out to Chris Richards woth an email. He responded later that day which was a Saturday and had me send him a video of some data he requested. There were a few back and forth emails between Saturday and Sunday. It was determine there was no signal from the crank sensor. Chris went above and beyond helping me out on a weekend. I decided to try a new crankshaft sensor. This is something I can buy at a parts store and do a slight modification for it ti fit.



If anyone ever runs into this issue with the crank sensor it's Standard Motor Products part number PC483. For it to fit a metal mounting nub needs to be cut off with a band saw, and then drilled out for a bolt to pass. Then the other open hole needs to be enlarged.
New one all mounted up
befcd49f-65fd-474c-a556-d4277a166243.webp


Now I have spark. So I set the base timing as outlined in the instructions. Next step is to enable the injectors and fire it up. Fingers crossed.

It started. I moved to the idle speed adjustment. Just like the TPS adjustment you drag over a function to the desktop window. In this case four parameters are needed to set idle- ECT, Idle RPM actual, Idle RPM target, and TP voltage. When you drag "set base idle" to the desktop window all these populate.
4bae4c6e-7c04-45e6-8012-f61d9c4c9a8b.webp



It's nice all the info needed for one adjustment is all loaded at once. So in this case ECT needs to be 190° minimum, TP voltage needs to stay below .95v. It's easily monitored. Then you type in what you want your idle to be and match the throttle blade position to that number. Rather than unplugging the TPS and Spout, you just click the enable box and it does all that for you. Make adjustment, uncheck enable, save and write to PCM. Pretty slick.

Next I need to set the low injector slope. It's running but not very well. I noticed my left bank is running full lean. AFR 21:1. Also if I give it any throttle input it revs up to 2500 RPM and won't come back down. So something is off. I'm going to fix the lean bank and see where I'm at. I did swap L&R wideband leads to the controller and the problem follows. So either the new wideband is junked, or the lead, or both. Or something else entirely.

This is going to take some time. I have some home renovations going on so I'm putting this on the back burner for a few weeks. Maybe my missing transmission harness will show up by then. This is goimg to be a learning experience and push me past my comfort zone which is the only way to learn new things. So I'll be patient and make the best of it.
 
Last edited:
Now where is that match book cover???
(Only 'old' guys will know that tool)

First, matches are dangerous. :hide:

Second, you match book as been replaced by this:





/* -----------------------------------------------------------------------
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;
}



See? Simple :shrug:
 
  • Haha
Reactions: General karthief