Files
SpotMicroESP32-Leika/mock/server.js
T
2024-02-05 21:04:53 +01:00

337 lines
9.0 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: 8080 });
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);
function createNewClientState() {
return {
model: JSON.parse(JSON.stringify(model)),
settings: JSON.parse(JSON.stringify(settings)),
logs: JSON.parse(JSON.stringify(logs)),
subscriptions: {},
};
}
function subscribeClientToCategory(ws, category) {
if (!subscriptions[category]) {
subscriptions[category] = new Set();
}
subscriptions[category].add(ws);
}
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,
};
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;
};
wss.on("connection", (ws) => {
const clientState = createNewClientState();
ws.clientState = clientState;
ws.on("error", console.error);
ws.on("message", (message) => {
let data = message;
try {
data = JSON.parse(message);
} catch (error) {
return;
}
switch (data.type) {
case "subscribe":
subscribeClientToCategory(ws, data.category);
break;
case "unsubscribe":
unsubscribeClientFromCategory(ws, data.category);
break;
case "sensor/battery":
ws.send({ type: "battery", battery: JSON.stringify(updateBattery()) });
break;
case "sensor/mpu":
ws.send({ type: "battery", mpu: 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",
angles: 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",
angles: 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",
angles: 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", logs:ws.clientState.logs }));
break;
case "system/info":
ws.send(JSON.stringify({ type: "info", info: 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", log:ws.clientState.logs.last()}));
break;
default:
ws.send(JSON.stringify({ error: "Unknown request type" }));
}
});
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}`));