From 3b07486e32b56303632f5807196d263028e32f73 Mon Sep 17 00:00:00 2001 From: max Date: Sun, 1 Feb 2026 20:42:33 +0100 Subject: [PATCH] added web-ui-control --- README.md | 36 ++ .../arduino-sketch-ps2-controller.ino.ino | 132 ++++++++ drive/etc/nginx/snippets/drive.conf | 18 + drive/etc/systemd/system/drive-ctl.service | 13 + drive/images/ps2-controller.gif | Bin 0 -> 37303 bytes drive/opt/drive-ctl/server.py | 65 ++++ drive/var/www/drive/index.html | 315 ++++++++++++++++++ face/etc/nginx/sites-available/face | 2 + 8 files changed, 581 insertions(+) create mode 100644 drive/arduino-sketch/arduino-sketch-ps2-controller.ino/arduino-sketch-ps2-controller.ino.ino create mode 100644 drive/etc/nginx/snippets/drive.conf create mode 100644 drive/etc/systemd/system/drive-ctl.service create mode 100644 drive/images/ps2-controller.gif create mode 100644 drive/opt/drive-ctl/server.py create mode 100644 drive/var/www/drive/index.html diff --git a/README.md b/README.md index a96e78e..49e9862 100644 --- a/README.md +++ b/README.md @@ -180,4 +180,40 @@ slow backward r 60 +## Web Control + +### Setup + + ln -s /opt/helva-robot/drive/opt/drive-ctl/ /opt/ + sudo apt update + sudo apt install -y python3-venv + python3 -m venv /opt/drive-ctl-venv + source /opt/drive-ctl-venv/bin/activate + pip install fastapi uvicorn pyserial + + +### Activate Web Control Interface + + sudo ln -s /opt/helva-robot/drive/etc/nginx/sites-available/drive /etc/nginx/sites-enabled/drive + sudo nginx -t && sudo systemctl reload nginx + + +### Systemd Service for Backend + + ln -s /opt/helva-robot/drive/etc/systemd/system/drive-ctl.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable --now drive-ctl + sudo systemctl status drive-ctl --no-pager -l + +Check Serial device is stable, otherwise create a udev rule + + ls -l /dev/ttyACM0 /dev/ttyUSB0 + dmesg | tail -n 50 + + + +### Open Web Control UI + + http://pi-ip/drive/ + diff --git a/drive/arduino-sketch/arduino-sketch-ps2-controller.ino/arduino-sketch-ps2-controller.ino.ino b/drive/arduino-sketch/arduino-sketch-ps2-controller.ino/arduino-sketch-ps2-controller.ino.ino new file mode 100644 index 0000000..d54efad --- /dev/null +++ b/drive/arduino-sketch/arduino-sketch-ps2-controller.ino/arduino-sketch-ps2-controller.ino.ino @@ -0,0 +1,132 @@ +// ===== Arduino UNO: Differential Drive รผber 2x BTS7960 ===== +// Serial command format: "L R \n" where int in [-255..255] +// Soft-start ramping + watchdog stop + +// --- Left BTS7960 pins --- +const uint8_t L_RPWM = 5; // PWM +const uint8_t L_LPWM = 6; // PWM +const uint8_t L_REN = 7; // enable +const uint8_t L_LEN = 8; // enable + +// --- Right BTS7960 pins --- +const uint8_t R_RPWM = 9; // PWM +const uint8_t R_LPWM = 10; // PWM +const uint8_t R_REN = 11; // enable +const uint8_t R_LEN = 12; // enable + +// Ramping +const uint8_t RAMP_STEP = 6; // speed change per loop (0..255) +const uint16_t LOOP_MS = 15; // ramp update interval +const uint16_t CMD_TIMEOUT_MS = 300; // stop if no command within x ms + +int targetL = 0, targetR = 0; +int currentL = 0, currentR = 0; + +unsigned long lastCmdMs = 0; +unsigned long lastLoopMs = 0; + +void setupPins() { + pinMode(L_RPWM, OUTPUT); pinMode(L_LPWM, OUTPUT); + pinMode(L_REN, OUTPUT); pinMode(L_LEN, OUTPUT); + + pinMode(R_RPWM, OUTPUT); pinMode(R_LPWM, OUTPUT); + pinMode(R_REN, OUTPUT); pinMode(R_LEN, OUTPUT); + + digitalWrite(L_REN, HIGH); digitalWrite(L_LEN, HIGH); + digitalWrite(R_REN, HIGH); digitalWrite(R_LEN, HIGH); + + analogWrite(L_RPWM, 0); analogWrite(L_LPWM, 0); + analogWrite(R_RPWM, 0); analogWrite(R_LPWM, 0); +} + +static int clamp255(int v) { + if (v > 255) return 255; + if (v < -255) return -255; + return v; +} + +void driveOne(int speed, uint8_t rpwm, uint8_t lpwm) { + speed = clamp255(speed); + if (speed > 0) { + analogWrite(rpwm, (uint8_t)speed); + analogWrite(lpwm, 0); + } else if (speed < 0) { + analogWrite(rpwm, 0); + analogWrite(lpwm, (uint8_t)(-speed)); + } else { + analogWrite(rpwm, 0); + analogWrite(lpwm, 0); + } +} + +static int rampTowards(int current, int target, uint8_t step) { + if (current < target) { + int n = current + step; + return (n > target) ? target : n; + } + if (current > target) { + int n = current - step; + return (n < target) ? target : n; + } + return current; +} + +bool parseLine(const String& line, int &outL, int &outR) { + // expected: "L R " + int idxL = line.indexOf('L'); + int idxR = line.indexOf('R'); + if (idxL < 0 || idxR < 0) return false; + + // crude parse; robust enough for our simple format + String partL = line.substring(idxL + 1, idxR); + String partR = line.substring(idxR + 1); + + partL.trim(); + partR.trim(); + + outL = partL.toInt(); + outR = partR.toInt(); + outL = clamp255(outL); + outR = clamp255(outR); + return true; +} + +void setup() { + Serial.begin(115200); + setupPins(); + lastCmdMs = millis(); + lastLoopMs = millis(); +} + +void loop() { + // --- read serial lines --- + while (Serial.available()) { + String line = Serial.readStringUntil('\n'); + line.trim(); + if (line.length() == 0) continue; + + int l, r; + if (parseLine(line, l, r)) { + targetL = l; + targetR = r; + lastCmdMs = millis(); + } + } + + // --- watchdog stop --- + if (millis() - lastCmdMs > CMD_TIMEOUT_MS) { + targetL = 0; + targetR = 0; + } + + // --- ramp update --- + if (millis() - lastLoopMs >= LOOP_MS) { + lastLoopMs = millis(); + + currentL = rampTowards(currentL, targetL, RAMP_STEP); + currentR = rampTowards(currentR, targetR, RAMP_STEP); + + driveOne(currentL, L_RPWM, L_LPWM); + driveOne(currentR, R_RPWM, R_LPWM); + } +} diff --git a/drive/etc/nginx/snippets/drive.conf b/drive/etc/nginx/snippets/drive.conf new file mode 100644 index 0000000..b4e9bd2 --- /dev/null +++ b/drive/etc/nginx/snippets/drive.conf @@ -0,0 +1,18 @@ + + location /drive/ { + alias /opt/helva-robot/drive/var/www/drive/; + try_files $uri $uri/ /drive/index.html; + } + + location /drive-ws { + proxy_pass http://127.0.0.1:8002/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + + location /drive-health { + proxy_pass http://127.0.0.1:8002/health; + } + diff --git a/drive/etc/systemd/system/drive-ctl.service b/drive/etc/systemd/system/drive-ctl.service new file mode 100644 index 0000000..a1ab46e --- /dev/null +++ b/drive/etc/systemd/system/drive-ctl.service @@ -0,0 +1,13 @@ +[Unit] +Description=Robot Drive Controller (FastAPI -> Serial) +After=network.target + +[Service] +WorkingDirectory=/opt/drive-ctl +ExecStart=/opt/drive-ctl-venv/bin/uvicorn server:app --host 127.0.0.1 --port 8002 --app-dir /opt/drive-ctl +Restart=always +RestartSec=1 + +[Install] +WantedBy=multi-user.target + diff --git a/drive/images/ps2-controller.gif b/drive/images/ps2-controller.gif new file mode 100644 index 0000000000000000000000000000000000000000..fea3b9c85c54ae0a72181c9e958dc79dd3b7eec1 GIT binary patch literal 37303 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 literal 0 HcmV?d00001 diff --git a/drive/opt/drive-ctl/server.py b/drive/opt/drive-ctl/server.py new file mode 100644 index 0000000..955f76f --- /dev/null +++ b/drive/opt/drive-ctl/server.py @@ -0,0 +1,65 @@ +import asyncio +import json +import time +import serial +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import HTMLResponse + +SERIAL_PORT = "/dev/ttyACM0" # ggf. /dev/ttyUSB0 +BAUD = 115200 + +# Safety: if websocket stops sending, we also stop. +STOP_AFTER_SEC = 0.35 + +app = FastAPI() + +ser = serial.Serial(SERIAL_PORT, BAUD, timeout=0) + +last_msg_ts = 0.0 + +def clamp255(v: int) -> int: + return max(-255, min(255, int(v))) + +def send_lr(l: int, r: int): + l = clamp255(l) + r = clamp255(r) + line = f"L {l} R {r}\n".encode("ascii") + ser.write(line) + +@app.get("/health") +def health(): + return {"ok": True, "serial": SERIAL_PORT} + +@app.websocket("/ws") +async def ws(websocket: WebSocket): + global last_msg_ts + await websocket.accept() + + async def watchdog(): + while True: + await asyncio.sleep(0.05) + if time.time() - last_msg_ts > STOP_AFTER_SEC: + send_lr(0, 0) + + wd_task = asyncio.create_task(watchdog()) + + try: + while True: + msg = await websocket.receive_text() + last_msg_ts = time.time() + + try: + data = json.loads(msg) + l = clamp255(data.get("l", 0)) + r = clamp255(data.get("r", 0)) + send_lr(l, r) + except Exception: + # On parse errors: stop + send_lr(0, 0) + + await websocket.send_text("ok") + except WebSocketDisconnect: + send_lr(0, 0) + finally: + wd_task.cancel() + diff --git a/drive/var/www/drive/index.html b/drive/var/www/drive/index.html new file mode 100644 index 0000000..d305c59 --- /dev/null +++ b/drive/var/www/drive/index.html @@ -0,0 +1,315 @@ + + + + + + Robot Dual Stick + + + +
+
+
๐ŸŽฎ Dual Stick Drive
+
offline
+
+ +
+
+
+ +
Links: Gas (โ†‘/โ†“)
+
+
+ +
Rechts: Lenken (โ†/โ†’)
+
+
+
+ +
+
+
Max Speed: 180
+ +
T 0 | Turn 0 โ†’ L 0 | R 0
+
+
+ + +
+
+ +
+ Loslassen = STOP. Wenn Verbindung/Tab weg: STOP (Watchdog). +
+
+ + + + + diff --git a/face/etc/nginx/sites-available/face b/face/etc/nginx/sites-available/face index acd1aa3..db0e50c 100644 --- a/face/etc/nginx/sites-available/face +++ b/face/etc/nginx/sites-available/face @@ -30,5 +30,7 @@ server { add_header Cache-Control no-cache; } + + include /opt/helva-robot/drive/etc/nginx/snippets/drive.conf; }