337 lines
9.0 KiB
JavaScript
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}`));
|