├── .gitattributes ├── .gitignore ├── .idea ├── .gitignore ├── esp32-hydronic-controller.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── libraries │ └── MicroPython.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── CONFIG_README.md ├── LICENSE.md ├── README.md ├── config.json ├── hardwareConfig.py ├── lib ├── fanPID.py ├── helpers.py ├── networking.py └── sensors.py ├── main.py ├── states ├── control.py ├── emergencyStop.py ├── shutdown.py ├── startup.py └── stateMachine.py ├── tools └── get_file.py └── webserver.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/misc.xml 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/esp32-hydronic-controller.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/MicroPython.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONFIG_README.md: -------------------------------------------------------------------------------- 1 | # General Settings 2 | - `USE_WEBSERVER`: Enables the built-in AP and webpage for modifying settings. (True/False) 3 | - `USE_WIFI`: Enables or disables Wi-Fi functionality. (True/False) 4 | - `USE_MQTT`: Enables or disables MQTT functionality. (True/False) 5 | - `IS_WATER_HEATER`: Set to True if this device is controlling a water or coolant heater. (True/False) 6 | - `HAS_SECOND_PUMP`: Set to True if there is a secondary water pump in the system. (True/False) 7 | - `IS_SIMULATION`: Set to True to simulate without sensors, etc., connected. Useful for development on an ESP32 without hardware. (True/False) 8 | 9 | # Network Settings 10 | - `SSID`: SSID of the WiFi network to connect to. 11 | - `PASSWORD`: Password of the WiFi network. 12 | - `MQTT_SERVER`: Address of the MQTT broker. 13 | - `MQTT_CLIENT_ID`: MQTT client ID. 14 | - `MQTT_USERNAME`: MQTT username. 15 | - `MQTT_PASSWORD`: MQTT password. 16 | - MQTT Topics: 17 | - `SENSOR_VALUES_TOPIC`: Topic to publish sensor values. 18 | - `SET_TEMP_TOPIC`: Topic to receive the target temperature. 19 | - `COMMAND_TOPIC`: Topic to receive commands like "start" and "stop". 20 | 21 | # Safety Limits 22 | - `EXHAUST_SAFE_TEMP`: Max safe temperature for exhaust in Celsius. 23 | - `OUTPUT_SAFE_TEMP`: Max safe temperature for output in Celsius. 24 | 25 | # Sensor Settings 26 | - Thermistor type options for `OUTPUT_SENSOR_TYPE` and `EXHAUST_SENSOR_TYPE`: 27 | - `'NTC_10k'`, `'NTC_50k'`, `'NTC_100k'`, `'PTC_500'`, `'PTC_1k'`, `'PTC_2.3k'`. 28 | - Use a matching resistor in your voltage divider for the thermistors, which is assumed for calculations. 29 | - `OUTPUT_SENSOR_BETA`: BETA value for the output temperature sensor. 30 | - `EXHAUST_SENSOR_BETA`: BETA value for the exhaust temperature sensor. 31 | 32 | # Device Control 33 | - Temperature Control: 34 | - `TARGET_TEMP`: Target temperature to maintain in Celsius. 35 | - `CONTROL_MAX_DELTA`: Maximum temperature delta for control logic in Celsius. 36 | - Fan Control: 37 | - `FAN_RPM_SENSOR`: If using a hall effect sensor for fan RPM (True/False). 38 | - `MIN_FAN_RPM`: Minimum fan RPM. 39 | - `MAX_FAN_RPM`: Maximum fan RPM. 40 | - `FAN_MAX_DUTY`: Maximum duty cycle for the fan's PWM signal. 41 | - Fuel Pump Control: 42 | - `MIN_PUMP_FREQUENCY`: Minimum frequency of the water pump in Hertz. 43 | - `MAX_PUMP_FREQUENCY`: Maximum frequency of the water pump in Hertz. 44 | - `PUMP_ON_TIME`: Duration the pump is on during each pulse, in seconds. 45 | - Emergency Handling: 46 | - `FAILURE_STATE_RETRIES`: How many times will we attempt a restart due to failed STARTING or flame out when RUNNING. 47 | - `EMERGENCY_STOP_TIMER`: Time after emergency stop triggered until system reboot, in milliseconds. 48 | 49 | # Startup Settings 50 | - `STARTUP_TIME_LIMIT`: Maximum time allowed for startup, in seconds. 51 | - `GLOW_PLUG_HEAT_UP_TIME`: Time for the glow plug to heat up, in seconds. 52 | - `INITIAL_FAN_SPEED_PERCENTAGE`: Initial fan speed as a percentage of the maximum speed. 53 | 54 | # Shutdown Settings 55 | - `SHUTDOWN_TIME_LIMIT`: Maximum time allowed for shutdown, in seconds. 56 | - `COOLDOWN_MIN_TIME`: Minimum time for the system to cool down, in seconds, regardless of temperature. 57 | - `EXHAUST_SHUTDOWN_TEMP`: Temperature at which we consider the heater cooled down in Celsius. 58 | 59 | # Flame-out Detection 60 | - `EXHAUST_TEMP_HISTORY_LENGTH`: Length of the deque storing the last N exhaust temperature readings. 61 | - `MIN_TEMP_DELTA`: The minimum meaningful temperature decrease in Celsius. 62 | 63 | # Logging Level 64 | - `LOG_LEVEL`: Logging level: 0 for None, 1 for Errors, 2 for Info, 3 for Debug. 65 | 66 | # Pin Assignments 67 | - `FUEL_PIN`: Pin assigned for fuel control. 68 | - `AIR_PIN`: Pin assigned for air control. 69 | - `GLOW_PIN`: Pin assigned for glow plug control. 70 | - `WATER_PIN`: Pin assigned for water control (if `IS_WATER_HEATER` is True). 71 | - `WATER_SECONDARY_PIN`: Pin assigned for secondary water pump control (if `HAS_SECOND_PUMP` is True). 72 | - `FAN_RPM_PIN`: Pin assigned for fan RPM sensor (if `FAN_RPM_SENSOR` is True). 73 | - `SWITCH_PIN`: Pin assigned for switch control. 74 | - `OUTPUT_TEMP_ADC`: Pin assigned for output temperature ADC. 75 | - `EXHAUST_TEMP_ADC`: Pin assigned for exhaust temperature ADC. 76 | 77 | # Calibration and Beta Value 78 | - To calibrate the beta value for a thermistor, follow these steps: 79 | 1. Measure the resistance of the thermistor at two known temperatures (e.g., ice water at 0°C and boiling water at 100°C). 80 | 2. Use the following formula to calculate the beta value: 81 | \[ 82 | \beta = \frac{\ln(R2/R1)}{\frac{1}{T2} - \frac{1}{T1}} 83 | \] 84 | where: 85 | - \( R1 \) is the resistance at temperature \( T1 \) (in Kelvin), 86 | - \( R2 \) is the resistance at temperature \( T2 \) (in Kelvin), 87 | - \( \ln \) is the natural logarithm. 88 | 3. Replace the `OUTPUT_SENSOR_BETA` or `EXHAUST_SENSOR_BETA` in the settings with the calculated beta value. 89 | 4. Ensure the beta value is in accordance with the thermistor's datasheet for accurate temperature measurement. 90 | 91 | # Notes: 92 | - The beta value is crucial for accurate temperature measurements. 93 | - Calibration should be performed in the environment where the sensor will be used. 94 | - A precise multimeter should be used for measuring thermistor resistance. 95 | - It is recommended to consult the thermistor's datasheet for detailed calibration instructions and beta value information. 96 | 97 | Remember to save changes to the configuration after editing. 98 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZorroHeater Project 2 | 3 | This project provides a controller for a heater system based on the ESP32 platform on MicroPython. It uses MQTT for remote communication, allowing the user to set the desired temperature and receive sensor readings. The codebase should be pretty universal. 4 | 5 | Note that I have yet to use this in everyday use and the project is very much considered Alpha/untested at this time. If you have an immediate need for a diesel air heater controller, I can highly recommend Afterburner by Ray Jones. I primairly built this to control a Diesel Water Heater for a hydronic install as the built in controller was poor, and most scavanged units out of Land Rovers/etc don't have a control board available. 6 | 7 | ## :fire: Liability Disclaimer :fire: 8 | 9 | **WARNING:** This code is provided "AS IS" without warranty of any kind. Use of this code in any form acknowledges your acceptance of these terms. 10 | 11 | Always approach such systems with caution. Ensure you understand the code, the systems involved, and the potential risks. If you're unsure, **DO NOT** use the code. 12 | 13 | Stay safe and think before you act. 14 | 15 | ## Simulator 16 | You can mess around with this project in the [ESP32 simulator](https://wokwi.com/projects/379601065746814977) 17 | Press play then mess with the switches and temp sensors 18 | Toggle IS_SIMULATION False if you'd like and manually simulate startup of a diesel heater (hint, increase exhaust temp during startup between each step) 19 | Note that the simulator code is now old, but it can still be useful and fun to play with 20 | 21 | ## Features: 22 | 23 | - **A full standalone Wi-Fi AP with built in web portal** 24 | - **Remote control via MQTT**: 25 | - Set target temperature 26 | - Start or stop the heater 27 | - Receive various readings 28 | - Set various parameters 29 | - **Temperature-based control** of air and fuel to regulate heating output. 30 | - **Safety shutdown** including an emergency stop thread and watchdogs. 31 | - **Reconnect mechanisms** for WiFi and MQTT in case of disconnection. 32 | - **Percentage and PID RPM Fan control** control the fan without RPM sensor, or be safer and use RPM based control with a hall effect sensor 33 | 34 | ## Hardware Requirements: 35 | 36 | - ESP32 board 37 | - Resistors, caps and such to build your board, NTC/PTC voltage divider 38 | - MOSFETs for controlling air, fuel, glow plug, and water pump. Relay can work for glow plug as high current 39 | - Single pole switch for manual start/stop 40 | 41 | ## Software Dependencies: 42 | 43 | - imports are all based on the built-in MicroPython distribution. Shouldn't need additional imports. 44 | 45 | ## Setup: 46 | 47 | 1. Connect the ESP32/Raspberry Pi Pico/MicroPython compataible board and other hardware components according to the pin definitions in the code (will be basing hardware on Webastardo, eventually) 48 | 2. Replace `MYSSID` and `PASSWORD` in the code with your WiFi SSID and password. 49 | 3. Set the `MQTT_SERVER` variable to your MQTT broker's IP address (need to make this optional) 50 | 4. GO THROUGH THE CONFIG.py and UNDERSTAND and READ THE COMMENTS. 51 | 5. Flash the repo onto your ESP32. 52 | 6. Pray, and keep a fire entinguisher on hand. 53 | 54 | ## Usage: 55 | 56 | - The system will automatically try to connect to the specified WiFi network and MQTT broker upon startup if configured. 57 | - Use the MQTT topics `heater/set_temp` and `heater/command` to set the target temperature and send start/stop commands, respectively. 58 | - The system will publish sensor readings to the `heater/sensor_values` topic at regular intervals. 59 | - If the exhaust temperature exceeds the safety threshold, the system will automatically shut down, or at least it's supposed to. 60 | - The switch can be used for manual start/stop control. 61 | 62 | ## Future Improvements/Ideas/Random notes: 63 | 64 | - ~~Possibly implement a PID controller for more accurate temperature control.~~ Implemented a fan PID controller. Linear is good enough for overall temp 65 | - Add support for more sensors and actuators, make things configurable. In progress. 66 | - Improve error handling and system resilience. 67 | - Possibly use an external ADC chip like the DS1232/ADS1234 to get around ESP32 ADC noise issues 68 | - Would be nice to have some sort of air/fuel autotune 69 | - Eventually would be nice to have a custom/own board that's universal use friendly, such as with screw wire terminals 70 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "FuelPumpControl": { 3 | "MAX_PUMP_FREQUENCY": 5.0, 4 | "PUMP_ON_TIME": 0.02, 5 | "MIN_PUMP_FREQUENCY": 1.0 6 | }, 7 | "ShutdownSettings": { 8 | "EXHAUST_SHUTDOWN_TEMP": 40.0, 9 | "SHUTDOWN_TIME_LIMIT": 300, 10 | "COOLDOWN_MIN_TIME": 30 11 | }, 12 | "NetworkSettings": { 13 | "MQTT_CLIENT_ID": "esp32_oshw_controller", 14 | "SET_TEMP_TOPIC": "set/temperature", 15 | "MQTT_USERNAME": "USERNAME", 16 | "COMMAND_TOPIC": "comm", 17 | "PASSWORD": "PASSWORD", 18 | "SENSOR_VALUES_TOPIC": "sensors/values", 19 | "MQTT_SERVER": "10.0.0.137", 20 | "SSID": "SSID", 21 | "MQTT_PASSWORD": "PASSWORD" 22 | }, 23 | "EmergencyHandling": { 24 | "EMERGENCY_STOP_TIMER": 600000 25 | }, 26 | "FanControl": { 27 | "MIN_FAN_PERCENTAGE": 20, 28 | "MIN_FAN_RPM": 2000, 29 | "MAX_FAN_PERCENTAGE": 60, 30 | "FAN_START_PERCENTAGE": 40, 31 | "FAN_MAX_DUTY": 1023, 32 | "FAN_RPM_SENSOR": false, 33 | "MAX_FAN_RPM": 5000 34 | }, 35 | "StartupSettings": { 36 | "STARTUP_TIME_LIMIT": 300, 37 | "FAILURE_STATE_RETRIES": 3, 38 | "GLOW_PLUG_HEAT_UP_TIME": 60, 39 | "INITIAL_FAN_SPEED_PERCENTAGE": 20 40 | }, 41 | "TemperatureControl": { 42 | "CONTROL_MAX_DELTA": 5, 43 | "TARGET_TEMP": 22.0 44 | }, 45 | "SensorSettings": { 46 | "OUTPUT_SENSOR_TYPE": "NTC_50k", 47 | "EXHAUST_SENSOR_TYPE": "PTC_1k", 48 | "OUTPUT_SENSOR_BETA": 3950, 49 | "EXHAUST_SENSOR_BETA": 3000 50 | }, 51 | "GeneralSettings": { 52 | "USE_WEBSERVER": true, 53 | "USE_WIFI": false, 54 | "IS_WATER_HEATER": false, 55 | "USE_MQTT": false, 56 | "HAS_SECOND_PUMP": false, 57 | "IS_SIMULATION": false 58 | }, 59 | "FlameOutDetection": { 60 | "EXHAUST_TEMP_HISTORY_LENGTH": 5, 61 | "MIN_TEMP_DELTA": 2.0 62 | }, 63 | "LoggingLevel": { 64 | "LOG_LEVEL": 3 65 | }, 66 | "SafetyLimits": { 67 | "EXHAUST_SAFE_TEMP": 160, 68 | "OUTPUT_SAFE_TEMP": 90 69 | } 70 | } -------------------------------------------------------------------------------- /hardwareConfig.py: -------------------------------------------------------------------------------- 1 | import machine 2 | import utime 3 | import json 4 | 5 | # Load the configuration from config.json 6 | with open('config.json') as config_file: 7 | config = json.load(config_file) 8 | 9 | # Assign the loaded values to variables 10 | # ┌─────────────────────┐ 11 | # │ General Settings │ 12 | # └─────────────────────┘ 13 | USE_WEBSERVER = config['GeneralSettings']['USE_WEBSERVER'] 14 | USE_WIFI = config['GeneralSettings']['USE_WIFI'] 15 | USE_MQTT = config['GeneralSettings']['USE_MQTT'] 16 | IS_WATER_HEATER = config['GeneralSettings']['IS_WATER_HEATER'] 17 | HAS_SECOND_PUMP = config['GeneralSettings']['HAS_SECOND_PUMP'] 18 | IS_SIMULATION = config['GeneralSettings']['IS_SIMULATION'] 19 | 20 | # ┌─────────────────────┐ 21 | # │ Network Settings │ 22 | # └─────────────────────┘ 23 | SSID = config['NetworkSettings']['SSID'] 24 | PASSWORD = config['NetworkSettings']['PASSWORD'] 25 | MQTT_SERVER = config['NetworkSettings']['MQTT_SERVER'] 26 | MQTT_CLIENT_ID = config['NetworkSettings']['MQTT_CLIENT_ID'] 27 | MQTT_USERNAME = config['NetworkSettings']['MQTT_USERNAME'] 28 | MQTT_PASSWORD = config['NetworkSettings']['MQTT_PASSWORD'] 29 | 30 | SENSOR_VALUES_TOPIC = config['NetworkSettings']['SENSOR_VALUES_TOPIC'] 31 | SET_TEMP_TOPIC = config['NetworkSettings']['SET_TEMP_TOPIC'] 32 | COMMAND_TOPIC = config['NetworkSettings']['COMMAND_TOPIC'] 33 | 34 | # ┌─────────────────────┐ 35 | # │ Safety Limits │ 36 | # └─────────────────────┘ 37 | EXHAUST_SAFE_TEMP = config['SafetyLimits']['EXHAUST_SAFE_TEMP'] 38 | OUTPUT_SAFE_TEMP = config['SafetyLimits']['OUTPUT_SAFE_TEMP'] 39 | 40 | # ┌─────────────────────┐ 41 | # │ Sensor Settings │ 42 | # └─────────────────────┘ 43 | OUTPUT_SENSOR_TYPE = config['SensorSettings']['OUTPUT_SENSOR_TYPE'] 44 | OUTPUT_SENSOR_BETA = config['SensorSettings']['OUTPUT_SENSOR_BETA'] 45 | EXHAUST_SENSOR_TYPE = config['SensorSettings']['EXHAUST_SENSOR_TYPE'] 46 | EXHAUST_SENSOR_BETA = config['SensorSettings']['EXHAUST_SENSOR_BETA'] 47 | 48 | # ┌─────────────────────┐ 49 | # │ Temperature Control │ 50 | # └─────────────────────┘ 51 | TARGET_TEMP = config['TemperatureControl']['TARGET_TEMP'] 52 | CONTROL_MAX_DELTA = config['TemperatureControl']['CONTROL_MAX_DELTA'] 53 | 54 | # ┌─────────────────────┐ 55 | # │ Fan Control │ 56 | # └─────────────────────┘ 57 | FAN_RPM_SENSOR = config['FanControl']['FAN_RPM_SENSOR'] 58 | MIN_FAN_RPM = config['FanControl']['MIN_FAN_RPM'] 59 | MAX_FAN_RPM = config['FanControl']['MAX_FAN_RPM'] 60 | FAN_MAX_DUTY = config['FanControl']['FAN_MAX_DUTY'] 61 | MIN_FAN_PERCENTAGE = config['FanControl']['MIN_FAN_PERCENTAGE'] 62 | MAX_FAN_PERCENTAGE = config['FanControl']['MAX_FAN_PERCENTAGE'] 63 | FAN_START_PERCENTAGE = config['FanControl']['FAN_START_PERCENTAGE'] 64 | 65 | # ┌─────────────────────┐ 66 | # │ Fuel Pump Control │ 67 | # └─────────────────────┘ 68 | MIN_PUMP_FREQUENCY = config['FuelPumpControl']['MIN_PUMP_FREQUENCY'] 69 | MAX_PUMP_FREQUENCY = config['FuelPumpControl']['MAX_PUMP_FREQUENCY'] 70 | PUMP_ON_TIME = config['FuelPumpControl']['PUMP_ON_TIME'] 71 | 72 | # ┌─────────────────────┐ 73 | # │ Emergency Handling │ 74 | # └─────────────────────┘ 75 | EMERGENCY_STOP_TIMER = config['EmergencyHandling']['EMERGENCY_STOP_TIMER'] 76 | 77 | # ┌─────────────────────┐ 78 | # │ Startup Settings │ 79 | # └─────────────────────┘ 80 | STARTUP_TIME_LIMIT = config['StartupSettings']['STARTUP_TIME_LIMIT'] 81 | FAILURE_STATE_RETRIES = config['StartupSettings']['FAILURE_STATE_RETRIES'] 82 | GLOW_PLUG_HEAT_UP_TIME = config['StartupSettings']['GLOW_PLUG_HEAT_UP_TIME'] 83 | INITIAL_FAN_SPEED_PERCENTAGE = config['StartupSettings']['INITIAL_FAN_SPEED_PERCENTAGE'] 84 | 85 | # ┌─────────────────────┐ 86 | # │ Shutdown Settings │ 87 | # └─────────────────────┘ 88 | SHUTDOWN_TIME_LIMIT = config['ShutdownSettings']['SHUTDOWN_TIME_LIMIT'] 89 | COOLDOWN_MIN_TIME = config['ShutdownSettings']['COOLDOWN_MIN_TIME'] 90 | EXHAUST_SHUTDOWN_TEMP = config['ShutdownSettings']['EXHAUST_SHUTDOWN_TEMP'] 91 | 92 | # ┌─────────────────────┐ 93 | # │ Flame-out Detection │ 94 | # └─────────────────────┘ 95 | EXHAUST_TEMP_HISTORY_LENGTH = config['FlameOutDetection']['EXHAUST_TEMP_HISTORY_LENGTH'] 96 | MIN_TEMP_DELTA = config['FlameOutDetection']['MIN_TEMP_DELTA'] 97 | 98 | # ┌─────────────────────┐ 99 | # │ Logging Level │ 100 | # └─────────────────────┘ 101 | LOG_LEVEL = config['LoggingLevel']['LOG_LEVEL'] 102 | 103 | # ┌─────────────────────┐ 104 | # │ Global Variables │ 105 | # └─────────────────────┘ 106 | pump_frequency = 0 107 | startup_attempts = 0 108 | startup_successful = True 109 | current_state = 'OFF' 110 | emergency_reason = None 111 | output_temp = 0 112 | exhaust_temp = 0 113 | heartbeat = utime.time() 114 | fan_speed_percentage = 0 115 | fan_rpm = 0 116 | 117 | # ┌─────────────────────┐ 118 | # │ Pin Assignments │ 119 | # └─────────────────────┘ 120 | 121 | # Fuel Control 122 | FUEL_PIN = machine.Pin(5, machine.Pin.OUT) 123 | FUEL_PIN.off() # Initialize to OFF 124 | 125 | # Air Control 126 | AIR_PIN = machine.Pin(23, machine.Pin.OUT) 127 | air_pwm = machine.PWM(AIR_PIN) 128 | air_pwm.freq(15000) 129 | air_pwm.duty(0) # Initialize to OFF 130 | 131 | # Glow Plug Control 132 | GLOW_PIN = machine.Pin(21, machine.Pin.OUT) 133 | GLOW_PIN.off() # Initialize to OFF 134 | 135 | # Water Control 136 | if IS_WATER_HEATER: 137 | WATER_PIN = machine.Pin(19, machine.Pin.OUT) 138 | WATER_PIN.off() # Initialize to OFF 139 | 140 | if HAS_SECOND_PUMP: 141 | WATER_SECONDARY_PIN = machine.Pin(18, machine.Pin.OUT) 142 | WATER_SECONDARY_PIN.off() # Initialize to OFF 143 | 144 | # Fan RPM Sensor 145 | if FAN_RPM_SENSOR: 146 | FAN_RPM_PIN = machine.Pin(22, machine.Pin.IN, machine.Pin.PULL_UP) 147 | 148 | # Switch Control 149 | SWITCH_PIN = machine.Pin(33, machine.Pin.IN, machine.Pin.PULL_UP) 150 | 151 | # ADC for Temp Sensor 152 | OUTPUT_TEMP_ADC = machine.ADC(machine.Pin(32)) 153 | OUTPUT_TEMP_ADC.atten(machine.ADC.ATTN_11DB) 154 | 155 | EXHAUST_TEMP_ADC = machine.ADC(machine.Pin(34)) 156 | EXHAUST_TEMP_ADC.atten(machine.ADC.ATTN_11DB) 157 | -------------------------------------------------------------------------------- /lib/fanPID.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import utime 31 | import hardwareConfig as config 32 | import machine 33 | 34 | # Initialize global variables 35 | rpm_interrupt_count = 0 36 | last_measurement_time = 0 37 | 38 | 39 | # Interrupt handler function for the Hall Effect sensor 40 | def rpm_interrupt_handler(pin): 41 | global rpm_interrupt_count 42 | rpm_interrupt_count += 1 43 | 44 | 45 | # Initialize the interrupt for the Hall Effect Sensor 46 | if config.FAN_RPM_SENSOR: 47 | config.FAN_RPM_PIN.irq(trigger=machine.Pin.IRQ_RISING, handler=rpm_interrupt_handler) 48 | 49 | 50 | class PIDController: 51 | def __init__(self, kp, ki, kd): 52 | self.kp = kp 53 | self.ki = ki 54 | self.kd = kd 55 | self.prev_error = 0 56 | self.integral = 0 57 | 58 | def calculate(self, setpoint, current_value): 59 | error = setpoint - current_value 60 | self.integral += error 61 | derivative = error - self.prev_error 62 | 63 | output = self.kp * error + self.ki * self.integral + self.kd * derivative 64 | self.prev_error = error 65 | 66 | return output 67 | 68 | 69 | def set_fan_duty_cycle(duty_cycle): 70 | # Clip the duty cycle within the allowed range 71 | duty_cycle = max(0, min(duty_cycle, config.FAN_MAX_DUTY)) 72 | config.air_pwm.duty(duty_cycle) 73 | 74 | 75 | def fan_control_thread(): 76 | pid = PIDController(kp=1.0, ki=0.1, kd=0.01) # Initialize your PID controller with appropriate constants 77 | 78 | global rpm_interrupt_count, last_measurement_time 79 | 80 | while True: 81 | # Read current RPM from sensor 82 | current_time = utime.ticks_ms() 83 | elapsed_time = utime.ticks_diff(current_time, last_measurement_time) / 1000.0 # Convert to seconds 84 | current_rpm = (rpm_interrupt_count / 2) / (elapsed_time / 60) 85 | rpm_interrupt_count = 0 86 | last_measurement_time = current_time 87 | 88 | # Write the current RPM to config 89 | config.fan_rpm = current_rpm 90 | 91 | # Calculate target RPM based on config.fan_speed_percentage 92 | target_rpm = config.MIN_FAN_RPM + ( 93 | config.fan_speed_percentage * (config.MAX_FAN_RPM - config.MIN_FAN_RPM) / 100) 94 | 95 | # Calculate the PID output 96 | pid_output = pid.calculate(target_rpm, current_rpm) 97 | 98 | # Calculate the new duty cycle 99 | new_duty_cycle = int((pid_output / 100) * config.FAN_MAX_DUTY) 100 | 101 | # Use the PID output to set the fan speed 102 | set_fan_duty_cycle(new_duty_cycle) 103 | 104 | # Sleep for a bit before the next iteration 105 | utime.sleep(0.2) # 200 ms 106 | -------------------------------------------------------------------------------- /lib/helpers.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | # Common helper functions 31 | import hardwareConfig as config 32 | 33 | 34 | def set_fan_percentage(speed_percentage): 35 | """ 36 | Set the fan speed according to the given percentage. 37 | 38 | :param speed_percentage: The speed percentage for the fan. 39 | """ 40 | if config.FAN_RPM_SENSOR: 41 | # Directly set the fan speed percentage for RPM control 42 | config.fan_speed_percentage = speed_percentage 43 | else: 44 | # Special case: 0% should equal 0% output 45 | if speed_percentage == 0: 46 | scaled_speed = 0 47 | else: 48 | # Ensure speed_percentage is within limits defined in config 49 | speed_percentage = max(config.MIN_FAN_PERCENTAGE, min(speed_percentage, config.MAX_FAN_PERCENTAGE)) # Limit to 0-100 50 | 51 | # Scale the speed_percentage taking into account the FAN_START_PERCENTAGE 52 | scaled_speed = config.FAN_START_PERCENTAGE + ( 53 | speed_percentage * (config.MAX_FAN_PERCENTAGE - config.FAN_START_PERCENTAGE) / 100) 54 | 55 | config.fan_speed_percentage = scaled_speed 56 | 57 | # Calculate the fan duty and set it 58 | fan_duty = int((config.fan_speed_percentage / 100) * config.FAN_MAX_DUTY) 59 | config.air_pwm.duty(fan_duty) 60 | -------------------------------------------------------------------------------- /lib/networking.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import hardwareConfig as config 31 | import utime 32 | import json 33 | import network 34 | from umqtt.simple import MQTTClient 35 | 36 | # Initialize global variables 37 | wlan = None 38 | mqtt_client = None 39 | wifi_initialized = False 40 | mqtt_initialized = False 41 | 42 | 43 | # Initialize WiFi 44 | def init_wifi(): 45 | global wlan 46 | wlan = network.WLAN(network.STA_IF) 47 | wlan.active(True) 48 | 49 | 50 | # Initialize MQTT with authentication 51 | def init_mqtt(): 52 | global mqtt_client 53 | mqtt_client = MQTTClient(config.MQTT_CLIENT_ID, config.MQTT_SERVER, user=config.MQTT_USERNAME, 54 | password=config.MQTT_PASSWORD) 55 | 56 | 57 | # Connect to WiFi 58 | def connect_wifi(): 59 | if wlan and not wlan.isconnected(): 60 | print('Attempting WiFi connection...') 61 | wlan.connect(config.SSID, config.PASSWORD) 62 | while not wlan.isconnected(): 63 | utime.sleep(1) 64 | print(f'WiFi connected! IP Address: {wlan.ifconfig()[0]}') 65 | 66 | 67 | # Connect to MQTT 68 | def connect_mqtt(): 69 | global mqtt_client 70 | if not mqtt_client: 71 | init_mqtt() 72 | try: 73 | print('Attempting MQTT connection...') 74 | mqtt_client.connect() 75 | print('MQTT connected!') 76 | except Exception as e: 77 | print(f'Failed to connect to MQTT: {e}') 78 | mqtt_client = None # Reset client to None to attempt re-initialization later 79 | 80 | 81 | # MQTT Callback 82 | # Add these new attributes to the payload in publish_sensor_values() 83 | def publish_sensor_values(): 84 | global mqtt_client 85 | if mqtt_client: 86 | payload = { 87 | "output_temp": config.output_temp, 88 | "exhaust_temp": config.exhaust_temp, 89 | "current_state": config.current_state, 90 | "fan_speed_percentage": config.fan_speed_percentage, 91 | "pump_frequency": config.pump_frequency, 92 | "startup_attempts": config.startup_attempts, 93 | "emergency_reason": config.emergency_reason, 94 | "heartbeat": config.heartbeat, 95 | "startup_successful": config.startup_successful 96 | } 97 | mqtt_client.publish(config.SENSOR_VALUES_TOPIC, json.dumps(payload)) 98 | 99 | 100 | # Extend mqtt_callback() to handle new settings 101 | def mqtt_callback(topic, msg): 102 | topic = topic.decode('utf-8') 103 | msg = msg.decode('utf-8') 104 | if topic == config.SET_TEMP_TOPIC: 105 | config.TARGET_TEMP = float(msg) 106 | elif topic == config.COMMAND_TOPIC: 107 | if msg == "start": 108 | config.current_state = 'STARTING' 109 | elif msg == "stop": 110 | config.current_state = 'STOPPING' 111 | elif topic == "set/exhaust_safe_temp": 112 | config.EXHAUST_SAFE_TEMP = float(msg) 113 | elif topic == "set/output_safe_temp": 114 | config.OUTPUT_SAFE_TEMP = float(msg) 115 | elif topic == "set/min_fan_percentage": 116 | config.MIN_FAN_PERCENTAGE = int(msg) 117 | elif topic == "set/max_fan_percentage": 118 | config.MAX_FAN_PERCENTAGE = int(msg) 119 | elif topic == "set/min_pump_frequency": 120 | config.MIN_PUMP_FREQUENCY = int(msg) 121 | elif topic == "set/max_pump_frequency": 122 | config.MAX_PUMP_FREQUENCY = int(msg) 123 | elif topic == "set/log_level": 124 | config.LOG_LEVEL = int(msg) 125 | elif topic == "set/startup_time_limit": 126 | config.STARTUP_TIME_LIMIT = int(msg) 127 | elif topic == "set/shutdown_time_limit": 128 | config.SHUTDOWN_TIME_LIMIT = int(msg) 129 | elif topic == "set/control_max_delta": 130 | config.CONTROL_MAX_DELTA = float(msg) 131 | elif topic == "set/emergency_stop_timer": 132 | config.EMERGENCY_STOP_TIMER = int(msg) 133 | 134 | 135 | # Main function for networking 136 | def run_networking(): 137 | global wifi_initialized, mqtt_initialized, wlan, mqtt_client 138 | if config.USE_WIFI and not wifi_initialized: 139 | init_wifi() 140 | wifi_initialized = True 141 | if config.USE_MQTT and not mqtt_initialized: 142 | init_mqtt() 143 | mqtt_initialized = True 144 | 145 | if wlan and not wlan.isconnected(): # Check wlan is not None 146 | connect_wifi() 147 | if wlan and wlan.isconnected(): # Check wlan is not None 148 | if mqtt_client is None: 149 | connect_mqtt() 150 | if mqtt_client: # Make sure mqtt_client is not None 151 | try: 152 | mqtt_client.set_callback(mqtt_callback) 153 | mqtt_client.subscribe(config.SET_TEMP_TOPIC) 154 | mqtt_client.subscribe(config.COMMAND_TOPIC) 155 | mqtt_client.check_msg() 156 | publish_sensor_values() 157 | except Exception as e: 158 | print(f'Failed in MQTT operation: {e}') 159 | mqtt_client = None # Reset client to None to attempt re-initialization later 160 | 161 | utime.sleep(0.1) # Add sleep to avoid CPU hogging 162 | -------------------------------------------------------------------------------- /lib/sensors.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import math 31 | import hardwareConfig as config 32 | 33 | # Predefined R0, and T0 values for common thermistors 34 | common_thermistors = { 35 | 'NTC_10k': {'R0': 10000, 'T0': 298.15}, 36 | 'NTC_50k': {'R0': 50000, 'T0': 298.15}, 37 | 'NTC_100k': {'R0': 100000, 'T0': 298.15}, 38 | 'PTC_500': {'R0': 500, 'T0': 298.15}, 39 | 'PTC_1k': {'R0': 1000, 'T0': 298.15}, 40 | 'PTC_2.3k': {'R0': 2300, 'T0': 298.15}, 41 | } 42 | 43 | 44 | def log(message, level=1): 45 | if config.LOG_LEVEL >= level: 46 | print(f"[Sensor] {message}") 47 | 48 | 49 | # Initialize an empty list to keep track of the last N temperature measurements for each sensor 50 | temp_history_output = [] 51 | temp_history_exhaust = [] 52 | 53 | # The number of past measurements to average 54 | TEMP_HISTORY_LENGTH = 3 55 | 56 | 57 | def read_temp(analog_value, sensor_type, sensor_beta, sensor_name="output"): 58 | global temp_history_output, temp_history_exhaust 59 | 60 | try: 61 | if analog_value == 4095: 62 | log("Warning: ADC max value reached, can't calculate resistance") 63 | return 999 64 | 65 | resistance = 10000 * (analog_value / (4095 - analog_value)) 66 | params = common_thermistors.get(sensor_type, {}) 67 | 68 | if not params: 69 | log("Invalid sensor type specified") 70 | return 999 71 | 72 | R0 = params['R0'] 73 | T0 = params['T0'] 74 | BETA = sensor_beta 75 | 76 | temperature_k = 1 / ( 77 | math.log(resistance / R0) / BETA + 1 / T0 78 | ) if 'NTC' in sensor_type else 1 / ( 79 | 1 / T0 + (1 / BETA) * math.log(resistance / R0) 80 | ) 81 | 82 | temperature_c = temperature_k - 273.15 83 | 84 | # Choose the history list based on the sensor name 85 | history_list = temp_history_output if sensor_name == "output" else temp_history_exhaust 86 | 87 | # Add the new temperature measurement to the history 88 | history_list.append(temperature_c) 89 | 90 | # Remove the oldest measurement if history is too long 91 | if len(history_list) > TEMP_HISTORY_LENGTH: 92 | history_list.pop(0) 93 | 94 | # Calculate and return the average temperature 95 | avg_temperature = sum(history_list) / len(history_list) 96 | return avg_temperature 97 | 98 | except Exception as e: 99 | log(f"An error occurred while reading the temperature sensor: {e}") 100 | return 999 101 | 102 | 103 | # Global variables for simulation 104 | simulated_output_temp = 20 # Simulated output temperature 105 | simulated_exhaust_temp = 20 # Simulated exhaust temperature 106 | output_temp_ramp_direction = 1 # 1 for ramping up, -1 for ramping down 107 | 108 | 109 | # Read simulated output temperature 110 | def read_output_temp(): 111 | global simulated_output_temp, output_temp_ramp_direction 112 | if config.IS_SIMULATION: 113 | if config.current_state == 'RUNNING': 114 | if simulated_output_temp >= 80: 115 | output_temp_ramp_direction = -1 # Change direction to ramp down 116 | elif simulated_output_temp <= 50: 117 | output_temp_ramp_direction = 1 # Change direction to ramp up 118 | 119 | simulated_output_temp += output_temp_ramp_direction # Increment or decrement based on direction 120 | else: 121 | simulated_output_temp = 20 # Reset to 20 for other states 122 | return simulated_output_temp 123 | else: 124 | return read_temp( 125 | config.OUTPUT_TEMP_ADC.read(), 126 | config.OUTPUT_SENSOR_TYPE, 127 | config.OUTPUT_SENSOR_BETA, 128 | sensor_name="output" 129 | ) 130 | 131 | 132 | # Read simulated exhaust temperature 133 | def read_exhaust_temp(): 134 | global simulated_exhaust_temp 135 | if config.IS_SIMULATION: 136 | if config.current_state == 'STARTING': 137 | simulated_exhaust_temp = min(simulated_exhaust_temp + 1, 120) 138 | elif config.current_state == 'RUNNING': 139 | simulated_exhaust_temp = 120 140 | elif config.current_state == 'STOPPING': 141 | simulated_exhaust_temp = max(simulated_exhaust_temp - 1, 20) 142 | else: 143 | simulated_exhaust_temp = 20 # Reset to 20 for other states 144 | return simulated_exhaust_temp 145 | else: 146 | return read_temp( 147 | config.EXHAUST_TEMP_ADC.read(), 148 | config.EXHAUST_SENSOR_TYPE, 149 | config.EXHAUST_SENSOR_BETA, 150 | sensor_name="exhaust" 151 | ) 152 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import machine 31 | import _thread 32 | import hardwareConfig as config 33 | import utime 34 | from machine import Timer 35 | from states import stateMachine, emergencyStop 36 | from lib import sensors, networking, fanPID 37 | import webserver 38 | 39 | # Initialize the WDT with a 10-second timeout 40 | wdt = machine.WDT(id=0, timeout=10000) # 10 seconds 41 | 42 | 43 | def log(message, level=2): 44 | if config.LOG_LEVEL >= level: 45 | print(message) 46 | 47 | 48 | def get_reset_reason(): 49 | reset_reason = machine.reset_cause() 50 | if reset_reason == machine.PWRON_RESET: 51 | print("Reboot was because of Power-On!") 52 | elif reset_reason == machine.WDT_RESET: 53 | print("Reboot was because of WDT!") 54 | return reset_reason 55 | 56 | 57 | pulse_timer = Timer(0) 58 | last_pulse_time = 0 59 | off_timer = Timer(1) 60 | 61 | 62 | def turn_off_pump(_): 63 | config.FUEL_PIN.off() 64 | 65 | 66 | def pulse_fuel_callback(_): 67 | global last_pulse_time 68 | current_time = utime.ticks_ms() 69 | 70 | if utime.ticks_diff(current_time, config.heartbeat) > 10000: 71 | config.FUEL_PIN.off() 72 | log("Heartbeat missing, fuel pump turned off.") 73 | elif config.pump_frequency > 0: 74 | period = 1000.0 / config.pump_frequency 75 | 76 | if utime.ticks_diff(current_time, last_pulse_time) >= period: 77 | last_pulse_time = current_time 78 | config.FUEL_PIN.on() 79 | off_timer.init(period=int(config.PUMP_ON_TIME * 1000), mode=Timer.ONE_SHOT, callback=turn_off_pump) 80 | else: 81 | config.FUEL_PIN.off() 82 | 83 | 84 | pulse_timer.init(period=100, mode=Timer.PERIODIC, callback=pulse_fuel_callback) 85 | 86 | 87 | def emergency_stop_thread(): 88 | while True: 89 | wdt.feed() 90 | current_time = utime.ticks_ms() # Use ticks_ms to get the current time in milliseconds 91 | 92 | if utime.ticks_diff(current_time, config.heartbeat) > 10000: # Compare in milliseconds (10 seconds = 10000 ms) 93 | emergencyStop.emergency_stop("No heartbeat detected") 94 | 95 | utime.sleep(1) 96 | 97 | 98 | def run_networking_thread(): 99 | while True: 100 | networking.run_networking() 101 | utime.sleep(5) 102 | 103 | 104 | def main(): 105 | while True: 106 | config.heartbeat = utime.ticks_ms() 107 | 108 | config.output_temp = sensors.read_output_temp() 109 | config.exhaust_temp = sensors.read_exhaust_temp() 110 | current_switch_value = config.SWITCH_PIN.value() 111 | 112 | config.current_state, config.emergency_reason = stateMachine.handle_state( 113 | config.current_state, 114 | current_switch_value, 115 | config.exhaust_temp, 116 | config.output_temp 117 | ) 118 | 119 | log(f"Current state: {config.current_state}") 120 | if config.emergency_reason: 121 | log(f"Emergency reason: {config.emergency_reason}") 122 | 123 | utime.sleep(1) 124 | 125 | 126 | if __name__ == "__main__": 127 | boot_reason = get_reset_reason() 128 | log(f"Reset/Boot Reason was: {boot_reason}") 129 | _thread.start_new_thread(emergency_stop_thread, ()) 130 | _thread.start_new_thread(run_networking_thread, ()) 131 | if config.FAN_RPM_SENSOR: 132 | _thread.start_new_thread(fanPID.fan_control_thread, ()) 133 | if config.USE_WEBSERVER: 134 | _thread.start_new_thread(webserver.web_server, ()) 135 | main() 136 | 137 | -------------------------------------------------------------------------------- /states/control.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import hardwareConfig as config 31 | from lib import helpers 32 | 33 | # Initialize a list to store the last N exhaust temperatures 34 | exhaust_temp_history = [] 35 | 36 | 37 | def log(message, level=1): 38 | if config.LOG_LEVEL >= level: 39 | print(f"[Control] {message}") 40 | 41 | 42 | def calculate_pump_frequency(target_temp, output_temp, max_delta, max_frequency, min_frequency): 43 | delta = target_temp - output_temp 44 | pump_frequency = min(max((delta / max_delta) * max_frequency, min_frequency), max_frequency) 45 | return pump_frequency 46 | 47 | 48 | def control_air_and_fuel(output_temp, exhaust_temp): 49 | log("Performing air and fuel control...") 50 | 51 | # Update the exhaust temperature history 52 | exhaust_temp_history.append(exhaust_temp) 53 | 54 | # Manually enforce maximum length 55 | while len(exhaust_temp_history) > config.EXHAUST_TEMP_HISTORY_LENGTH: 56 | exhaust_temp_history.pop(0) 57 | 58 | # Check for decreasing exhaust temperature over the last N readings 59 | if len(exhaust_temp_history) == config.EXHAUST_TEMP_HISTORY_LENGTH: 60 | if all(earlier - later > config.MIN_TEMP_DELTA for earlier, later in 61 | zip(exhaust_temp_history, exhaust_temp_history[1:])): 62 | log("Flame out detected based on decreasing exhaust temperature. Exiting...", level=0) 63 | return "FLAME_OUT" 64 | 65 | # Calculate the fan speed percentage based on temperature delta 66 | delta = config.TARGET_TEMP - output_temp 67 | config.fan_speed_percentage = min(max((delta / config.CONTROL_MAX_DELTA) * 100, config.MIN_FAN_PERCENTAGE), 68 | config.MAX_FAN_PERCENTAGE) 69 | 70 | # Use the helper function to set the fan speed 71 | helpers.set_fan_percentage(config.fan_speed_percentage) 72 | 73 | pump_frequency = calculate_pump_frequency( 74 | config.TARGET_TEMP, output_temp, config.CONTROL_MAX_DELTA, 75 | config.MAX_PUMP_FREQUENCY, config.MIN_PUMP_FREQUENCY 76 | ) 77 | 78 | # Update global variables 79 | config.pump_frequency = pump_frequency 80 | 81 | log(f"Fan speed: {config.fan_speed_percentage}%, Pump frequency: {pump_frequency} Hz, Exhaust Temp: {config.exhaust_temp}, Output Temp: {config.output_temp}") 82 | 83 | # Additional hardware controls 84 | if config.IS_WATER_HEATER: 85 | config.WATER_PIN.on() 86 | 87 | if config.HAS_SECOND_PUMP: 88 | config.WATER_SECONDARY_PIN.on() 89 | -------------------------------------------------------------------------------- /states/emergencyStop.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import hardwareConfig as config 31 | from machine import Timer, reset 32 | 33 | 34 | def log(message, level=1): 35 | if config.LOG_LEVEL >= level: 36 | print(f"[Emergency Stop] {message}") 37 | 38 | 39 | def turn_off_pumps(timer): 40 | config.air_pwm.duty(0) 41 | log("Fan turned off after 10 minutes.") 42 | 43 | if config.IS_WATER_HEATER: 44 | config.WATER_PIN.off() 45 | log("Water pump turned off after 10 minutes.") 46 | 47 | if config.HAS_SECOND_PUMP: 48 | config.WATER_SECONDARY_PIN.off() 49 | log("Secondary water pump turned off after 10 minutes.") 50 | 51 | timer.deinit() # Stop the timer 52 | log("Performing hard reset...") 53 | reset() # Perform a hard reset 54 | 55 | 56 | def emergency_stop(reason): 57 | log(f"Triggered due to {reason}. Initiating emergency stop sequence.") 58 | 59 | # Create a timer that will call `turn_off_pumps` after 10 minutes 60 | pump_timer = Timer(-1) 61 | pump_timer.init(period=config.EMERGENCY_STOP_TIMER, mode=Timer.ONE_SHOT, callback=turn_off_pumps) 62 | 63 | while True: 64 | config.current_state = 'EMERGENCY_STOP' 65 | config.GLOW_PIN.off() 66 | config.FUEL_PIN.off() 67 | if config.IS_WATER_HEATER: 68 | config.WATER_PIN.on() 69 | if config.HAS_SECOND_PUMP: 70 | config.WATER_SECONDARY_PIN.on() 71 | config.air_pwm.duty(config.FAN_MAX_DUTY) 72 | config.pump_frequency = 0 73 | log("All pins and frequencies set to safe states. Please reboot to continue.") 74 | -------------------------------------------------------------------------------- /states/shutdown.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import hardwareConfig as config 31 | import utime 32 | from lib import helpers 33 | 34 | 35 | def log(message, level=2): 36 | if config.LOG_LEVEL >= level: 37 | print(f"[Shutdown] {message}") 38 | 39 | 40 | def shut_down(): 41 | log("Shutting Down") 42 | step = 0 43 | config.current_state = 'STOPPING' 44 | cooldown_start_time = None 45 | shutdown_start_time = utime.time() 46 | 47 | while True: 48 | config.heartbeat = utime.ticks_ms() 49 | config.exhaust_temp = config.exhaust_temp 50 | if utime.time() - shutdown_start_time > config.SHUTDOWN_TIME_LIMIT: 51 | log("Shutdown took too long, triggering emergency stop.") 52 | return 53 | 54 | if step == 0: 55 | log("Stopping fuel supply...") 56 | config.pump_frequency = 0 57 | step += 1 58 | 59 | elif step == 1: 60 | if cooldown_start_time is None: 61 | log("Activating glow plug and fan for purging and cooling...") 62 | helpers.set_fan_percentage(config.MAX_FAN_PERCENTAGE) 63 | config.GLOW_PIN.on() 64 | cooldown_start_time = utime.time() 65 | 66 | current_exhaust_temp = config.exhaust_temp 67 | elapsed_time = utime.time() - cooldown_start_time 68 | 69 | log( 70 | f"Cooling down... Elapsed Time: {elapsed_time}s, Target Exhaust Temp: {config.EXHAUST_SHUTDOWN_TEMP}C, Current Exhaust Temp: {current_exhaust_temp}C") 71 | 72 | if elapsed_time >= config.COOLDOWN_MIN_TIME and current_exhaust_temp <= config.EXHAUST_SHUTDOWN_TEMP: 73 | step += 1 74 | 75 | elif step == 2: 76 | log("Turning off electrical components...") 77 | helpers.set_fan_percentage(0) 78 | config.GLOW_PIN.off() 79 | if config.IS_WATER_HEATER: 80 | config.WATER_PIN.off() 81 | if config.HAS_SECOND_PUMP: 82 | config.WATER_SECONDARY_PIN.off() 83 | log("Finished Shutting Down") 84 | break 85 | 86 | utime.sleep(1) 87 | -------------------------------------------------------------------------------- /states/startup.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import hardwareConfig as config 31 | import utime 32 | import main 33 | from lib import helpers 34 | 35 | 36 | def state_message(state, message): 37 | print(f"[Current Startup Procedure: - {state}] {message}") 38 | 39 | 40 | def start_up(): 41 | state = "WARMING_GLOW_PLUG" 42 | step = 1 43 | exhaust_temps = [] 44 | initial_exhaust_temp = None 45 | last_time_checked = utime.time() 46 | if config.IS_SIMULATION: 47 | glow_plug_heat_up_end_time = last_time_checked + 1 48 | else: 49 | glow_plug_heat_up_end_time = last_time_checked + 60 50 | startup_start_time = last_time_checked 51 | startup_time_limit = 300 # 5 minutes in seconds 52 | 53 | while True: 54 | current_time = utime.time() 55 | config.heartbeat = utime.ticks_ms() 56 | main.wdt.feed() 57 | 58 | if current_time - startup_start_time > startup_time_limit: 59 | state_message("TIMEOUT", "Startup took too long. Changing state to STOPPING.") 60 | config.startup_successful = False 61 | return 62 | 63 | if state == "WARMING_GLOW_PLUG": 64 | state_message(state, "Initializing system...") 65 | config.startup_successful = False # Assume startup will fail 66 | initial_exhaust_temp = config.exhaust_temp 67 | if initial_exhaust_temp > 100: 68 | state_message(state, "Initial exhaust temperature too high. Stopping...") 69 | config.startup_successful = False 70 | return 71 | helpers.set_fan_percentage(config.FAN_START_PERCENTAGE) 72 | config.GLOW_PIN.on() 73 | if config.IS_WATER_HEATER: 74 | config.WATER_PIN.on() 75 | if config.HAS_SECOND_PUMP: 76 | config.WATER_SECONDARY_PIN.on() 77 | state_message(state, f"Fan: {config.fan_speed_percentage}%, Glow plug: On") 78 | state = "INITIAL_FUELING" 79 | 80 | elif state == "INITIAL_FUELING": 81 | # state_message(state, "Waiting for glow plug to heat up...") 82 | if current_time >= glow_plug_heat_up_end_time: 83 | config.pump_frequency = config.MIN_PUMP_FREQUENCY 84 | state_message(state, f"Fuel Pump: {config.pump_frequency} Hz") 85 | state = "RAMPING_UP" 86 | last_time_checked = current_time 87 | exhaust_temps = [] 88 | 89 | elif state == "RAMPING_UP": 90 | if current_time - last_time_checked >= 1: 91 | last_time_checked = current_time 92 | exhaust_temps.append(config.exhaust_temp) 93 | 94 | if len(exhaust_temps) >= 20: 95 | avg_exhaust_temp = sum(exhaust_temps) / len(exhaust_temps) 96 | state_message(state, f"Average Exhaust Temp at step {step}: {avg_exhaust_temp}C") 97 | 98 | if avg_exhaust_temp >= 100: 99 | state_message("COMPLETED", "Reached target exhaust temperature. Startup Procedure Completed.") 100 | config.startup_successful = True 101 | config.startup_attempts = 0 102 | config.GLOW_PIN.off() 103 | return 104 | 105 | elif initial_exhaust_temp + 5 < avg_exhaust_temp: 106 | config.fan_speed_percentage = min(config.fan_speed_percentage + 20, 100) 107 | helpers.set_fan_percentage(config.fan_speed_percentage) 108 | config.pump_frequency = min(config.pump_frequency + 1, config.MAX_PUMP_FREQUENCY) 109 | state_message(state, 110 | f"Step {step} successful. Fan: {config.fan_speed_percentage}%, Fuel Pump: {config.pump_frequency} Hz") 111 | initial_exhaust_temp = avg_exhaust_temp 112 | step += 1 113 | 114 | if step > 5: 115 | state_message("COMPLETED", "Startup Procedure Completed") 116 | config.startup_successful = True 117 | config.startup_attempts = 0 118 | return 119 | 120 | exhaust_temps = [] 121 | else: 122 | state_message(state, "Temperature not rising as expected. Changing state to STOPPING.") 123 | config.current_state = 'STOPPING' 124 | config.startup_attempts += 1 125 | return 126 | -------------------------------------------------------------------------------- /states/stateMachine.py: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # WARNING # 3 | #################################################################### 4 | # This code is provided "AS IS" without warranty of any kind. # 5 | # Use of this code in any form acknowledges your acceptance of # 6 | # these terms. # 7 | # # 8 | # This code has NOT been tested in real-world scenarios. # 9 | # Improper usage, lack of understanding, or any combination # 10 | # thereof can result in significant property damage, injury, # 11 | # loss of life, or worse. # 12 | # Specifically, this code is related to controlling heating # 13 | # elements and systems, and there's a very real risk that it # 14 | # can BURN YOUR SHIT DOWN. # 15 | # # 16 | # By using, distributing, or even reading this code, you agree # 17 | # to assume all responsibility and risk associated with it. # 18 | # The author(s), contributors, and distributors of this code # 19 | # will NOT be held liable for any damages, injuries, or other # 20 | # consequences you may face as a result of using or attempting # 21 | # to use this code. # 22 | # # 23 | # Always approach such systems with caution. Ensure you understand # 24 | # the code, the systems involved, and the potential risks. # 25 | # If you're unsure, DO NOT use the code. # 26 | # # 27 | # Stay safe and think before you act. # 28 | #################################################################### 29 | 30 | import hardwareConfig as config 31 | from states import startup, shutdown, control 32 | 33 | 34 | def log(message, level=2): 35 | if config.LOG_LEVEL >= level: 36 | print(message) 37 | 38 | 39 | def handle_state(current_state, switch_value, exhaust_temp, output_temp): 40 | emergency_reason = None 41 | 42 | # When we are in OFF and the switch is OFF, we stay in OFF 43 | if current_state == 'OFF': 44 | if switch_value == 1: 45 | config.startup_attempts = 0 46 | return 'OFF', None 47 | else: 48 | return 'STARTING', None 49 | 50 | # When starting is successful, we transition into RUNNING 51 | if current_state == 'STARTING': 52 | startup.start_up() 53 | if config.startup_successful: 54 | config.startup_attempts = 0 55 | return 'RUNNING', None 56 | else: 57 | config.startup_attempts += 1 58 | if config.startup_attempts >= config.FAILURE_STATE_RETRIES: 59 | shutdown.shut_down() 60 | return 'FAILURE', None 61 | return 'STARTING', None 62 | 63 | if current_state == 'RUNNING': 64 | if output_temp > config.TARGET_TEMP + 2: 65 | shutdown.shut_down() 66 | return 'STANDBY', None 67 | elif switch_value == 1: 68 | config.startup_attempts = 0 69 | shutdown.shut_down() 70 | return 'OFF', None 71 | else: 72 | flame_status = control.control_air_and_fuel(output_temp, exhaust_temp) 73 | if flame_status == "FLAME_OUT": 74 | shutdown.shut_down() 75 | config.startup_attempts += 1 76 | return 'STARTING', None 77 | return 'RUNNING', None 78 | 79 | # When in STANDBY and the temps drops 10C under the set state, we transition from STANDBY to STARTING, then RUNNING 80 | if current_state == 'STANDBY': 81 | if output_temp < config.TARGET_TEMP - 2 and switch_value == 0: 82 | return 'STARTING', None 83 | elif switch_value == 1: 84 | config.startup_attempts = 0 85 | return 'OFF', None 86 | return 'STANDBY', None 87 | 88 | # When in FAILURE, the user can switch off and back on to start over 89 | if current_state == 'FAILURE': 90 | if switch_value == 1: 91 | config.startup_attempts = 0 92 | return 'OFF', None 93 | return 'FAILURE', None 94 | 95 | return current_state, emergency_reason 96 | -------------------------------------------------------------------------------- /tools/get_file.py: -------------------------------------------------------------------------------- 1 | from ampy.pyboard import Pyboard 2 | from ampy.files import Files 3 | 4 | # Replace 'COM5' with the appropriate port on your system 5 | port = 'COM5' 6 | 7 | 8 | def get_file(filename): 9 | try: 10 | pyb = Pyboard(port) 11 | files = Files(pyb) 12 | contents = files.get(filename) 13 | with open(filename, 'wb') as file: 14 | file.write(contents) 15 | print(f"File {filename} has been successfully downloaded.") 16 | except Exception as e: 17 | print(f"An error occurred: {e}") 18 | finally: 19 | pyb.close() 20 | 21 | 22 | # Call the function with the name of the file you want to retrieve 23 | get_file('config.json') 24 | -------------------------------------------------------------------------------- /webserver.py: -------------------------------------------------------------------------------- 1 | import network 2 | import socket 3 | import machine 4 | import json 5 | import utime 6 | 7 | 8 | def unquote_plus(string): 9 | # Replace '+' with ' ' and decode percent-encoded characters 10 | string = string.replace('+', ' ') 11 | parts = string.split('%') 12 | if len(parts) > 1: 13 | string = parts[0] 14 | for item in parts[1:]: 15 | try: 16 | if len(item) >= 2: 17 | string += chr(int(item[:2], 16)) + item[2:] 18 | else: 19 | string += '%' + item 20 | except ValueError: 21 | string += '%' + item 22 | return string 23 | 24 | 25 | # Define HTML escape function 26 | def escape_html(text): 27 | html_escape_table = { 28 | "&": "&", 29 | '"': """, 30 | "'": "'", 31 | ">": ">", 32 | "<": "<", 33 | } 34 | return "".join(html_escape_table.get(c, c) for c in text) 35 | 36 | 37 | # Configure Access Point 38 | ap = network.WLAN(network.AP_IF) 39 | ap.active(True) 40 | ap.config(essid='esp32-diesel-ecu', password='794759876') 41 | 42 | # HTML page template 43 | HTML_PAGE = """ 44 | 45 | 46 | 47 | 48 | 49 | ZorroBurner Configuration 50 | 51 | 52 |
53 |

