mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
feat: implement WebRTC signaling and UDP-based video streaming and control service
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
@@ -172,7 +173,6 @@ func (s *UDPVideoService) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
}
|
||||
|
||||
if !hasDevice {
|
||||
s.logger.Warn("cannot send command: no ESP32 devices discovered yet")
|
||||
continue
|
||||
@@ -188,13 +188,12 @@ func (s *UDPVideoService) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
}()
|
||||
}
|
||||
|
||||
// SendCommand sends a command string to the ESP32 representing deviceID.
|
||||
func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error {
|
||||
// SendControlPacket sends a 5-byte binary control packet to the ESP32 representing deviceID.
|
||||
func (s *UDPVideoService) SendControlPacket(deviceID uint8, packet [5]byte) error {
|
||||
s.mu.RLock()
|
||||
dev, ok := s.devices[deviceID]
|
||||
conn := s.conn
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok || dev.addr == nil {
|
||||
return fmt.Errorf("ESP32 device %d address unknown (no video packets received yet)", deviceID)
|
||||
}
|
||||
@@ -207,10 +206,35 @@ func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error {
|
||||
Port: s.espCmdPort,
|
||||
}
|
||||
|
||||
_, err := conn.WriteToUDP([]byte(cmd), cmdAddr)
|
||||
_, err := conn.WriteToUDP(packet[:], cmdAddr)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendCommand sends a command string to the ESP32 representing deviceID by mapping it to a 5-byte packet.
|
||||
func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error {
|
||||
var packet [5]byte
|
||||
packet[0] = 100 // Steer: center
|
||||
packet[1] = 0 // Throttle: 0%
|
||||
packet[2] = 0 // Brake: 0%
|
||||
packet[3] = 1 // Gear: Neutral
|
||||
packet[4] = 0 // Flags: 0
|
||||
|
||||
switch cmd {
|
||||
case "start":
|
||||
packet[1] = 100 // Throttle: 100%
|
||||
packet[3] = 2 // Gear: Drive
|
||||
case "stop":
|
||||
packet[2] = 100 // Brake: 100%
|
||||
packet[3] = 1 // Gear: Neutral
|
||||
case "left":
|
||||
packet[0] = 0 // Steer: full left
|
||||
case "right":
|
||||
packet[0] = 200 // Steer: full right
|
||||
}
|
||||
|
||||
return s.SendControlPacket(deviceID, packet)
|
||||
}
|
||||
|
||||
func (s *UDPVideoService) broadcastFrame(deviceID uint8, frame []byte) {
|
||||
// dev.latestFrame is updated inside the caller's lock (Start loop) to avoid races
|
||||
if dev, ok := s.devices[deviceID]; ok {
|
||||
@@ -328,13 +352,14 @@ func (s *UDPVideoService) StreamHandler() http.HandlerFunc {
|
||||
|
||||
// ControlHandler godoc
|
||||
// @Summary Send command to ESP32
|
||||
// @Description Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888).
|
||||
// @Description Sends a text command or a JSON control packet to the ESP32 command port (UDP 8888).
|
||||
// @Tags video
|
||||
// @Param id query int false "Device ID (0..127)"
|
||||
// @Param cmd query string true "Command text"
|
||||
// @Param cmd query string false "Command text (legacy)"
|
||||
// @Param body body map[string]int false "Control packet JSON (steer, throttle, brake, gear, flags)"
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "Command sent"
|
||||
// @Failure 400 {string} string "Missing cmd parameter"
|
||||
// @Failure 400 {string} string "Missing parameter or invalid body"
|
||||
// @Failure 500 {string} string "Internal error"
|
||||
// @Router /api/video/control [post]
|
||||
func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
|
||||
@@ -343,14 +368,6 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed")
|
||||
return
|
||||
}
|
||||
cmd := r.URL.Query().Get("cmd")
|
||||
if cmd == "" {
|
||||
cmd = r.FormValue("cmd")
|
||||
}
|
||||
if cmd == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "Missing 'cmd' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
idVal := r.URL.Query().Get("id")
|
||||
if idVal == "" {
|
||||
@@ -366,7 +383,109 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
err := s.SendCommand(deviceID, cmd)
|
||||
var packet [5]byte
|
||||
packet[0] = 100 // Steer: center
|
||||
packet[1] = 0 // Throttle: 0%
|
||||
packet[2] = 0 // Brake: 0%
|
||||
packet[3] = 1 // Gear: Neutral
|
||||
packet[4] = 0 // Flags: 0
|
||||
|
||||
isJSON := strings.Contains(r.Header.Get("Content-Type"), "application/json")
|
||||
var cmdUsed string
|
||||
|
||||
if isJSON {
|
||||
var req struct {
|
||||
Steer *int `json:"steer"`
|
||||
Throttle *int `json:"throttle"`
|
||||
Brake *int `json:"brake"`
|
||||
Gear *int `json:"gear"`
|
||||
Flags *int `json:"flags"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "invalid JSON payload")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Steer != nil {
|
||||
val := *req.Steer
|
||||
if val < 0 {
|
||||
val = 0
|
||||
}
|
||||
if val > 200 {
|
||||
val = 200
|
||||
}
|
||||
packet[0] = uint8(val)
|
||||
}
|
||||
if req.Throttle != nil {
|
||||
val := *req.Throttle
|
||||
if val < 0 {
|
||||
val = 0
|
||||
}
|
||||
if val > 100 {
|
||||
val = 100
|
||||
}
|
||||
packet[1] = uint8(val)
|
||||
}
|
||||
if req.Brake != nil {
|
||||
val := *req.Brake
|
||||
if val < 0 {
|
||||
val = 0
|
||||
}
|
||||
if val > 100 {
|
||||
val = 100
|
||||
}
|
||||
packet[2] = uint8(val)
|
||||
}
|
||||
if req.Gear != nil {
|
||||
val := *req.Gear
|
||||
if val < 0 {
|
||||
val = 0
|
||||
}
|
||||
if val > 2 {
|
||||
val = 2
|
||||
}
|
||||
packet[3] = uint8(val)
|
||||
}
|
||||
if req.Flags != nil {
|
||||
val := *req.Flags
|
||||
if val < 0 {
|
||||
val = 0
|
||||
}
|
||||
if val > 255 {
|
||||
val = 255
|
||||
}
|
||||
packet[4] = uint8(val)
|
||||
}
|
||||
cmdUsed = fmt.Sprintf("json(steer=%d,throttle=%d,brake=%d,gear=%d,flags=%d)", packet[0], packet[1], packet[2], packet[3], packet[4])
|
||||
} else {
|
||||
cmd := r.URL.Query().Get("cmd")
|
||||
if cmd == "" {
|
||||
cmd = r.FormValue("cmd")
|
||||
}
|
||||
if cmd == "" {
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "Missing 'cmd' parameter")
|
||||
return
|
||||
}
|
||||
cmdUsed = cmd
|
||||
|
||||
switch cmd {
|
||||
case "start":
|
||||
packet[1] = 100
|
||||
packet[3] = 2
|
||||
case "stop":
|
||||
packet[2] = 100
|
||||
packet[3] = 1
|
||||
case "left":
|
||||
packet[0] = 0
|
||||
case "right":
|
||||
packet[0] = 200
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "bad_request", "Unknown command: "+cmd)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := s.SendControlPacket(deviceID, packet)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", err.Error())
|
||||
return
|
||||
@@ -375,7 +494,8 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"status": "success",
|
||||
"device_id": deviceID,
|
||||
"command": cmd,
|
||||
"command": cmdUsed,
|
||||
"packet": packet[:],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user