I have been working on a project, it’s a fan controller based on ESP32 and Micropython. MQTT is used for communicating with the ESP32 and I’m using Node-RED to configure settings and display temp and fan speed graphs. Both Node-RED and the MQTT broker are installed on a Raspberry pi.
Features implemented to far:
- Read temperature with a DS1820 sensor (placed in the chassis close to the GFX card)
- Generate a PWM signal for controlling the duty cycle of 3 fans (two front intake, one rear exhaust).
- Manual control of fan speed
- Automatic control of fan speed based on temperature
It works nicely, fans only speed up when the chassis is getting warm, and I don’t need to mess with some other fan control software.
Here is the UI (Node-RED dashboard):
Node-RED nodes:
ESP32 (Adafruit HUZZAH32):
DS1820 temp sensor (excuse the “cable management”):
Mighty 140mm Noctua intake fans, providing Moar airflow…
Mighty 140mm Noctua rear exhaust fan:
Main Python code (a bit messy with global variables, I will rewrite as a class), note that I also have a “boot.py” setting up the WiFi connection:
import time
import json
from umqttsimple import MQTTClient
import ubinascii
import machine
import micropython
from machine import Pin, PWM, ADC
import onewire
import ds18x20
mqtt_server = '192.168.1.50'
client_id = ubinascii.hexlify(machine.unique_id())
topic_pub = b'fancontrol'
topic_sub = b'setfan'
topic_init = b'init'
last_message = 0
message_interval = 2 # seconds
auto = True
roms = None
duty_changed = False
frequency = 25000 # Hz
duty_percent = 30
# A0 on board, temperature sensor
ds_pin = Pin(26)
ds_sensor = ds18x20.DS18X20(onewire.OneWire(ds_pin))
# A2 on board
#pot = ADC(Pin(34))
#pot.width(ADC.WIDTH_10BIT)
#pot.atten(ADC.ATTN_11DB)
# 21 on board, PWM output signal
led = PWM(Pin(21), frequency)
def sub_cb(topic, msg):
'''Callback handling MQTT messages'''
global duty_percent
global duty_changed
global auto
try:
message = json.loads(msg)
# Fan speed has been changed from UI
if 'fanspeed' in message:
print('Set fan message')
duty_percent_new = message.get('fanspeed', 100)
if duty_percent_new != duty_percent:
duty_changed = True
duty_percent = duty_percent_new
else:
duty_changed = False
print('Setting duty_percent to: ' + str(duty_percent))
# Automatic mode enabled/disabled in UI
if 'auto' in message:
print('Change auto message')
auto = message['auto']
except ValueError as e:
print('Not a valid JSON')
def connect_mqtt():
print('Trying to connect to MQTT broker...')
global client_id, mqtt_server, topic_sub
client = MQTTClient(client_id, mqtt_server)
client.set_callback(sub_cb)
client.connect()
client.subscribe(topic_sub)
print('Connected to %s MQTT broker' % (mqtt_server))
return client
def restart_and_reconnect():
print('Failed to connect to MQTT broker. Reconnecting...')
time.sleep(1)
machine.reset()
def get_roms():
try:
global roms
roms = ds_sensor.scan()
except OSError as e:
return('Failed to read sensor.')
def read_sensor():
'''Read the DS1820 sensor'''
try:
ds_sensor.convert_temp()
time.sleep_ms(750)
for rom in roms:
temp = ds_sensor.read_temp(rom)
# uncomment for Fahrenheit
# temp = temp * (9/5) + 32.0
if (isinstance(temp, float) or (isinstance(temp, int))):
#temp = (b'{0:3.1f},'.format(temp))
temp = round(temp, 1)
return temp
else:
return('Invalid sensor readings.')
except OSError as e:
return('Failed to read sensor.')
def set_duty_cycle(led, duty_percent):
'''Set output pin duty cycle, based on 0%-100% duty_percent'''
duty_full_range = (1023 / 100) * duty_percent
# Invert
duty_full_range = int(1023 - duty_full_range)
led.duty(duty_full_range)
print ("Set duty cycle: ", duty_percent, duty_full_range)
def printtime():
print('{:02d}:{:02d}:{:02d} '.format(time.localtime()[3], time.localtime()[4], time.localtime()[5]), end='')
def get_duty_from_temp(temp):
'''Calculate fan duty cycle based on temp, linear equation (graph)'''
# Linear equation calculation
# y = k*x + m
# k = (y2-y1) / (x2-x1)
min_temp = 30 # x1
min_duty = 25 # y1
max_temp = 50 # x2
max_duty = 100 # y2
k = (max_duty-min_duty) / (max_temp-min_temp)
m = min_duty - k*min_temp
# Lower temp interval
if temp <= min_temp:
return min_duty
# Middle temp interval (use line in graph)
elif temp <= max_temp:
return int(round(k*temp + m, 0))
# Upper temp interval
else:
return max_duty
try:
client = connect_mqtt()
except OSError as e:
restart_and_reconnect()
get_roms()
# Run fans at 100% for 2 seconds
set_duty_cycle(led, 100)
time.sleep(2)
set_duty_cycle(led, duty_percent)
# Initialize the speed setting gauge and turn on automatic mode
payload_init = {"initial_duty_cycle" : duty_percent,
"auto" : True}
print("Publish init message...")
client.publish(topic_init, json.dumps(payload_init))
# Main loop
while True:
try:
client.check_msg()
if (time.time() - last_message) > message_interval:
temp = read_sensor()
#fixa
fan = round(duty_percent, 1)
# Manual mode
if not auto:
fan = round(duty_percent, 1)
payload = {"temp" : temp,
"speed" : fan}
print("Publish message... ")
print('Temp', str(temp))
print('Calculated duty', str(get_duty_from_temp(temp)))
print()
client.publish(topic_pub, json.dumps(payload))
last_message = time.time()
except OSError as e:
print('Error checking for messages, restart')
restart_and_reconnect()
# Automatic mode enabled in UI, calculate fan speed bases on temp
if auto:
print('Auto mode')
duty_percent_new = get_duty_from_temp(temp)
# Only update if duty cycle has changed
if duty_percent_new != duty_percent:
duty_changed = True
duty_percent = duty_percent_new
else:
duty_changed = False
if duty_changed:
set_duty_cycle(led, duty_percent)
duty_changed = False
time.sleep(1)