What's new

Open Source PWM Fan Controller

I ended up leaving the inverted code, adding a stanrdard (non inverted) code, and just commented out the one I’m not using. With a second comment just to articulate how to choose between the two. Quick and dirty, but meh haha.

It’s in the jeep and it works. I’m probably going to broaden the range available on the knob, and I really need to add some smoothing to how frequently the temperature updates. I don’t know if I should make it update like once per second, or have it do some averaging. Averaging is probably better, but open to ideas there. Very happy with how everything is working though, just need to finalize the wiring and do some driving.


Way to fuckin go:beer:
 
Probably best to do an average or weighted average
A simple filter like this is handy. Make FF easily adjustable using a #define or const.

FILT <-- FILT + FF(NEW - FILT)

Sweet. Johnny, could you break that concept/those terms down for me? I'm a little rusty, playing some catch up.

I've done some initial searching and a running average looks appealing. It's slower in regards to response time, but for this application a second or less realistically won't be detrimental. But still have more research to do.

Three Methods to Filter Noisy Arduino Measurements - Coding - MegunoLink

C-like:
const int RunningAverageCount = 16;
float RunningAverageBuffer[RunningAverageCount];
int NextRunningAverage;
 
void loop()
{
  float RawTemperature = MeasureTemperature();
 
  RunningAverageBuffer[NextRunningAverage++] = RawTemperature;
  if (NextRunningAverage >= RunningAverageCount)
  {
    NextRunningAverage = 0;
  }
  float RunningAverageTemperature = 0;
  for(int i=0; i< RunningAverageCount; ++i)
  {
    RunningAverageTemperature += RunningAverageBuffer[i];
  }
  RunningAverageTemperature /= RunningAverageCount;
 
  delay(100);
}


Way to fuckin go:beer:

Thanks man, hell of a learning experience to get this far haha.


So the temperature scaling isn't perfect yet, needs a little tuning. I went for a drive and got everything up to temp, and it bumped the fan up to full duty cycle even though things were at a healthy operating temp. Pretty satisfying amount of suction/air volume flowing through the cooling stack though, I'm pumped.


 
Sweet. Johnny, could you break that concept/those terms down for me? I'm a little rusty, playing some catch up.

I've done some initial searching and a running average looks appealing. It's slower in regards to response time, but for this application a second or less realistically won't be detrimental. But still have more research to do.

Three Methods to Filter Noisy Arduino Measurements - Coding - MegunoLink

C-like:
const int RunningAverageCount = 16;
float RunningAverageBuffer[RunningAverageCount];
int NextRunningAverage;
 
void loop()
{
  float RawTemperature = MeasureTemperature();
 
  RunningAverageBuffer[NextRunningAverage++] = RawTemperature;
  if (NextRunningAverage >= RunningAverageCount)
  {
    NextRunningAverage = 0;
  }
  float RunningAverageTemperature = 0;
  for(int i=0; i< RunningAverageCount; ++i)
  {
    RunningAverageTemperature += RunningAverageBuffer[i];
  }
  RunningAverageTemperature /= RunningAverageCount;
 
  delay(100);
}




Thanks man, hell of a learning experience to get this far haha.


So the temperature scaling isn't perfect yet, needs a little tuning. I went for a drive and got everything up to temp, and it bumped the fan up to full duty cycle even though things were at a healthy operating temp. Pretty satisfying amount of suction/air volume flowing through the cooling stack though, I'm pumped.


You gonna put the controller in the cab within view of the driver?

This project if I was good enough.. could use many sensors and just display the readings for troubleshooting purposes.

That might not be useful for the actual fan controller but a separate tool with just pressure/temp sensors.

The sensors I bought are addressable and are cheap under $20 for 5. I think they can all run on one sensor wire at different addresses making wiring simple.

Thermal epoxy them into brass 1/4 plugs to take radiator delta, engine delta etc. temperatures.
 
You gonna put the controller in the cab within view of the driver?

This project if I was good enough.. could use many sensors and just display the readings for troubleshooting purposes.

That might not be useful for the actual fan controller but a separate tool with just pressure/temp sensors.

The sensors I bought are addressable and are cheap under $20 for 5. I think they can all run on one sensor wire at different addresses making wiring simple.

