From e94d49253ea99636aa7812bb4876b0fc09bd3be4 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Wed, 5 Aug 2026 12:03:52 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20v0.3.0=20=E2=80=94=20real=20ONVIF=20TC?= =?UTF-8?q?P=20probe=20in=20/api/scan,=20greenfield=20wizard=20with=20IP?= =?UTF-8?q?=20range?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/api.go b/api.go index c7f1b82..4bd4481 100644 --- a/api.go +++ b/api.go @@ -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) {