From 9646403938b328fa843d8833c03d8463e93b1b81 Mon Sep 17 00:00:00 2001 From: Lucca Pirovano Date: Sat, 15 Nov 2025 19:12:31 -0500 Subject: [PATCH] added functionality for a second fan on pin 10 --- app.py | 77 +++++------ ...dynamic_pythonkeys_copy_20251115184748.ino | 66 +++++----- templates/index.html | 124 ++++++++++++------ 3 files changed, 156 insertions(+), 111 deletions(-) diff --git a/app.py b/app.py index 748955f..b8fa923 100644 --- a/app.py +++ b/app.py @@ -1,15 +1,10 @@ #!/usr/bin/env python3 """ -Flask web UI for ultra-responsive fan control. -Works with the Arduino sketch above (pin 11 PWM). +Flask Dual Fan Control – Pin 11 (A), Pin 10 (B) """ - -from flask import Flask, render_template, jsonify, request -import serial -import serial.tools.list_ports -import threading -import time -import sys +from flask import Flask, render_template, jsonify, Response +import serial, serial.tools.list_ports, threading, time, sys +from collections import deque app = Flask(__name__) @@ -24,58 +19,60 @@ def find_arduino(): PORT = find_arduino() BAUD = 115200 - -# Global serial ser = None serial_lock = threading.Lock() -# ------------------------------------------------- -# Serial init with retry on "busy" -# ------------------------------------------------- def init_serial(): global ser for attempt in range(6): try: ser = serial.Serial(PORT, BAUD, timeout=0) - time.sleep(2) # Arduino reset + time.sleep(2) print(f"[OK] Connected to {PORT}") return except serial.SerialException as e: if 'Device or resource busy' in str(e): - print(f"[BUSY] Port busy, retry {attempt+1}/6 in 1s...") + print(f"[BUSY] Retry {attempt+1}/6...") time.sleep(1) else: - print(f"[ERROR] Serial: {e}") + print(f"[ERROR] {e}") sys.exit(1) - sys.exit("[FATAL] Could not open serial port after retries") - + sys.exit("[FATAL] Port open failed") init_serial() # ------------------------------------------------- -# Safe send +# Send command: 'a5', 'bf', 'as', etc. # ------------------------------------------------- -def send_command(cmd: str): +def send_command(fan: str, cmd: str): + """fan = 'a' or 'b', cmd = '0'-'9','f','s'""" + if fan not in ('a', 'b') or cmd not in '0123456789fs': + return with serial_lock: if ser and ser.is_open: try: + ser.write(fan.encode()) + time.sleep(0.001) ser.write(cmd.encode()) ser.flush() except Exception as e: print(f"[WRITE FAIL] {e}") # ------------------------------------------------- -# Background reader – pushes feedback to browser via SSE +# Background reader → SSE # ------------------------------------------------- -from collections import deque -feedback_buffer = deque(maxlen=50) # last 50 messages +feedback_buffer = deque(maxlen=50) def serial_reader(): + buffer = "" while True: if ser and ser.in_waiting: try: - line = ser.readline().decode(errors='ignore').strip() - if line: - feedback_buffer.append(line) + buffer += ser.read(ser.in_waiting).decode(errors='ignore') + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + line = line.strip() + if line: + feedback_buffer.append(line) except: pass time.sleep(0.005) @@ -89,18 +86,15 @@ threading.Thread(target=serial_reader, daemon=True).start() def index(): return render_template('index.html') -@app.route('/cmd/') -def command(key): - if len(key) == 1 and key.lower() in '0123456789fs': - send_command(key) - return jsonify(status="ok", key=key) +@app.route('/cmd//') +def command(fan, key): + """fan = a|b, key = 0-9,f,s""" + if fan in ('a', 'b') and len(key) == 1 and key.lower() in '0123456789fs': + send_command(fan, key.lower()) + return jsonify(status="ok", fan=fan.upper(), key=key.upper()) return jsonify(status="invalid"), 400 -# ------------------------------------------------- -# Server-Sent Events (SSE) – live Arduino feedback -# ------------------------------------------------- -from flask import Response - +# SSE Stream @app.route('/stream') def stream(): def event_stream(): @@ -113,11 +107,10 @@ def stream(): time.sleep(0.05) return Response(event_stream(), mimetype="text/event-stream") -# ------------------------------------------------- -# Run # ------------------------------------------------- if __name__ == '__main__': - # Accessible from phone/tablet on same WiFi - print(f"\nWeb UI: http://YOUR_IP:5000") - print(" (Find YOUR_IP with: ip addr show | grep inet)") + ip = [l.split()[1] for l in __import__('subprocess') + .check_output(["ip", "route"]).decode().splitlines() + if 'default' in l][0].split('/')[0] + print(f"\nWeb UI: http://{ip}:5000") app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) diff --git a/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748.ino b/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748.ino index e5ebb12..224a149 100644 --- a/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748.ino +++ b/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748/speed_fan_controller_dynamic_pythonkeys_copy_20251115184748.ino @@ -1,42 +1,48 @@ -// Fan control via Serial - works with ultra-responsive Python script -// Pin 11 = PWM output to fan (use transistor/MOSFET/driver for high current!) +// Dual Fan control via Serial – works with the Python Flask UI +// Pin 11 = Fan A (PWM) Pin 10 = Fan B (PWM) +// Use transistor/MOSFET/driver for high-current fans! -const int FAN_PIN = 11; +const int FAN_A_PIN = 11; +const int FAN_B_PIN = 10; void setup() { Serial.begin(115200); - pinMode(FAN_PIN, OUTPUT); - analogWrite(FAN_PIN, 0); // Start stopped - Serial.println("Fan controller ready. Use 0-9, f=full, s=stop"); + pinMode(FAN_A_PIN, OUTPUT); + pinMode(FAN_B_PIN, OUTPUT); + analogWrite(FAN_A_PIN, 0); + analogWrite(FAN_B_PIN, 0); + Serial.println("Dual fan controller ready."); + Serial.println("Format: (a/b)(0-9fs)"); } void loop() { - if (Serial.available() > 0) { + if (Serial.available() < 2) return; // need fan + cmd + + char fan = Serial.read(); char cmd = Serial.read(); - int speed = 0; // 0 to 255 for analogWrite + // ----- validate fan ----- + const int *pin; + if (fan == 'a' || fan == 'A') pin = &FAN_A_PIN; + else if (fan == 'b' || fan == 'B') pin = &FAN_B_PIN; + else { Serial.println("?"); return; } - if (cmd >= '0' && cmd <= '9') { - speed = map(cmd - '0', 0, 9, 0, 255); // 0-9 → 0-255 - } - else if (cmd == 'f' || cmd == 'F') { - speed = 255; - } - else if (cmd == 's' || cmd == 'S') { - speed = 0; - } - else { - // Ignore invalid commands, but optionally echo - Serial.print("?"); - return; - } - - // Apply speed - analogWrite(FAN_PIN, speed); - - // Send feedback: percentage - int percent = map(speed, 0, 255, 0, 100); - Serial.print(percent); - Serial.println("%"); + // ----- compute speed ----- + int speed = 0; + cmd = tolower(cmd); + if (cmd >= '0' && cmd <= '9') { + speed = map(cmd - '0', 0, 9, 0, 255); // 0-9 → 0-255 } + else if (cmd == 'f') speed = 255; + else if (cmd == 's') speed = 0; + else { Serial.println("?"); return; } + + // ----- apply ----- + analogWrite(*pin, speed); + + // ----- feedback (percent + which fan) ----- + int percent = map(speed, 0, 255, 0, 100); + Serial.print((fan == 'a' || fan == 'A') ? "A" : "B"); + Serial.print(percent); + Serial.println("%"); } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 6391539..8a050a9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,65 +3,111 @@ - Fan Speed Control + Dual Fan Speed Control -

Fan Speed Control

-

Click buttons or press keys: 0–9, f, s

+

Dual Fan Speed Control

-
- - - - - - - - - - - + +
+

Fan A (Pin 11)

+

Keys: 0–9, f = full, s = stop

+
+ + + + + + + + + + + +
+
+ + +
+

Fan B (Pin 10)

+

Keys: q = stop, w–p = 10–90%, [ = full

+
+ + + + + + + + + + + +
Ready. Press keys or click buttons.
-
Keyboard: 0–9, f = full, s = stop
+
+ Fan A: 0–9, f = full, s = stop  |  Fan B: q = stop, w–p = 10–90%, [ = full +
+ + + +