Compare commits
2 Commits
07df61cbc4
...
6f2f6d2d72
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f2f6d2d72 | ||
|
|
ad6c197f5c |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,3 +2,8 @@ data/
|
||||
output/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
data/
|
||||
viewer/data/
|
||||
viewer/screenshots/
|
||||
screenshots/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
1
vendor/Kogger-Protocol
vendored
1
vendor/Kogger-Protocol
vendored
Submodule vendor/Kogger-Protocol deleted from d62576fee5
BIN
vendor/Kogger-Protocol/Kogger SB protocol.odt
vendored
Normal file
BIN
vendor/Kogger-Protocol/Kogger SB protocol.odt
vendored
Normal file
Binary file not shown.
BIN
vendor/Kogger-Protocol/Kogger SB protocol.pdf
vendored
Normal file
BIN
vendor/Kogger-Protocol/Kogger SB protocol.pdf
vendored
Normal file
Binary file not shown.
51
vendor/Kogger-Protocol/README.md
vendored
Normal file
51
vendor/Kogger-Protocol/README.md
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# Open Serial Binary Protocol (SBP) specification
|
||||
|
||||
## Protocol frame structure
|
||||
|
||||
SYNC1 | SYNC2 | ROUTE | MODE | ID | LENGTH | PAYLOAD | CHECK1 | CHECK2|
|
||||
|----------|----------|----------|----------|----------|----------|----------|----------|----------|
|
||||
U1 | U1 | U1 | U1 | U1 | U1 | BYTE[LENGTH] | U1 | U1 |
|
||||
0xBB | 0x55 | BITFIELD | BITFIELD | 1 … 255 | 0 … 128 | BYTEARRAY | 0 … 0xFF | 0 … 0xFF |
|
||||
|
||||
### ROUTE
|
||||
Name | Bits | Description
|
||||
|----------|----------|----------|
|
||||
DEV_ADDRESS | 0:3 bit | Device address. Default and broadcast address is 0x0.
|
||||
RESERVED | 2 bit | Reserved
|
||||
|
||||
### MODE
|
||||
Name | Bits | Description
|
||||
|----------|----------|----------|
|
||||
TYPE | 0:1 bit | 0 — Reserved, 1 — CONTENT: DEVICE → HOST, 2 — SETTING: HOST → DEVICE, 3 —GETTING: HOST → DEVICE
|
||||
RESERVED | 2 bit | Reserved
|
||||
VERSION | 3:5 bit | Field defines the payload data version
|
||||
MARK | 6 bit | Once device is switched on, this flag is always in reset state (ZERO). It can be set to active state (ONE) by the host (see the CMD_MARK command) and the slave device keeps the flag in active state in every frame until hardware reset occurs or is reset by the host. Therefore the host monitors the device's actual settings.
|
||||
RESPONSE | 7 bit | HOST → DEVICE: Set the flag to active state (ONE) in order to get the result of processing the command. The flag doesn't affect the response if one is provided by the TYPE field. DEVICE → HOST: The flag is in reset state (ZERO) by default. Payload goes according to the command specification. If flag is set, the payload contains the result of command processing (see CMD_RESP command).
|
||||
|
||||
## Checksum
|
||||
The checksum algorithm used is the Fletcher-16.
|
||||
Example source code for calculating the checksum:
|
||||
```
|
||||
uint8_t CHECK1 = 0;
|
||||
uint8_t CHECK2 = 0;
|
||||
void CheckSumUpdate(uint8_t byte) {
|
||||
CHECK1 += byte;
|
||||
CHECK2 += CHECK1;
|
||||
}
|
||||
```
|
||||
|
||||
## Number Formats
|
||||
- Multi-byte values are ordered in Little Endian format
|
||||
- Floating point values are transmitted in IEEE754 single or double precision
|
||||
- bit-field in LSB format
|
||||
|
||||
Name | Type | Size (Bytes) | Range
|
||||
|----------|----------|----------|----------|
|
||||
S1 | int8_t | 1 | -128 ... 127
|
||||
U1 | uint8_t | 1 | 0 … 255
|
||||
S2 | int16_t | 2 | -32768 … 32767
|
||||
U2 | uint16_t | 2 | 0 … 65535
|
||||
S4 | int32_t | 4 | -2'147'483'648 ... 2'147'483'647
|
||||
U4 | uint32_t | 4 | 0 … 4'294'967'295
|
||||
F4 | float | 4 | -1*2^+127 ... 2^+127
|
||||
D8 | double | 8 | -1*2^+1023 ... 2^+1023
|
||||
@@ -2,10 +2,9 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>COSMA — NAV Viewer v6</title>
|
||||
<title>COSMA — NAV Viewer v5</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nouislider@15.8.1/dist/nouislider.min.css"/>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
@@ -32,10 +31,11 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
#btn-today:hover { background: #00b4d8; color: #1a1a2e; }
|
||||
#mission-label { font-size: 11px; font-family: monospace; padding: 2px 8px; }
|
||||
#mission-label {
|
||||
font-size: 11px; font-family: monospace; padding: 2px 8px;
|
||||
}
|
||||
#mission-label.has-data { color: #06d6a0; }
|
||||
#mission-label.no-data { color: #555; }
|
||||
#load-status { font-size: 10px; color: #888; flex: 1; }
|
||||
|
||||
/* Row 1: header */
|
||||
#header {
|
||||
@@ -50,7 +50,7 @@
|
||||
font-family: monospace; font-size: 10px; padding: 2px 7px; cursor: pointer;
|
||||
border-radius: 2px; border: 1px solid; background: transparent;
|
||||
}
|
||||
.layer-btn.active { opacity: 1; }
|
||||
.layer-btn.active { opacity: 1; }
|
||||
.layer-btn.inactive { opacity: 0.35; }
|
||||
#btn-usv { color: #00b4d8; border-color: #00b4d8; }
|
||||
#btn-auv { color: #ff8800; border-color: #ff8800; }
|
||||
@@ -114,6 +114,8 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
#btn-viewall:hover, #btn-play:hover { background: #00b4d8; color: #1a1a2e; }
|
||||
#ctrl-labels { display: flex; align-items: center; gap: 6px; font-size: 10px; color: #666; }
|
||||
label[for="trail-select"] { font-size: 10px; color: #666; }
|
||||
|
||||
/* Row 4: 2×2 charts grid */
|
||||
#graphs-section {
|
||||
@@ -132,26 +134,26 @@
|
||||
padding: 4px 8px 3px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.chart-title { font-size: 10px; color: #a0c4ff; margin-bottom: 2px; flex-shrink: 0; }
|
||||
.chart-wrap .plotly-wrap { flex: 1; min-height: 0; position: relative; }
|
||||
.chart-wrap .plotly-wrap > div { width: 100% !important; height: 100% !important; }
|
||||
.chart-wrap canvas { flex: 1; min-height: 0; display: block; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3/dist/chartjs-plugin-annotation.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Row 0: Datebar -->
|
||||
<div id="datebar">
|
||||
<input type="date" id="date-picker">
|
||||
<input type="date" id="date-picker" list="available-dates">
|
||||
<datalist id="available-dates"></datalist>
|
||||
<button id="btn-today" onclick="datePickerToday()">Aujourd'hui</button>
|
||||
<span id="mission-label" class="no-data">Chargement...</span>
|
||||
<span id="load-status"></span>
|
||||
</div>
|
||||
|
||||
<!-- Row 1: Header -->
|
||||
<div id="header">
|
||||
<span id="title">COSMA NAV v6</span>
|
||||
<span id="title">COSMA NAV v5</span>
|
||||
<span id="stats">Chargement...</span>
|
||||
<div id="layer-toggles">
|
||||
<button class="layer-btn active" id="btn-usv" onclick="toggleLayer('usv')">USV</button>
|
||||
@@ -197,48 +199,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 4: 2×2 Plotly charts -->
|
||||
<!-- Row 4: 2×2 Charts -->
|
||||
<div id="graphs-section">
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Depth AUV (m)</div>
|
||||
<div class="plotly-wrap"><div id="chart-depth"></div></div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Motors AUV (PWM)</div>
|
||||
<div class="plotly-wrap"><div id="chart-pwm-auv"></div></div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">Motors USV (PWM)</div>
|
||||
<div class="plotly-wrap"><div id="chart-pwm-usv"></div></div>
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-title">USBL Distance (m)</div>
|
||||
<div class="plotly-wrap"><div id="chart-usbl"></div></div>
|
||||
</div>
|
||||
<div class="chart-wrap"><div class="chart-title">Profondeur AUV (m)</div><canvas id="chart-depth"></canvas></div>
|
||||
<div class="chart-wrap"><div class="chart-title">PWM AUV (canaux)</div><canvas id="chart-pwm-auv"></canvas></div>
|
||||
<div class="chart-wrap"><div class="chart-title">PWM USV (M1-M8)</div><canvas id="chart-pwm-usv"></canvas></div>
|
||||
<div class="chart-wrap"><div class="chart-title">USBL Distance (m)</div><canvas id="chart-usbl"></canvas></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/nouislider@15.8.1/dist/nouislider.min.js"></script>
|
||||
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.7.1/nouislider.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.7.1/nouislider.min.css"/>
|
||||
<script>
|
||||
// == Constants ==
|
||||
const API = 'http://192.168.0.83:8766';
|
||||
const COLORS = ['#00b4d8','#06d6a0','#ffd166','#e94560','#a855f7','#ff8800','#7dc8e0','#b8f0d4'];
|
||||
const COLORS = ['#00b4d8','#e94560','#06d6a0','#ffd166','#a855f7','#f97316'];
|
||||
const AUV_COLOR = '#ff8800';
|
||||
const PLOTLY_LAYOUT = {
|
||||
paper_bgcolor: '#12122a',
|
||||
plot_bgcolor: '#12122a',
|
||||
margin: { t: 2, r: 8, b: 24, l: 42 },
|
||||
font: { color: '#a0c4ff', size: 9, family: 'monospace' },
|
||||
xaxis: {
|
||||
gridcolor: '#1a1a3e', zerolinecolor: '#1a1a3e',
|
||||
tickformat: '%H:%M:%S', color: '#666'
|
||||
},
|
||||
yaxis: { gridcolor: '#1a1a3e', zerolinecolor: '#1a1a3e', color: '#666' },
|
||||
showlegend: false,
|
||||
autosize: true,
|
||||
};
|
||||
const PLOTLY_CONFIG = { responsive: true, displayModeBar: false };
|
||||
const PWM_COLORS_AUV = ['#ff8800','#ffd166','#06d6a0','#00b4d8','#a855f7','#f97316','#e94560','#88c0d0'];
|
||||
const PWM_COLORS_USV = ['#00b4d8','#0096b4','#0077a0','#005f8c','#ffd166','#e6b800','#c49a00','#a07d00'];
|
||||
|
||||
// == Map init ==
|
||||
const map = L.map('map', { zoomControl: true });
|
||||
@@ -297,27 +274,17 @@ function fmtDur(ms) {
|
||||
const s = Math.floor(ms/1000), h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sc = s%60;
|
||||
return h>0 ? `${h}h${String(m).padStart(2,'0')}m` : `${m}m${String(sc).padStart(2,'0')}s`;
|
||||
}
|
||||
// Convert ISO timestamp string to ms
|
||||
function isoToMs(t) { return new Date(t).getTime(); }
|
||||
// Convert Unix seconds to ms
|
||||
function unixToMs(t) { return t * 1000; }
|
||||
|
||||
// == State ==
|
||||
let allPoints = []; // {t_ms, lat, lon, heading, source}
|
||||
let usblPoints = []; // {t_ms, auv_lat, auv_lon, dist, az, elev, snr}
|
||||
let sessionsMeta = []; // loaded session metadata
|
||||
let tMin = 0, tMax = 0, tNow = 0;
|
||||
let trailMs = 60000;
|
||||
let trackLayers = [], auvTrackLayer = null, cursorMarker = null, auvMarker = null, usblVector = null;
|
||||
const layerVis = { usv: true, auv: true, vec: true, panel: true };
|
||||
let playTimer = null;
|
||||
let cursorSlider = null;
|
||||
|
||||
// Plotly chart data state
|
||||
let depthTraces = [];
|
||||
let pwmAuvTraces = [];
|
||||
let pwmUsvTraces = [];
|
||||
let usblDistTraces = [];
|
||||
let allPoints=[], usblPoints=[], manifest=null, usblMeta=null;
|
||||
let mcapSignals=null, usvPwm=null;
|
||||
let tMin=0, tMax=0;
|
||||
let tNow=0;
|
||||
let trailMs=60000;
|
||||
let charts={};
|
||||
let trackLayers=[], auvTrackLayer=null, cursorMarker=null, auvMarker=null, usblVector=null;
|
||||
const layerVis = { usv:true, auv:true, vec:true, panel:true };
|
||||
let playTimer=null;
|
||||
|
||||
// == Layer toggles ==
|
||||
function toggleLayer(name) {
|
||||
@@ -331,7 +298,7 @@ function toggleLayer(name) {
|
||||
if (auvMarker) layerVis.auv ? map.addLayer(auvMarker) : map.removeLayer(auvMarker);
|
||||
}
|
||||
if (name==='vec' && usblVector) layerVis.vec ? map.addLayer(usblVector) : map.removeLayer(usblVector);
|
||||
if (name==='panel') document.getElementById('usbl-panel').style.display = layerVis.panel && usblPoints.length ? 'block' : 'none';
|
||||
if (name==='panel') document.getElementById('usbl-panel').style.display = layerVis.panel ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// == View All ==
|
||||
@@ -341,37 +308,54 @@ function viewAll() {
|
||||
applyTrailAndCursor();
|
||||
}
|
||||
|
||||
// == Plotly charts init ==
|
||||
// == Charts ==
|
||||
function makeChartOptions() {
|
||||
return {
|
||||
animation: false, parsing: false, responsive: true, maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false, labels: { color:'#a0c4ff', font:{size:9,family:'monospace'}, boxWidth:12 } },
|
||||
annotation: { annotations: {} }
|
||||
},
|
||||
scales: {
|
||||
x: { type:'linear', ticks:{ color:'#666', font:{size:9,family:'monospace'}, maxTicksLimit:6,
|
||||
callback:(v)=>new Date(v).toISOString().substr(11,8) }, grid:{color:'#1a1a3e'} },
|
||||
y: { ticks:{ color:'#666', font:{size:9,family:'monospace'}, maxTicksLimit:5 }, grid:{color:'#1a1a3e'} }
|
||||
}
|
||||
};
|
||||
}
|
||||
function initCharts() {
|
||||
const base = JSON.parse(JSON.stringify(PLOTLY_LAYOUT));
|
||||
Plotly.newPlot('chart-depth', [], { ...base, yaxis: { ...base.yaxis, autorange: 'reversed' } }, PLOTLY_CONFIG);
|
||||
Plotly.newPlot('chart-pwm-auv', [], { ...base }, PLOTLY_CONFIG);
|
||||
Plotly.newPlot('chart-pwm-usv', [], { ...base }, PLOTLY_CONFIG);
|
||||
Plotly.newPlot('chart-usbl', [], { ...base }, PLOTLY_CONFIG);
|
||||
const mkDs = (lbl, col) => ({ label:lbl, data:[], borderColor:col, borderWidth:1.5, pointRadius:0, tension:0 });
|
||||
charts.depth = new Chart(document.getElementById('chart-depth'), { type:'line', data:{datasets:[mkDs('depth','#06d6a0')]}, options:makeChartOptions() });
|
||||
charts.pwmAuv = new Chart(document.getElementById('chart-pwm-auv'), { type:'line', data:{datasets:[]}, options:makeChartOptions() });
|
||||
charts.pwmUsv = new Chart(document.getElementById('chart-pwm-usv'), { type:'line', data:{datasets:[]}, options:makeChartOptions() });
|
||||
charts.usbl = new Chart(document.getElementById('chart-usbl'), { type:'line', data:{datasets:[mkDs('dist','#a855f7')]}, options:makeChartOptions() });
|
||||
}
|
||||
|
||||
function populatePlotlyCharts() {
|
||||
const layout = JSON.parse(JSON.stringify(PLOTLY_LAYOUT));
|
||||
const depthLayout = { ...layout, yaxis: { ...layout.yaxis, autorange: 'reversed' } };
|
||||
|
||||
Plotly.react('chart-depth', depthTraces, depthLayout, PLOTLY_CONFIG);
|
||||
Plotly.react('chart-pwm-auv', pwmAuvTraces, { ...layout, showlegend: pwmAuvTraces.length > 1 }, PLOTLY_CONFIG);
|
||||
Plotly.react('chart-pwm-usv', pwmUsvTraces, { ...layout, showlegend: pwmUsvTraces.length > 1 }, PLOTLY_CONFIG);
|
||||
Plotly.react('chart-usbl', usblDistTraces, layout, PLOTLY_CONFIG);
|
||||
}
|
||||
|
||||
// Update cursor line on all Plotly charts
|
||||
function updateChartsCursor() {
|
||||
if (!tMin || !tMax) return;
|
||||
const tNowDate = new Date(tNow);
|
||||
const t0Date = new Date(trailMs === 0 ? tMin : Math.max(tMin, tNow - trailMs));
|
||||
const t1Date = new Date(trailMs === 0 ? tMax : tNow);
|
||||
const shapeBase = { type: 'line', x0: tNowDate, x1: tNowDate, y0: 0, y1: 1, yref: 'paper',
|
||||
line: { color: '#e94560', width: 1.5, dash: 'dot' } };
|
||||
const rangeUpdate = { 'xaxis.range': [t0Date, t1Date] };
|
||||
['chart-depth','chart-pwm-auv','chart-pwm-usv','chart-usbl'].forEach(id => {
|
||||
Plotly.relayout(id, { ...rangeUpdate, shapes: [shapeBase] });
|
||||
});
|
||||
function populateCharts() {
|
||||
if (mcapSignals) {
|
||||
if (mcapSignals.depth) { charts.depth.data.datasets[0].data = mcapSignals.depth.map(p=>({x:p.t,y:p.v})); charts.depth.update('none'); }
|
||||
if (mcapSignals.pwm_auv) {
|
||||
const {channels,samples} = mcapSignals.pwm_auv;
|
||||
charts.pwmAuv.data.datasets = channels.map((ch,i)=>({
|
||||
label:'Ch'+ch, data:samples.map(s=>({x:s.t,y:s.v[i]})),
|
||||
borderColor:PWM_COLORS_AUV[i%8], borderWidth:1, pointRadius:0, tension:0
|
||||
}));
|
||||
charts.pwmAuv.options.plugins.legend.display = channels.length>1;
|
||||
charts.pwmAuv.update('none');
|
||||
}
|
||||
}
|
||||
if (usvPwm && usvPwm.M) {
|
||||
const keys = Object.keys(usvPwm.M).sort();
|
||||
charts.pwmUsv.data.datasets = keys.map((k,i)=>({
|
||||
label:k, data:usvPwm.M[k].map(p=>({x:p.t,y:p.v})),
|
||||
borderColor:PWM_COLORS_USV[i%8], borderWidth:1, pointRadius:0, tension:0
|
||||
}));
|
||||
charts.pwmUsv.options.plugins.legend.display = keys.length>1;
|
||||
charts.pwmUsv.update('none');
|
||||
}
|
||||
if (usblPoints && usblPoints.length) {
|
||||
charts.usbl.data.datasets[0].data = usblPoints.map(p=>({x:p.t_ms,y:p.dist!==undefined?p.dist:p.distance}));
|
||||
charts.usbl.update('none');
|
||||
}
|
||||
}
|
||||
|
||||
// == Apply trail + cursor ==
|
||||
@@ -381,22 +365,35 @@ function applyTrailAndCursor() {
|
||||
const t0 = trailMs===0 ? tMin : Math.max(tMin, tNow - trailMs);
|
||||
const t1 = tNow;
|
||||
|
||||
const trailPtsUsv = filterWindow(allPoints, t0, t1);
|
||||
// Update chart x-window
|
||||
const ann = { type:'line', xMin:tNow, xMax:tNow, borderColor:'#e94560', borderWidth:1.5, borderDash:[4,2] };
|
||||
const chartT0 = trailMs===0 ? tMin : t0;
|
||||
const chartT1 = trailMs===0 ? tMax : Math.max(t1, tMin+1);
|
||||
for (const c of Object.values(charts)) {
|
||||
c.options.scales.x.min = chartT0;
|
||||
c.options.scales.x.max = chartT1;
|
||||
c.options.plugins.annotation.annotations = { cursor: ann };
|
||||
c.update('none');
|
||||
}
|
||||
|
||||
// Trail track on map
|
||||
const trailPtsUsv = filterWindow(allPoints, t0, t1);
|
||||
const trailPtsUsbl = filterWindow(usblPoints, t0, t1);
|
||||
|
||||
// Rebuild USV trail layers
|
||||
trackLayers.forEach(l => map.removeLayer(l));
|
||||
trackLayers = [];
|
||||
const sourceNames = [...new Set(allPoints.map(p => p.source))];
|
||||
const groups = {};
|
||||
trailPtsUsv.forEach(p => { if (!groups[p.source]) groups[p.source]=[]; groups[p.source].push(p); });
|
||||
sourceNames.forEach((src, i) => {
|
||||
const pts = groups[src] || [];
|
||||
if (pts.length < 2) return;
|
||||
const layer = L.polyline(pts.map(p=>[p.lat,p.lon]), { color:COLORS[i%COLORS.length], weight:2.5, opacity:0.85 });
|
||||
trackLayers.push(layer);
|
||||
if (layerVis.usv) layer.addTo(map);
|
||||
});
|
||||
if (manifest) {
|
||||
const groups = {};
|
||||
trailPtsUsv.forEach(p => { if (!groups[p.source]) groups[p.source]=[]; groups[p.source].push(p); });
|
||||
manifest.sessions.forEach((sess, i) => {
|
||||
const pts = groups[sess.source_name]||[];
|
||||
if (pts.length < 2) return;
|
||||
const layer = L.polyline(pts.map(p=>[p.lat,p.lon]), { color:COLORS[i%COLORS.length], weight:2.5, opacity:0.85 });
|
||||
trackLayers.push(layer);
|
||||
if (layerVis.usv) layer.addTo(map);
|
||||
});
|
||||
}
|
||||
|
||||
// Rebuild AUV trail layer
|
||||
if (auvTrackLayer) { map.removeLayer(auvTrackLayer); auvTrackLayer=null; }
|
||||
@@ -408,8 +405,8 @@ function applyTrailAndCursor() {
|
||||
// USV cursor marker at tNow
|
||||
const pUsv = findNearest(allPoints, tNow);
|
||||
if (pUsv) {
|
||||
const srcIdx = sourceNames.indexOf(pUsv.source);
|
||||
const color = COLORS[Math.max(0, srcIdx) % COLORS.length];
|
||||
const sessIdx = manifest ? manifest.sessions.findIndex(s=>s.source_name===pUsv.source) : 0;
|
||||
const color = COLORS[Math.max(0,sessIdx)%COLORS.length];
|
||||
if (!cursorMarker) {
|
||||
cursorMarker = L.marker([pUsv.lat,pUsv.lon], { icon:makeArrowIcon(pUsv.heading||0,color), zIndexOffset:1000 }).addTo(map);
|
||||
} else {
|
||||
@@ -417,7 +414,7 @@ function applyTrailAndCursor() {
|
||||
cursorMarker.setIcon(makeArrowIcon(pUsv.heading||0,color));
|
||||
}
|
||||
document.getElementById('cursor-info').textContent =
|
||||
`${fmtMs(pUsv.t_ms)} | ${pUsv.lat.toFixed(6)}, ${pUsv.lon.toFixed(6)} | cap: ${pUsv.heading!=null?pUsv.heading.toFixed(1):'N/A'} | ${pUsv.source}`;
|
||||
`${pUsv.t} | ${pUsv.lat.toFixed(6)}, ${pUsv.lon.toFixed(6)} | cap: ${pUsv.heading!==null&&pUsv.heading!==undefined?pUsv.heading.toFixed(1):'N/A'} | ${pUsv.source}`;
|
||||
}
|
||||
|
||||
// AUV + USBL at tNow
|
||||
@@ -431,7 +428,7 @@ function applyTrailAndCursor() {
|
||||
auvMarker.setLatLng([pUsbl.auv_lat,pUsbl.auv_lon]);
|
||||
}
|
||||
}
|
||||
const usvPos = pUsv ? [pUsv.lat,pUsv.lon] : [pUsbl.auv_lat,pUsbl.auv_lon];
|
||||
const usvPos = pUsv ? [pUsv.lat,pUsv.lon] : [pUsbl.usv_lat,pUsbl.usv_lon];
|
||||
if (layerVis.vec) {
|
||||
if (!usblVector) {
|
||||
usblVector = L.polyline([usvPos,[pUsbl.auv_lat,pUsbl.auv_lon]], { color:'#888', weight:1.5, dashArray:'6,4', opacity:0.9 }).addTo(map);
|
||||
@@ -440,28 +437,27 @@ function applyTrailAndCursor() {
|
||||
usblVector.setLatLngs([usvPos,[pUsbl.auv_lat,pUsbl.auv_lon]]);
|
||||
}
|
||||
}
|
||||
if (layerVis.panel && pUsbl.dist != null) {
|
||||
document.getElementById('usbl-panel').style.display = 'block';
|
||||
if (layerVis.panel) {
|
||||
document.getElementById('usbl-panel').style.display='block';
|
||||
document.getElementById('up-dist').textContent = `${pUsbl.dist.toFixed(2)} m`;
|
||||
document.getElementById('up-az').textContent = `${(pUsbl.az||0).toFixed(1)} deg`;
|
||||
document.getElementById('up-elev').textContent = `${(pUsbl.elev||0).toFixed(1)} deg`;
|
||||
document.getElementById('up-snr').textContent = `${(pUsbl.snr||0).toFixed(1)}`;
|
||||
document.getElementById('up-az').textContent = `${pUsbl.az.toFixed(1)} deg`;
|
||||
document.getElementById('up-elev').textContent = `${pUsbl.elev.toFixed(1)} deg`;
|
||||
document.getElementById('up-snr').textContent = `${pUsbl.snr.toFixed(1)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// stats
|
||||
const dur = tNow - (trailMs===0?tMin:t0);
|
||||
document.getElementById('stats').textContent =
|
||||
`t: ${fmtMs(tNow)} | USV ${trailPtsUsv.length} pts | AUV ${trailPtsUsbl.length} pts | total ${fmtDur(tMax-tMin)}`;
|
||||
|
||||
updateChartsCursor();
|
||||
`trail: ${fmtMs(tNow)} | USV ${trailPtsUsv.length} pts | AUV ${trailPtsUsbl.length} pts | total ${fmtDur(tMax-tMin)}`;
|
||||
}
|
||||
|
||||
// == Cursor slider ==
|
||||
let cursorSlider = null;
|
||||
function initCursorSlider() {
|
||||
if (cursorSlider) { cursorSlider.destroy(); cursorSlider = null; }
|
||||
const el = document.getElementById('cursor-slider');
|
||||
cursorSlider = noUiSlider.create(el, {
|
||||
cursorSlider = noUiSlider.create(document.getElementById('cursor-slider'), {
|
||||
start: [tMin],
|
||||
range: { min: tMin, max: tMax > tMin ? tMax : tMin + 1000 },
|
||||
range: { min: tMin, max: tMax },
|
||||
step: 1000,
|
||||
});
|
||||
cursorSlider.on('update', (values) => {
|
||||
@@ -469,6 +465,7 @@ function initCursorSlider() {
|
||||
document.getElementById('cursor-time').textContent = fmtMs(tNow);
|
||||
applyTrailAndCursor();
|
||||
});
|
||||
// trail select
|
||||
document.getElementById('trail-select').addEventListener('change', () => applyTrailAndCursor());
|
||||
}
|
||||
|
||||
@@ -484,241 +481,72 @@ document.getElementById('btn-play').addEventListener('click', () => {
|
||||
});
|
||||
|
||||
// == Legend ==
|
||||
function buildLegend(sourceNames) {
|
||||
function buildLegend() {
|
||||
let html = '';
|
||||
sourceNames.forEach((src, i) => {
|
||||
const name = src.replace(/_navigation_log|\.csv/g,'');
|
||||
html += `<div class="legend-item"><div class="legend-dot" style="background:${COLORS[i%COLORS.length]}"></div><span>USV ${name}</span></div>`;
|
||||
});
|
||||
if (manifest) {
|
||||
manifest.sessions.forEach((s,i) => {
|
||||
const name = s.source_name.replace('_navigation_log','').replace('.csv','');
|
||||
html += `<div class="legend-item"><div class="legend-dot" style="background:${COLORS[i%COLORS.length]}"></div><span>USV ${name}</span></div>`;
|
||||
});
|
||||
}
|
||||
html += `<div class="legend-item"><div class="legend-dot" style="background:${AUV_COLOR}"></div><span>AUV (USBL proj.)</span></div>`;
|
||||
html += `<div class="legend-item"><div class="legend-dashed"></div><span>Vecteur USBL</span></div>`;
|
||||
document.getElementById('legend').innerHTML = html;
|
||||
}
|
||||
|
||||
// == Clear map layers ==
|
||||
function clearMapLayers() {
|
||||
trackLayers.forEach(l => map.removeLayer(l));
|
||||
trackLayers = [];
|
||||
if (auvTrackLayer) { map.removeLayer(auvTrackLayer); auvTrackLayer=null; }
|
||||
if (cursorMarker) { map.removeLayer(cursorMarker); cursorMarker=null; }
|
||||
if (auvMarker) { map.removeLayer(auvMarker); auvMarker=null; }
|
||||
if (usblVector) { map.removeLayer(usblVector); usblVector=null; }
|
||||
}
|
||||
|
||||
// == Load data for a given date ==
|
||||
async function loadDate(date) {
|
||||
setStatus('Chargement...');
|
||||
clearMapLayers();
|
||||
allPoints = [];
|
||||
usblPoints = [];
|
||||
depthTraces = [];
|
||||
pwmAuvTraces = [];
|
||||
pwmUsvTraces = [];
|
||||
usblDistTraces = [];
|
||||
|
||||
// Find which missions/dives match this date
|
||||
// == Load ==
|
||||
async function loadData() {
|
||||
try {
|
||||
const mResp = await fetch(`${API}/api/missions`);
|
||||
const missions = await mResp.json();
|
||||
const [trackResp, pointsResp, manifestResp, usblResp] = await Promise.all([
|
||||
fetch('data/track.geojson'), fetch('data/points.json'),
|
||||
fetch('data/manifest.json'), fetch('data/usbl.json'),
|
||||
]);
|
||||
if (!trackResp.ok) throw new Error('track.geojson not found');
|
||||
if (!pointsResp.ok) throw new Error('points.json not found');
|
||||
if (!manifestResp.ok) throw new Error('manifest.json not found');
|
||||
if (!usblResp.ok) throw new Error('usbl.json not found');
|
||||
|
||||
const dateStr = date.replace(/-/g,''); // YYYYMMDD
|
||||
|
||||
const fetches = missions.map(async mission => {
|
||||
const dResp = await fetch(`${API}/api/missions/${mission.id}/dives`);
|
||||
const dives = await dResp.json();
|
||||
return { mission, dives: dives.filter(d => d.id.startsWith(dateStr)) };
|
||||
});
|
||||
|
||||
const missionDives = (await Promise.all(fetches)).filter(md => md.dives.length > 0);
|
||||
|
||||
if (!missionDives.length) {
|
||||
setStatus('Aucune donnée pour cette date');
|
||||
document.getElementById('stats').textContent = 'Aucune donnée';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all sessions for matching dives
|
||||
let totalShip = 0, totalSub = 0;
|
||||
const allFetches = [];
|
||||
|
||||
for (const { mission, dives } of missionDives) {
|
||||
for (const dive of dives) {
|
||||
allFetches.push(loadDiveData(mission.id, dive.id));
|
||||
totalShip += dive.ship_session_count || 0;
|
||||
totalSub += dive.sub_session_count || 0;
|
||||
}
|
||||
}
|
||||
await Promise.all(allFetches);
|
||||
|
||||
// Sort
|
||||
allPoints.sort((a,b)=>a.t_ms-b.t_ms);
|
||||
usblPoints.sort((a,b)=>a.t_ms-b.t_ms);
|
||||
|
||||
if (!allPoints.length && !usblPoints.length) {
|
||||
setStatus('Pas de points pour cette date');
|
||||
return;
|
||||
}
|
||||
|
||||
const allTimes = [
|
||||
...allPoints.map(p=>p.t_ms),
|
||||
...usblPoints.map(p=>p.t_ms)
|
||||
];
|
||||
tMin = Math.min(...allTimes);
|
||||
tMax = Math.max(...allTimes);
|
||||
tNow = tMin;
|
||||
|
||||
// Fit map
|
||||
const allLats = [
|
||||
...allPoints.map(p=>p.lat),
|
||||
...usblPoints.map(p=>p.auv_lat)
|
||||
].filter(Boolean);
|
||||
const allLons = [
|
||||
...allPoints.map(p=>p.lon),
|
||||
...usblPoints.map(p=>p.auv_lon)
|
||||
].filter(Boolean);
|
||||
if (allLats.length) {
|
||||
map.fitBounds([
|
||||
[Math.min(...allLats), Math.min(...allLons)],
|
||||
[Math.max(...allLats), Math.max(...allLons)]
|
||||
], { padding: [40,40] });
|
||||
}
|
||||
const trackGeo = await trackResp.json();
|
||||
allPoints = await pointsResp.json();
|
||||
manifest = await manifestResp.json();
|
||||
usblMeta = await usblResp.json();
|
||||
usblPoints = usblMeta.points.sort((a,b)=>a.t_ms-b.t_ms);
|
||||
|
||||
// Static faded USV track background
|
||||
const sourceNames = [...new Set(allPoints.map(p=>p.source))];
|
||||
const groups = {};
|
||||
allPoints.forEach(p => { if (!groups[p.source]) groups[p.source]=[]; groups[p.source].push(p); });
|
||||
sourceNames.forEach((src,i) => {
|
||||
const pts = groups[src]||[];
|
||||
if (pts.length < 2) return;
|
||||
L.polyline(pts.map(p=>[p.lat,p.lon]), { color:COLORS[i%COLORS.length], weight:1.5, opacity:0.2 }).addTo(map);
|
||||
});
|
||||
L.geoJSON(trackGeo, { style:(f)=>({ color:f.properties.color||'#00b4d8', weight:2, opacity:0.3 }) }).addTo(map);
|
||||
|
||||
buildLegend(sourceNames);
|
||||
populatePlotlyCharts();
|
||||
if (allPoints.length > 0) {
|
||||
const lats=allPoints.map(p=>p.lat), lons=allPoints.map(p=>p.lon);
|
||||
map.fitBounds([[Math.min(...lats),Math.min(...lons)],[Math.max(...lats),Math.max(...lons)]], {padding:[40,40]});
|
||||
}
|
||||
|
||||
tMin = manifest.t_min_ms || Math.min(...allPoints.map(p=>p.t_ms||Infinity));
|
||||
tMax = manifest.t_max_ms || Math.max(...allPoints.map(p=>p.t_ms||-Infinity));
|
||||
if (tMin===tMax) tMax=tMin+1000;
|
||||
tNow = tMin;
|
||||
|
||||
document.getElementById('title').textContent =
|
||||
`COSMA v5 — USV ${manifest.n_sessions} sess. ${manifest.n_points_sampled} pts | AUV ${usblMeta.n_points} pts`;
|
||||
|
||||
buildLegend();
|
||||
initCursorSlider();
|
||||
applyTrailAndCursor();
|
||||
|
||||
document.getElementById('title').textContent = `COSMA v6 — ${date}`;
|
||||
setStatus(`${totalShip} USV sess. | ${totalSub} AUV sess. | ${allPoints.length} pts USV | ${usblPoints.length} pts USBL`);
|
||||
|
||||
} catch(e) {
|
||||
setStatus('Erreur: ' + e.message);
|
||||
document.getElementById('stats').textContent = 'Erreur: '+e.message;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiveData(missionId, diveId) {
|
||||
async function loadGraphData() {
|
||||
initCharts();
|
||||
try {
|
||||
const sResp = await fetch(`${API}/api/dives/${missionId}/${diveId}/sessions`);
|
||||
const sessions = await sResp.json();
|
||||
const sessionFetches = [];
|
||||
|
||||
// Ship sessions
|
||||
if (sessions.ship) {
|
||||
sessions.ship.forEach(sess => {
|
||||
sessionFetches.push(loadShipSession(missionId, diveId, sess.id));
|
||||
});
|
||||
}
|
||||
// Sub sessions
|
||||
if (sessions.sub) {
|
||||
sessions.sub.forEach(sess => {
|
||||
sessionFetches.push(loadSubSession(missionId, diveId, sess.id));
|
||||
});
|
||||
}
|
||||
await Promise.all(sessionFetches);
|
||||
} catch(e) { console.warn('loadDiveData error', diveId, e); }
|
||||
}
|
||||
|
||||
async function loadShipSession(missionId, diveId, sessionId) {
|
||||
try {
|
||||
const [trackResp, seriesResp] = await Promise.all([
|
||||
fetch(`${API}/api/ship/${missionId}/${diveId}/${sessionId}/track`),
|
||||
fetch(`${API}/api/ship/${missionId}/${diveId}/${sessionId}/series`),
|
||||
const [mcapResp, usvPwmResp] = await Promise.allSettled([
|
||||
fetch('data/mcap_signals.json'), fetch('data/usv_pwm.json'),
|
||||
]);
|
||||
if (trackResp.ok) {
|
||||
const d = await trackResp.json();
|
||||
const pts = (d.points||[]).map(p => ({
|
||||
t_ms: isoToMs(p.t), lat: p.lat, lon: p.lon,
|
||||
heading: p.heading || null, source: sessionId
|
||||
}));
|
||||
allPoints.push(...pts);
|
||||
}
|
||||
if (seriesResp.ok) {
|
||||
const d = await seriesResp.json();
|
||||
// USV PWM: M1..M8
|
||||
const motorKeys = Object.keys(d).filter(k => /^M\d+$/.test(k));
|
||||
motorKeys.forEach((k, i) => {
|
||||
const pts = d[k];
|
||||
if (!pts || !pts.length) return;
|
||||
pwmUsvTraces.push({
|
||||
x: pts.map(p => new Date(isoToMs(p.t))),
|
||||
y: pts.map(p => p.v),
|
||||
name: k,
|
||||
type: 'scatter', mode: 'lines',
|
||||
line: { color: COLORS[i % COLORS.length], width: 1 },
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch(e) { console.warn('loadShipSession error', sessionId, e); }
|
||||
}
|
||||
|
||||
async function loadSubSession(missionId, diveId, sessionId) {
|
||||
try {
|
||||
const [seriesResp, usblResp] = await Promise.all([
|
||||
fetch(`${API}/api/sub/${missionId}/${diveId}/${sessionId}/series`),
|
||||
fetch(`${API}/api/sub/${missionId}/${diveId}/${sessionId}/usbl_track`),
|
||||
]);
|
||||
if (seriesResp.ok) {
|
||||
const d = await seriesResp.json();
|
||||
// Depth trace
|
||||
if (d.depth && d.depth.length) {
|
||||
depthTraces.push({
|
||||
x: d.depth.map(p => new Date(unixToMs(p.t))),
|
||||
y: d.depth.map(p => p.v),
|
||||
name: sessionId,
|
||||
type: 'scatter', mode: 'lines',
|
||||
line: { color: '#06d6a0', width: 1.5 },
|
||||
});
|
||||
}
|
||||
// AUV motors: m1..m8
|
||||
const motorKeys = Object.keys(d).filter(k => /^m\d+$/.test(k));
|
||||
motorKeys.forEach((k, i) => {
|
||||
const pts = d[k];
|
||||
if (!pts || !pts.length) return;
|
||||
pwmAuvTraces.push({
|
||||
x: pts.map(p => new Date(unixToMs(p.t))),
|
||||
y: pts.map(p => p.v),
|
||||
name: k,
|
||||
type: 'scatter', mode: 'lines',
|
||||
line: { color: COLORS[(i + 4) % COLORS.length], width: 1 },
|
||||
});
|
||||
});
|
||||
}
|
||||
if (usblResp.ok) {
|
||||
const d = await usblResp.json();
|
||||
const pts = (d.points||[]);
|
||||
if (pts.length) {
|
||||
// Build usblPoints — API fields: t(unix s), lat/lon(AUV), usv_lat/usv_lon, distance_m, azimuth_deg, snr
|
||||
usblPoints.push(...pts.map(p => ({
|
||||
t_ms: unixToMs(p.t),
|
||||
auv_lat: p.lat, auv_lon: p.lon,
|
||||
dist: p.distance_m, az: p.azimuth_deg, elev: p.elevation_deg || 0, snr: p.snr,
|
||||
})));
|
||||
// USBL distance trace
|
||||
usblDistTraces.push({
|
||||
x: pts.map(p => new Date(unixToMs(p.t))),
|
||||
y: pts.map(p => p.distance_m),
|
||||
name: sessionId,
|
||||
type: 'scatter', mode: 'lines',
|
||||
line: { color: '#a855f7', width: 1.5 },
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch(e) { console.warn('loadSubSession error', sessionId, e); }
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
document.getElementById('load-status').textContent = msg;
|
||||
if (mcapResp.status==='fulfilled' && mcapResp.value.ok) mcapSignals = await mcapResp.value.json();
|
||||
if (usvPwmResp.status==='fulfilled' && usvPwmResp.value.ok) usvPwm = await usvPwmResp.value.json();
|
||||
populateCharts();
|
||||
} catch(e) { console.warn('Graph data error:',e); }
|
||||
}
|
||||
|
||||
// == Datebar ==
|
||||
@@ -727,9 +555,10 @@ let availableDates = [];
|
||||
async function initDatebar() {
|
||||
const label = document.getElementById('mission-label');
|
||||
const picker = document.getElementById('date-picker');
|
||||
const dl = document.getElementById('available-dates');
|
||||
try {
|
||||
const resp = await fetch(`${API}/api/data-dates`);
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const resp = await fetch('http://192.168.0.83:8766/api/data-dates');
|
||||
if (!resp.ok) throw new Error('data-dates HTTP ' + resp.status);
|
||||
const data = await resp.json();
|
||||
availableDates = data.dates || [];
|
||||
if (!availableDates.length) {
|
||||
@@ -737,21 +566,25 @@ async function initDatebar() {
|
||||
label.className = 'no-data';
|
||||
return;
|
||||
}
|
||||
// Populate datalist
|
||||
dl.innerHTML = '';
|
||||
availableDates.forEach(d => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = d.date;
|
||||
dl.appendChild(opt);
|
||||
});
|
||||
// Min/max
|
||||
const dates = availableDates.map(d => d.date).sort();
|
||||
picker.min = dates[0];
|
||||
picker.max = dates[dates.length-1];
|
||||
picker.value = dates[dates.length-1];
|
||||
picker.max = dates[dates.length - 1];
|
||||
// Default: last date
|
||||
picker.value = dates[dates.length - 1];
|
||||
updateMissionLabel(picker.value);
|
||||
picker.addEventListener('change', () => {
|
||||
updateMissionLabel(picker.value);
|
||||
loadDate(picker.value);
|
||||
});
|
||||
loadDate(picker.value);
|
||||
picker.addEventListener('change', () => updateMissionLabel(picker.value));
|
||||
} catch(e) {
|
||||
label.textContent = 'API indisponible';
|
||||
label.className = 'no-data';
|
||||
setStatus('API 8766 inaccessible');
|
||||
console.warn(e);
|
||||
console.warn('datebar error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,16 +603,15 @@ function updateMissionLabel(date) {
|
||||
}
|
||||
|
||||
function datePickerToday() {
|
||||
const today = new Date().toISOString().slice(0,10);
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const picker = document.getElementById('date-picker');
|
||||
picker.value = today;
|
||||
updateMissionLabel(today);
|
||||
loadDate(today);
|
||||
}
|
||||
|
||||
// == Init ==
|
||||
initCharts();
|
||||
initDatebar();
|
||||
loadData();
|
||||
loadGraphData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user