rebuilt the arduino code and website, since i had lost it like an idiot.
This commit is contained in:
@@ -1,13 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from flask import Flask, render_template, jsonify
|
"""
|
||||||
|
Flask web UI for ultra-responsive fan control.
|
||||||
|
Works with the Arduino sketch above (pin 11 PWM).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template, jsonify, request
|
||||||
import serial
|
import serial
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# --- Auto-find Arduino ---
|
# -------------------------------------------------
|
||||||
|
# Auto-detect Arduino
|
||||||
|
# -------------------------------------------------
|
||||||
def find_arduino():
|
def find_arduino():
|
||||||
for p in serial.tools.list_ports.comports():
|
for p in serial.tools.list_ports.comports():
|
||||||
if 'Arduino' in p.description or 'ACM' in p.device or 'USB' in p.device:
|
if 'Arduino' in p.description or 'ACM' in p.device or 'USB' in p.device:
|
||||||
@@ -17,57 +25,99 @@ def find_arduino():
|
|||||||
PORT = find_arduino()
|
PORT = find_arduino()
|
||||||
BAUD = 115200
|
BAUD = 115200
|
||||||
|
|
||||||
# Global serial object
|
# Global serial
|
||||||
ser = None
|
ser = None
|
||||||
serial_lock = threading.Lock()
|
serial_lock = threading.Lock()
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Serial init with retry on "busy"
|
||||||
|
# -------------------------------------------------
|
||||||
def init_serial():
|
def init_serial():
|
||||||
global ser
|
global ser
|
||||||
try:
|
for attempt in range(6):
|
||||||
ser = serial.Serial(PORT, BAUD, timeout=0)
|
try:
|
||||||
time.sleep(2) # wait for Arduino reset
|
ser = serial.Serial(PORT, BAUD, timeout=0)
|
||||||
print(f"Connected to {PORT}")
|
time.sleep(2) # Arduino reset
|
||||||
except Exception as e:
|
print(f"[OK] Connected to {PORT}")
|
||||||
print(f"Serial error: {e}")
|
return
|
||||||
ser = None
|
except serial.SerialException as e:
|
||||||
|
if 'Device or resource busy' in str(e):
|
||||||
|
print(f"[BUSY] Port busy, retry {attempt+1}/6 in 1s...")
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Serial: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit("[FATAL] Could not open serial port after retries")
|
||||||
|
|
||||||
init_serial()
|
init_serial()
|
||||||
|
|
||||||
# --- Send command safely ---
|
# -------------------------------------------------
|
||||||
def send_command(cmd):
|
# Safe send
|
||||||
|
# -------------------------------------------------
|
||||||
|
def send_command(cmd: str):
|
||||||
with serial_lock:
|
with serial_lock:
|
||||||
if ser and ser.is_open:
|
if ser and ser.is_open:
|
||||||
try:
|
try:
|
||||||
ser.write(cmd.encode())
|
ser.write(cmd.encode())
|
||||||
ser.flush()
|
ser.flush()
|
||||||
except:
|
except Exception as e:
|
||||||
print("Serial write failed")
|
print(f"[WRITE FAIL] {e}")
|
||||||
else:
|
|
||||||
print("Serial not open")
|
# -------------------------------------------------
|
||||||
|
# Background reader – pushes feedback to browser via SSE
|
||||||
|
# -------------------------------------------------
|
||||||
|
from collections import deque
|
||||||
|
feedback_buffer = deque(maxlen=50) # last 50 messages
|
||||||
|
|
||||||
# --- Background reader (optional: show live feedback) ---
|
|
||||||
def serial_reader():
|
def serial_reader():
|
||||||
while True:
|
while True:
|
||||||
if ser and ser.in_waiting:
|
if ser and ser.in_waiting:
|
||||||
line = ser.readline().decode(errors='ignore').strip()
|
try:
|
||||||
if line:
|
line = ser.readline().decode(errors='ignore').strip()
|
||||||
print(f"Arduino: {line}")
|
if line:
|
||||||
time.sleep(0.01)
|
feedback_buffer.append(line)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(0.005)
|
||||||
|
|
||||||
threading.Thread(target=serial_reader, daemon=True).start()
|
threading.Thread(target=serial_reader, daemon=True).start()
|
||||||
|
|
||||||
# --- Routes ---
|
# -------------------------------------------------
|
||||||
|
# Routes
|
||||||
|
# -------------------------------------------------
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
@app.route('/cmd/<key>')
|
@app.route('/cmd/<key>')
|
||||||
def command(key):
|
def command(key):
|
||||||
if len(key) == 1 and key in '0123456789fFsS':
|
if len(key) == 1 and key.lower() in '0123456789fs':
|
||||||
send_command(key)
|
send_command(key)
|
||||||
return jsonify(status="ok", key=key)
|
return jsonify(status="ok", key=key)
|
||||||
return jsonify(status="invalid"), 400
|
return jsonify(status="invalid"), 400
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Server-Sent Events (SSE) – live Arduino feedback
|
||||||
|
# -------------------------------------------------
|
||||||
|
from flask import Response
|
||||||
|
|
||||||
|
@app.route('/stream')
|
||||||
|
def stream():
|
||||||
|
def event_stream():
|
||||||
|
last_len = 0
|
||||||
|
while True:
|
||||||
|
if len(feedback_buffer) > last_len:
|
||||||
|
msg = feedback_buffer[-1]
|
||||||
|
last_len = len(feedback_buffer)
|
||||||
|
yield f"data: {msg}\n\n"
|
||||||
|
time.sleep(0.05)
|
||||||
|
return Response(event_stream(), mimetype="text/event-stream")
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Run
|
||||||
|
# -------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Allow access from your phone/tablet on same WiFi
|
# Accessible from phone/tablet on same WiFi
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
print(f"\nWeb UI: http://YOUR_IP:5000")
|
||||||
|
print(" (Find YOUR_IP with: ip addr show | grep inet)")
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
// Fan control via Serial - works with ultra-responsive Python script
|
||||||
|
// Pin 11 = PWM output to fan (use transistor/MOSFET/driver for high current!)
|
||||||
|
|
||||||
|
const int FAN_PIN = 11;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (Serial.available() > 0) {
|
||||||
|
char cmd = Serial.read();
|
||||||
|
|
||||||
|
int speed = 0; // 0 to 255 for analogWrite
|
||||||
|
|
||||||
|
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("%");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user