added functionality for a second fan on pin 10
This commit is contained in:
@@ -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/<key>')
|
||||
def command(key):
|
||||
if len(key) == 1 and key.lower() in '0123456789fs':
|
||||
send_command(key)
|
||||
return jsonify(status="ok", key=key)
|
||||
@app.route('/cmd/<fan>/<key>')
|
||||
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)
|
||||
|
||||
+36
-30
@@ -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: <fan><cmd> (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("%");
|
||||
}
|
||||
+85
-39
@@ -3,65 +3,111 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Fan Speed Control</title>
|
||||
<title>Dual Fan Speed Control</title>
|
||||
<style>
|
||||
body { font-family: Arial; text-align: center; padding: 2rem; background: #f4f4f4; }
|
||||
.btn {
|
||||
font-size: 1.5rem; padding: 1rem 1.5rem; margin: 0.5rem;
|
||||
background: #007bff; color: white; border: none; border-radius: 8px;
|
||||
cursor: pointer; width: 80px;
|
||||
}
|
||||
.btn:hover { background: #0056b3; }
|
||||
.btn.stop { background: #dc3545; }
|
||||
.btn.stop:hover { background: #b02a37; }
|
||||
.btn.full { background: #28a745; }
|
||||
.btn.full:hover { background: #218838; }
|
||||
#status { margin-top: 1rem; font-size: 1.2rem; color: #333; }
|
||||
.keys { margin-top: 2rem; font-size: 1rem; color: #666; }
|
||||
body {font-family:Arial;text-align:center;padding:2rem;background:#f4f4f4;}
|
||||
.fan {display:inline-block;margin:1rem;vertical-align:top;width:45%;}
|
||||
.btn {font-size:1.5rem;padding:1rem 1.5rem;margin:0.5rem;
|
||||
background:#007bff;color:white;border:none;border-radius:8px;
|
||||
cursor:pointer;width:80px;}
|
||||
.btn:hover {background:#0056b3;}
|
||||
.btn.stop {background:#dc3545;}
|
||||
.btn.stop:hover {background:#b02a37;}
|
||||
.btn.full {background:#28a745;}
|
||||
.btn.full:hover {background:#218838;}
|
||||
#status {margin-top:1rem;font-size:1.2rem;color:#333;}
|
||||
.keys {margin-top:2rem;font-size:1rem;color:#666;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Fan Speed Control</h1>
|
||||
<p>Click buttons or press keys: <strong>0–9, f, s</strong></p>
|
||||
<h1>Dual Fan Speed Control</h1>
|
||||
|
||||
<div>
|
||||
<button class="btn stop" onclick="send('s')">S<br>Stop</button>
|
||||
<button class="btn" onclick="send('1')">1<br>~11%</button>
|
||||
<button class="btn" onclick="send('2')">2<br>~20%</button>
|
||||
<button class="btn" onclick="send('3')">3<br>~30%</button>
|
||||
<button class="btn" onclick="send('4')">4<br>~40%</button>
|
||||
<button class="btn" onclick="send('5')">5<br>50%</button>
|
||||
<button class="btn" onclick="send('6')">6<br>~60%</button>
|
||||
<button class="btn" onclick="send('7')">7<br>~70%</button>
|
||||
<button class="btn" onclick="send('8')">8<br>~80%</button>
|
||||
<button class="btn" onclick="send('9')">9<br>~90%</button>
|
||||
<button class="btn full" onclick="send('f')">F<br>Full</button>
|
||||
<!-- ==================== FAN A (Pin 11) ==================== -->
|
||||
<div class="fan">
|
||||
<h2>Fan A (Pin 11)</h2>
|
||||
<p>Keys: <strong>0–9, f = full, s = stop</strong></p>
|
||||
<div>
|
||||
<button class="btn stop" onclick="send('a','s')">S<br>Stop</button>
|
||||
<button class="btn" onclick="send('a','1')">1<br>~11%</button>
|
||||
<button class="btn" onclick="send('a','2')">2<br>~20%</button>
|
||||
<button class="btn" onclick="send('a','3')">3<br>~30%</button>
|
||||
<button class="btn" onclick="send('a','4')">4<br>~40%</button>
|
||||
<button class="btn" onclick="send('a','5')">5<br>50%</button>
|
||||
<button class="btn" onclick="send('a','6')">6<br>~60%</button>
|
||||
<button class="btn" onclick="send('a','7')">7<br>~70%</button>
|
||||
<button class="btn" onclick="send('a','8')">8<br>~80%</button>
|
||||
<button class="btn" onclick="send('a','9')">9<br>~90%</button>
|
||||
<button class="btn full" onclick="send('a','f')">F<br>Full</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== FAN B (Pin 10) ==================== -->
|
||||
<div class="fan">
|
||||
<h2>Fan B (Pin 10)</h2>
|
||||
<p>Keys: <strong>q = stop, w–p = 10–90%, [ = full</strong></p>
|
||||
<div>
|
||||
<button class="btn stop" onclick="send('b','s')">Q<br>Stop</button>
|
||||
<button class="btn" onclick="send('b','1')">W<br>~10%</button>
|
||||
<button class="btn" onclick="send('b','2')">E<br>~20%</button>
|
||||
<button class="btn" onclick="send('b','3')">R<br>~30%</button>
|
||||
<button class="btn" onclick="send('b','4')">T<br>~40%</button>
|
||||
<button class="btn" onclick="send('b','5')">Y<br>50%</button>
|
||||
<button class="btn" onclick="send('b','6')">U<br>~60%</button>
|
||||
<button class="btn" onclick="send('b','7')">I<br>~70%</button>
|
||||
<button class="btn" onclick="send('b','8')">O<br>~80%</button>
|
||||
<button class="btn" onclick="send('b','9')">P<br>~90%</button>
|
||||
<button class="btn full" onclick="send('b','f')">[ <br>Full</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status">Ready. Press keys or click buttons.</div>
|
||||
<div class="keys">Keyboard: 0–9, f = full, s = stop</div>
|
||||
<div class="keys">
|
||||
Fan A: 0–9, f = full, s = stop | Fan B: q = stop, w–p = 10–90%, [ = full
|
||||
</div>
|
||||
|
||||
<!-- ==================== SSE Live Feedback ==================== -->
|
||||
<script>
|
||||
function send(key) {
|
||||
fetch(`/cmd/${key}`)
|
||||
const statusEl = document.getElementById('status');
|
||||
const evtSource = new EventSource('/stream');
|
||||
evtSource.onmessage = function(e) {
|
||||
if (e.data) {
|
||||
statusEl.textContent = `Live: ${e.data}`;
|
||||
}
|
||||
};
|
||||
evtSource.onerror = function() {
|
||||
statusEl.textContent = 'SSE connection lost...';
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- ==================== Command Sender ==================== -->
|
||||
<script>
|
||||
function send(fan, key) {
|
||||
fetch(`/cmd/${fan}/${key}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'ok') {
|
||||
document.getElementById('status').textContent = `Sent: ${key.toUpperCase()} → Fan updated`;
|
||||
statusEl.textContent = `Sent: ${data.fan}${data.key} → Fan updated`;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
document.getElementById('status').textContent = 'Error: Is Arduino connected?';
|
||||
statusEl.textContent = 'Error: Is Arduino connected?';
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard control
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// ------------------- Keyboard mapping -------------------
|
||||
const mapA = { // Fan A – unchanged
|
||||
'0':'0','1':'1','2':'2','3':'3','4':'4','5':'5',
|
||||
'6':'6','7':'7','8':'8','9':'9','f':'f','s':'s'
|
||||
};
|
||||
const mapB = { // Fan B – NEW layout
|
||||
'q':'s', 'w':'1','e':'2','r':'3','t':'4','y':'5',
|
||||
'u':'6','i':'7','o':'8','p':'9','[':'f'
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
const k = e.key.toLowerCase();
|
||||
if ('0123456789fs'.includes(k)) {
|
||||
e.preventDefault();
|
||||
send(k);
|
||||
}
|
||||
if (k in mapA) { e.preventDefault(); send('a', mapA[k]); }
|
||||
else if (k in mapB) { e.preventDefault(); send('b', mapB[k]); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user