ZorroBurner Configuration

54 |
55 | {} 56 | 57 |
58 |
59 | 60 |
61 |
62 | 63 | 64 | """ 65 | 66 | 67 | # Read config.json and return as dictionary 68 | def read_config_params(): 69 | try: 70 | with open('config.json', 'r') as f: 71 | return json.load(f) 72 | except OSError: 73 | return {} 74 | 75 | 76 | # Generate HTML page based on config.json 77 | def generate_html_page(params): 78 | input_fields = "" 79 | for section, settings in params.items(): 80 | input_fields += f"

{escape_html(section)}

" 81 | for key, value in settings.items(): 82 | safe_key = escape_html(key) 83 | safe_value = escape_html(str(value)) 84 | input_fields += f'{safe_key}:
' 85 | return HTML_PAGE.format(input_fields) 86 | 87 | 88 | # Custom pretty-print function for JSON-like dictionaries 89 | def pretty_print_json(data, indent=4, level=0): 90 | if not isinstance(data, dict): # if the data is not a dictionary, just return it as a string 91 | return str(data) 92 | items = [] 93 | for key, value in data.items(): 94 | items.append(' ' * (level * indent) + f'"{key}": ' + ( 95 | pretty_print_json(value, indent, level + 1) if isinstance(value, dict) else json.dumps(value))) 96 | return "{\n" + ",\n".join(items) + "\n" + ' ' * (level - 1) * indent + "}" 97 | 98 | 99 | def handle_post_data(data): 100 | params = read_config_params() 101 | 102 | # Parse POST data 103 | lines = data.split('&') 104 | for line in lines: 105 | section_key_value = line.split('=') 106 | if len(section_key_value) == 2: 107 | section_key, value = map(unquote_plus, section_key_value) 108 | section, key = section_key.split('.') 109 | if value.lower() in ('true', 'on'): 110 | value = True 111 | elif value.lower() == 'off': 112 | value = False 113 | else: 114 | try: 115 | value = float(value) if '.' in value else int(value) 116 | except ValueError: 117 | pass # If not a number, leave as string 118 | # Ensure that both the section and the key exist before updating 119 | if section not in params: 120 | params[section] = {} 121 | params[section][key] = value 122 | 123 | # Write updated parameters back to config.json with custom pretty-printing 124 | with open('config.json', 'w') as f: 125 | f.write(pretty_print_json(params)) 126 | return params # Returning params is optional, depending on whether you want to use it after calling this function 127 | 128 | 129 | # Web server function 130 | def web_server(): 131 | addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1] 132 | s = socket.socket() 133 | s.bind(addr) 134 | s.listen(1) 135 | 136 | while True: 137 | conn, addr = s.accept() 138 | request = conn.recv(1024) 139 | request_str = str(request, 'utf-8') 140 | 141 | if request_str.startswith('POST'): 142 | post_data = request_str.split('\r\n\r\n')[-1] 143 | if "/restart" in request_str: 144 | conn.sendall("HTTP/1.1 200 OK\r\n\r\nRestarting...".encode('utf-8')) 145 | conn.close() 146 | utime.sleep(1) # Delay to ensure the response is sent before resetting 147 | machine.reset() 148 | else: 149 | handle_post_data(post_data) 150 | # Redirect to root 151 | conn.sendall("HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n".encode('utf-8')) 152 | else: 153 | params = read_config_params() 154 | html_page = generate_html_page(params) 155 | conn.sendall("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n".encode('utf-8') + html_page.encode('utf-8')) 156 | 157 | conn.close() 158 | --------------------------------------------------------------------------------