├── .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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/libraries/MicroPython.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
58 |
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 |
--------------------------------------------------------------------------------