Files
2025-03-23 07:16:40 +01:00

483 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//Infos
const INFOS_POSITION = 0x01;
const INFOS_LED = 0x02;
const INFOS_MOTORS = 0x04;
const INFOS_WHEELS = 0x08;
const INFOS_SPEED = 0x10;
const INFOS_RANGEFINDER = 0x20;
let infos_ws = null;
let period_ms = 500;
let mask = 0;
function on_msg(e) {
const view = new DataView(e.data);
let pos = 0;
const mask = view.getUint8(pos);
pos++;
if (mask & INFOS_POSITION) {
const position_x = view.getFloat32(pos + 0);
const position_y = view.getFloat32(pos + 4);
const position_a = view.getFloat32(pos + 8);
pos += 3*4;
document.body.dispatchEvent(new CustomEvent('h:infos:position', {
'detail': [position_x, position_y, position_a]
}));
}
if (mask & INFOS_LED) {
const led_r = view.getUint8(pos + 0);
const led_g = view.getUint8(pos + 1);
const led_b = view.getUint8(pos + 2);
pos += 3;
document.body.dispatchEvent(new CustomEvent('h:infos:led', {
'detail': [led_r, led_g, led_b]
}));
}
if (mask & INFOS_MOTORS) {
const ml = view.getFloat32(pos + 0);
const mr = view.getFloat32(pos + 4);
pos += 2*4;
document.body.dispatchEvent(new CustomEvent('h:infos:motors', {
'detail': [ml, mr]
}));
}
if (mask & INFOS_WHEELS) {
const wl = view.getInt16(pos + 0);
const wr = view.getInt16(pos + 2);
const tl = view.getInt16(pos + 4);
const tr = view.getInt16(pos + 6);
pos += 4*2;
document.body.dispatchEvent(new CustomEvent('h:infos:wheels', {
'detail': [wl, wr, tl, tr]
}));
}
if (mask & INFOS_SPEED) {
const w = view.getFloat32(pos + 0);
const v = view.getFloat32(pos + 4);
pos += 4*2;
document.body.dispatchEvent(new CustomEvent('h:infos:speed', {
'detail': [w, v]
}));
}
if (mask & INFOS_RANGEFINDER) {
const d = view.getUint16(pos + 0);
pos += 2;
console.log(d);
document.body.dispatchEvent(new CustomEvent('h:infos:rangefinder', {
'detail': [d]
}));
}
}
function send_mask() {
/*if (mask == 0) {
if (infos_ws) {
infos_ws.close();
infos_ws = null;
}
} else {
if (infos_ws) {
if (infos_ws.readyState == WebSocket.OPEN) {
const buf = new Uint8Array(2);
buf[0] = period_ms / 10;
buf[1] = mask;
infos_ws.send(buf);
}
} else {
infos_ws = new WebSocket('ws://' + document.location.host + '/infos.ws');
infos_ws.binaryType = "arraybuffer";
infos_ws.addEventListener("open", send_mask);
infos_ws.addEventListener("message", on_msg);
}
}*/
}
function infos_mask(v, on) {
if (on)
mask |= v;
else
mask &= ~v;
send_mask();
}
//--------------------------------
//main
document.body.innerHTML = `
<header class="pico">
<details class="dropdown">
<summary>Sections…</summary>
<ul id="ul_header_menu"></ul>
</details>
<h1>MinHAUMtaure</h1>
</header>
`;
const sections_opened = new Set();
if (sessionStorage.getItem('sections_opened')) {
for (const s of sessionStorage.getItem('sections_opened').split('\n'))
sections_opened.add(s);
}
const sections = [
{ 'id': 'led', 'title': 'Led' },
{ 'id': 'motors', 'title': 'Moteurs' },
{ 'id': 'wheels', 'title': 'Retour roues' },
{ 'id': 'position', 'title': 'Position' },
{ 'id': 'turtle', 'title': 'Turtle' },
{ 'id': 'rangefinder', 'title': 'Télémètre' },
{ 'id': 'config', 'title': 'Config' },
{ 'id': 'wlan', 'title': 'Wifi' },
];
const ul_header_menu = document.getElementById('ul_header_menu');
for (const d of sections) {
const s = document.createElement('section');
const id = d['id'];
const title = d['title'];
s.id = 'section_' + id;
document.body.append(s);
const li = document.createElement('li');
ul_header_menu.append(li);
const label = document.createElement('label');
li.append(label);type="module"
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = false;
label.append(checkbox, title);
const h2 = document.createElement('h2');
const b_spans = document.createElement('b');
const span_fs = document.createElement('span');
span_fs.innerText = "⛶";
const span_close = document.createElement('span');
span_close.innerText = "×";
b_spans.append(span_fs, span_close);
h2.append(title, b_spans);
s.insertBefore(h2, s.firstChild);
s.classList.add('pico');
span_close.addEventListener('click', () => {
if (document.fullscreenElement != null)
document.exitFullscreen();
toggle(false);
});
span_fs.addEventListener('click', () => {
if (document.fullscreenElement == null)
s.requestFullscreen();
else
document.exitFullscreen();
});
const toggle = on => {
s.classList.toggle('opened', on);
checkbox.checked = on;
if (on)
sections_opened.add(id);
else
sections_opened.delete(id);
sessionStorage.setItem('sections_opened', Array.from(sections_opened).join('\n'));
if (on)
import('./' + id + '.js');
document.body.dispatchEvent(new CustomEvent('h:section:statechanged', {
'detail': {
'id': id,
'on': on,
'section': s
}
}));
}
checkbox.addEventListener('click', e => toggle(e.target.checked));
if (sections_opened.has(id)) toggle(true);
}
async function form_post(form) {
const data = new URLSearchParams();
for (const pair of new FormData(form)) {
data.append(pair[0], pair[1]);
}
return await fetch(form.action, {
method: 'post',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: data
});
}
//--------------------------------
//motors
const section = document.getElementById('section_motors');
section.insertAdjacentHTML('beforeend', `
<div class="grid">
<p><canvas id="motors_joystick" width="400" height="400"></canvas></p>
<p>
<input type="range" min="-100" max="100" value="0" id="motors_ml" /><br/>
<input type="range" min="-100" max="100" value="0" id="motors_mr" /><br/>
<input type="button" id="motors_stop" value="Stop" />
<canvas id="motors_canvas" width="350" height="40"></canvas>
</p>
</div>
`);
const motors_buf = new Float32Array(2);
const motors_ml = document.getElementById("motors_ml")
const motors_mr = document.getElementById("motors_mr")
const motors_stop = document.getElementById("motors_stop")
const motors_joystick = document.getElementById('motors_joystick');
const motors_canvas = document.getElementById('motors_canvas');
let motors_ws = null;
const send_motors = (vl, vr) => {
/*if (motors_ws && motors_ws.readyState == WebSocket.OPEN) {
const lim = v => Math.max(-1, Math.min(v, 1));
motors_buf[0] = lim(vl);
motors_buf[1] = lim(vr);
motors_ml.value = vl * 100;
motors_mr.value = vr * 100;
motors_ws.send(motors_buf);
} else if (!motors_ws || motors_ws.readyState != WebSocket.CONNECTING) {
motors_ws = new WebSocket('ws://' + document.location.host + '/motors.ws');
motors_ws.addEventListener("open", () => send_motors(vl, vr));
}*/
};
const update_motors = () => {
const vl = motors_ml.value / 100;
const vr = motors_mr.value / 100;
send_motors(vl, vr);
}
motors_ml.addEventListener('change', update_motors);
motors_mr.addEventListener('change', update_motors);
motors_stop.addEventListener('click', e => {
e.preventDefault();
motors_ml.value = 0;
motors_mr.value = 0;
update_motors();
});
const btnsz = 40;
let positionX = 0, positionY = 0;
let clicked = false;
let lastsend = performance.now();
const joystick_draw = () => {
const ctx = motors_joystick.getContext("2d");
const w = motors_joystick.width;
const h = motors_joystick.height;
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#00aaff40";
ctx.beginPath();
ctx.arc(w/2, h/2, w/2-btnsz, 0, 2 * Math.PI);
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = "#0099ee";
ctx.fillStyle = "#00aaff";
ctx.beginPath();
ctx.arc(w/2 + positionX, h/2 + positionY, btnsz, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
}
const updatePosition = e => {
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
const r = motors_joystick.getBoundingClientRect();
const kw = r.width/2 - btnsz;
let vx = Math.max(-1, Math.min((clientX - r.left - r.width/2) / kw, 1));
let vy = Math.max(-1, Math.min((clientY - r.top - r.height/2) / kw, 1));
if (vx*vx + vy*vy > 1) {
const a = Math.atan2(vy, vx);
vx = Math.cos(a);
vy = Math.sin(a);
}
positionX = vx * kw;
positionY = vy * kw;
vy *= -1;
if (performance.now() - lastsend > 250) {
send_motors(vy+vx, vy-vx);
lastsend = performance.now();
}
joystick_draw();
}
const ondown = e => {
clicked = true;
motors_joystick.style.cursor = 'none'
updatePosition(e);
};
const onmove = e => {
if (clicked) {
updatePosition(e);
}
};
const onup = () => {
if (clicked) {
send_motors(0, 0);
motors_joystick.style.cursor = ''
clicked = false;
positionX = positionY = 0;
joystick_draw();
}
};
joystick_draw();
motors_joystick.addEventListener('mousedown', ondown);
motors_joystick.addEventListener('mousemove', onmove);
motors_joystick.addEventListener('mouseup', onup);
motors_joystick.addEventListener('mouseleave', onup);
motors_joystick.addEventListener('touchstart', ondown);
motors_joystick.addEventListener('touchmove', onmove);
motors_joystick.addEventListener('touchend', onup);
motors_joystick.addEventListener('touchcancel', onup);
const text_color = getComputedStyle(motors_canvas).color;
document.body.addEventListener('h:infos:motors', e => {
const ctx = motors_canvas.getContext("2d");
const w = motors_canvas.width;
const h = motors_canvas.height;
const m = 5;
const ml = e.detail[0];
const mr = e.detail[1];
ctx.clearRect(0, 0, w, h);
ctx.lineWidth = 3;
ctx.strokeStyle = "#808080"
ctx.fillStyle = "#008000"
ctx.beginPath();
ctx.rect(m, m, w-2*m, (h/2-m));
ctx.stroke();
ctx.beginPath();
ctx.rect(m, (h/2-m)+m, w-2*m, (h/2-m));
ctx.stroke();
ctx.beginPath();
if (ml > 0)
ctx.rect(w/2, m+1, ml*(w/2-m), (h/2-m)-2);
else
ctx.rect(w/2+ml*(w/2-m), m+1, -ml*(w/2-m), (h/2-m)-2);
ctx.fill();
ctx.beginPath();
if (ml > 0)
ctx.rect(w/2, (h/2-m)+m+1, mr*(w/2-m), (h/2-m)-2);
else
ctx.rect(w/2+mr*(w/2-m), (h/2-m)+m+1, -mr*(w/2-m), (h/2-m)-2);
ctx.fill();
});
document.body.addEventListener('h:section:statechanged', e => {
infos_mask(INFOS_MOTORS, e.detail['on']);
});
infos_mask(INFOS_MOTORS, true);
//--------------------------------------------------
//position
const section2 = document.getElementById('section_position');
section2.insertAdjacentHTML('beforeend', `
<p>
<input type="range" min="190" max="1000" value="1000" id="map_scale" /><br/>
<input type="button" value="RàZ position" id="position_reset" /><br/>
<canvas id="map" width="800" height="800"></canvas>
</p>
`);
const position_reset = document.getElementById('position_reset');
position_reset.addEventListener('click', async () => {
position_reset.disabled = true;
await fetch('/position/reset')
position_reset.disabled = false;
});
const map_canvas = document.getElementById('map');
const map_scale_range = document.getElementById('map_scale');
const pos_hist = [];
const text_color2 = getComputedStyle(map_canvas).color;
const map_draw = (x, y, a) => {
const ctx = map_canvas.getContext("2d");
const w = map_canvas.width;
const h = map_canvas.height;
const map_scale = Number(map_scale_range.value);
ctx.clearRect(0, 0, w, h);
pos_hist.unshift([x, y]);
if (pos_hist.length > 10) pos_hist.pop();
ctx.font = '30px sans-serif';
ctx.fillStyle = text_color2;
ctx.textAlign = 'right';
ctx.fillText(Math.round(x*1000) + ' mm <X>', w-5, 30);
ctx.fillText(Math.round(y*1000) + ' mm <Y>', w-5, 60);
ctx.fillText(Math.round(a*180/Math.PI) + ' deg <A>', w-5, 90);
ctx.save();
ctx.translate(w/2, h/2);
ctx.scale(map_scale/1000, map_scale/1000);
ctx.lineWidth = Math.round(2*1000/map_scale);
ctx.beginPath();
for (let i = -10; i <= 11; i++) {
ctx.moveTo(-95+190*i, -95-2090);
ctx.lineTo(-95+190*i, -95+2090+190);
ctx.moveTo(-95-2090, -95+190*i);
ctx.lineTo(-95+2090+190, -95+190*i);
}
ctx.strokeStyle = 'rgba(180, 180, 180, 50%)';
ctx.stroke();
const hl = pos_hist.length;
for (let i = 1; i < hl; i++) {
ctx.beginPath();
ctx.moveTo(1000*pos_hist[i-1][0], -1000*pos_hist[i-1][1]);
ctx.lineTo(1000*pos_hist[i][0], -1000*pos_hist[i][1]);
const v = Math.round((hl-i)/(hl-1)*100);
ctx.strokeStyle = 'rgba(180, 200, 0, ' + v + '%)';
ctx.stroke();
}
ctx.translate(x*1000, -y*1000);
ctx.rotate(-a);
ctx.beginPath();
ctx.moveTo(40, 0);
ctx.lineTo(35, 77/4);
ctx.lineTo(24, 77/2);
ctx.lineTo(-68, 77/2);
ctx.lineTo(-68, -77/2);
ctx.lineTo(24, -77/2);
ctx.lineTo(35, -77/4);
ctx.lineTo(40, 0);
ctx.strokeStyle = '#008000'
ctx.stroke();
ctx.restore();
};
document.body.addEventListener('h:infos:position', e => {
map_draw(...e.detail);
});
document.body.addEventListener('h:section:statechanged', e => {
infos_mask(INFOS_POSITION, e.detail['on']);
});
infos_mask(INFOS_POSITION, true);