Files
SpotMicroESP32-Leika/mock/server.js
T
Rune Harlyk 2d8137772a 🫁 Adds rest mode
2024-02-26 14:08:29 +01:00

497 lines
12 KiB
JavaScript

import express from "express";
import cors from "cors";
import Kinematic from "./kinematic.js";
import { WebSocketServer } from "ws";
const app = express();
const kinematic = new Kinematic();
const wss = new WebSocketServer({ port: 2096 });
app.use(cors());
app.use(express.json());
const port = 3000;
const subscriptions = {};
const randomFloatFromInterval = (min, max) =>
Math.floor((Math.random() * (max - min + 1) + min) * 100) / 100;
const radToDeg = (val) => val * (180 / Math.PI);
const degToRad = (val) => val * (Math.PI / 180);
const lerp = (start, end, amt) => {
return (1 - amt) * start + amt * end;
};
function createNewClientState() {
return {
model: JSON.parse(JSON.stringify(model)),
settings: JSON.parse(JSON.stringify(settings)),
logs: JSON.parse(JSON.stringify(logs)),
subscriptions: {},
mode: "idle",
controller: {
stop: 0,
lx: 0,
ly: 0,
rx: 0,
ry: 0,
h: 70,
s: 0,
},
};
}
const unsubscribeClientFromCategory = (ws, category) => {
if (!subscriptions[category]) return;
subscriptions[category].delete(ws);
if (subscriptions[category].size === 0) {
delete subscriptions[category].size;
}
};
const sendUpdateToSubscribers = (category, data) => {
if (subscriptions[category]) {
const message = JSON.stringify(data);
for (const client of subscriptions[category]) {
client.send(message);
}
}
};
if (!Array.prototype.last) {
Array.prototype.last = function () {
return this[this.length - 1];
};
}
const model = {
battery: {
voltage: randomFloatFromInterval(7.6, 8.2),
ampere: randomFloatFromInterval(0.2, 3),
power_button: false,
},
servos: {
angles: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
dir: [-1, -1, -1, 1, -1, -1, -1, -1, -1, 1, -1, -1],
on: true,
},
mpu: {
x: 0,
y: 0,
z: 0,
heading: 240,
temperature: 21,
},
display: [[]],
distance_sensors: {
left: 22,
right: 23,
},
appTime: 1123321,
connectivity: {
ssid: "best network",
ip: "192.168.0.118",
mDNS: "leika.local",
rssi: 100,
},
running: true,
mode: "stand",
rotation: [0, 0, 0],
position: [0, 0, 0],
};
const settings = {
useMetric: true,
name: "Leika",
ssid: "Rune private network",
pass: "12345678",
ap: "Leika",
apPass: "12345678",
apChannel: 1,
};
const logs = [
"[2023-02-05 10:00:00] [verbose] Booting up",
"[2023-02-05 10:00:10] [verbose] Starting webserver",
"[2023-02-05 10:00:20] [verbose] Loading setting",
"[2023-02-05 10:00:30] [verbose] Connected to Rune private network",
];
const system = {
HeapSize: 400000,
HeapFree: 0,
HeapMin: 0,
DmaHeapSize: 0,
DmaHeapFree: 0,
DmaHeapMin: 0,
PsramSize: 400000,
PsramFree: 0,
PsramMin: 0,
ChipModel: 0,
ChipRevision: 0,
ChipCores: 2,
CpuFreqMHz: 80,
SketchSize: 0,
FreeSketchSpace: 10200,
FlashChipSize: 0,
CpuUsed: 0,
CpuUsedCore0: 0,
CpuUsedCore1: 0,
arduinoVersion: "3.2.1",
};
const updateBattery = () => {
model.battery.voltage = randomFloatFromInterval(7.6, 8.2);
model.battery.ampere = randomFloatFromInterval(0.2, 3);
return model.battery;
};
const updateMpu = () => {
model.mpu.x = randomFloatFromInterval(0, 1);
model.mpu.y = randomFloatFromInterval(0, 1);
model.mpu.z = randomFloatFromInterval(0, 1);
model.mpu.temperature = randomFloatFromInterval(20, 22);
return model.mpu;
};
const updateDistances = () => {
model.distance_sensors = {
left: randomFloatFromInterval(10, 220),
right: randomFloatFromInterval(10, 220),
};
return model.distance_sensors;
};
const updateDistance = (position) => {
model.distance_sensors[position] = randomFloatFromInterval(10, 220);
return model.distance_sensors[position];
};
const updateSystem = () => {
system.CpuUsedCore0 = randomFloatFromInterval(0, 100);
system.CpuUsedCore1 = randomFloatFromInterval(0, 100);
system.CpuUsed =
Math.floor((system.CpuUsedCore0 + system.CpuUsedCore1) / 0.02) / 100;
system.HeapFree = randomFloatFromInterval(0, 20000);
system.HeapMin = randomFloatFromInterval(0, 20000);
return system;
};
const updateBodyState = (model, angles, position) => {
const Lp = [
[100, -100, 100, 1],
[100, -100, -100, 1],
[-100, -100, 100, 1],
[-100, -100, -100, 1],
];
model.servos.angles = kinematic
.calcIK(
Lp,
angles.map((x) => degToRad(x)),
position
)
.flat()
.map((x, i) => radToDeg(x * model.servos.dir[i]));
return model.servos.angles;
};
const updateAngle = (id, angle) => {
model.servos.angles[id] = angle;
return model.servos.angles;
};
const updateAngles = (angles) => {
model.servos.angles = angles;
return model.servos.angles;
};
const bufferToController = (buffer) => {
return {
stop: buffer[0],
lx: buffer[1],
ly: buffer[2],
rx: buffer[3],
ry: buffer[4],
h: buffer[5],
s: buffer[6],
};
};
const unpackMessageBuffer = (data) => {
return {
angles: [0, data.rx / 4, data.ry / 4],
position: [data.ly / 2, 70, data.lx / 2],
};
};
const rest_stance = {
rotation: [0, 0, 0],
position: [0, 10, 0],
};
const idle = () => {};
const rest = (client) => {
for (let i = 0; i < 3; i++) {
client.clientState.model.position[i] = lerp(
client.clientState.model.position[i],
rest_stance.position[i],
0.01
);
client.clientState.model.rotation[i] = lerp(
client.clientState.model.rotation[i],
rest_stance.rotation[i],
0.01
);
}
client.send(
JSON.stringify({
type: "angles",
data: updateBodyState(
client.clientState.model,
client.clientState.model.rotation,
client.clientState.model.position
),
})
);
};
const stand = (client) => {
if (!client.clientState.model.running) return;
const data = unpackMessageBuffer(client.clientState.controller);
client.send(
JSON.stringify({
type: "angles",
data: updateBodyState(
client.clientState.model,
data.angles,
data.position
),
})
);
};
// https://www.hindawi.com/journals/cin/2016/9853070/
const step = (model, controller, tick) => {
const y1 = -100 * Math.sin(-0.05 * tick) - 150;
const y2 = -100 * Math.sin(-0.05 * tick + Math.PI) - 150;
const x1 = Math.abs((tick % 120) - 60) - 60;
const Lp = [
// -50 is minimum
[100, y1, 100, 1],
[100, y2, -100, 1],
[-100, y2, 100, 1],
[-100, y1, -100, 1],
];
model.servos.angles = kinematic
.calcIK(
Lp,
model.rotation.map((x) => degToRad(x)),
model.position
)
.flat()
.map((x, i) => radToDeg(x * model.servos.dir[i]));
return model.servos.angles;
};
const walk = (client) => {
const angles = step(
client.clientState.model,
client.clientState.controller,
client.tick
);
client.send(JSON.stringify({ type: "angles", data: angles }));
};
const start_dynamics = (client) => {
client.tick = 0;
client.clientState.mode = "idle";
client.clientState.next_mode = "walk";
const modes = { rest, idle, stand, walk };
client.id = setInterval(() => {
client.tick += 1;
if (client.clientState.mode !== client.clientState.next_mode) {
// Transition
client.clientState.mode = client.clientState.next_mode;
} else {
modes[client.clientState.mode](client);
}
}, 10);
};
const handelController = (ws, buffer) => {
const controllerData = bufferToController(new Int8Array(buffer));
ws.clientState.controller = controllerData;
if (controllerData.stop) {
ws.clientState.model.running = false;
ws.clientState.logs.push("[2024-02-05 19:10:00] [Warning] STOPPING SERVOS");
ws.send(JSON.stringify({ type: "log", data: ws.clientState.logs.last() }));
return;
}
};
const handleBufferMessage = (ws, buffer) => {
if (buffer.length === 6) {
handelController(ws, buffer);
}
};
const handleJsonMessage = (ws, data) => {
switch (data.type) {
case "mode":
ws.clientState.next_mode = data.data;
// ws.send({ type: "battery", data: JSON.stringify(updateBattery()) });
break;
case "sensor/battery":
ws.send({ type: "battery", data: JSON.stringify(updateBattery()) });
break;
case "sensor/mpu":
ws.send({ type: "battery", data: JSON.stringify(updateMpu()) });
break;
case "sensor/distances":
ws.send(JSON.stringify(updateDistances()));
break;
case "sensor/distance":
ws.send(JSON.stringify({ distance: updateDistance(data.position) }));
break;
case "kinematic/angle":
if (data.angle && data.id) {
ws.clientState.model.servos.angles[data.id] = data.angle;
ws.send(
JSON.stringify({
type: "angles",
data: ws.clientState.model.servos.angles,
})
);
} else {
ws.send(JSON.stringify(updateAngle(data.id, data.angle)));
}
break;
case "kinematic/angles":
if (data.angles) {
ws.clientState.model.servos.angles = data.angles;
ws.send(
JSON.stringify({
type: "angles",
data: ws.clientState.model.servos.angles,
})
);
} else {
ws.send(JSON.stringify(updateAngles(data.angles)));
}
break;
case "kinematic/bodystate":
if (data.angles) {
ws.send(
JSON.stringify({
type: "angles",
data: updateBodyState(
ws.clientState.model,
data.angles,
data.position
),
})
);
} else {
ws.send(JSON.stringify({ angles: model.servos.angles }));
}
break;
case "system/logs":
ws.send(JSON.stringify({ type: "logs", data: ws.clientState.logs }));
break;
case "system/info":
ws.send(JSON.stringify({ type: "info", data: updateSystem() }));
break;
case "system/settings":
if (data.settings) {
Object.entries(data.settings).forEach(
([key, value]) => (ws.clientState.settings[key] = value)
);
ws.send(JSON.stringify(ws.clientState.settings));
} else {
ws.send(
JSON.stringify({
type: "settings",
settings: ws.clientState.settings,
})
);
}
break;
case "system/stop":
ws.clientState.model.running = false;
ws.clientState.logs.push(
"[2024-02-05 19:10:00] [Warning] STOPPING SERVOS"
);
ws.send(
JSON.stringify({ type: "log", data: ws.clientState.logs.last() })
);
break;
default:
ws.send(JSON.stringify({ error: "Unknown request type" }));
}
};
wss.on("connection", (ws) => {
const clientState = createNewClientState();
ws.clientState = clientState;
start_dynamics(ws);
ws.on("error", console.error);
ws.on("message", (message) => {
let data = message;
try {
data = JSON.parse(message);
handleJsonMessage(ws, data);
} catch (error) {
handleBufferMessage(ws, message);
}
});
ws.on("close", () => {
for (const category in subscriptions) {
unsubscribeClientFromCategory(ws, category);
}
});
});
app.get("/sensor/battery", (req, res) => res.send(updateBattery()));
app.get("/sensor/mpu", (req, res) => res.send(updateMpu()));
app.get("/sensor/distances", (req, res) => res.send(updateDistances()));
app.get("/sensor/distance/:position", (req, res) =>
res.send({ distance: updateDistance(req.params.position) })
);
// ----------------------------------------------------------- //
app.post("/kinematic/angle/:id", (req, res) =>
res.send(updateAngle(req.params.id, req.body.angle))
);
app.post("/kinematic/angles/", (req, res) =>
res.send(updateAngles(req.body.angles))
);
app.get("/kinematic/bodystate", (req, res) => res.send(model.servos.angles));
app.post("/kinematic/bodystate", (req, res) => {
sendUpdateToSubscribers("angles", model.servos.angles);
res.send(updateBodyState(model, req.body.angles, req.body.position));
});
// ----------------------------------------------------------- //
app.get("/system/log", (req, res) => res.send(logs));
app.get("/system/info", (req, res) => res.send(updateSystem()));
app.get("/system/settings", (req, res) => res.send(settings));
app.post("/system/settings", (req, res) => {
Object.entries(req.body).forEach((x) => (settings[x[0]] = x[1]));
res.send(settings);
});
app.post("/system/stop", (req, res) => {
model.running = false;
model.res.send(settings);
});
app.listen(port, () => console.log(`Open at http://localhost:${port}`));