Thermal epoxy them into brass 1/4 plugs to take radiator delta, engine delta etc. temperatures.

Nah it won't be in the cab, it'll live under the hood and realistically hardly get looked at unless something fails. The current temperature and current duty cycle readings on the screen realistically only exist for troubleshooting. If the jeep ends up getting hot, I can pop the hood and verify that the controller is indeed outputting PWM based on the duty cycle, so that narrows it down to a failure of that wire, the power supply to the fan or the fan itself. Then if the jeep is hot but the duty cycle isn't showing an output, I can flip to the other screen and make sure the arduino is receiving a reading from the temp sensor. so I can narrow down where the failure is pretty rapidly without tools.

But along the same lines as everything else you're talking about, I have definitely become super interested in more data acquisition and data logging with this project. For all sorts of sensors hehe. So that may end up being another future project, maybe in the form of a Speeduino ECM setup
 
No need to save off a buffer of data.

<< some sort of code of that converts A/D counts to Temp and puts it into eng_temp. Going from there:

Code:
const float eng_temp_filter_coeff = 0.05;
float eng_temp;
float eng_temp_filtered;

in your time based task / main loop / whatever you got for scheduling:

Code:
eng_temp_filtered = eng_temp_filtered + (eng_temp_filter_coeff * (eng_temp - eng_temp_filtered));

From there, fiddle around with the coeff to adjust the filter. There's a way to turn it into the frequency, but the reality is that you just fiddle with the number between 0..1 and make yourself happy with the noise rejection.
 
These are the sensors I used, different from normal resistance to ground types. Very cheap (maybe a bad thing) and since they can be connected in series they would make really simple wiring. Of course I bailed on my project so no idea if they are worth a shit...



An example of their use.
 
No need to save off a buffer of data.

<< some sort of code of that converts A/D counts to Temp and puts it into eng_temp. Going from there:

Code:
const float eng_temp_filter_coeff = 0.05;
float eng_temp;
float eng_temp_filtered;

in your time based task / main loop / whatever you got for scheduling:

Code:
eng_temp_filtered = eng_temp_filtered + (eng_temp_filter_coeff * (eng_temp - eng_temp_filtered));

From there, fiddle around with the coeff to adjust the filter. There's a way to turn it into the frequency, but the reality is that you just fiddle with the number between 0..1 and make yourself happy with the noise rejection.

Ahhhh super interesting. So basically for each update of the data, it only allows the output to vary from the previous update by a certain small or large amount, depending on the chosen filter coefficient? Thereby softening any dramatic swings from one update to the next? I like it.

On your second statement, it starts with "eng temp filtered = engine temp filtered...", is that second supposed to be a reference to the previous update? If so, is that understood automatically or does it need something in the code?


These are the sensors I used, different from normal resistance to ground types. Very cheap (maybe a bad thing) and since they can be connected in series they would make really simple wiring. Of course I bailed on my project so no idea if they are worth a shit...



An example of their use.

I didn't know about the functionality of those, yeah that's cool. Simplifies the wiring as the sensor counts get higher
 
Ahhhh super interesting. So basically for each update of the data, it only allows the output to vary from the previous update by a certain small or large amount, depending on the chosen filter coefficient? Thereby softening any dramatic swings from one update to the next? I like it.

Yep. Keep it simple.

On your second statement, it starts with "eng temp filtered = engine temp filtered...", is that second supposed to be a reference to the previous update? If so, is that understood automatically or does it need something in the code?

That's the code. It's that easy.

Example use Inserted in your code from page 1:

Code:
const float filter_temp_coeff = 0.05;
float filtered_temp ;


