chore: v0.3.0 — real ONVIF TCP probe in /api/scan, greenfield wizard with IP range

This commit is contained in:
Claus Lohmar 2026-08-05 12:03:52 +01:00
parent e8b795503f
commit e94d49253e

66
api.go
View file

@ -4,6 +4,7 @@ package main
import (
"encoding/json"
"net"
"net/http"
"os"
"strings"
@ -119,14 +120,50 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
// handleScan triggers ONVIF network discovery.
// POST /api/scan
// Body: {"method":"range","from":"192.168.1.200","to":"192.168.1.210","username":"admin","password":"..."}
func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Error: "method not allowed"})
return
}
// TODO: M3 — real ONVIF WS-Discovery.
// For now, return a stub response with the expected IP range.
var req ScanRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// Fall back to legacy stub scan if no body provided.
s.legacyScan(w)
return
}
if req.From == "" || req.To == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{Error: "from and to IP addresses required"})
return
}
fromIP := net.ParseIP(req.From)
toIP := net.ParseIP(req.To)
if fromIP == nil || toIP == nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{Error: "invalid IP address"})
return
}
results := make([]ONVIFDiscovery, 0)
// Inclusive scan from fromIP to toIP.
ip := make(net.IP, len(fromIP))
copy(ip, fromIP)
for {
d := probeDevice(ip.String(), req.Username, req.Password)
results = append(results, d)
if ip.Equal(toIP) {
break
}
ip = nextIP(ip)
}
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: results})
}
// legacyScan is the old stub handler for backward compatibility.
func (s *Server) legacyScan(w http.ResponseWriter) {
type DiscoveredCamera struct {
IP string `json:"ip"`
Manufacturer string `json:"manufacturer"`
@ -135,7 +172,6 @@ func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
RTSPSub string `json:"rtsp_sub"`
Found bool `json:"found"`
}
results := make([]DiscoveredCamera, 0)
for ip := 201; ip <= 209; ip++ {
if ip == 204 {
@ -143,18 +179,28 @@ func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
}
ipStr := "192.168.1." + itoa(ip)
results = append(results, DiscoveredCamera{
IP: ipStr,
Manufacturer: "Unknown",
Model: "ONVIF Camera",
RTSPMain: "rtsp://" + ipStr + ":554/Streaming/Channels/101",
RTSPSub: "rtsp://" + ipStr + ":554/Streaming/Channels/102",
Found: false, // requires real ONVIF probe in M3
IP: ipStr,
Found: false,
RTSPMain: "rtsp://" + ipStr + ":554/Streaming/Channels/101",
RTSPSub: "rtsp://" + ipStr + ":554/Streaming/Channels/102",
})
}
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: results})
}
// nextIP returns the next IP address in sequence.
func nextIP(ip net.IP) net.IP {
next := make(net.IP, len(ip))
copy(next, ip)
for i := len(next) - 1; i >= 0; i-- {
next[i]++
if next[i] != 0 {
break
}
}
return next
}
// handleStatus returns server health and runtime information.
// GET /api/status
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {