From bf55e7cebd6d9c844fe9497b7bac7752eb1309fe Mon Sep 17 00:00:00 2001 From: gferraro Date: Mon, 19 Feb 2024 10:48:12 +1300 Subject: [PATCH 1/6] remove track on preview --- static/js/camera.ts | 133 ++++++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 61 deletions(-) diff --git a/static/js/camera.ts b/static/js/camera.ts index 670090a..7f70364 100644 --- a/static/js/camera.ts +++ b/static/js/camera.ts @@ -32,7 +32,7 @@ export enum CameraConnectionState { } const UUID = new Date().getTime(); - +const showTracks = false; interface CameraStats { skippedFramesServer: number; skippedFramesClient: number; @@ -69,22 +69,22 @@ function restartCameraViewing() { } async function triggerTrap() { - document.getElementById("trigger-trap")!.innerText = 'Triggering trap'; + document.getElementById("trigger-trap")!.innerText = "Triggering trap"; document.getElementById("trigger-trap")!.setAttribute("disabled", "true"); console.log("triggering trap"); - fetch('/api/trigger-trap', { - method: 'PUT', - headers: { - 'Authorization': 'Basic YWRtaW46ZmVhdGhlcnM=' - }}) - - .then(response => console.log(response)) - .then(data => console.log(data)) - .catch(error => console.error(error)) + fetch("/api/trigger-trap", { + method: "PUT", + headers: { + Authorization: "Basic YWRtaW46ZmVhdGhlcnM=", + }, + }) + .then((response) => console.log(response)) + .then((data) => console.log(data)) + .catch((error) => console.error(error)); //TODO handle errors better and check that recording was made properly instead of just waiting.. - await new Promise(r => setTimeout(r, 3000)); + await new Promise((r) => setTimeout(r, 3000)); document.getElementById("trigger-trap")!.removeAttribute("disabled"); - document.getElementById("trigger-trap")!.innerText = 'Trigger trap'; + document.getElementById("trigger-trap")!.innerText = "Trigger trap"; } window.onload = function () { @@ -94,7 +94,8 @@ window.onload = function () { } document.getElementById("snapshot-restart")!.onclick = restartCameraViewing; document.getElementById("trigger-trap")!.onclick = triggerTrap; - document.getElementById("take-snapshot-recording")!.onclick = takeTestRecording; + document.getElementById("take-snapshot-recording")!.onclick = + takeTestRecording; cameraConnection = new CameraConnection( window.location.hostname, window.location.port, @@ -104,22 +105,28 @@ window.onload = function () { }; async function takeTestRecording() { - document.getElementById("take-snapshot-recording")!.innerText = 'Making a test recording'; - document.getElementById("take-snapshot-recording")!.setAttribute("disabled", "true"); + document.getElementById("take-snapshot-recording")!.innerText = + "Making a test recording"; + document + .getElementById("take-snapshot-recording")! + .setAttribute("disabled", "true"); console.log("making a test recording"); - fetch('/api/camera/snapshot-recording', { - method: 'PUT', - headers: { - 'Authorization': 'Basic YWRtaW46ZmVhdGhlcnM=' - }}) - - .then(response => console.log(response)) - .then(data => console.log(data)) - .catch(error => console.error(error)) + fetch("/api/camera/snapshot-recording", { + method: "PUT", + headers: { + Authorization: "Basic YWRtaW46ZmVhdGhlcnM=", + }, + }) + .then((response) => console.log(response)) + .then((data) => console.log(data)) + .catch((error) => console.error(error)); //TODO handle errors better and check that recording was made properly instead of just waiting.. - await new Promise(r => setTimeout(r, 3000)); - document.getElementById("take-snapshot-recording")!.removeAttribute("disabled"); - document.getElementById("take-snapshot-recording")!.innerText = 'Take test recording'; + await new Promise((r) => setTimeout(r, 3000)); + document + .getElementById("take-snapshot-recording")! + .removeAttribute("disabled"); + document.getElementById("take-snapshot-recording")!.innerText = + "Take test recording"; } function stopSnapshots(message: string) { @@ -197,13 +204,13 @@ async function processFrame(frame: Frame) { if (canvas == null) { return; } - if( canvas.width != frame.frameInfo.Camera.ResX){ - canvas.width = frame.frameInfo.Camera.ResX - trackCanvas.width = frame.frameInfo.Camera.ResX + if (canvas.width != frame.frameInfo.Camera.ResX) { + canvas.width = frame.frameInfo.Camera.ResX; + trackCanvas.width = frame.frameInfo.Camera.ResX; } - if(canvas.height != frame.frameInfo.Camera.ResY){ - canvas.height = frame.frameInfo.Camera.ResY - trackCanvas.height = frame.frameInfo.Camera.ResY + if (canvas.height != frame.frameInfo.Camera.ResY) { + canvas.height = frame.frameInfo.Camera.ResY; + trackCanvas.height = frame.frameInfo.Camera.ResY; } const context = canvas.getContext("2d") as CanvasRenderingContext2D; const imgData = context.getImageData( @@ -214,26 +221,26 @@ async function processFrame(frame: Frame) { ); // gp hack to see if ir camera, dbus from python makes dictionary have to be all int type let irCamera = frame.frameInfo.Camera.ResX >= 640; - if(irCamera){ + if (irCamera) { document.getElementById("trigger-trap")!.style.display = ""; - }else{ + } else { document.getElementById("trigger-trap")!.style.display = "none"; } - let max=0; - let min=0; - let range=0; - if (!irCamera){ + let max = 0; + let min = 0; + let range = 0; + if (!irCamera) { max = Math.max(...frame.frame); min = Math.min(...frame.frame); range = max - min; } let maxI = 0; for (let i = 0; i < frame.frame.length; i++) { - let pix = 0 - if(irCamera){ - pix = frame.frame[i] - }else{ - pix = Math.min(255, ((frame.frame[i] - min) / range) * 255.0); + let pix = 0; + if (irCamera) { + pix = frame.frame[i]; + } else { + pix = Math.min(255, ((frame.frame[i] - min) / range) * 255.0); } let index = i * 4; imgData.data[index] = pix; @@ -244,24 +251,28 @@ async function processFrame(frame: Frame) { } context.putImageData(imgData, 0, 0); - const trackContext = trackCanvas.getContext("2d") as CanvasRenderingContext2D; - trackContext.clearRect(0, 0, trackCanvas.width, trackCanvas.height); + if (showTracks) { + const trackContext = trackCanvas.getContext( + "2d" + ) as CanvasRenderingContext2D; + trackContext.clearRect(0, 0, trackCanvas.width, trackCanvas.height); - let index = 0; - if (frame.frameInfo.Tracks) { - for (const track of frame.frameInfo.Tracks) { - let what = null; - if (track.predictions && track.predictions.length > 0) { - what = track.predictions[0].label; + let index = 0; + if (frame.frameInfo.Tracks) { + for (const track of frame.frameInfo.Tracks) { + let what = null; + if (track.predictions && track.predictions.length > 0) { + what = track.predictions[0].label; + } + drawRectWithText( + trackContext, + frame.frameInfo.Camera, + track.positions[track.positions.length - 1], + what, + index + ); + index += 1; } - drawRectWithText( - trackContext, - frame.frameInfo.Camera, - track.positions[track.positions.length - 1], - what, - index - ); - index += 1; } } document.getElementById( From 056a2f4291fcaf92168ab6f80ed41ac410bd9b5c Mon Sep 17 00:00:00 2001 From: Patrick Baxter Date: Wed, 6 Mar 2024 12:02:40 +1300 Subject: [PATCH 2/6] add network endpoints for pi Adds several enpoints around wifi using wpa_cli and moves hotspot.go --- .gitignore | 2 + api/api.go | 703 +++++++++++++++++++++++++++- {cmd/managementd => api}/hotspot.go | 143 ++++-- cmd/managementd/main.go | 41 +- static/js/camera.ts | 46 +- test.sh | 88 ++++ 6 files changed, 931 insertions(+), 92 deletions(-) rename {cmd/managementd => api}/hotspot.go (62%) create mode 100644 test.sh diff --git a/.gitignore b/.gitignore index b2963f2..c7c8e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ api/types.js api/types.js.map static/js/camera.js static/js/camera.js.map +go.work +go.work.sum diff --git a/api/api.go b/api/api.go index 5b822e0..8f9469f 100644 --- a/api/api.go +++ b/api/api.go @@ -20,16 +20,19 @@ package api import ( "bufio" + "context" "encoding/json" "errors" "fmt" "io" "log" + "net" "net/http" "net/url" "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "time" @@ -54,24 +57,60 @@ const ( ) type ManagementAPI struct { - cptvDir string - config *goconfig.Config - appVersion string + config *goconfig.Config + router *mux.Router + hotspotTimer *time.Ticker + cptvDir string + appVersion string } -func NewAPI(config *goconfig.Config, appVersion string) (*ManagementAPI, error) { +func NewAPI(router *mux.Router, config *goconfig.Config, appVersion string) (*ManagementAPI, error) { thermalRecorder := goconfig.DefaultThermalRecorder() if err := config.Unmarshal(goconfig.ThermalRecorderKey, &thermalRecorder); err != nil { return nil, err } return &ManagementAPI{ - cptvDir: thermalRecorder.OutputDir, config: config, + router: router, + cptvDir: thermalRecorder.OutputDir, appVersion: appVersion, }, nil } +func (s *ManagementAPI) StartHotspotTimer() { + s.hotspotTimer = time.NewTicker(5 * time.Minute) + go func() { + s.router.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.hotspotTimer.Reset(5 * time.Minute) + next.ServeHTTP(w, r) + }) + }) + <-s.hotspotTimer.C + if err := stopHotspot(); err != nil { + log.Println("Failed to stop hotspot:", err) + } + }() +} + +func (s *ManagementAPI) StopHotspotTimer() { + if s.hotspotTimer != nil { + s.hotspotTimer.Stop() + } +} + +func (server *ManagementAPI) ManageHotspot() { + if err := initilseHotspot(); err != nil { + log.Println("Failed to initialise hotspot:", err) + if err := stopHotspot(); err != nil { + log.Println("Failed to stop hotspot:", err) + } + } else { + server.StartHotspotTimer() + } +} + func (api *ManagementAPI) GetVersion(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "apiVersion": apiVersion, @@ -81,6 +120,15 @@ func (api *ManagementAPI) GetVersion(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(data) } +func readFile(file string) string { + // The /etc/salt/minion_id file contains the ID. + out, err := os.ReadFile(file) + if err != nil { + return "" + } + return string(out) +} + // GetDeviceInfo returns information about this device func (api *ManagementAPI) GetDeviceInfo(w http.ResponseWriter, r *http.Request) { var device goconfig.Device @@ -96,6 +144,7 @@ func (api *ManagementAPI) GetDeviceInfo(w http.ResponseWriter, r *http.Request) Groupname string `json:"groupname"` Devicename string `json:"devicename"` DeviceID int `json:"deviceID"` + SaltID string `json:"saltID"` Type string `json:"type"` } info := deviceInfo{ @@ -103,6 +152,7 @@ func (api *ManagementAPI) GetDeviceInfo(w http.ResponseWriter, r *http.Request) Groupname: device.Group, Devicename: device.Name, DeviceID: device.ID, + SaltID: strings.TrimSpace(readFile("/etc/salt/minion_id")), Type: getDeviceType(), } w.WriteHeader(http.StatusOK) @@ -291,7 +341,7 @@ func (api *ManagementAPI) GetConfig(w http.ResponseWriter, r *http.Request) { goconfig.ModemdKey: goconfig.DefaultModemd(), goconfig.PortsKey: goconfig.DefaultPorts(), goconfig.TestHostsKey: goconfig.DefaultTestHosts(), - goconfig.ThermalMotionKey: goconfig.DefaultThermalMotion(lepton3.Model35), //TODO don't assume that model 3.5 is being used + goconfig.ThermalMotionKey: goconfig.DefaultThermalMotion(lepton3.Model35), // TODO don't assume that model 3.5 is being used goconfig.ThermalRecorderKey: goconfig.DefaultThermalRecorder(), goconfig.ThermalThrottlerKey: goconfig.DefaultThermalThrottler(), goconfig.WindowsKey: goconfig.DefaultWindows(), @@ -616,6 +666,539 @@ func (api *ManagementAPI) GetServiceStatus(w http.ResponseWriter, r *http.Reques json.NewEncoder(w).Encode(serviceStatus) } +// Network API +func (api *ManagementAPI) GetNetworkInterfaces(w http.ResponseWriter, r *http.Request) { + interfaces, err := net.Interfaces() + if err != nil { + log.Printf("Error getting network interfaces: %v", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, "failed to get network interfaces\n") + return + } + + var result []map[string]interface{} + for _, iface := range interfaces { + addrs, err := iface.Addrs() + if err != nil { + log.Printf("Error getting addresses for interface %s: %v", iface.Name, err) + continue // Skip this interface + } + + var addrStrings []string + for _, addr := range addrs { + addrStrings = append(addrStrings, addr.String()) + } + + result = append(result, map[string]interface{}{ + "name": iface.Name, + "addresses": addrStrings, + "mtu": iface.MTU, + "macAddress": iface.HardwareAddr.String(), + "flags": iface.Flags.String(), + }) + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(result) +} + +func getCurrentWifiNetwork() (string, error) { + cmd := exec.Command("iwgetid", "wlan0", "-r") + output, err := cmd.Output() + if err != nil { + if len(output) == 0 { + return "", nil + } + return "", err + } + + return strings.TrimSpace(string(output)), nil +} + +// CheckInternetConnection checks if a specified network interface has internet access +func CheckInternetConnection(interfaceName string) bool { + iface, err := net.InterfaceByName(interfaceName) + if err != nil { + fmt.Println("Error getting interface details:", err) + return false + } + args := []string{"-I", iface.Name, "-c", "3", "-n", "-W", "15", "1.1.1.1"} + + if err := exec.Command("ping", args...).Run(); err != nil { + fmt.Println("Error pinging:", err) + return false + } + return true +} + +func (api *ManagementAPI) CheckModemInternetConnection(w http.ResponseWriter, r *http.Request) { + // Check if connected to modem + log.Println("Checking modem connection") + connected := CheckInternetConnection("eth1") + log.Printf("Modem connection: %v", connected) + + // Send the current network as a JSON response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]bool{"connected": connected}) +} + +// Check Wifi Connection +func (api *ManagementAPI) CheckWifiInternetConnection(w http.ResponseWriter, r *http.Request) { + // Check if connected to Wi-Fi + log.Println("Checking Wi-Fi connection") + connected := CheckInternetConnection("wlan0") + log.Printf("Wi-Fi connection: %v", connected) + + // Send the current network as a JSON response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]bool{"connected": connected}) +} + +func (api *ManagementAPI) GetCurrentWifiNetwork(w http.ResponseWriter, r *http.Request) { + // Get the current Wi-Fi network + log.Println("Getting current Wi-Fi network") + currentNetwork, err := getCurrentWifiNetwork() + if err != nil { + log.Printf("Error getting current Wi-Fi network: %v", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, "failed to get current Wi-Fi network\n") + return + } + log.Printf("Current Wi-Fi network: %s", currentNetwork) + + // Send the current network as a JSON response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"SSID": currentNetwork}) +} + +func (api *ManagementAPI) ConnectToWifi(w http.ResponseWriter, r *http.Request) { + var wifiDetails struct { + SSID string `json:"ssid"` + Password string `json:"password"` + } + + // Decode the JSON body + if err := json.NewDecoder(r.Body).Decode(&wifiDetails); err != nil { + log.Printf("Error decoding request: %v", err) + http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) + return + } + + log.Printf("Attempting to connect to Wi-Fi SSID: %s", wifiDetails.SSID) + + if savedId, err := checkIsSavedNetwork(wifiDetails.SSID); err != nil { + log.Printf("Error checking if Wi-Fi network is saved: %v", err) + if err := connectToWifi(wifiDetails.SSID, wifiDetails.Password); err != nil { + log.Printf("Error connecting to Wi-Fi: %v", err) + http.Error(w, "Failed to connect to Wi-Fi: "+err.Error(), http.StatusInternalServerError) + go api.ManageHotspot() + return + } + } else { + log.Printf("Wi-Fi network is saved: %s", wifiDetails.SSID) + if err := exec.Command("wpa_cli", "-i", "wlan0", "select_network", savedId).Run(); err != nil { + log.Printf("Error enabling Wi-Fi network: %v", err) + http.Error(w, "Failed to connect to Wi-Fi: "+err.Error(), http.StatusInternalServerError) + go api.ManageHotspot() + return + } + if err := exec.Command("wpa_cli", "-i", "wlan0", "reassociate").Run(); err != nil { + log.Printf("Error reassociating Wi-Fi network: %v", err) + http.Error(w, "Failed to connect to Wi-Fi: "+err.Error(), http.StatusInternalServerError) + go api.ManageHotspot() + return + } + if err := EnableAllSavedNetworks(); err != nil { + log.Printf("Error enabling all saved networks: %v", err) + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if connected := streamWifiState(ctx, wifiDetails.SSID); !connected { + if err := removeNetworkFromWPA(wifiDetails.SSID); err != nil { + log.Printf("Error removing Wi-Fi network: %v", err) + } + if err := EnableAllSavedNetworks(); err != nil { + log.Printf("Error enabling all saved networks: %v", err) + } + // Reconfigure the Wi-Fi + if err := exec.Command("wpa_cli", "-i", "wlan0", "reconfigure").Run(); err != nil { + log.Printf("Error reconfiguring Wi-Fi: %v", err) + } + + log.Printf("Failed to connect to Wi-Fi SSID: %s", wifiDetails.SSID) + http.Error(w, "Failed to connect to Wi-Fi: timed out", http.StatusInternalServerError) + go api.ManageHotspot() + return + } else { + log.Printf("Successfully connected to Wi-Fi SSID: %s", wifiDetails.SSID) + if err := exec.Command("systemctl", "restart", "dhcpcd").Run(); err != nil { + log.Printf("Error restarting DHCP client: %v", err) + } + log.Println("Connected to Wi-Fi successfully") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Connected to Wi-Fi successfully")) + } +} + +func EnableAllSavedNetworks() error { + ssidIds, err := getSSIDIds() + if err != nil { + return err + } + for _, id := range ssidIds { + if err := exec.Command("wpa_cli", "-i", "wlan0", "enable_network", id).Run(); err != nil { + return err + } + } + return nil +} + +func streamWifiState(ctx context.Context, ssid string) bool { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Check the Wi-Fi connection status + statusCmd := exec.Command("wpa_cli", "-i", "wlan0", "status") + statusOut, err := statusCmd.Output() + if err != nil { + log.Printf("Error checking Wi-Fi status: %v", err) + continue + } + + // Parse the status output + status := strings.TrimSpace(string(statusOut)) + if strings.Contains(status, "ssid="+ssid) && strings.Contains(status, "wpa_state=COMPLETED") { + // Connected to the specified SSID + return true + } + case <-ctx.Done(): + // Timeout + return false + } + } +} + +func saveConfig() error { + // Enable all saved networks + ssidIds, err := getSSIDIds() + if err != nil { + return err + } + + for _, id := range ssidIds { + if err := exec.Command("wpa_cli", "-i", "wlan0", "enable_network", id).Run(); err != nil { + return err + } + } + + // Save the config + if err := exec.Command("wpa_cli", "-i", "wlan0", "save_config").Run(); err != nil { + return err + } + + return nil +} + +func checkIsSavedNetwork(ssid string) (string, error) { + ssidIds, err := getSSIDIds() + if err != nil { + return "", err + } + for ssidId, ssidName := range ssidIds { + if ssidName == ssid { + return ssidId, nil + } + } + return "", errors.New("ssid not found") +} + +// connectToWifi connects to a Wi-Fi network using wpa_cli +func connectToWifi(ssid string, passkey string) error { + output, err := exec.Command("wpa_cli", "-i", "wlan0", "add_network").Output() + if err != nil { + log.Printf("Error adding Wi-Fi network to config: %v", err) + return err + } + + id := strings.TrimSpace(string(output)) + + if err := exec.Command("wpa_cli", "-i", "wlan0", "set_network", id, "ssid", "\""+ssid+"\"").Run(); err != nil { + log.Printf("Error adding Wi-Fi network to config: %v", err) + return err + } + + if err := exec.Command("wpa_cli", "-i", "wlan0", "set_network", id, "psk", "\""+passkey+"\"").Run(); err != nil { + log.Printf("Error adding Wi-Fi network to config: %v", err) + return err + } + + // Reload the wpa_supplicant config + if err := exec.Command("wpa_cli", "-i", "wlan0", "save_config").Run(); err != nil { + log.Printf("Error reconfiguring Wi-Fi: %v", err) + // Remove the network from the config + if err := removeNetworkFromWPA(ssid); err != nil { + log.Printf("Error removing Wi-Fi network: %v", err) + return err + } + return err + } + + if err := exec.Command("wpa_cli", "-i", "wlan0", "select_network", id).Run(); err != nil { + log.Printf("Error enabling Wi-Fi network: %v", err) + return err + } + + // Reassociate the Wi-Fi + if err := exec.Command("wpa_cli", "-i", "wlan0", "reassociate").Run(); err != nil { + log.Printf("Error reassociating Wi-Fi: %v", err) + return err + } + + if err := EnableAllSavedNetworks(); err != nil { + log.Printf("Error enabling all saved networks: %v", err) + } + + return nil +} + +func getSSIDIds() (map[string]string, error) { + // Get the list of Wi-Fi networks + cmd := exec.Command("wpa_cli", "-i", "wlan0", "list_networks") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + // Parse the output + ssidIds := make(map[string]string) + lines := strings.Split(string(output), "\n") + for _, line := range lines[1:] { + if line == "" { + continue + } + fields := strings.Fields(line) + ssidIds[fields[1]] = fields[0] + } + + return ssidIds, nil +} + +func removeNetworkFromWPA(ssid string) error { + if ssid == "" || ssid == "bushnet" || ssid == "Bushnet" { + return nil + } + ssidIds, err := getSSIDIds() + if err != nil { + return err + } + + id, ok := ssidIds[ssid] + if !ok { + return nil // Network not found + } + + // Remove the network from the config + if err := exec.Command("wpa_cli", "-i", "wlan0", "remove_network", id).Run(); err != nil { + return err + } + + // Save the config + if err := saveConfig(); err != nil { + return err + } + + return nil +} + +func (api *ManagementAPI) GetWifiNetworks(w http.ResponseWriter, r *http.Request) { + // Execute the command to scan for Wi-Fi networks + log.Println("Scanning for Wi-Fi networks") + var output []byte + var err error + maxRetries := 3 + retryInterval := time.Second * 2 + + for i := 0; i < maxRetries; i++ { + cmd := exec.Command("iwlist", "wlan0", "scan") + output, err = cmd.Output() + if err == nil { + break + } + log.Printf("Error scanning for Wi-Fi networks: %v", err) + time.Sleep(retryInterval) + } + + if err != nil { + log.Printf("Failed to scan for Wi-Fi networks after %d retries", maxRetries) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, "failed to scan for Wi-Fi networks\n") + return + } + + // Parse the output to extract network information + networks := parseWiFiScanOutput(string(output)) + log.Printf("Found %d Wi-Fi networks", len(networks)) + // Send the list of networks as a JSON response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(networks) +} + +func parseWiFiScanOutput(output string) []map[string]string { + var networks []map[string]string + var currentNetwork map[string]string + + lines := strings.Split(output, "\n") + for _, line := range lines { + if strings.Contains(line, "Cell") { + if currentNetwork != nil { + networks = append(networks, currentNetwork) + } + currentNetwork = make(map[string]string) + } else if strings.Contains(line, "ESSID:") { + ssid := strings.Split(line, "\"")[1] + currentNetwork["SSID"] = ssid + } else if strings.Contains(line, "Quality=") { + quality := regexp.MustCompile("Quality=([0-9]+/[0-9]+)").FindStringSubmatch(line)[1] + signalLevel := regexp.MustCompile("Signal level=(-?[0-9]+ dBm)").FindStringSubmatch(line)[1] + currentNetwork["Quality"] = quality + currentNetwork["Signal Level"] = signalLevel + } else if strings.Contains(line, "Encryption key:on") { + currentNetwork["Security"] = "On" + } else if strings.Contains(line, "IE: IEEE 802.11i/WPA2") { + currentNetwork["Security"] = "WPA2" + } else if strings.Contains(line, "IE: WPA Version 1") { + currentNetwork["Security"] = "WPA" + } else if strings.Contains(line, "IE: Unknown") && currentNetwork["Security"] == "" { + // Default to 'Unknown' if no other security information is found + currentNetwork["Security"] = "Unknown" + } + } + + // Append the last network if not empty + if currentNetwork != nil { + networks = append(networks, currentNetwork) + } + + return networks +} + +func (api *ManagementAPI) DisconnectFromWifi(w http.ResponseWriter, r *http.Request) { + // Respond to client before actually disconnecting + log.Printf("Will disconnect from Wi-Fi network") + w.WriteHeader(http.StatusOK) + io.WriteString(w, "will disconnect from Wi-Fi network shortly\n") + + go func() { + if err := exec.Command("wpa_cli", "-i", "wlan0", "disconnect").Run(); err != nil { + log.Printf("Error disconnecting from Wi-Fi network: %v", err) + // Handle the error + } + + if err := RestartDHCP(); err != nil { + log.Printf("Error restarting DHCP client: %v", err) + } + // if no current network, then restart hotspot + currentSSID, err := checkIsConnectedToNetwork() + if err != nil { + log.Printf("Error getting current Wi-Fi network: %v", err) + } + if currentSSID == "" { + log.Printf("No current Wi-Fi network, restarting hotspot") + go api.ManageHotspot() + } + }() +} + +func (api *ManagementAPI) ForgetWifiNetwork(w http.ResponseWriter, r *http.Request) { + // Parse request for SSID + var wifiDetails struct { + SSID string `json:"ssid"` + } + + currentSSID, err := checkIsConnectedToNetwork() + if err != nil { + log.Printf("Error getting current Wi-Fi network: %v", err) + } + + // Decode the JSON body + if err := json.NewDecoder(r.Body).Decode(&wifiDetails); err != nil { + log.Printf("Error decoding request: %v", err) + http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) + return + } + if currentSSID == wifiDetails.SSID { + log.Printf("Will forget Wi-Fi network: %s", wifiDetails.SSID) + w.WriteHeader(http.StatusOK) + io.WriteString(w, "will forget Wi-Fi network shortly\n") + // Forget the network + go func() { + if err := removeNetworkFromWPA(wifiDetails.SSID); err != nil { + log.Printf("Error removing Wi-Fi network: %v", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, "failed to remove Wi-Fi network\n") + } + if err := RestartDHCP(); err != nil { + log.Printf("Error restarting DHCP client: %v", err) + } + // if no current network, then restart hotspot + currentSSID, err := checkIsConnectedToNetwork() + if err != nil { + log.Printf("Error getting current Wi-Fi network: %v", err) + } + if currentSSID == "" { + log.Printf("No current Wi-Fi network, restarting hotspot") + go api.ManageHotspot() + } + }() + } else { + // Remove the network using wpa_cli + // Get the list of Wi-Fi networks + ssids, err := getSSIDIds() + if err != nil { + log.Printf("Error getting Wi-Fi networks: %v", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, "failed to get Wi-Fi networks\n") + return + } + + // Find the network ID + id, ok := ssids[wifiDetails.SSID] + if !ok { + log.Printf("Wi-Fi network not found: %s", wifiDetails.SSID) + w.WriteHeader(http.StatusNotFound) + io.WriteString(w, "Wi-Fi network not found\n") + return + } + + // Remove the network using wpa_cli + cmd := exec.Command("wpa_cli", "-i", "wlan0", "remove_network", id) + if err := cmd.Run(); err != nil { + log.Printf("Error removing Wi-Fi network: %v", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, "failed to remove Wi-Fi network\n") + return + } + + // Save the changes + if err := saveConfig(); err != nil { + log.Printf("Error reloading Wi-Fi config: %v", err) + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, "failed to reload Wi-Fi config\n") + return + } + + log.Printf("Successfully removed Wi-Fi network: %s", wifiDetails.SSID) + w.WriteHeader(http.StatusOK) + io.WriteString(w, "successfully removed Wi-Fi network\n") + } +} + func (api *ManagementAPI) GetModem(w http.ResponseWriter, r *http.Request) { // Send dbus call to modem service to get all modem statuses conn, err := dbus.SystemBus() @@ -757,3 +1340,111 @@ func getServiceLogs(service string, lines int) ([]string, error) { } return logLines, nil } + +func (api *ManagementAPI) SaveWifiNetwork(w http.ResponseWriter, r *http.Request) { + var wifiDetails struct { + SSID string `json:"ssid"` + Password string `json:"password"` + } + + // Decode the JSON body + if err := json.NewDecoder(r.Body).Decode(&wifiDetails); err != nil { + log.Printf("Error decoding request: %v", err) + http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + io.WriteString(w, "will save Wi-Fi network shortly\n") + + go func() { + // Save the Wi-Fi network using wpa_cli + if err := saveWifiNetwork(wifiDetails.SSID, wifiDetails.Password); err != nil { + log.Printf("Error saving Wi-Fi network: %v", err) + http.Error(w, "Failed to save Wi-Fi network: "+err.Error(), http.StatusInternalServerError) + return + } + }() +} + +func saveWifiNetwork(ssid string, passkey string) error { + // add using wpa_cli + if output, err := exec.Command("wpa_cli", "-i", "wlan0", "add_network").Output(); err != nil { + log.Printf("Error adding Wi-Fi network: %v, %s", err, output) + return err + } + if output, err := exec.Command("wpa_cli", "-i", "wlan0", "set_network", "0", "ssid", "\""+ssid+"\"").Output(); err != nil { + log.Printf("Error setting Wi-Fi network SSID: %v, %s", err, output) + return err + } + if output, err := exec.Command("wpa_cli", "-i", "wlan0", "set_network", "0", "psk", "\""+passkey+"\"").Output(); err != nil { + log.Printf("Error setting Wi-Fi network PSK: %v, %s", err, output) + return err + } + + // Save the config + if err := saveConfig(); err != nil { + log.Printf("Error saving Wi-Fi network: %v", err) + return err + } + + return nil +} + +func parseSavedWiFiNetworks(output string) []string { + var networks []string + lines := strings.Split(output, "\n") + for _, line := range lines[1:] { + if line == "" { + continue + } + fields := strings.Fields(line) + networks = append(networks, fields[1]) + } + return networks +} + +// Return array of saved wifi networks +func getSavedWifiNetworks() ([]string, error) { + // Get the list of Wi-Fi networks + cmd := exec.Command("wpa_cli", "-i", "wlan0", "list_networks") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + // Parse the output to extract network information + networks := parseSavedWiFiNetworks(string(output)) + return networks, nil +} + +func (api *ManagementAPI) GetSavedWifiNetworks(w http.ResponseWriter, r *http.Request) { + networks, err := getSavedWifiNetworks() + if err != nil { + serverError(&w, err) + return + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(networks) +} + +func (api *ManagementAPI) UploadLogs(w http.ResponseWriter, r *http.Request) { + if err := exec.Command("cp", "/var/log/syslog", "/tmp/syslog").Run(); err != nil { + log.Printf("Error copying syslog: %v", err) + serverError(&w, err) + return + } + + if err := exec.Command("gzip", "/tmp/syslog", "-f").Run(); err != nil { + log.Printf("Error compressing syslog: %v", err) + serverError(&w, err) + return + } + + if err := exec.Command("salt-call", "cp.push", "/tmp/syslog.gz").Run(); err != nil { + log.Printf("Error pushing syslog with salt: %v", err) + serverError(&w, err) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/cmd/managementd/hotspot.go b/api/hotspot.go similarity index 62% rename from cmd/managementd/hotspot.go rename to api/hotspot.go index a8b748d..0c70d40 100644 --- a/cmd/managementd/hotspot.go +++ b/api/hotspot.go @@ -1,9 +1,10 @@ -package main +package api import ( "bufio" "fmt" "log" + "net" "os" "os/exec" "strings" @@ -27,11 +28,36 @@ func createAPConfig(name string) error { "wpa_pairwise=TKIP", "rsn_pairwise=CCMP", } - return creatConfigFile(file_name, config_lines) + return createConfigFile(file_name, config_lines) } -func startAccessPoint(name string) error { - return exec.Command("systemctl", "restart", "hostapd").Start() +func isInterfaceUp(interfaceName string) bool { + iface, err := net.InterfaceByName(interfaceName) + if err != nil { + log.Printf("Error getting interface %s: %v", interfaceName, err) + return false + } + return iface.Flags&net.FlagUp == net.FlagUp +} + +func waitForInterface(interfaceName string, timeout time.Duration) error { + end := time.Now().Add(timeout) + for time.Now().Before(end) { + if isInterfaceUp(interfaceName) { + return nil + } + time.Sleep(time.Second) + } + return fmt.Errorf("interface %s did not come up within the specified timeout", interfaceName) +} + +func startAccessPoint(_ string) error { + err := exec.Command("systemctl", "restart", "hostapd").Run() + if err != nil { + return err + } + // Wait for wlan0 to be up before proceeding + return waitForInterface("wlan0", 30*time.Second) } func stopAccessPoint() error { @@ -44,6 +70,7 @@ var dhcp_config_default = []string{ "hostname", "clientid", "persistent", + "noipv6", "option rapid_commit", "option domain_name_servers, domain_name, domain_search, host_name", "option classless_static_routes", @@ -59,7 +86,6 @@ var dhcp_config_default = []string{ var dhcp_config_lines = []string{ "interface wlan0", "static ip_address=" + router_ip + "/24", - "nohook wpa_supplicant", } func createDHCPConfig() (bool, error) { @@ -110,10 +136,9 @@ func startDHCP() error { return exec.Command("systemctl", "restart", "dhcpcd").Run() } return exec.Command("systemctl", "start", "dhcpcd").Run() - } -func restartDHCP() error { +func RestartDHCP() error { // Only restart if config has changed configModified, err := writeLines("/etc/dhcpcd.conf", dhcp_config_default) if err != nil { @@ -145,17 +170,17 @@ func checkIsConnectedToNetwork() (string, error) { func checkIsConnectedToNetworkWithRetries() (string, error) { var err error var ssid string - for i := 0; i < 10; i++ { + for i := 0; i < 3; i++ { ssid, err = checkIsConnectedToNetwork() if ssid != "" { return ssid, nil } - time.Sleep(time.Second) + time.Sleep(time.Second * 2) } return ssid, err } -func createDNSConfig(router_ip string, ip_range string) error { +func createDNSConfig(ip_range string) error { // DNSMASQ config file_name := "/etc/dnsmasq.conf" config_lines := []string{ @@ -163,8 +188,7 @@ func createDNSConfig(router_ip string, ip_range string) error { "dhcp-range=" + ip_range + ",12h", "domain=wlan", } - return creatConfigFile(file_name, config_lines) - + return createConfigFile(file_name, config_lines) } func startDNS() error { @@ -172,10 +196,11 @@ func startDNS() error { } func stopDNS() error { + println("Stopping DNS") return exec.Command("systemctl", "stop", "dnsmasq").Start() } -func creatConfigFile(name string, config []string) error { +func createConfigFile(name string, config []string) error { file, err := os.Create(name) if err != nil { return err @@ -196,43 +221,81 @@ func creatConfigFile(name string, config []string) error { return nil } +func enableBushnet() error { + // Wpa_cli + ssids, err := getSSIDIds() + if err != nil { + return err + } + if len(ssids) == 0 { + return fmt.Errorf("no networks found") + } + // Enable bushnet using wpa_cli and it's id + // Get id of bushnet and Bushnet + if bushnetId, ok := ssids["bushnet"]; ok { + if err := exec.Command("wpa_cli", "enable_network", bushnetId).Run(); err != nil { + return err + } + } + + if BushnetId, ok := ssids["Bushnet"]; ok { + if err := exec.Command("wpa_cli", "enable_network", BushnetId).Run(); err != nil { + return err + } + } + + return nil +} + // Setup Hotspot and stop after 5 minutes using a new goroutine func initilseHotspot() error { ssid := "bushnet" - router_ip := "192.168.4.1" log.Printf("Setting DHCP to default...") - if err := restartDHCP(); err != nil { + if err := EnableAllSavedNetworks(); err != nil { + log.Printf("Error enabling all saved networks: %s", err) + } + // Ensure bushnet and Bushnet are enabled + if err := enableBushnet(); err != nil { + log.Printf("Error enabling bushnet: %s", err) + } + + if err := RestartDHCP(); err != nil { log.Printf("Error restarting dhcpcd: %s", err) } // Check if already connected to a network + if val, err := exec.Command("iwgetid", "wlan0", "-r").Output(); err != nil { + log.Printf("Error checking if connected to network: %s", err) + } else { + log.Printf("Wlan0 is connected to: %s", val) + } // If not connected to a network, start hotspot log.Printf("Checking if connected to network...") - if network, err := checkIsConnectedToNetworkWithRetries(); err != nil { - log.Printf("Starting Hotspot...") - log.Printf("Creating Configs...") - if err := createAPConfig(ssid); err != nil { - return err - } - if err := createDNSConfig(router_ip, "192.168.4.2,192.168.4.20"); err != nil { - return err - } - - log.Printf("Starting DHCP...") - if err := startDHCP(); err != nil { - return err - } - log.Printf("Starting DNS...") - if err := startDNS(); err != nil { - return err - } - log.Printf("Starting Access Point...") - if err := startAccessPoint(ssid); err != nil { - return err - } - return nil - } else { + if network, err := checkIsConnectedToNetworkWithRetries(); err == nil { + // Check if the hotspot is already running return fmt.Errorf("already connected to a network: %s", network) } + log.Printf("Starting Hotspot...") + log.Printf("Creating Configs...") + if err := createAPConfig(ssid); err != nil { + return err + } + if err := createDNSConfig("192.168.4.2,192.168.4.20"); err != nil { + return err + } + + log.Printf("Starting Access Point...") + if err := startAccessPoint(ssid); err != nil { + return err + } + log.Printf("Starting DNS...") + if err := startDNS(); err != nil { + return err + } + log.Printf("Starting DHCP...") + if err := startDHCP(); err != nil { + return err + } + return nil } func stopHotspot() error { @@ -243,7 +306,7 @@ func stopHotspot() error { if err := stopDNS(); err != nil { return err } - if err := restartDHCP(); err != nil { + if err := RestartDHCP(); err != nil { return err } return nil diff --git a/cmd/managementd/main.go b/cmd/managementd/main.go index 45d5254..4cf8ada 100644 --- a/cmd/managementd/main.go +++ b/cmd/managementd/main.go @@ -93,13 +93,12 @@ func main() { router.HandleFunc("/modem", managementinterface.Modem).Methods("GET") // API - apiObj, err := api.NewAPI(config.config, version) - + apiRouter := router.PathPrefix("/api").Subrouter() + apiObj, err := api.NewAPI(apiRouter, config.config, version) if err != nil { log.Fatal(err) return } - apiRouter := router.PathPrefix("/api").Subrouter() apiRouter.HandleFunc("/device-info", apiObj.GetDeviceInfo).Methods("GET") apiRouter.HandleFunc("/recordings", apiObj.GetRecordings).Methods("GET") apiRouter.HandleFunc("/recording/{id}", apiObj.GetRecording).Methods("GET") @@ -133,34 +132,23 @@ func main() { apiRouter.HandleFunc("/service", apiObj.GetServiceStatus).Methods("GET") apiRouter.HandleFunc("/service-restart", apiObj.RestartService).Methods("POST") apiRouter.HandleFunc("/modem", apiObj.GetModem).Methods("GET") + apiRouter.HandleFunc("/network/interfaces", apiObj.GetNetworkInterfaces).Methods("GET") + apiRouter.HandleFunc("/network/wifi", apiObj.GetWifiNetworks).Methods("GET") + apiRouter.HandleFunc("/network/wifi", apiObj.ConnectToWifi).Methods("POST") + apiRouter.HandleFunc("/network/wifi/save", apiObj.SaveWifiNetwork).Methods("POST") + apiRouter.HandleFunc("/network/wifi/saved", apiObj.GetSavedWifiNetworks).Methods("GET") + apiRouter.HandleFunc("/network/wifi/forget", apiObj.ForgetWifiNetwork).Methods("DELETE") + apiRouter.HandleFunc("/network/wifi/current", apiObj.GetCurrentWifiNetwork).Methods("GET") + apiRouter.HandleFunc("/network/wifi/current", apiObj.DisconnectFromWifi).Methods("DELETE") + apiRouter.HandleFunc("/wifi-check", apiObj.CheckWifiInternetConnection).Methods("GET") + apiRouter.HandleFunc("/modem-check", apiObj.CheckModemInternetConnection).Methods("GET") + apiRouter.HandleFunc("/upload-logs", apiObj.UploadLogs).Methods("PUT") apiRouter.Use(basicAuth) - - go func() { - if err := initilseHotspot(); err != nil { - if err := stopHotspot(); err != nil { - log.Println("Failed to stop hotspot:", err) - } - log.Println("Failed to initialise hotspot:", err) - } else { - t := time.NewTimer(5 * time.Minute) - apiRouter.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Reset(5 * time.Minute) - next.ServeHTTP(w, r) - }) - }) - - <-t.C - if err := stopHotspot(); err != nil { - log.Println("Failed to stop hotspot:", err) - } - } - }() + go apiObj.ManageHotspot() listenAddr := fmt.Sprintf(":%d", config.Port) log.Printf("listening on %s", listenAddr) log.Fatal(http.ListenAndServe(listenAddr, router)) - } func basicAuth(next http.Handler) http.Handler { @@ -341,7 +329,6 @@ type FrameData struct { } func GetFrame() *FrameData { - conn, err := dbus.SystemBus() if err != nil { return nil diff --git a/static/js/camera.ts b/static/js/camera.ts index 7f70364..6a9add6 100644 --- a/static/js/camera.ts +++ b/static/js/camera.ts @@ -96,6 +96,7 @@ window.onload = function () { document.getElementById("trigger-trap")!.onclick = triggerTrap; document.getElementById("take-snapshot-recording")!.onclick = takeTestRecording; + takeTestRecording; cameraConnection = new CameraConnection( window.location.hostname, window.location.port, @@ -104,22 +105,28 @@ window.onload = function () { ); }; -async function takeTestRecording() { document.getElementById("take-snapshot-recording")!.innerText = "Making a test recording"; document .getElementById("take-snapshot-recording")! .setAttribute("disabled", "true"); - console.log("making a test recording"); + "Making a test recording"; fetch("/api/camera/snapshot-recording", { method: "PUT", - headers: { + .setAttribute("disabled", "true"); Authorization: "Basic YWRtaW46ZmVhdGhlcnM=", }, }) .then((response) => console.log(response)) .then((data) => console.log(data)) .catch((error) => console.error(error)); + }) + await new Promise((r) => setTimeout(r, 3000)); + document + .getElementById("take-snapshot-recording")! + .removeAttribute("disabled"); + document.getElementById("take-snapshot-recording")!.innerText = + "Take test recording"; //TODO handle errors better and check that recording was made properly instead of just waiting.. await new Promise((r) => setTimeout(r, 3000)); document @@ -197,13 +204,13 @@ function drawRectWithText( async function processFrame(frame: Frame) { const canvas = document.getElementById("frameCanvas") as HTMLCanvasElement; - - const trackCanvas = document.getElementById( - "trackCanvas" + if (canvas.width != frame.frameInfo.Camera.ResX) { + canvas.width = frame.frameInfo.Camera.ResX; + trackCanvas.width = frame.frameInfo.Camera.ResX; ) as HTMLCanvasElement; - if (canvas == null) { - return; - } + if (canvas.height != frame.frameInfo.Camera.ResY) { + canvas.height = frame.frameInfo.Camera.ResY; + trackCanvas.height = frame.frameInfo.Camera.ResY; if (canvas.width != frame.frameInfo.Camera.ResX) { canvas.width = frame.frameInfo.Camera.ResX; trackCanvas.width = frame.frameInfo.Camera.ResX; @@ -214,25 +221,25 @@ async function processFrame(frame: Frame) { } const context = canvas.getContext("2d") as CanvasRenderingContext2D; const imgData = context.getImageData( + if (irCamera) { 0, - 0, - frame.frameInfo.Camera.ResX, + } else { frame.frameInfo.Camera.ResY ); - // gp hack to see if ir camera, dbus from python makes dictionary have to be all int type - let irCamera = frame.frameInfo.Camera.ResX >= 640; - if (irCamera) { - document.getElementById("trigger-trap")!.style.display = ""; + let max = 0; + let min = 0; + let range = 0; + if (!irCamera) { } else { document.getElementById("trigger-trap")!.style.display = "none"; } let max = 0; let min = 0; let range = 0; - if (!irCamera) { - max = Math.max(...frame.frame); - min = Math.min(...frame.frame); - range = max - min; + let pix = 0; + if (irCamera) { + pix = frame.frame[i]; + } else { } let maxI = 0; for (let i = 0; i < frame.frame.length; i++) { @@ -262,6 +269,7 @@ async function processFrame(frame: Frame) { for (const track of frame.frameInfo.Tracks) { let what = null; if (track.predictions && track.predictions.length > 0) { + } what = track.predictions[0].label; } drawRectWithText( diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..0f42756 --- /dev/null +++ b/test.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Start the SSH agent +eval "$(ssh-agent -s)" +ssh_key_location="$HOME/.ssh/cacophony-pi" +ssh-add "$ssh_key_location" + +# Discover Raspberry Pi services on the network +echo "Discovering Raspberry Pis with service _cacophonator-management._tcp..." +readarray -t services < <(avahi-browse -t -r _cacophonator-management._tcp | grep 'hostname' | awk '{print $3}' | sed 's/\[//' | sed 's/\]//' | sed 's/\.$/.local/') + +if [ ${#services[@]} -eq 0 ]; then + echo "No Raspberry Pi services found on the network." + exit 1 +fi + +# Display the discovered services +echo "Found Raspberry Pi services:" +for i in "${!services[@]}"; do + echo "$((i + 1))) ${services[i]}" +done + +# Let the user select a service +read -p "Select a Raspberry Pi to deploy to (1-${#services[@]}): " selection +pi_address=${services[$((selection - 1))]} + +if [ -z "$pi_address" ]; then + echo "Invalid selection." + exit 1 +fi + +echo "Selected Raspberry Pi at: $pi_address" + +while true; do + # Deployment + echo "Deploying to Raspberry Pi..." + make + + # Stop the service + ssh_stop_command=("ssh" "-i" "$ssh_key_location" "pi@$pi_address" "sudo systemctl stop managementd.service") + echo "Executing: ${ssh_stop_command[*]}" + "${ssh_stop_command[@]}" + if [ $? -ne 0 ]; then + echo "Error: SSH stop command failed" + break + fi + # Copy the file to a temporary location + scp_command=("scp" "-i" "$ssh_key_location" "./managementd" "pi@$pi_address:/home/pi") + echo "Executing: ${scp_command[*]}" + "${scp_command[@]}" + if [ $? -ne 0 ]; then + echo "Error: SCP failed" + break + fi + + # Move the file to /usr/bin with sudo + ssh_move_command=("ssh" "-i" "$ssh_key_location" "pi@$pi_address" "sudo mv /home/pi/managementd /usr/bin/") + echo "Executing: ${ssh_move_command[*]}" + "${ssh_move_command[@]}" + if [ $? -ne 0 ]; then + echo "Error: SSH move command failed" + break + fi + + # Restart the service + ssh_command=("ssh" "-i" "$ssh_key_location" "pi@$pi_address" "sudo systemctl restart managementd.service") + echo "Executing: ${ssh_command[*]}" + "${ssh_command[@]}" + if [ $? -ne 0 ]; then + echo "Error: SSH command failed" + break + fi + + # Stream logs from the service + log_command=("ssh" "-i" "$ssh_key_location" "pi@$pi_address" "sudo journalctl -u managementd.service -f") + echo "Streaming logs from managementd.service... (press Ctrl+C to stop)" + "${log_command[@]}" + + echo "Deployment completed. Press any key to deploy again or Ctrl+C to exit." + read -n 1 -s + if [ $? -ne 0 ]; then + break + fi + echo # new line for readability +done + +# Kill the SSH agent when done +eval "$(ssh-agent -k)" From d9351ac9c22ba2f592d1a50a425c5a27e0fe2496 Mon Sep 17 00:00:00 2001 From: Patrick Baxter Date: Wed, 6 Mar 2024 16:24:34 +1300 Subject: [PATCH 3/6] cleanup hotspot check dhcp conf has static ip on hotspot --- api/api.go | 11 +- api/hotspot.go | 302 +++++++++++++++++++++---------------------------- 2 files changed, 129 insertions(+), 184 deletions(-) diff --git a/api/api.go b/api/api.go index 8f9469f..e77db96 100644 --- a/api/api.go +++ b/api/api.go @@ -101,7 +101,7 @@ func (s *ManagementAPI) StopHotspotTimer() { } func (server *ManagementAPI) ManageHotspot() { - if err := initilseHotspot(); err != nil { + if err := initializeHotspot(); err != nil { log.Println("Failed to initialise hotspot:", err) if err := stopHotspot(); err != nil { log.Println("Failed to stop hotspot:", err) @@ -834,9 +834,6 @@ func (api *ManagementAPI) ConnectToWifi(w http.ResponseWriter, r *http.Request) return } else { log.Printf("Successfully connected to Wi-Fi SSID: %s", wifiDetails.SSID) - if err := exec.Command("systemctl", "restart", "dhcpcd").Run(); err != nil { - log.Printf("Error restarting DHCP client: %v", err) - } log.Println("Connected to Wi-Fi successfully") w.WriteHeader(http.StatusOK) w.Write([]byte("Connected to Wi-Fi successfully")) @@ -1100,7 +1097,7 @@ func (api *ManagementAPI) DisconnectFromWifi(w http.ResponseWriter, r *http.Requ // Handle the error } - if err := RestartDHCP(); err != nil { + if err := restartDHCP(); err != nil { log.Printf("Error restarting DHCP client: %v", err) } // if no current network, then restart hotspot @@ -1143,7 +1140,7 @@ func (api *ManagementAPI) ForgetWifiNetwork(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusInternalServerError) io.WriteString(w, "failed to remove Wi-Fi network\n") } - if err := RestartDHCP(); err != nil { + if err := restartDHCP(); err != nil { log.Printf("Error restarting DHCP client: %v", err) } // if no current network, then restart hotspot @@ -1157,8 +1154,6 @@ func (api *ManagementAPI) ForgetWifiNetwork(w http.ResponseWriter, r *http.Reque } }() } else { - // Remove the network using wpa_cli - // Get the list of Wi-Fi networks ssids, err := getSSIDIds() if err != nil { log.Printf("Error getting Wi-Fi networks: %v", err) diff --git a/api/hotspot.go b/api/hotspot.go index 0c70d40..5016d55 100644 --- a/api/hotspot.go +++ b/api/hotspot.go @@ -11,24 +11,41 @@ import ( "time" ) -// refactor createAPConfig to remove duplication -func createAPConfig(name string) error { - file_name := "/etc/hostapd/hostapd.conf" - config_lines := []string{ +const ( + routerIP = "192.168.4.1" + hotspotSSID = "bushnet" + hotspotPassword = "feathers" +) + +var dhcpConfigDefault = []string{ + "hostname", + "clientid", + "persistent", + "option rapid_commit", + "option domain_name_servers, domain_name, domain_search, host_name", + "option classless_static_routes", + "option interface_mtu", + "require dhcp_server_identifier", + "slaac private", +} + +func createAPConfig() error { + fileName := "/etc/hostapd/hostapd.conf" + configLines := []string{ "country_code=NZ", "interface=wlan0", - "ssid=" + name, + fmt.Sprintf("ssid=%s", hotspotSSID), "hw_mode=g", "channel=7", "macaddr_acl=0", "ignore_broadcast_ssid=0", "wpa=2", - "wpa_passphrase=feathers", + fmt.Sprintf("wpa_passphrase=%s", hotspotPassword), "wpa_key_mgmt=WPA-PSK", "wpa_pairwise=TKIP", "rsn_pairwise=CCMP", } - return createConfigFile(file_name, config_lines) + return createConfigFile(fileName, configLines) } func isInterfaceUp(interfaceName string) bool { @@ -41,8 +58,8 @@ func isInterfaceUp(interfaceName string) bool { } func waitForInterface(interfaceName string, timeout time.Duration) error { - end := time.Now().Add(timeout) - for time.Now().Before(end) { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { if isInterfaceUp(interfaceName) { return nil } @@ -51,153 +68,90 @@ func waitForInterface(interfaceName string, timeout time.Duration) error { return fmt.Errorf("interface %s did not come up within the specified timeout", interfaceName) } -func startAccessPoint(_ string) error { - err := exec.Command("systemctl", "restart", "hostapd").Run() - if err != nil { +func startAccessPoint() error { + if err := exec.Command("systemctl", "restart", "hostapd").Run(); err != nil { return err } - // Wait for wlan0 to be up before proceeding return waitForInterface("wlan0", 30*time.Second) } func stopAccessPoint() error { - return exec.Command("systemctl", "stop", "hostapd").Start() -} - -const router_ip = "192.168.4.1" - -var dhcp_config_default = []string{ - "hostname", - "clientid", - "persistent", - "noipv6", - "option rapid_commit", - "option domain_name_servers, domain_name, domain_search, host_name", - "option classless_static_routes", - "option interface_mtu", - "require dhcp_server_identifier", - "slaac private", - "interface usb0", - "metric 300", - "interface wlan0", - "metric 200", + return exec.Command("systemctl", "stop", "hostapd").Run() } -var dhcp_config_lines = []string{ - "interface wlan0", - "static ip_address=" + router_ip + "/24", -} +func createDHCPConfig(isHotspot bool) error { + filePath := "/etc/dhcpcd.conf" + configLines := dhcpConfigDefault -func createDHCPConfig() (bool, error) { - file_path := "/etc/dhcpcd.conf" + if isHotspot { + configLines = append(configLines, "interface wlan0", fmt.Sprintf("static ip_address=%s/24", routerIP)) + } - // append to dhcpcd.conf if lines don't already exist - config_lines := append(dhcp_config_default, dhcp_config_lines...) - return writeLines(file_path, config_lines) + return writeLines(filePath, configLines) } -func writeLines(file_path string, lines []string) (bool, error) { - // Check if file already exists with the same config. - if _, err := os.Stat(file_path); err == nil { - currentContent, err := os.ReadFile(file_path) - if err != nil { - return false, err - } - newContent := strings.Join(lines, "\n") + "\n" - if string(currentContent) == newContent { - return false, nil - } - } - - file, err := os.Create(file_path) +func writeLines(filePath string, lines []string) error { + file, err := os.Create(filePath) if err != nil { - return false, err + return err } defer file.Close() - w := bufio.NewWriter(file) + writer := bufio.NewWriter(file) for _, line := range lines { - _, _ = fmt.Fprintln(w, line) - } - if err := w.Flush(); err != nil { - return false, err - } - - return true, nil -} - -func startDHCP() error { - // modify /etc/dhcpcd.conf - configModified, err := createDHCPConfig() - if err != nil { - return err + fmt.Fprintln(writer, line) } - if configModified { - return exec.Command("systemctl", "restart", "dhcpcd").Run() - } - return exec.Command("systemctl", "start", "dhcpcd").Run() + return writer.Flush() } -func RestartDHCP() error { - // Only restart if config has changed - configModified, err := writeLines("/etc/dhcpcd.conf", dhcp_config_default) - if err != nil { - return err - } - +func restartDHCP() error { if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil { return err } - if configModified { - return exec.Command("systemctl", "restart", "dhcpcd").Run() - } - return exec.Command("systemctl", "start", "dhcpcd").Run() + return exec.Command("systemctl", "restart", "dhcpcd").Run() } func checkIsConnectedToNetwork() (string, error) { - if val, err := exec.Command("iwgetid", "wlan0", "-r").Output(); err != nil { + output, err := exec.Command("iwgetid", "wlan0", "-r").Output() + if err != nil { return "", err - } else { - network := strings.TrimSpace(string(val)) - if network == "" { - return "", fmt.Errorf("not connected to a network") - } else { - return string(network), nil - } } + network := strings.TrimSpace(string(output)) + if network == "" { + return "", fmt.Errorf("not connected to a network") + } + return network, nil } func checkIsConnectedToNetworkWithRetries() (string, error) { + var network string var err error - var ssid string for i := 0; i < 3; i++ { - ssid, err = checkIsConnectedToNetwork() - if ssid != "" { - return ssid, nil + network, err = checkIsConnectedToNetwork() + if err == nil { + return network, nil } - time.Sleep(time.Second * 2) + time.Sleep(2 * time.Second) } - return ssid, err + return "", err } -func createDNSConfig(ip_range string) error { - // DNSMASQ config - file_name := "/etc/dnsmasq.conf" - config_lines := []string{ +func createDNSConfig(ipRange string) error { + fileName := "/etc/dnsmasq.conf" + configLines := []string{ "interface=wlan0", - "dhcp-range=" + ip_range + ",12h", + fmt.Sprintf("dhcp-range=%s,12h", ipRange), "domain=wlan", } - return createConfigFile(file_name, config_lines) + return createConfigFile(fileName, configLines) } func startDNS() error { - return exec.Command("systemctl", "restart", "dnsmasq").Start() + return exec.Command("systemctl", "restart", "dnsmasq").Run() } func stopDNS() error { - println("Stopping DNS") - return exec.Command("systemctl", "stop", "dnsmasq").Start() + return exec.Command("systemctl", "stop", "dnsmasq").Run() } func createConfigFile(name string, config []string) error { @@ -207,107 +161,103 @@ func createConfigFile(name string, config []string) error { } defer file.Close() - w := bufio.NewWriter(file) + writer := bufio.NewWriter(file) for _, line := range config { - _, err = fmt.Fprintln(w, line) - if err != nil { - return err - } - } - err = w.Flush() - if err != nil { - return err + fmt.Fprintln(writer, line) } - return nil + return writer.Flush() } -func enableBushnet() error { - // Wpa_cli - ssids, err := getSSIDIds() +func enableNetwork(ssid string) error { + output, err := exec.Command("wpa_cli", "list_networks").Output() if err != nil { return err } - if len(ssids) == 0 { - return fmt.Errorf("no networks found") - } - // Enable bushnet using wpa_cli and it's id - // Get id of bushnet and Bushnet - if bushnetId, ok := ssids["bushnet"]; ok { - if err := exec.Command("wpa_cli", "enable_network", bushnetId).Run(); err != nil { - return err - } - } - if BushnetId, ok := ssids["Bushnet"]; ok { - if err := exec.Command("wpa_cli", "enable_network", BushnetId).Run(); err != nil { - return err + networks := parseNetworks(string(output)) + for id, network := range networks { + if network == ssid { + return exec.Command("wpa_cli", "enable_network", id).Run() } } return nil } -// Setup Hotspot and stop after 5 minutes using a new goroutine -func initilseHotspot() error { - ssid := "bushnet" - log.Printf("Setting DHCP to default...") - if err := EnableAllSavedNetworks(); err != nil { - log.Printf("Error enabling all saved networks: %s", err) +func parseNetworks(output string) map[string]string { + networks := make(map[string]string) + lines := strings.Split(output, "\n") + for _, line := range lines[1:] { + fields := strings.Fields(line) + if len(fields) > 1 { + networks[fields[0]] = fields[1] + } } - // Ensure bushnet and Bushnet are enabled - if err := enableBushnet(); err != nil { - log.Printf("Error enabling bushnet: %s", err) + return networks +} + +func initializeHotspot() error { + log.Printf("Initializing hotspot...") + + if err := enableNetwork("bushnet"); err != nil { + log.Printf("Failed to enable bushnet network: %v", err) } - if err := RestartDHCP(); err != nil { - log.Printf("Error restarting dhcpcd: %s", err) + if err := enableNetwork("Bushnet"); err != nil { + log.Printf("Failed to enable Bushnet network: %v", err) } - // Check if already connected to a network - if val, err := exec.Command("iwgetid", "wlan0", "-r").Output(); err != nil { - log.Printf("Error checking if connected to network: %s", err) - } else { - log.Printf("Wlan0 is connected to: %s", val) + + network, err := checkIsConnectedToNetworkWithRetries() + if err == nil { + return fmt.Errorf("already connected to network: %s", network) } - // If not connected to a network, start hotspot - log.Printf("Checking if connected to network...") - if network, err := checkIsConnectedToNetworkWithRetries(); err == nil { - // Check if the hotspot is already running - return fmt.Errorf("already connected to a network: %s", network) + if err := createDHCPConfig(true); err != nil { + return fmt.Errorf("failed to create DHCP config: %v", err) } - log.Printf("Starting Hotspot...") - log.Printf("Creating Configs...") - if err := createAPConfig(ssid); err != nil { - return err + + if err := restartDHCP(); err != nil { + return fmt.Errorf("failed to restart DHCP: %v", err) + } + + if err := createAPConfig(); err != nil { + return fmt.Errorf("failed to create AP config: %v", err) } + if err := createDNSConfig("192.168.4.2,192.168.4.20"); err != nil { - return err + return fmt.Errorf("failed to create DNS config: %v", err) } - log.Printf("Starting Access Point...") - if err := startAccessPoint(ssid); err != nil { - return err + if err := startAccessPoint(); err != nil { + return fmt.Errorf("failed to start access point: %v", err) } - log.Printf("Starting DNS...") + if err := startDNS(); err != nil { - return err - } - log.Printf("Starting DHCP...") - if err := startDHCP(); err != nil { - return err + return fmt.Errorf("failed to start DNS: %v", err) } + + log.Printf("Hotspot initialized successfully") return nil } func stopHotspot() error { - log.Printf("Stopping Hotspot") + log.Printf("Stopping hotspot...") + if err := stopAccessPoint(); err != nil { - return err + return fmt.Errorf("failed to stop access point: %v", err) } + if err := stopDNS(); err != nil { - return err + return fmt.Errorf("failed to stop DNS: %v", err) } - if err := RestartDHCP(); err != nil { - return err + + if err := createDHCPConfig(false); err != nil { + return fmt.Errorf("failed to create DHCP config: %v", err) } + + if err := restartDHCP(); err != nil { + return fmt.Errorf("failed to restart DHCP: %v", err) + } + + log.Printf("Hotspot stopped successfully") return nil } From d7b45d5f47f2c9acc0e3aa4e7a950248feb01baa Mon Sep 17 00:00:00 2001 From: Patrick Baxter Date: Tue, 7 May 2024 15:01:10 +1200 Subject: [PATCH 4/6] stop hotspot, add reregister authorized --- api/api.go | 48 +++++++++++++++++++++++++++++++++++++++++ cmd/managementd/main.go | 17 +++++++++------ go.mod | 2 +- go.sum | 8 +++---- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/api/api.go b/api/api.go index e77db96..df15b3f 100644 --- a/api/api.go +++ b/api/api.go @@ -300,6 +300,50 @@ func (api *ManagementAPI) Reregister(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func (api *ManagementAPI) ReregisterAuthorized(w http.ResponseWriter, r *http.Request) { + var req struct { + Group string `json:"newGroup"` + Name string `json:"newName"` + AuthorizedUser string `json:"authorizedUser"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + badRequest(&w, err) + return + } + + if req.Group == "" && req.Name == "" { + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, "must set name or group\n") + return + } + apiClient, err := goapi.New() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, fmt.Sprintf("failed to get api client for device: %s", err.Error())) + return + } + if req.Group == "" { + req.Group = apiClient.GroupName() + } + if req.Name == "" { + req.Name = apiClient.DeviceName() + } + + if err := apiClient.ReRegisterByAuthorized(req.Name, req.Group, randString(20), req.AuthorizedUser); err != nil { + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, err.Error()) + return + } + + if err := api.config.Reload(); err != nil { + serverError(&w, err) + return + } + + w.WriteHeader(http.StatusOK) +} + // Reboot will reboot the device after a delay so a response can be sent back func (api *ManagementAPI) Reboot(w http.ResponseWriter, r *http.Request) { go func() { @@ -812,6 +856,9 @@ func (api *ManagementAPI) ConnectToWifi(w http.ResponseWriter, r *http.Request) log.Printf("Error enabling all saved networks: %v", err) } } + if err := stopHotspot(); err != nil { + log.Println("Failed to stop hotspot:", err) + } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -833,6 +880,7 @@ func (api *ManagementAPI) ConnectToWifi(w http.ResponseWriter, r *http.Request) go api.ManageHotspot() return } else { + // restart dhcp log.Printf("Successfully connected to Wi-Fi SSID: %s", wifiDetails.SSID) log.Println("Connected to Wi-Fi successfully") w.WriteHeader(http.StatusOK) diff --git a/cmd/managementd/main.go b/cmd/managementd/main.go index 4cf8ada..7adf0ac 100644 --- a/cmd/managementd/main.go +++ b/cmd/managementd/main.go @@ -45,13 +45,15 @@ const ( socketTimeout = 7 * time.Second ) -var haveClients = make(chan bool) -var version = "" -var sockets = make(map[int64]*WebsocketRegistration) -var socketsLock sync.RWMutex -var cameraInfo map[string]interface{} -var lastFrame *FrameData -var currentFrame = -1 +var ( + haveClients = make(chan bool) + version = "" + sockets = make(map[int64]*WebsocketRegistration) + socketsLock sync.RWMutex + cameraInfo map[string]interface{} + lastFrame *FrameData + currentFrame = -1 +) // Set up and handle page requests. func main() { @@ -107,6 +109,7 @@ func main() { apiRouter.HandleFunc("/camera/snapshot-recording", apiObj.TakeSnapshotRecording).Methods("PUT") apiRouter.HandleFunc("/signal-strength", apiObj.GetSignalStrength).Methods("GET") apiRouter.HandleFunc("/reregister", apiObj.Reregister).Methods("POST") + apiRouter.HandleFunc("/reregister-authorized", apiObj.ReregisterAuthorized).Methods("POST") apiRouter.HandleFunc("/reboot", apiObj.Reboot).Methods("POST") apiRouter.HandleFunc("/config", apiObj.GetConfig).Methods("GET") apiRouter.HandleFunc("/config", apiObj.SetConfig).Methods("POST") diff --git a/go.mod b/go.mod index 12fda50..a45e2b5 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/TheCacophonyProject/audiobait/v3 v3.0.1 github.com/TheCacophonyProject/event-reporter v1.3.2-0.20200210010421-ca3fcb76a231 - github.com/TheCacophonyProject/go-api v1.0.2 + github.com/TheCacophonyProject/go-api v1.1.0 github.com/TheCacophonyProject/go-config v1.9.1 github.com/TheCacophonyProject/go-cptv v0.0.0-20201215230510-ae7134e91a71 github.com/TheCacophonyProject/lepton3 v0.0.0-20211005194419-22311c15d6ee diff --git a/go.sum b/go.sum index 1054cc2..3684070 100644 --- a/go.sum +++ b/go.sum @@ -56,12 +56,12 @@ github.com/TheCacophonyProject/event-reporter/v3 v3.2.1/go.mod h1:0ejh8kTFM9iC3/ github.com/TheCacophonyProject/event-reporter/v3 v3.3.0 h1:/leMKQ0G+nEzTOgd+d/WMT20U7zenTmhpcNcf4bQ8xE= github.com/TheCacophonyProject/event-reporter/v3 v3.3.0/go.mod h1:dGIYfhABsJHKjcsxtftDwpdcfLOWTYKeIyCYxCOIMrc= github.com/TheCacophonyProject/go-api v0.0.0-20190923033957-174cea2ac81c/go.mod h1:FfMpa4cFhNXQ9tuKG18HO6yLExezcJhzjUjBOFocrQw= -github.com/TheCacophonyProject/go-api v1.0.2 h1:6fVnts23OFsUuUec9GjGEhH86WmOkev/X9FzSVCEzso= -github.com/TheCacophonyProject/go-api v1.0.2/go.mod h1:SH5Jo4bH5UdMetAUpoDeFwMYxV1RdiektVNQT11k9ow= +github.com/TheCacophonyProject/go-api v1.1.0 h1:WIU28X3CrnhhGBnGxi+udrj2EoDPl3reVNRXRcxmLyc= +github.com/TheCacophonyProject/go-api v1.1.0/go.mod h1:F7UUNgsLhbw7hsiNBMRB9kQz9uXXosVmNToqImz7EA8= github.com/TheCacophonyProject/go-config v0.0.0-20190922224052-7c2a21bc6b88/go.mod h1:gPUJLVu408NRz9/P3BrsxzOzLc+KJLrv+jVdDw3RI0Y= github.com/TheCacophonyProject/go-config v0.0.0-20190927054511-c93578ae648a/go.mod h1:QCgT+KCrz1CmLVpeeOMl5L8/X1QvWwpsLzR7afTmEJc= github.com/TheCacophonyProject/go-config v1.4.0/go.mod h1:oARW/N3eJbcewCqB+Jc7TBwuODawwYgpo56UO6yBdKU= -github.com/TheCacophonyProject/go-config v1.7.0/go.mod h1:2VGuQR5dATuq8nzdBMQd7mbc6OhCimkSsGHRLWcer2c= +github.com/TheCacophonyProject/go-config v1.9.0/go.mod h1:+y80PSRZudMYuVrYTGOvzc66NxVJWKS4TuU442vmvhY= github.com/TheCacophonyProject/go-config v1.9.1 h1:TCeogtNYg5eHx2q97DQ1B+RsbjacW+jr7h1TCv1FpAk= github.com/TheCacophonyProject/go-config v1.9.1/go.mod h1:XZwQmNl2wQXhYR18RQtwZ6LwFwgAx73yzJfymYLz68s= github.com/TheCacophonyProject/go-cptv v0.0.0-20200116020937-858bd8b71512/go.mod h1:8H6Aaft5549sIWxcsuCIL2o60/TQkoF93fVoSTpgZb8= @@ -70,7 +70,6 @@ github.com/TheCacophonyProject/go-cptv v0.0.0-20201215230510-ae7134e91a71 h1:g6X github.com/TheCacophonyProject/go-cptv v0.0.0-20201215230510-ae7134e91a71/go.mod h1:pExPO/gk28kgWnd1z55xJ7YtC0KgQBDKvJoGYExc+l0= github.com/TheCacophonyProject/lepton3 v0.0.0-20200121020734-2ae28662e1bc/go.mod h1:xzPAWtvVCbJdJC2Gn1cG0Ovs/VP7XGGiQpUU8wU4HME= github.com/TheCacophonyProject/lepton3 v0.0.0-20200213011619-1934a9300bd3/go.mod h1:xzPAWtvVCbJdJC2Gn1cG0Ovs/VP7XGGiQpUU8wU4HME= -github.com/TheCacophonyProject/lepton3 v0.0.0-20210324024142-003e5546e30f/go.mod h1:+FTQKx63hdEbuTe/nxNv9TQ2EWqdlzMZx7UNLGCX9SE= github.com/TheCacophonyProject/lepton3 v0.0.0-20211005194419-22311c15d6ee h1:whilFI36xLtsd7p6blNKH3LKdU/g1gZv1SClUPJrpgs= github.com/TheCacophonyProject/lepton3 v0.0.0-20211005194419-22311c15d6ee/go.mod h1:+FTQKx63hdEbuTe/nxNv9TQ2EWqdlzMZx7UNLGCX9SE= github.com/TheCacophonyProject/modemd v0.0.0-20190605010435-ae5b0f2eb760/go.mod h1:bfwJ/WcvDY9XtHKC5tcRfVrU8RWaW8DLYAAUfsrJr/4= @@ -519,7 +518,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210927181540-4e4d966f7476/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= From bca32b917786e76f60425fa61361e95542499466 Mon Sep 17 00:00:00 2001 From: Patrick Baxter Date: Tue, 7 May 2024 16:56:24 +1200 Subject: [PATCH 5/6] merge with tc2 latest This commit tries to get any of the latest changes that are usable on the old cameras, will need further cleaning --- api/api.go | 66 ++++++++++--- api/clock.go | 115 ++++++++++++++++++++++ go.mod | 21 ++-- go.sum | 38 +++++--- html/about.html | 4 + html/camera.html | 15 ++- html/clock.html | 4 + static/js/about.js | 26 +++++ static/js/camera.ts | 227 +++++++++++++++++++++++++------------------- static/js/clock.js | 6 +- 10 files changed, 388 insertions(+), 134 deletions(-) diff --git a/api/api.go b/api/api.go index df15b3f..ec70b72 100644 --- a/api/api.go +++ b/api/api.go @@ -33,6 +33,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime" "strconv" "strings" "time" @@ -44,6 +45,8 @@ import ( saltrequester "github.com/TheCacophonyProject/salt-updater" "github.com/godbus/dbus" "github.com/gorilla/mux" + "golang.org/x/text/cases" + "golang.org/x/text/language" "github.com/TheCacophonyProject/event-reporter/eventclient" "github.com/TheCacophonyProject/trap-controller/trapdbusclient" @@ -121,6 +124,10 @@ func (api *ManagementAPI) GetVersion(w http.ResponseWriter, r *http.Request) { } func readFile(file string) string { + if runtime.GOOS == "windows" { + return "" + } + // The /etc/salt/minion_id file contains the ID. out, err := os.ReadFile(file) if err != nil { @@ -129,6 +136,19 @@ func readFile(file string) string { return string(out) } +// Return the time of the last salt update. +func getLastSaltUpdate() string { + timeStr := strings.TrimSpace(readFile("/etc/cacophony/last-salt-update")) + if timeStr == "" { + return "" + } + t, err := time.Parse(time.RFC3339, timeStr) + if err != nil { + return "" + } + return t.Format("2006-01-02 15:04:05") +} + // GetDeviceInfo returns information about this device func (api *ManagementAPI) GetDeviceInfo(w http.ResponseWriter, r *http.Request) { var device goconfig.Device @@ -140,20 +160,22 @@ func (api *ManagementAPI) GetDeviceInfo(w http.ResponseWriter, r *http.Request) } type deviceInfo struct { - ServerURL string `json:"serverURL"` - Groupname string `json:"groupname"` - Devicename string `json:"devicename"` - DeviceID int `json:"deviceID"` - SaltID string `json:"saltID"` - Type string `json:"type"` + ServerURL string `json:"serverURL"` + GroupName string `json:"groupname"` + Devicename string `json:"devicename"` + DeviceID int `json:"deviceID"` + SaltID string `json:"saltID"` + LastUpdated string `json:"lastUpdated"` + Type string `json:"type"` } info := deviceInfo{ - ServerURL: device.Server, - Groupname: device.Group, - Devicename: device.Name, - DeviceID: device.ID, - SaltID: strings.TrimSpace(readFile("/etc/salt/minion_id")), - Type: getDeviceType(), + ServerURL: device.Server, + GroupName: device.Group, + Devicename: device.Name, + DeviceID: device.ID, + SaltID: strings.TrimSpace(readFile("/etc/salt/minion_id")), + LastUpdated: getLastSaltUpdate(), + Type: getDeviceType(), } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(info) @@ -411,6 +433,12 @@ func (api *ManagementAPI) GetConfig(w http.ResponseWriter, r *http.Request) { } } + configValuesCC := map[string]interface{}{} + for k, v := range configValues { + configValuesCC[toCamelCase(k)] = v + } + configValues = configValuesCC + valuesAndDefaults := map[string]interface{}{ "values": configValues, "defaults": configDefaults, @@ -424,6 +452,17 @@ func (api *ManagementAPI) GetConfig(w http.ResponseWriter, r *http.Request) { w.Write(jsonString) } +func toCamelCase(s string) string { + words := strings.FieldsFunc(s, func(r rune) bool { + return r == '-' || r == '_' + }) + c := cases.Title(language.English) + for i := 1; i < len(words); i++ { + words[i] = c.String(words[i]) + } + return strings.Join(words, "") +} + // ClearConfigSection will delete the config from a section so the default values will be used. func (api *ManagementAPI) ClearConfigSection(w http.ResponseWriter, r *http.Request) { section := r.FormValue("section") @@ -774,11 +813,10 @@ func CheckInternetConnection(interfaceName string) bool { } return true } - func (api *ManagementAPI) CheckModemInternetConnection(w http.ResponseWriter, r *http.Request) { // Check if connected to modem log.Println("Checking modem connection") - connected := CheckInternetConnection("eth1") + connected := CheckInternetConnection("usb0") log.Printf("Modem connection: %v", connected) // Send the current network as a JSON response diff --git a/api/clock.go b/api/clock.go index 8821532..d645890 100644 --- a/api/clock.go +++ b/api/clock.go @@ -21,12 +21,14 @@ package api import ( "encoding/json" "fmt" + "log" "net/http" "os/exec" "strings" "time" "github.com/TheCacophonyProject/rtc-utils/rtc" + "github.com/godbus/dbus" ) const ( @@ -41,9 +43,14 @@ type clockInfo struct { LowRTCBattery bool RTCIntegrity bool NTPSynced bool + Timezone string } func (api *ManagementAPI) GetClock(w http.ResponseWriter, r *http.Request) { + if getDeviceType() == "tc2" { + api.GetClockTC2(w, r) + return + } out, err := exec.Command("date", dateCmdFormat).CombinedOutput() if err != nil { serverError(&w, err) @@ -72,6 +79,7 @@ func (api *ManagementAPI) GetClock(w http.ResponseWriter, r *http.Request) { LowRTCBattery: rtcState.LowBattery, RTCIntegrity: rtcState.ClockIntegrity, NTPSynced: ntpSynced, + Timezone: getTimezone(), }) if err != nil { serverError(&w, err) @@ -81,6 +89,19 @@ func (api *ManagementAPI) GetClock(w http.ResponseWriter, r *http.Request) { } func (api *ManagementAPI) PostClock(w http.ResponseWriter, r *http.Request) { + timezone := r.FormValue("timezone") + if timezone != "" { + cmd := exec.Command("timedatectl", "set-timezone", timezone) + _, err := cmd.CombinedOutput() + if err != nil { + log.Println(err) + } + } + + if getDeviceType() == "tc2" { + api.PostClockTC2(w, r) + return + } date, err := time.Parse(timeFormat, r.FormValue("date")) if err != nil { badRequest(&w, err) @@ -96,3 +117,97 @@ func (api *ManagementAPI) PostClock(w http.ResponseWriter, r *http.Request) { serverError(&w, err) } } + +func getTimezone() string { + cmd := exec.Command("timedatectl", "show", "-p", "Timezone", "--value") + + out, err := cmd.Output() + if err != nil { + fmt.Printf("Error getting timezone: %v\n", err) + return "" + } + return strings.TrimSpace(string(out)) +} + +func (api *ManagementAPI) GetClockTC2(w http.ResponseWriter, r *http.Request) { + conn, err := dbus.SystemBus() + if err != nil { + log.Println(err) + http.Error(w, "Failed to connect to DBus", http.StatusInternalServerError) + return + } + rtcDBus := conn.Object("org.cacophony.RTC", "/org/cacophony/RTC") + + var t string + var integrity bool + err = rtcDBus.Call("org.cacophony.RTC.GetTime", 0).Store(&t, &integrity) + if err != nil { + log.Println(err) + http.Error(w, "Failed to get rtc status", http.StatusInternalServerError) + return + } + rtcTime, err := time.Parse("2006-01-02T15:04:05Z07:00", t) + if err != nil { + log.Println(err) + http.Error(w, "Failed to get rtc status", http.StatusInternalServerError) + return + } + + out, err := exec.Command("date", dateCmdFormat).CombinedOutput() + if err != nil { + serverError(&w, err) + return + } + systemTime, err := time.Parse(timeFormat, strings.TrimSpace(string(out))) + if err != nil { + serverError(&w, err) + return + } + + ntpSynced, err := isNTPSynced() + if err != nil { + serverError(&w, err) + return + } + + b, err := json.Marshal(&clockInfo{ + RTCTimeUTC: rtcTime.UTC().Format(timeFormat), + RTCTimeLocal: rtcTime.Local().Format(timeFormat), + SystemTime: systemTime.Format(timeFormat), + RTCIntegrity: integrity, + NTPSynced: ntpSynced, + Timezone: getTimezone(), + }) + if err != nil { + serverError(&w, err) + return + } + w.Write(b) +} + +func (api *ManagementAPI) PostClockTC2(w http.ResponseWriter, r *http.Request) { + date, err := time.Parse(timeFormat, r.FormValue("date")) + if err != nil { + badRequest(&w, err) + return + } + + conn, err := dbus.SystemBus() + if err != nil { + log.Println(err) + http.Error(w, "Failed to connect to DBus", http.StatusInternalServerError) + return + } + rtcDBus := conn.Object("org.cacophony.RTC", "/org/cacophony/RTC") + err = rtcDBus.Call("org.cacophony.RTC.SetTime", 0, date.Format("2006-01-02T15:04:05Z07:00")).Store() + if err != nil { + log.Println(err) + http.Error(w, "Failed to get rtc status", http.StatusInternalServerError) + return + } +} + +func isNTPSynced() (bool, error) { + out, err := exec.Command("timedatectl", "status").Output() + return strings.Contains(string(out), "synchronized: yes"), err +} diff --git a/go.mod b/go.mod index a45e2b5..751a5d6 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/TheCacophonyProject/audiobait/v3 v3.0.1 github.com/TheCacophonyProject/event-reporter v1.3.2-0.20200210010421-ca3fcb76a231 github.com/TheCacophonyProject/go-api v1.1.0 - github.com/TheCacophonyProject/go-config v1.9.1 + github.com/TheCacophonyProject/go-config v1.9.2 github.com/TheCacophonyProject/go-cptv v0.0.0-20201215230510-ae7134e91a71 github.com/TheCacophonyProject/lepton3 v0.0.0-20211005194419-22311c15d6ee github.com/TheCacophonyProject/rtc-utils v1.2.0 @@ -15,33 +15,36 @@ require ( github.com/godbus/dbus v4.1.0+incompatible github.com/gorilla/mux v1.8.0 github.com/nathan-osman/go-sunrise v1.0.0 // indirect - golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 + golang.org/x/net v0.10.0 ) -require github.com/TheCacophonyProject/trap-controller v0.0.0-20230227002937-262a1adfaa47 +require ( + github.com/TheCacophonyProject/trap-controller v0.0.0-20230227002937-262a1adfaa47 + golang.org/x/text v0.14.0 +) require ( github.com/TheCacophonyProject/event-reporter/v3 v3.3.0 // indirect github.com/TheCacophonyProject/window v0.0.0-20200312071457-7fc8799fdce7 // indirect github.com/boltdb/bolt v1.3.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/gobuffalo/envy v1.9.0 // indirect - github.com/gobuffalo/packd v1.0.0 // indirect + github.com/gobuffalo/envy v1.10.2 // indirect + github.com/gobuffalo/packd v1.0.2 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/joho/godotenv v1.4.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/pelletier/go-toml v1.9.4 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.9.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.14.0 // indirect gopkg.in/ini.v1 v1.64.0 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 3684070..6a42279 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/TheCacophonyProject/go-config v0.0.0-20190922224052-7c2a21bc6b88/go.m github.com/TheCacophonyProject/go-config v0.0.0-20190927054511-c93578ae648a/go.mod h1:QCgT+KCrz1CmLVpeeOMl5L8/X1QvWwpsLzR7afTmEJc= github.com/TheCacophonyProject/go-config v1.4.0/go.mod h1:oARW/N3eJbcewCqB+Jc7TBwuODawwYgpo56UO6yBdKU= github.com/TheCacophonyProject/go-config v1.9.0/go.mod h1:+y80PSRZudMYuVrYTGOvzc66NxVJWKS4TuU442vmvhY= -github.com/TheCacophonyProject/go-config v1.9.1 h1:TCeogtNYg5eHx2q97DQ1B+RsbjacW+jr7h1TCv1FpAk= -github.com/TheCacophonyProject/go-config v1.9.1/go.mod h1:XZwQmNl2wQXhYR18RQtwZ6LwFwgAx73yzJfymYLz68s= +github.com/TheCacophonyProject/go-config v1.9.2 h1:9QEWceOla56SIXPu6gtGmykwDeXwLtbZI1ABwumdU6Q= +github.com/TheCacophonyProject/go-config v1.9.2/go.mod h1:XZwQmNl2wQXhYR18RQtwZ6LwFwgAx73yzJfymYLz68s= github.com/TheCacophonyProject/go-cptv v0.0.0-20200116020937-858bd8b71512/go.mod h1:8H6Aaft5549sIWxcsuCIL2o60/TQkoF93fVoSTpgZb8= github.com/TheCacophonyProject/go-cptv v0.0.0-20200616224711-fc633122087a/go.mod h1:Vg73Ezn4kr8qDNP9LNgjki9qgi+5T/0Uc9oDyflaYUY= github.com/TheCacophonyProject/go-cptv v0.0.0-20201215230510-ae7134e91a71 h1:g6XLYIt3hFo2JOQhbfySLxcFFv0kofg0L1Z7MyNXsLc= @@ -156,12 +156,13 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE= github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= +github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= -github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= -github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= +github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw= +github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= @@ -280,8 +281,9 @@ github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 h1:Mo9W14pwbO9VfRe+y github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -362,8 +364,10 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= @@ -392,13 +396,16 @@ github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -478,6 +485,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -518,8 +527,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= -golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -611,8 +620,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -622,8 +632,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -835,8 +846,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/html/about.html b/html/about.html index c1d1814..8bf0e72 100644 --- a/html/about.html +++ b/html/about.html @@ -78,6 +78,10 @@

Device

+
+ +
+

Installed Packages

diff --git a/html/camera.html b/html/camera.html index 490e060..f6c19a3 100644 --- a/html/camera.html +++ b/html/camera.html @@ -20,7 +20,6 @@

Camera

Take test recording +
Clock

NTP Synced:

+
+

Timezone:

+

+
diff --git a/static/js/about.js b/static/js/about.js index cfb4201..084ba53 100644 --- a/static/js/about.js +++ b/static/js/about.js @@ -84,6 +84,32 @@ function runSaltUpdate() { xmlHttp.send(null); } +async function uploadLogs() { + $("#upload-logs-button").attr("disabled", true); + $("#upload-logs-button").html("Uploading logs..."); + try { + const response = await fetch("/api/upload-logs", { + method: "PUT", + headers: { + "Authorization": "Basic " + btoa("admin:feathers"), + "Content-Type": "application/json" + } + }); + + if (response.ok) { + alert("Logs uploaded"); + } else { + alert("Error uploading logs"); + console.error("Error with response:", await response.text()); + } + } catch (error) { + alert("Error uploading logs"); + console.error("Error with uploading logs:", error); + } + $("#upload-logs-button").attr("disabled", false); + $("#upload-logs-button").html("Upload logs"); +} + var runningSaltUpdate = true; // Check salt update state. Returns true if it is no longer running function checkSaltUpdateState() { diff --git a/static/js/camera.ts b/static/js/camera.ts index 6a9add6..eaaf732 100644 --- a/static/js/camera.ts +++ b/static/js/camera.ts @@ -32,7 +32,7 @@ export enum CameraConnectionState { } const UUID = new Date().getTime(); -const showTracks = false; + interface CameraStats { skippedFramesServer: number; skippedFramesClient: number; @@ -69,22 +69,22 @@ function restartCameraViewing() { } async function triggerTrap() { - document.getElementById("trigger-trap")!.innerText = "Triggering trap"; + document.getElementById("trigger-trap")!.innerText = 'Triggering trap'; document.getElementById("trigger-trap")!.setAttribute("disabled", "true"); console.log("triggering trap"); - fetch("/api/trigger-trap", { - method: "PUT", - headers: { - Authorization: "Basic YWRtaW46ZmVhdGhlcnM=", - }, - }) - .then((response) => console.log(response)) - .then((data) => console.log(data)) - .catch((error) => console.error(error)); + fetch('/api/trigger-trap', { + method: 'PUT', + headers: { + 'Authorization': 'Basic YWRtaW46ZmVhdGhlcnM=' + }}) + + .then(response => console.log(response)) + .then(data => console.log(data)) + .catch(error => console.error(error)) //TODO handle errors better and check that recording was made properly instead of just waiting.. - await new Promise((r) => setTimeout(r, 3000)); + await new Promise(r => setTimeout(r, 3000)); document.getElementById("trigger-trap")!.removeAttribute("disabled"); - document.getElementById("trigger-trap")!.innerText = "Trigger trap"; + document.getElementById("trigger-trap")!.innerText = 'Trigger trap'; } window.onload = function () { @@ -94,46 +94,34 @@ window.onload = function () { } document.getElementById("snapshot-restart")!.onclick = restartCameraViewing; document.getElementById("trigger-trap")!.onclick = triggerTrap; - document.getElementById("take-snapshot-recording")!.onclick = - takeTestRecording; - takeTestRecording; + document.getElementById("take-snapshot-recording")!.onclick = takeTestRecording; + document.getElementById("play-test-video")!.onclick = playTestVideo; cameraConnection = new CameraConnection( window.location.hostname, window.location.port, processFrame, onConnectionStateChange ); + updateTestVideos(); }; - document.getElementById("take-snapshot-recording")!.innerText = - "Making a test recording"; - document - .getElementById("take-snapshot-recording")! - .setAttribute("disabled", "true"); - "Making a test recording"; - fetch("/api/camera/snapshot-recording", { - method: "PUT", - .setAttribute("disabled", "true"); - Authorization: "Basic YWRtaW46ZmVhdGhlcnM=", - }, - }) - .then((response) => console.log(response)) - .then((data) => console.log(data)) - .catch((error) => console.error(error)); - }) - await new Promise((r) => setTimeout(r, 3000)); - document - .getElementById("take-snapshot-recording")! - .removeAttribute("disabled"); - document.getElementById("take-snapshot-recording")!.innerText = - "Take test recording"; +async function takeTestRecording() { + document.getElementById("take-snapshot-recording")!.innerText = 'Making a test recording'; + document.getElementById("take-snapshot-recording")!.setAttribute("disabled", "true"); + console.log("making a test recording"); + fetch('/api/camera/snapshot-recording', { + method: 'PUT', + headers: { + 'Authorization': 'Basic YWRtaW46ZmVhdGhlcnM=' + }}) + + .then(response => console.log(response)) + .then(data => console.log(data)) + .catch(error => console.error(error)) //TODO handle errors better and check that recording was made properly instead of just waiting.. - await new Promise((r) => setTimeout(r, 3000)); - document - .getElementById("take-snapshot-recording")! - .removeAttribute("disabled"); - document.getElementById("take-snapshot-recording")!.innerText = - "Take test recording"; + await new Promise(r => setTimeout(r, 3000)); + document.getElementById("take-snapshot-recording")!.removeAttribute("disabled"); + document.getElementById("take-snapshot-recording")!.innerText = 'Take test recording'; } function stopSnapshots(message: string) { @@ -204,50 +192,50 @@ function drawRectWithText( async function processFrame(frame: Frame) { const canvas = document.getElementById("frameCanvas") as HTMLCanvasElement; - if (canvas.width != frame.frameInfo.Camera.ResX) { - canvas.width = frame.frameInfo.Camera.ResX; - trackCanvas.width = frame.frameInfo.Camera.ResX; + + const trackCanvas = document.getElementById( + "trackCanvas" ) as HTMLCanvasElement; - if (canvas.height != frame.frameInfo.Camera.ResY) { - canvas.height = frame.frameInfo.Camera.ResY; - trackCanvas.height = frame.frameInfo.Camera.ResY; - if (canvas.width != frame.frameInfo.Camera.ResX) { - canvas.width = frame.frameInfo.Camera.ResX; - trackCanvas.width = frame.frameInfo.Camera.ResX; + if (canvas == null) { + return; + } + if( canvas.width != frame.frameInfo.Camera.ResX){ + canvas.width = frame.frameInfo.Camera.ResX + trackCanvas.width = frame.frameInfo.Camera.ResX } - if (canvas.height != frame.frameInfo.Camera.ResY) { - canvas.height = frame.frameInfo.Camera.ResY; - trackCanvas.height = frame.frameInfo.Camera.ResY; + if(canvas.height != frame.frameInfo.Camera.ResY){ + canvas.height = frame.frameInfo.Camera.ResY + trackCanvas.height = frame.frameInfo.Camera.ResY } const context = canvas.getContext("2d") as CanvasRenderingContext2D; const imgData = context.getImageData( - if (irCamera) { 0, - } else { + 0, + frame.frameInfo.Camera.ResX, frame.frameInfo.Camera.ResY ); - let max = 0; - let min = 0; - let range = 0; - if (!irCamera) { - } else { + // gp hack to see if ir camera, dbus from python makes dictionary have to be all int type + let irCamera = frame.frameInfo.Camera.ResX >= 640; + if(irCamera){ + document.getElementById("trigger-trap")!.style.display = ""; + }else{ document.getElementById("trigger-trap")!.style.display = "none"; } - let max = 0; - let min = 0; - let range = 0; - let pix = 0; - if (irCamera) { - pix = frame.frame[i]; - } else { + let max=0; + let min=0; + let range=0; + if (!irCamera){ + max = Math.max(...frame.frame); + min = Math.min(...frame.frame); + range = max - min; } let maxI = 0; for (let i = 0; i < frame.frame.length; i++) { - let pix = 0; - if (irCamera) { - pix = frame.frame[i]; - } else { - pix = Math.min(255, ((frame.frame[i] - min) / range) * 255.0); + let pix = 0 + if(irCamera){ + pix = frame.frame[i] + }else{ + pix = Math.min(255, ((frame.frame[i] - min) / range) * 255.0); } let index = i * 4; imgData.data[index] = pix; @@ -258,29 +246,24 @@ async function processFrame(frame: Frame) { } context.putImageData(imgData, 0, 0); - if (showTracks) { - const trackContext = trackCanvas.getContext( - "2d" - ) as CanvasRenderingContext2D; - trackContext.clearRect(0, 0, trackCanvas.width, trackCanvas.height); + const trackContext = trackCanvas.getContext("2d") as CanvasRenderingContext2D; + trackContext.clearRect(0, 0, trackCanvas.width, trackCanvas.height); - let index = 0; - if (frame.frameInfo.Tracks) { - for (const track of frame.frameInfo.Tracks) { - let what = null; - if (track.predictions && track.predictions.length > 0) { - } - what = track.predictions[0].label; - } - drawRectWithText( - trackContext, - frame.frameInfo.Camera, - track.positions[track.positions.length - 1], - what, - index - ); - index += 1; + let index = 0; + if (frame.frameInfo.Tracks) { + for (const track of frame.frameInfo.Tracks) { + let what = null; + if (track.predictions && track.predictions.length > 0) { + what = track.predictions[0].label; } + drawRectWithText( + trackContext, + frame.frameInfo.Camera, + track.positions[track.positions.length - 1], + what, + index + ); + index += 1; } } document.getElementById( @@ -430,3 +413,55 @@ export class CameraConnection { return null; } } + +function updateTestVideos(): void { + fetch("/api/test-videos", + { + method: "GET", + headers : { + "Authorization": "Basic " + btoa("admin:feathers"), + } + }) + .then(response => response.json()) + .then((videos: string[]) => { + if (videos.length === 0) { + return; + } + document.getElementById("test-videos")!.style.display = "block" + const dropdown = document.getElementById("test-video-options") as HTMLSelectElement; + videos.forEach(video => { + const option = document.createElement("option"); + option.value = video; + option.innerText = video; + dropdown.appendChild(option); + }) + }) + .catch(error => { + console.error("Error fetching test videos:", error); + }); +} + +function playTestVideo(): void { + const dropdown = document.getElementById("test-video-options") as HTMLSelectElement; + const selectedVideo: string = dropdown.value; + + if (selectedVideo && selectedVideo !== "Select Video Option") { + sendVideoRequest(selectedVideo); + } else { + alert("Please select a video option first!"); + } +} + +function sendVideoRequest(videoName: string): void { + fetch("/api/play-test-video", { + method: "POST", + headers: { + "Authorization": "Basic " + btoa("admin:feathers"), + "Content-Type": "application/json", + }, + body: JSON.stringify({ video: videoName }) + }) + .catch(error => { + console.error("Error playing test video:", error); + }); +} diff --git a/static/js/clock.js b/static/js/clock.js index e3dfb86..9cbbc29 100644 --- a/static/js/clock.js +++ b/static/js/clock.js @@ -10,6 +10,7 @@ async function getState() { $("#rtc-date-utc").html(response.RTCTimeUTC); $("#rtc-date-local").html(response.RTCTimeLocal); $("#system-date").html(response.SystemTime); + $("#timezone").html(response.Timezone); if (response.LowRTCBattery) { $("#rtc-battery").html("Low/Empty. Replace soon."); } else { @@ -32,7 +33,10 @@ async function getState() { async function setTime() { var now = new Date(); - var data = { date: now.toISOString() }; + var data = { + date: now.toISOString(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; try { await apiFormURLEncodedPost("/api/clock", data); alert("udpated time"); From 3764206c76ac9667824786175b7dddb9dfc2a71e Mon Sep 17 00:00:00 2001 From: Patrick Baxter Date: Wed, 15 May 2024 13:36:36 +1200 Subject: [PATCH 6/6] improve hotspot implementaiton --- api/hotspot.go | 56 ++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/api/hotspot.go b/api/hotspot.go index 5016d55..c48c323 100644 --- a/api/hotspot.go +++ b/api/hotspot.go @@ -68,15 +68,25 @@ func waitForInterface(interfaceName string, timeout time.Duration) error { return fmt.Errorf("interface %s did not come up within the specified timeout", interfaceName) } +func runCommand(command string, args ...string) error { + cmd := exec.Command(command, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("command %s failed: %w", command, err) + } + return nil +} + func startAccessPoint() error { - if err := exec.Command("systemctl", "restart", "hostapd").Run(); err != nil { + if err := runCommand("systemctl", "restart", "hostapd"); err != nil { return err } return waitForInterface("wlan0", 30*time.Second) } func stopAccessPoint() error { - return exec.Command("systemctl", "stop", "hostapd").Run() + return runCommand("systemctl", "stop", "hostapd") } func createDHCPConfig(isHotspot bool) error { @@ -93,28 +103,33 @@ func createDHCPConfig(isHotspot bool) error { func writeLines(filePath string, lines []string) error { file, err := os.Create(filePath) if err != nil { - return err + return fmt.Errorf("could not create file %s: %w", filePath, err) } defer file.Close() writer := bufio.NewWriter(file) for _, line := range lines { - fmt.Fprintln(writer, line) + if _, err := fmt.Fprintln(writer, line); err != nil { + return fmt.Errorf("could not write to file %s: %w", filePath, err) + } + } + if err := writer.Flush(); err != nil { + return fmt.Errorf("could not flush writer for file %s: %w", filePath, err) } - return writer.Flush() + return nil } func restartDHCP() error { - if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil { + if err := runCommand("systemctl", "daemon-reload"); err != nil { return err } - return exec.Command("systemctl", "restart", "dhcpcd").Run() + return runCommand("systemctl", "restart", "dhcpcd") } func checkIsConnectedToNetwork() (string, error) { output, err := exec.Command("iwgetid", "wlan0", "-r").Output() if err != nil { - return "", err + return "", fmt.Errorf("could not get network status: %w", err) } network := strings.TrimSpace(string(output)) if network == "" { @@ -147,41 +162,31 @@ func createDNSConfig(ipRange string) error { } func startDNS() error { - return exec.Command("systemctl", "restart", "dnsmasq").Run() + return runCommand("systemctl", "restart", "dnsmasq") } func stopDNS() error { - return exec.Command("systemctl", "stop", "dnsmasq").Run() + return runCommand("systemctl", "stop", "dnsmasq") } func createConfigFile(name string, config []string) error { - file, err := os.Create(name) - if err != nil { - return err - } - defer file.Close() - - writer := bufio.NewWriter(file) - for _, line := range config { - fmt.Fprintln(writer, line) - } - return writer.Flush() + return writeLines(name, config) } func enableNetwork(ssid string) error { output, err := exec.Command("wpa_cli", "list_networks").Output() if err != nil { - return err + return fmt.Errorf("could not list networks: %w", err) } networks := parseNetworks(string(output)) for id, network := range networks { if network == ssid { - return exec.Command("wpa_cli", "enable_network", id).Run() + return runCommand("wpa_cli", "enable_network", id) } } - return nil + return fmt.Errorf("network %s not found", ssid) } func parseNetworks(output string) map[string]string { @@ -199,10 +204,10 @@ func parseNetworks(output string) map[string]string { func initializeHotspot() error { log.Printf("Initializing hotspot...") + // Ensure that bushnet and Bushnet networks are enabled if err := enableNetwork("bushnet"); err != nil { log.Printf("Failed to enable bushnet network: %v", err) } - if err := enableNetwork("Bushnet"); err != nil { log.Printf("Failed to enable Bushnet network: %v", err) } @@ -211,6 +216,7 @@ func initializeHotspot() error { if err == nil { return fmt.Errorf("already connected to network: %s", network) } + if err := createDHCPConfig(true); err != nil { return fmt.Errorf("failed to create DHCP config: %v", err) }