diff --git a/main.go b/main.go index 33d4f7e..7dd8609 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( "time" ) -const Version = "0.1.0" +const Version = "0.2.1" // ─── QC Thresholds (edit here) ─────────────────────────────────────────────── const ( @@ -36,6 +36,7 @@ type Config struct { Host string Port int Path string + URL string // full URL override (http or https), replaces host:port/path Interval int // seconds CSV string Web int @@ -93,6 +94,8 @@ func loadIni(path string, cfg *Config) { } case "mode": cfg.Mode = v + case "url": + cfg.URL = v } } } @@ -136,6 +139,8 @@ func parseFlags(cfg *Config) { } case "mode": cfg.Mode = val + case "url": + cfg.URL = val } } } @@ -191,8 +196,8 @@ type Telemetry struct { // Row enriched with PC timestamp type Row struct { - Timestamp string - T Telemetry + Timestamp string `json:"Timestamp"` + T Telemetry `json:"T"` } // ─── Ring buffer ───────────────────────────────────────────────────────────── @@ -368,6 +373,7 @@ type App struct { espHost string espPort int espPath string + espURL string // full URL override } func (a *App) poll() { @@ -376,8 +382,14 @@ func (a *App) poll() { defer ticker.Stop() for range ticker.C { a.mu.RLock() - url := fmt.Sprintf("http://%s:%d%s", a.espHost, a.espPort, a.espPath) + var pollURL string + if a.espURL != "" { + pollURL = a.espURL + } else { + pollURL = fmt.Sprintf("http://%s:%d%s", a.espHost, a.espPort, a.espPath) + } a.mu.RUnlock() + url := pollURL resp, err := client.Get(url) if err != nil { @@ -460,27 +472,37 @@ func (a *App) apiLatest(w http.ResponseWriter, r *http.Request) { } func (a *App) apiConfig(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") if r.Method == http.MethodGet { a.mu.RLock() type Out struct { - Host string `json:"host"` - Port int `json:"port"` - Path string `json:"path"` + Host string `json:"host"` + Port int `json:"port"` + Path string `json:"path"` + URL string `json:"url"` + Interval int `json:"interval"` } - json.NewEncoder(w).Encode(Out{Host: a.espHost, Port: a.espPort, Path: a.espPath}) + out := Out{Host: a.espHost, Port: a.espPort, Path: a.espPath, URL: a.espURL, Interval: a.cfg.Interval} a.mu.RUnlock() + json.NewEncoder(w).Encode(out) return } if r.Method == http.MethodPost { var in struct { - Host string `json:"host"` - Port int `json:"port"` - Path string `json:"path"` + Host string `json:"host"` + Port int `json:"port"` + Path string `json:"path"` + URL string `json:"url"` + Interval int `json:"interval"` } json.NewDecoder(r.Body).Decode(&in) a.mu.Lock() if in.Host != "" { a.espHost = in.Host + // switching to host mode: clear url override + if in.URL == "" { + a.espURL = "" + } } if in.Port != 0 { a.espPort = in.Port @@ -488,8 +510,14 @@ func (a *App) apiConfig(w http.ResponseWriter, r *http.Request) { if in.Path != "" { a.espPath = in.Path } + if in.URL != "" { + a.espURL = in.URL + } + if in.Interval > 0 { + a.cfg.Interval = in.Interval + } a.mu.Unlock() - log.Printf("[CFG] ESP32 → %s:%d%s", a.espHost, a.espPort, a.espPath) + log.Printf("[CFG] cible changée → %s:%d%s (url=%q)", a.espHost, a.espPort, a.espPath, a.espURL) w.WriteHeader(200) w.Write([]byte(`{"ok":true}`)) return @@ -539,6 +567,7 @@ footer{font-size:0.7rem;color:#2a4060;margin-top:8px} +
@@ -589,10 +618,11 @@ function cls(v,ok,warn){ } function setCard(id,text,cls_name){ const el=document.getElementById(id); + if(!el)return; el.textContent=text; el.className='value '+cls_name; } -function fmt(v,d=1){return v==null?'–':Number(v).toFixed(d);} +function fmt(v,d=1){return(v==null||v===undefined)?'–':Number(v).toFixed(d);} function drawSpark(id,data,color,minV,maxV){ const canvas=document.getElementById(id); @@ -616,11 +646,20 @@ function drawSpark(id,data,color,minV,maxV){ ctx.stroke(); } -function push(arr,v){arr.push(v);if(arr.length>SPARK_N)arr.shift();} +// Pré-chargement config au démarrage (BUG1 fix) +async function loadConfig(){ + try{ + const resp=await fetch('api/config'); + const cfg=await resp.json(); + if(cfg.host)document.getElementById('cfg-host').value=cfg.host; + if(cfg.port)document.getElementById('cfg-port').value=cfg.port; + if(cfg.path)document.getElementById('cfg-path').value=cfg.path; + }catch(e){} +} async function fetchAndUpdate(){ try{ - const resp=await fetch('/api/latest?n='+SPARK_N); + const resp=await fetch('api/latest?n='+SPARK_N); const data=await resp.json(); const linked=data.linked; const age=data.age_sec; @@ -637,51 +676,58 @@ async function fetchAndUpdate(){ } if(rows.length===0)return; + // JSON sérialisé par Go : Row.Timestamp="Timestamp", Row.T="T" + // Telemetry fields utilisent les json tags (minuscules) const r=rows[rows.length-1]; - const t=r.T; + const t=r.T||{}; - // GPS - const gv=t.GPS&&t.GPS.Valid===1; - setCard('c-gps-valid',gv?'FIX':'NO FIX',gv?'ok':'err'); - const sats=t.GPS?t.GPS.Sats:0; + // GPS — json tags: gps.valid, gps.sats, gps.lat, gps.lon, gps.hdg, gps.speed_kn, gps.status + const gps=t.gps||{}; + const gv=gps.valid===1; + setCard('c-gps-valid',gv?(gps.status||'FIX'):'NO FIX',gv?'ok':'err'); + const sats=gps.sats!=null?gps.sats:0; setCard('c-gps-sats',sats,sats>=THRESH_SATS_OK?'ok':sats>0?'warn':'err'); - setCard('c-lat',fmt(t.GPS&&t.GPS.Lat,7),'na'); - setCard('c-lon',fmt(t.GPS&&t.GPS.Lon,7),'na'); - setCard('c-hdg',fmt(t.GPS&&t.GPS.Hdg,1),'na'); - const spd=t.GPS?t.GPS.SpeedKn:0; - setCard('c-speed',fmt(spd,2),'na'); + setCard('c-lat',fmt(gps.lat,7),'na'); + setCard('c-lon',fmt(gps.lon,7),'na'); + setCard('c-hdg',fmt(gps.hdg,1),'na'); + setCard('c-speed',fmt(gps.speed_kn,2),'na'); - // RSSI - const rssi=t.RSSI||0; + // RSSI — json tag: rssi + const rssi=t.rssi!=null?t.rssi:0; setCard('c-rssi',rssi,cls(rssi,THRESH_RSSI_OK,THRESH_RSSI_WARN)); - // Power - const volt=t.Power?t.Power.Voltage:0; + // Power — json tags: power.voltage, power.current_mA + const pwr=t.power||{}; + const volt=pwr.voltage!=null?pwr.voltage:0; setCard('c-volt',fmt(volt,2),cls(volt,THRESH_BATT_OK,THRESH_BATT_WARN)); - setCard('c-amp',fmt(t.Power&&t.Power.CurrentMA,0),'na'); + setCard('c-amp',fmt(pwr.current_mA,0),'na'); - // Seaker - setCard('c-sk-ang',fmt(t.Seaker&&t.Seaker.Angle,1),'na'); - setCard('c-sk-dist',fmt(t.Seaker&&t.Seaker.Dist,1),'na'); - setCard('c-sk-freq',fmt(t.Seaker&&t.Seaker.RxFreq,1),'na'); + // Seaker — json tags: seaker.angle, seaker.dist, seaker.rx_freq + const sk=t.seaker||{}; + setCard('c-sk-ang',fmt(sk.angle,1),'na'); + setCard('c-sk-dist',fmt(sk.dist,1),'na'); + setCard('c-sk-freq',fmt(sk.rx_freq,1),'na'); - // Target / ROV - setCard('c-rov-lat',fmt(t.TargetF&&t.TargetF.Lat,7),'na'); - setCard('c-rov-lon',fmt(t.TargetF&&t.TargetF.Lon,7),'na'); - setCard('c-rov-r95',fmt(t.TargetF&&t.TargetF.R95M,2),'na'); + // Target / ROV — json tags: targetf.lat, targetf.lon, targetf.r95_m + const tf=t.targetf||{}; + setCard('c-rov-lat',fmt(tf.lat,7),'na'); + setCard('c-rov-lon',fmt(tf.lon,7),'na'); + setCard('c-rov-r95',fmt(tf.r95_m,2),'na'); - setCard('c-devid',t.DeviceID||'–','na'); - setCard('c-fw',(t.Firmware&&t.Firmware.Version)||'–','na'); - setCard('c-ip',t.IP||'–','na'); + // json tags: deviceId, firmware.version, ip + setCard('c-devid',t.deviceId||'–','na'); + const fw=t.firmware||{}; + setCard('c-fw',fw.version||'–','na'); + setCard('c-ip',t.ip||'–','na'); - // Sparklines — fill from all rows + // Sparklines — alimentées depuis rows (historique ring-buffer) sparkData.volt.length=0;sparkData.rssi.length=0;sparkData.dist.length=0;sparkData.speed.length=0; rows.forEach(row=>{ - const tt=row.T; - sparkData.volt.push(tt.Power?tt.Power.Voltage:0); - sparkData.rssi.push(tt.RSSI||0); - sparkData.dist.push(tt.Seaker?tt.Seaker.Dist:0); - sparkData.speed.push(tt.GPS?tt.GPS.SpeedKn:0); + const tt=row.T||{}; + sparkData.volt.push((tt.power||{}).voltage||0); + sparkData.rssi.push(tt.rssi||0); + sparkData.dist.push((tt.seaker||{}).dist||0); + sparkData.speed.push((tt.gps||{}).speed_kn||0); }); drawSpark('sp-volt',sparkData.volt,'#4ade80'); @@ -696,12 +742,27 @@ async function fetchAndUpdate(){ } async function saveConfig(){ - const host=document.getElementById('cfg-host').value; + const host=document.getElementById('cfg-host').value.trim(); const port=parseInt(document.getElementById('cfg-port').value)||0; - const path=document.getElementById('cfg-path').value; - await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host,port,path})}); + const path=document.getElementById('cfg-path').value.trim(); + try{ + const resp=await fetch('api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({host,port,path})}); + const ok=await resp.json(); + const st=document.getElementById('cfg-status'); + if(ok.ok){ + st.textContent='Appliqué';st.style.color='#4ade80'; + }else{ + st.textContent='Erreur';st.style.color='#f87171'; + } + st.style.display='inline'; + setTimeout(()=>{st.style.display='none';},3000); + }catch(e){ + const st=document.getElementById('cfg-status'); + st.textContent='Erreur: '+e.message;st.style.color='#f87171';st.style.display='inline'; + } } +loadConfig(); setInterval(fetchAndUpdate,1000); fetchAndUpdate(); @@ -734,6 +795,7 @@ func main() { espHost: cfg.Host, espPort: cfg.Port, espPath: cfg.Path, + espURL: cfg.URL, } // Start poller or receiver