Creation nouvelle interface Docker Vehicule et Multimedia
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY server ./server
|
||||
USER node
|
||||
ENV PORT=8080
|
||||
EXPOSE 8080
|
||||
CMD ["node", "server/server.mjs"]
|
||||
@@ -0,0 +1,38 @@
|
||||
import http from 'node:http';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolve } from 'node:path';
|
||||
import { telemetry, profiles } from './telemetry.mjs';
|
||||
|
||||
const assets = new Map([
|
||||
['/', ['index.html', 'text/html']], ['/index.html', ['index.html', 'text/html']],
|
||||
['/app.js', ['app.js', 'text/javascript']], ['/styles.css', ['styles.css', 'text/css']]
|
||||
]);
|
||||
export function createServer() {
|
||||
return http.createServer(async (req, res) => {
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
const send = (status, value) => {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify(value));
|
||||
};
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') return send(405, { error: 'Lecture seule' });
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
if (url.pathname === '/api/health') return send(200, { status: 'ok', source: 'demo' });
|
||||
if (url.pathname === '/api/telemetry') {
|
||||
const profile = url.searchParams.get('profile') || 'route';
|
||||
return profiles.includes(profile) ? send(200, telemetry(profile)) : send(400, { error: 'Profil inconnu' });
|
||||
}
|
||||
const asset = assets.get(url.pathname);
|
||||
if (!asset) return send(404, { error: 'Introuvable' });
|
||||
try {
|
||||
const content = await readFile(new URL(`../ui/${asset[0]}`, import.meta.url));
|
||||
res.writeHead(200, { 'Content-Type': `${asset[1]}; charset=utf-8` });
|
||||
res.end(req.method === 'HEAD' ? undefined : content);
|
||||
} catch { send(500, { error: 'Interface indisponible' }); }
|
||||
});
|
||||
}
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
const port = Number(process.env.PORT || 8080);
|
||||
createServer().listen(port, process.env.HOST || '0.0.0.0', () => console.log(`CARIA : http://localhost:${port}`));
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const profiles = ['route', 'ville', 'arret'];
|
||||
|
||||
// All displays use the same clock: no GPIO, CAN or vehicle commands here.
|
||||
export function telemetry(profile = 'route', now = Date.now()) {
|
||||
if (!profiles.includes(profile)) throw new RangeError('Profil inconnu');
|
||||
const t = now / 1000;
|
||||
const speed = profile === 'arret' ? 0 : Math.round(profile === 'ville'
|
||||
? 23 + 23 * Math.sin(t / 18) : 82 + 12 * Math.sin(t / 15));
|
||||
const gear = speed === 0 ? 'N' : String(Math.min(6, Math.max(1, Math.ceil(speed / 20))));
|
||||
return {
|
||||
schemaVersion: 1, source: 'demo', timestamp: now, profile,
|
||||
speedKph: speed, rpm: speed === 0 ? 850 : Math.round(1400 + speed * 13 + 100 * Math.sin(t / 3)),
|
||||
fuelPercent: 68, coolantCelsius: profile === 'arret' ? 76 : 89,
|
||||
batteryVolts: 14.2, outsideCelsius: 21, rangeKm: 540,
|
||||
tripKm: 42.6, consumptionL100: 5.8, gear,
|
||||
weather: { source: 'demo', label: 'Éclaircies', celsius: 21 }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { telemetry, profiles } from './telemetry.mjs';
|
||||
import { createServer } from './server.mjs';
|
||||
|
||||
test('demo values remain bounded and identical across displays', () => {
|
||||
for (const profile of profiles) for (let t = 0; t < 300000; t += 250) {
|
||||
const d = telemetry(profile, t);
|
||||
assert.ok(d.speedKph >= 0 && d.speedKph <= 220);
|
||||
assert.ok(d.rpm >= 0 && d.rpm <= 7000);
|
||||
assert.equal(d.source, 'demo');
|
||||
assert.deepEqual(d, telemetry(profile, t));
|
||||
if (profile === 'arret') assert.equal(d.speedKph, 0);
|
||||
}
|
||||
assert.throws(() => telemetry('can'), RangeError);
|
||||
});
|
||||
|
||||
test('HTTP contract, invalid profiles, isolation from legacy and read-only API', async () => {
|
||||
const server = createServer();
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
try {
|
||||
assert.equal((await (await fetch(`${base}/api/health`)).json()).status, 'ok');
|
||||
assert.equal((await (await fetch(`${base}/api/telemetry?profile=arret`)).json()).speedKph, 0);
|
||||
assert.equal((await fetch(`${base}/api/telemetry?profile=wrong`)).status, 400);
|
||||
assert.equal((await fetch(`${base}/api/telemetry`, { method: 'POST' })).status, 405);
|
||||
for (const path of ['/WebControl/main.py', '/ssl/key.pem', '/package.json', '/cmd']) {
|
||||
assert.equal((await fetch(base + path)).status, 404);
|
||||
}
|
||||
assert.match(await (await fetch(base)).text(), /CARIA/);
|
||||
} finally { await new Promise(resolve => server.close(resolve)); }
|
||||
});
|
||||
Reference in New Issue
Block a user