This repository has no description
1import "./global.css";
2import * as faceapi from "face-api.js";
3
4let port: SerialPort;
5let videoElement: HTMLVideoElement;
6let lastError = 0;
7let integral = 0;
8
9// PID constants
10const Kp = 0.5;
11const Ki = 0.1;
12const Kd = 0.2;
13
14// Check if Serial API is supported
15if (!("serial" in navigator)) {
16 alert(
17 "Web Serial API is not supported in this browser. Please use Chrome or Edge.",
18 );
19}
20
21const createTemplate = () => `
22 <div style="display: flex; flex-direction: row; height: 100vh;">
23 <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 20px;">
24 <button id="connect">Connect Serial Port</button>
25 <button id="startTracking">Start Face Tracking</button>
26 <video id="webcam" width="640" height="480" autoplay muted></video>
27 <canvas id="overlay" style="position: absolute;"></canvas>
28
29 <div>
30 <label>Left Motor (1) Rotations:</label>
31 <input type="range" id="motor1" min="-10" max="10" step="0.1" value="0">
32 <span id="motor1Value">0</span>
33 <button id="send1">Send</button>
34 </div>
35
36 <div>
37 <label>Right Motor (2) Rotations:</label>
38 <input type="range" id="motor2" min="-10" max="10" step="0.1" value="0">
39 <span id="motor2Value">0</span>
40 <button id="send2">Send</button>
41 </div>
42 </div>
43 <div style="width: 300px; padding: 20px; border-left: 1px solid #ccc; overflow-y: auto;">
44 <h3>Serial Log</h3>
45 <pre id="serialLog" style="white-space: pre-wrap; margin: 0;"></pre>
46 </div>
47 </div>
48`;
49
50async function loadFaceDetectionModels() {
51 await faceapi.nets.tinyFaceDetector.loadFromUri("/models");
52 await faceapi.nets.faceLandmark68Net.loadFromUri("/models");
53}
54
55async function startWebcam() {
56 try {
57 const stream = await navigator.mediaDevices.getUserMedia({ video: true });
58 videoElement.srcObject = stream;
59 } catch (err) {
60 console.error("Error accessing webcam:", err);
61 alert("Failed to access webcam");
62 }
63}
64
65function calculatePID(error: number) {
66 integral += error;
67 const derivative = error - lastError;
68 lastError = error;
69
70 return Kp * error + Ki * integral + Kd * derivative;
71}
72
73async function trackFaces() {
74 const canvas = document.getElementById("overlay") as HTMLCanvasElement;
75 canvas.width = videoElement.width;
76 canvas.height = videoElement.height;
77 const displaySize = {
78 width: videoElement.width,
79 height: videoElement.height,
80 };
81
82 setInterval(async () => {
83 const detections = await faceapi.detectAllFaces(
84 videoElement,
85 new faceapi.TinyFaceDetectorOptions(),
86 );
87
88 if (detections.length > 0) {
89 const face = detections[0];
90 const centerX = face.box.x + face.box.width / 2;
91 const targetX = videoElement.width / 2;
92 const error = (centerX - targetX) / videoElement.width;
93
94 const adjustment = calculatePID(error);
95 await sendMotorCommand(1, adjustment);
96 await sendMotorCommand(2, -adjustment);
97
98 // Draw face detection
99 const context = canvas.getContext("2d");
100 if (context) {
101 context.clearRect(0, 0, canvas.width, canvas.height);
102 faceapi.draw.drawDetections(canvas, detections);
103 }
104 }
105 }, 100);
106}
107
108async function connectSerial() {
109 try {
110 port = await navigator.serial.requestPort();
111 await port.open({ baudRate: 115200 });
112
113 if (port.writable == null) {
114 throw new Error("Failed to open serial port - port is not writable");
115 }
116
117 console.log("Connected to serial port");
118 appendToLog("Connected to serial port");
119
120 while (port.readable) {
121 const reader = port.readable.getReader();
122 try {
123 while (true) {
124 const { value, done } = await reader.read();
125 if (done) break;
126 const decoded = new TextDecoder().decode(value);
127 appendToLog(decoded);
128 }
129 } catch (error) {
130 console.error(error);
131 } finally {
132 reader.releaseLock();
133 }
134 }
135 } catch (err) {
136 console.error("Serial port error:", err);
137 alert(
138 "Failed to open serial port. Please check your connection and permissions.",
139 );
140 }
141}
142
143function appendToLog(message: string) {
144 const log = document.getElementById("serialLog");
145 if (log) {
146 log.textContent += message + "\n";
147 log.scrollTop = log.scrollHeight;
148 }
149}
150
151async function sendMotorCommand(motorNum: number, rotation: number) {
152 if (!port) {
153 alert("Please connect serial port first");
154 return;
155 }
156
157 if (!port.writable) {
158 alert("Serial port is not writable");
159 return;
160 }
161
162 const writer = port.writable.getWriter();
163 const encoder = new TextEncoder();
164 const data = `${motorNum} ${rotation}\r`;
165
166 try {
167 await writer.write(encoder.encode(data));
168 appendToLog(`Sent to motor ${motorNum}: ${rotation} rotations`);
169 } catch (err) {
170 console.error("Write error:", err);
171 appendToLog(`Error sending to motor ${motorNum}: ${err}`);
172 } finally {
173 writer.releaseLock();
174 }
175}
176
177function defaultPageRender() {
178 const app = document.querySelector<HTMLDivElement>("#app");
179 if (!app) throw new Error("App element not found");
180 app.innerHTML = createTemplate();
181
182 videoElement = document.getElementById("webcam") as HTMLVideoElement;
183
184 document.getElementById("motor1")?.addEventListener("input", (e) => {
185 const value = (e.target as HTMLInputElement).value;
186 const display = document.getElementById("motor1Value");
187 if (display) display.textContent = value;
188 });
189
190 document.getElementById("motor2")?.addEventListener("input", (e) => {
191 const value = (e.target as HTMLInputElement).value;
192 const display = document.getElementById("motor2Value");
193 if (display) display.textContent = value;
194 });
195
196 document.getElementById("connect")?.addEventListener("click", connectSerial);
197 document
198 .getElementById("startTracking")
199 ?.addEventListener("click", async () => {
200 await loadFaceDetectionModels();
201 await startWebcam();
202 trackFaces();
203 });
204 document.getElementById("send1")?.addEventListener("click", () => {
205 const value = (document.getElementById("motor1") as HTMLInputElement).value;
206 sendMotorCommand(1, parseFloat(value));
207 });
208 document.getElementById("send2")?.addEventListener("click", () => {
209 const value = (document.getElementById("motor2") as HTMLInputElement).value;
210 sendMotorCommand(2, parseFloat(value));
211 });
212}
213
214function handleRoute() {
215 defaultPageRender();
216}
217
218handleRoute();