import "./global.css";
import * as faceapi from "face-api.js";
let port: SerialPort;
let videoElement: HTMLVideoElement;
let lastError = 0;
let integral = 0;
let isDragging = false;
let joystickX = 0;
let joystickY = 0;
// PID constants
const Kp = 0.5;
const Ki = 0.1;
const Kd = 0.2;
// Check if Serial API is supported
if (!("serial" in navigator)) {
alert(
"Web Serial API is not supported in this browser. Please use Chrome or Edge.",
);
}
const createTemplate = () => `
Motor Values - X: 0, Y: 0
Serial Log
`;
async function loadFaceDetectionModels() {
await faceapi.nets.tinyFaceDetector.loadFromUri("/models");
await faceapi.nets.faceLandmark68Net.loadFromUri("/models");
}
async function startWebcam() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
videoElement.srcObject = stream;
} catch (err) {
console.error("Error accessing webcam:", err);
alert("Failed to access webcam");
}
}
function calculatePID(error: number) {
integral += error;
const derivative = error - lastError;
lastError = error;
return Kp * error + Ki * integral + Kd * derivative;
}
async function trackFaces() {
const canvas = document.getElementById("overlay") as HTMLCanvasElement;
canvas.width = videoElement.width;
canvas.height = videoElement.height;
const displaySize = {
width: videoElement.width,
height: videoElement.height,
};
setInterval(async () => {
const detections = await faceapi.detectAllFaces(
videoElement,
new faceapi.TinyFaceDetectorOptions(),
);
if (detections.length > 0) {
const face = detections[0];
const centerX = face.box.x + face.box.width / 2;
const targetX = videoElement.width / 2;
const error = (centerX - targetX) / videoElement.width;
const adjustment = calculatePID(error);
await sendMotorCommand(1, adjustment);
await sendMotorCommand(2, -adjustment);
// Draw face detection
const context = canvas.getContext("2d");
if (context) {
context.clearRect(0, 0, canvas.width, canvas.height);
faceapi.draw.drawDetections(canvas, detections);
}
}
}, 100);
}
async function connectSerial() {
try {
port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
if (port.writable == null) {
throw new Error("Failed to open serial port - port is not writable");
}
console.log("Connected to serial port");
appendToLog("Connected to serial port");
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
const decoded = new TextDecoder().decode(value);
appendToLog(decoded);
}
} catch (error) {
console.error(error);
} finally {
reader.releaseLock();
}
}
} catch (err) {
console.error("Serial port error:", err);
alert(
"Failed to open serial port. Please check your connection and permissions.",
);
}
}
function appendToLog(message: string) {
const log = document.getElementById("serialLog");
if (log) {
log.textContent += message + "\n";
log.scrollTop = log.scrollHeight;
}
}
async function sendMotorCommand(motorNum: number, rotation: number) {
if (!port) {
alert("Please connect serial port first");
return;
}
if (!port.writable) {
alert("Serial port is not writable");
return;
}
const writer = port.writable.getWriter();
const encoder = new TextEncoder();
const data = `${motorNum} ${rotation}\r`;
try {
await writer.write(encoder.encode(data));
appendToLog(`Sent to motor ${motorNum}: ${rotation} rotations`);
} catch (err) {
console.error("Write error:", err);
appendToLog(`Error sending to motor ${motorNum}: ${err}`);
} finally {
writer.releaseLock();
}
}
function updateJoystickPosition(x: number, y: number) {
const joystick = document.getElementById("joystick");
const handle = document.getElementById("joystickHandle");
if (!joystick || !handle) return;
const bounds = joystick.getBoundingClientRect();
const radius = bounds.width / 2;
// Calculate relative position from center
const relX = x - bounds.left - radius;
const relY = y - bounds.top - radius;
// Calculate distance from center
const distance = Math.sqrt(relX * relX + relY * relY);
// Normalize to radius if outside circle
const normalizedX = distance > radius ? (relX / distance) * radius : relX;
const normalizedY = distance > radius ? (relY / distance) * radius : relY;
// Update handle position
handle.style.left = normalizedX + radius - 10 + "px";
handle.style.top = normalizedY + radius - 10 + "px";
// Update values (-0.5 to 0.5 range)
joystickX = normalizedX / (radius * 2);
joystickY = normalizedY / (radius * 2);
document.getElementById("motor1Value")!.textContent = joystickX.toFixed(2);
document.getElementById("motor2Value")!.textContent = joystickY.toFixed(2);
}
function defaultPageRender() {
const app = document.querySelector("#app");
if (!app) throw new Error("App element not found");
app.innerHTML = createTemplate();
videoElement = document.getElementById("webcam") as HTMLVideoElement;
const joystick = document.getElementById("joystick");
const handle = document.getElementById("joystickHandle");
if (joystick && handle) {
handle.addEventListener("mousedown", () => {
isDragging = true;
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
updateJoystickPosition(e.clientX, e.clientY);
}
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
}
});
}
document.getElementById("connect")?.addEventListener("click", connectSerial);
document
.getElementById("startTracking")
?.addEventListener("click", async () => {
await loadFaceDetectionModels();
await startWebcam();
trackFaces();
});
document.getElementById("sendJoystick")?.addEventListener("click", () => {
sendMotorCommand(1, joystickX);
sendMotorCommand(2, joystickY);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
sendMotorCommand(1, joystickX);
sendMotorCommand(2, joystickY);
}
});
}
function handleRoute() {
defaultPageRender();
}
handleRoute();