void loop() {
// Read current temperature sensor resistance and calculate temperature
float sensor_voltage = analogRead(TEMP_SENSOR_PIN) * 5.0 / 1023.0;
float sensor_resistance = (5.0 - sensor_voltage) / sensor_voltage * 1000.0;
current_temp = calc_temp(sensor_resistance);


filtered_temp = filtered_temp + (filter_temp_coeff * (current_temp - filtered_temp));


// Read current potentiometer value
current_pot_value = analogRead(POT_PIN);

// Check if switch is pressed and update current screen
if (digitalRead(SWITCH_PIN) == LOW) {
current_screen++;

filtered_temp is defined as a global, so it's value is kept while the unit has powered. The filter updates the value every time loop is called. It works best if loop is called at a periodic rate, otherwise you have a variable filter frequency.

Also, filtering will help the FET live longer. They don't like wild changes. Most stuff I've worked at is controlled around 100-200Hz, and inputs have filters to keep from rapid changes to the outputs.

If you plan to PWM control a FET, I'd recommend turning it 100% to start during the avalanche (or in rush) current spike, then once it's past that you move to frequency. Generally want a lower bound on your frequency as well, as if the thing you're driving starts to stall, current on the driver will increase. The bigger the actuator the FET is driving, the longer 100% start.
 
Last edited:
Yep. Keep it simple.



That's the code. It's that easy.

Example use Inserted in your code from page 1:

Code:
const float filter_temp_coeff = 0.05;
float filtered_temp ;


void loop() {
// Read current temperature sensor resistance and calculate temperature
float sensor_voltage = analogRead(TEMP_SENSOR_PIN) * 5.0 / 1023.0;
float sensor_resistance = (5.0 - sensor_voltage) / sensor_voltage * 1000.0;
current_temp = calc_temp(sensor_resistance);


filtered_temp = filtered_temp + (filter_temp_coeff * (current_temp - filtered_temp));


// Read current potentiometer value
current_pot_value = analogRead(POT_PIN);

// Check if switch is pressed and update current screen
if (digitalRead(SWITCH_PIN) == LOW) {
current_screen++;

filtered_temp is defined as a global, so it's value is kept while the unit has powered. The filter updates the value every time loop is called. It works best if loop is called at a periodic rate, otherwise you have a variable filter frequency.

Also, filtering will help the FET live longer. They don't like wild changes. Most stuff I've worked at is controlled around 100-200Hz, and inputs have filters to keep from rapid changes to the outputs.

If you plan to PWM control a FET, I'd recommend turning it 100% to start during the avalanche (or in rush) current spike, then once it's past that you move to frequency. Generally want a lower bound on your frequency as well, as if the thing you're driving starts to stall, current on the driver will increase. The bigger the actuator the FET is driving, the longer 100% start.

Excellent. I just wasn't sure if having a value reference itself within a line of code could cause some sort of...recursive loop or something. I'm still working out the coding building blocks through all this haha. But the concept makes sense overall, I'll work on implementing it into the next iteration of the code.

Yeah the fan wants 100hz, so I have the code outputting at 100hz as well. I do indeed have a logic level FET stepping the arduino's 5v output up to 12v for the fans happy place, super interesting note about the FET not liking those rapid changes in the long run.

As far as the idea of 100% at fan start, that's an interesting thing about this fan. Don't know if it relates to all OEM (and even aftermarket?) brushless fans, but upon startup it bumps the fan once just to give it a wiggle (I assume for position sensing), then ramps up slowly on its own. When shutting the PWM signal off, the fan seems to have its own spin down logic as well. Either way, power changes are super smooth thanks to the fans integrated motor driver



Then just for grins, here's the current state of the functional code, but before filtering. It has a few temperature refinements since those videos, but more to come

FUNCTIONAL CODE, V6
Not filtered yet


C-like:
// Include required libraries
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <PWM.h>

#define SCREEN_WIDTH 128             // OLED display width, in pixels
#define SCREEN_HEIGHT 32             // OLED display height, in pixels



// Initialize OLED display object
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// Define constants for pins and values
const int FAN_PIN = 9;               //Fan output pin
const int AC_PIN = 8;                //AC detection pin
const int TEMP_SENSOR_PIN = A1;      //Temp sensor pin
const int POT_PIN = A2;              //Potentiometer pin
const int buttonPin = 11;            //Momentary switch pin
const int MIN_TEMP = 160;            //Minimum duty cycle temperature
const int MAX_TEMP = 230;            //Maximum duty cycle temperature
const int MIN_DUTY = 9;              //Minimum duty cycle percentage
const int MAX_DUTY = 85;             //Maximum duty cycle percentage
const int DUTY_INCREMENT = 1;        //Duty cycle percentage increments
const int POT_RANGE = 10000;         //Potentiometer resistance

// Define variables for current values
int current_duty = 0;                //Current duty cycle
int current_screen = 1;              //Current screen (may be deleted)

int current_pot_value = 0;           //Current raw potentiometer value
int mapped_pot = 0;                  //Potentiometer values mapped to range of pMin to pMax
int pMin = -30;                      //Minimum temperature variation for minimum potentiometer value
int pMax = 30;                       //Maximum temperature variation for maximum potentiometer value

int current_temp_sensor = 0;         //Current raw temperature sensor output
int mapped_temp = 0;                 //Temperature sensor values mapped to Fahrenheit range
int tuned_MIN_TEMP = 0;              //Minimum temp range after modification by potentiometer value
int tuned_MAX_TEMP = 0;              //Maximum temp range after modification by potentiometer value

int buttonPushCounter = 0;           //Counter for the number of button presses
boolean buttonState = LOW;           //Current state of the button
boolean lastButtonState = LOW;       //Previous state of the button

// Set PWM Frequency
int32_t frequency = 100;             //Frequency (in Hz)



void setup() {
  // Initialize serial communication
  //Serial.begin(9600);

  // Set up momentary switch as input
  pinMode(buttonPin, INPUT_PULLUP);

  // Set up AC pin to be grounded for activation
  pinMode(AC_PIN, INPUT_PULLUP);

  // Initialize OLED display
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);

  //Prepare Timers for PWM frequency on FAN_PIN, activate onboard LED (pin 13) if successful
  InitTimersSafe();
  bool success = SetPinFrequencySafe(FAN_PIN, frequency);
  if(success) {
    pinMode(13, OUTPUT);
    digitalWrite(13, HIGH);
  }
}



void loop() {
  // Read current potentiometer value
  current_pot_value = analogRead(POT_PIN);

  //Map potentiometer data to a range from negative (-)30 to +30, a simple variable added to the commanded temperature range
  mapped_pot = map(current_pot_value, 1023, 0, pMin, pMax);

  tuned_MIN_TEMP = (MIN_TEMP + mapped_pot);
  tuned_MAX_TEMP = (MAX_TEMP + mapped_pot);

  // Read current temperature sensor value
  current_temp_sensor = analogRead(TEMP_SENSOR_PIN); //Raw unmapped temp sensor data

  //Map voltages from temp sensor to Fahrenheit. First two numbers are observed resistance range, second two numbers are correlating temp in Fahrenheit
  mapped_temp = map(current_temp_sensor, 600, 0, 45, 230);

  // Define function for updating the duty cycle based on temperature and potentiometer
  current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (mapped_temp - tuned_MIN_TEMP) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
  current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;

  //Detect if AC is engaged, add 20% duty cycle
  if (digitalRead(AC_PIN) == HIGH) {current_duty = current_duty + 20;}

  //Duty cycle under minimum = zero, above max = defined maximum
  if (current_duty < MIN_DUTY) {current_duty = 0;}
  else if (current_duty > MAX_DUTY) {current_duty = MAX_DUTY;}

  //Output PWM to FAN_PIN based on calculated duty cycle. Use Regular OR Inverted, comment out unused option.
  //Regular:
  pwmWrite(FAN_PIN, round(current_duty * 255.0 / 100.0));
  //Inverted:
  //pwmWrite(FAN_PIN, round((100 - current_duty) * 255.0 / 100.0));

 

  // Read the state of the pushbutton value:
  buttonState = digitalRead(buttonPin);


  //Serial.println(current_temp_sensor);
  //Serial.println(mapped_temp);

 
  switch (buttonPushCounter) // choose what to display based on buttonPushCounter value
  {
    case 0:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      display.print("RG:");
      display.print(tuned_MIN_TEMP) ;
      display.print("-") ;
      display.println(tuned_MAX_TEMP);
      display.display();
      break;


    case 1:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      // Display static text
      display.print("C.Temp:");
      display.println(mapped_temp);
      display.display();
      break;

    case 2:

      display.setTextColor(WHITE);
      display.clearDisplay();
      display.setCursor(0, 10);
      // Display static text
      display.print("C.Duty:");
      display.println(current_duty);
      display.display();
      break;

  }

  if (buttonState != lastButtonState)
  {
    if (buttonState == HIGH)
    {
      // if the current state is HIGH then the button
      // went from off to on:
      buttonPushCounter++;  // add one to counter
      display.clearDisplay();
      display.display();
      if (buttonPushCounter > 2)
      {
        buttonPushCounter = 0;
      }

    }
    // save the current state as the last state,
    //for next time through the loop
    lastButtonState = buttonState;
  }

}
 
Just got a chance to actually implement the filter. Fawking fantastic results :beer:. The temp is so smooth and stable now, I couldn't be happier. You had filtered_temp called out as a float value, but that ended up adding zeros where the screen doesn't have room so I'm going to make that an int instead, which I think should work and not cause issues?

I did notice I have a flaw in the math/logic defining duty cycle though. The fan is running below the specified duty cycle range.

I'm pretty confident my issue is in these two lines, but I need to wrap my head fully back around what I was achieving here

C-like:
// Define function for updating the duty cycle based on temperature and potentiometer
  current_duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * (filtered_temp - tuned_MIN_TEMP) / (tuned_MAX_TEMP - tuned_MIN_TEMP);
  current_duty = round(current_duty / DUTY_INCREMENT) * DUTY_INCREMENT;


 
Just got a chance to actually implement the filter. Fawking fantastic results :beer:. The temp is so smooth and stable now, I couldn't be happier. You had filtered_temp called out as a float value, but that ended up adding zeros where the screen doesn't have room so I'm going to make that an int instead, which I think should work and not cause issues?

Yes and no. Float is easier because it takes care of the fractional bits for you. I have seen stalling (filtered never reaching unfiltered) with integer math. It can be minimized, but it takes some fiddly bits.

The easiest way to do this is utilizing fixed point math. Not sure if you have messed with it. It works, but floats are miles easier.

What about keeping the filtered value in float, then converting back to int for display?

article on fixed point math: https://www.embedded.com/fixed-point-math-in-c/
 
Yes and no. Float is easier because it takes care of the fractional bits for you. I have seen stalling (filtered never reaching unfiltered) with integer math. It can be minimized, but it takes some fiddly bits.

The easiest way to do this is utilizing fixed point math. Not sure if you have messed with it. It works, but floats are miles easier.

What about keeping the filtered value in float, then converting back to int for display?

article on fixed point math: https://www.embedded.com/fixed-point-math-in-c/

Nice, I could see that stalling happening. I don't have any fixed point math experience, but will learn what I need to haha

But I like your point about leaving the filtering alone and just bringing it back to int just for the display, should be pretty easy to implement I think.
 
Ah I think I might have found the issue with the duty cycle. The math above seems alright, but I now think my "AC mode" that adds +20% duty cycle might be engaged due to giving that pin a pull up resistor, and having it look for a high signal to activate. Oops.
 
Sweet. Johnny, could you break that concept/those terms down for me? I'm a little rusty, playing some catch up.

I've done some initial searching and a running average looks appealing. It's slower in regards to response time, but for this application a second or less realistically won't be detrimental. But still have more research to do.

Three Methods to Filter Noisy Arduino Measurements - Coding - MegunoLink

C-like:
const int RunningAverageCount = 16;
float RunningAverageBuffer[RunningAverageCount];
int NextRunningAverage;
 
void loop()
{
  float RawTemperature = MeasureTemperature();
 
  RunningAverageBuffer[NextRunningAverage++] = RawTemperature;
  if (NextRunningAverage >= RunningAverageCount)
  {
    NextRunningAverage = 0;
  }
  float RunningAverageTemperature = 0;
  for(int i=0; i< RunningAverageCount; ++i)
  {
    RunningAverageTemperature += RunningAverageBuffer[i];
  }
  RunningAverageTemperature /= RunningAverageCount;
 
  delay(100);
}




Thanks man, hell of a learning experience to get this far haha.


So the temperature scaling isn't perfect yet, needs a little tuning. I went for a drive and got everything up to temp, and it bumped the fan up to full duty cycle even though things were at a healthy operating temp. Pretty satisfying amount of suction/air volume flowing through the cooling stack though, I'm pumped.



AWESOME work on this.

But... That TP looked used... :flipoff2:
 
So I've done some test drives these past few days that went well. Went for a longer drive yesterday (60 miles each way) and had a hiccup about 10 miles in. Noticed the jeeps temp was 1 tick above operating temp, so I pulled over thinking I would adjust the temperature range. Fan wasn't spinning. And the arduino was active, but super sluggish to respond to button pushes. But it showed that it was in the operating range, and that it was calling for duty cycle. Tried power cycling it a few times with no luck on the fan. Figured I toasted the mosfet or hurt the output of the arduino. So I unplugged it and carried on

Finished the ~50 miles left in the drive with no fan, and drove back with no fan. I've gotta say I'm impressed, with the new radiator + the huge fan allowing better natural flow than the Volvo fan's restrictive shroud, the jeep had absolutely no problem keeping itself cool above ~25mph, if I didn't have to sit at too many back to back stoplights. Granted this wasn't a tough drive, flat ground mostly 50+mph in 60 degree temps, but reassuring for what I expect it to be capable of in that scenario.

But the interesting part - once I got back last night, I plugged the fan controller back in while the jeep was still hot and it worked as expected, fan ran and everything was updating as it should. I don't fully understand the initial failure, especially because it worked normal later. At first I thought heat soak may have been the issue, but it was hot later when it worked. Could I have introduced some sort of memory leak or something, that saturates over time?

I do have the 100a auto circuit breaker on the fan power wires, I suppose that could have tripped and I didn't have a multimeter to test, but I never heard the fan hit full speed (which should only be ~70a) so that seems unlikely
 
Last edited:
What form factor is the auto breaker? For loads that high, and especially critical circuits like cooling fans, I prefer the mechanical reset type or good old fashioned maxi fuses. Too many “is it on?” type situations where it’s hard to diagnose without a multimeter.
 
The sluggish operation is a clue to the Arduino problem not really a fan power problem?
 
It’s the bolt down style that can be had in either automatic or manual format. I was thinking the auto would be a good and convenient idea, but yeah not knowing if its tripped without testing the output kinda sucks.

IMG_0143.jpeg


The sluggish operation is a clue to the Arduino problem not really a fan power problem?

It gives me the same feeling overall, that the arduino wasn’t at 100% when it was sluggish, even though it did seem to successfully be running the program just…awkwardly. I’m going to have to do a bit more testing if it lets me down again, start to narrow things down
 
Where is the arduino mounted? is it in a place you can hit it with a IR temp gun easily? If you can, might be good to use it on the micro, the mosfet, and if it's a IC based power supply, that too. Might grab temps at various times, see if it's getting warm. With extended PWM, it could be warming up that mosfet.

Can you find anything in an Arduino manual if they throttle back the micro in the event of higher temps?

I'm working on putting an MSD solid state relay in the buggy, and noticed this in the instructions. Shows even commercial stuff has limits.

PWM Operation:A PWM signal can be used in the Solid-State Relay Module with a maximum frequency of 150 hertz and a duty cycle range from 50% to 90%. The maximum time in PWM mode should not exceed 30 minutes.Duty cycle values below 50% may cause a faster thermal increase and trigger over-temp protection, especially with inductive loads (i.e. big fans).If you run below a 50% duty cycle, you may experience thermal over-temp protection within a few minutes.If operating in PWM mode for only a few seconds, the range can be extended from 30% to 90%.
 
Where is the arduino mounted? is it in a place you can hit it with a IR temp gun easily? If you can, might be good to use it on the micro, the mosfet, and if it's a IC based power supply, that too. Might grab temps at various times, see if it's getting warm. With extended PWM, it could be warming up that mosfet.

Can you find anything in an Arduino manual if they throttle back the micro in the event of higher temps?

I'm working on putting an MSD solid state relay in the buggy, and noticed this in the instructions. Shows even commercial stuff has limits.
Based on this I'd say it's HOT lol
1712008817437.png
 
Yeahhh so I knew it was going to be warm mounted to the shroud right there, but hoped it wouldn't be an issue because it was a convenient spot lol. But thermal issues do seem like a probably cause of what's going on, so pulling the controller off the shroud and putting it somewhere as close to ambient as possible will be the first step, damnit :laughing:
 
I figured driving rain would be as much of a problem as anything...
 
Top Back Refresh