feat: add comprehensive ONVIF test reports and enhance documentation

- Introduced CAMERA_TEST_REPORT.md and COMPREHENSIVE_TEST_SUMMARY.md to document testing results for the Bosch FLEXIDOME indoor 5100i IR camera.
- Added detailed analysis of ONVIF Media Service operations and implementation status in MEDIA_OPERATIONS_ANALYSIS.md and MEDIA_WSDL_OPERATIONS_ANALYSIS.md.
- Updated implementation status documentation to reflect the completion of all 79 operations in the ONVIF Media Service.
- Enhanced existing comments and documentation across various files for better clarity and consistency.
This commit is contained in:
0x524a
2025-12-02 02:29:51 -05:00
parent e530575bc1
commit 9e3b5e0170
61 changed files with 3001 additions and 1070 deletions
+17 -17
View File
@@ -9,7 +9,7 @@ import (
"strings"
)
// ASCIIConfig controls ASCII art generation parameters
// ASCIIConfig controls ASCII art generation parameters.
type ASCIIConfig struct {
Width int // Output width in characters
Height int // Output height in characters
@@ -17,7 +17,7 @@ type ASCIIConfig struct {
Quality string // "high", "medium", "low"
}
// DefaultASCIIConfig returns a sensible default configuration
// DefaultASCIIConfig returns a sensible default configuration.
func DefaultASCIIConfig() ASCIIConfig {
return ASCIIConfig{
Width: 120,
@@ -27,27 +27,26 @@ func DefaultASCIIConfig() ASCIIConfig {
}
}
// ASCIICharsets define different character options
// ASCIICharsets define different character options.
var (
// Full charset with many shades
// Full charset with many shades.
charsetFull = []rune{' ', '.', ':', '-', '=', '+', '*', '#', '%', '@'}
// Medium charset - balanced
// Medium charset - balanced.
charsetMedium = []rune{' ', '.', '-', '=', '+', '#', '@'}
// Simple charset - just a few chars
// Simple charset - just a few chars.
charsetSimple = []rune{' ', '-', '#', '@'}
// Block charset - using block characters
// Block charset - using block characters.
charsetBlock = []rune{' ', '░', '▒', '▓', '█'}
// Detailed charset
// Detailed charset.
charsetDetailed = []rune{' ', '`', '.', ',', ':', ';', '!', 'i', 'l', 'I',
'o', 'O', '0', 'e', 'E', 'p', 'P', 'x', 'X', '$', 'D', 'W', 'M', '@', '#'}
)
// ImageToASCII converts image bytes to ASCII art
// Supports JPEG and PNG formats
// Supports JPEG and PNG formats.
func ImageToASCII(imageData []byte, config ASCIIConfig) (string, error) {
// Decode image from bytes
img, _, err := image.Decode(bytes.NewReader(imageData))
@@ -58,7 +57,7 @@ func ImageToASCII(imageData []byte, config ASCIIConfig) (string, error) {
return imageToASCIIFromImage(img, config, "unknown")
}
// imageToASCIIFromImage is the core conversion function
// imageToASCIIFromImage is the core conversion function.
func imageToASCIIFromImage(img image.Image, config ASCIIConfig, format string) (string, error) {
// Validate configuration
if config.Width <= 0 {
@@ -139,8 +138,7 @@ func imageToASCIIFromImage(img image.Image, config ASCIIConfig, format string) (
return result.String(), nil
}
// calculateBrightness converts RGB to brightness (0-255)
// Uses standard luminance formula
// Uses standard luminance formula.
func calculateBrightness(r, g, b uint32) int {
// Convert 16-bit color to 8-bit
r8 := uint8(r >> 8)
@@ -161,7 +159,7 @@ func calculateBrightness(r, g, b uint32) int {
return brightness
}
// FormatASCIIOutput formats ASCII art with header and footer info
// FormatASCIIOutput formats ASCII art with header and footer info.
func FormatASCIIOutput(ascii string, imageInfo ImageInfo) string {
var result strings.Builder
@@ -199,7 +197,7 @@ func FormatASCIIOutput(ascii string, imageInfo ImageInfo) string {
return result.String()
}
// ImageInfo holds metadata about the snapshot
// ImageInfo holds metadata about the snapshot.
type ImageInfo struct {
Width int // Original width in pixels
Height int // Original height in pixels
@@ -208,7 +206,7 @@ type ImageInfo struct {
CaptureTime string // Capture timestamp
}
// formatBytes converts bytes to human-readable format
// formatBytes converts bytes to human-readable format.
func formatBytes(bytes int64) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
@@ -216,10 +214,11 @@ func formatBytes(bytes int64) string {
if bytes < 1024*1024 {
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
}
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
}
// CreateASCIIHighQuality creates a high-quality ASCII representation
// CreateASCIIHighQuality creates a high-quality ASCII representation.
func CreateASCIIHighQuality(imageData []byte) (string, error) {
config := ASCIIConfig{
Width: 160,
@@ -227,5 +226,6 @@ func CreateASCIIHighQuality(imageData []byte) (string, error) {
Invert: false,
Quality: "high",
}
return ImageToASCII(imageData, config)
}
+20
View File
@@ -0,0 +1,20 @@
package main
import "errors"
var (
// ErrNoNetworkInterfaces is returned when no network interfaces are found.
ErrNoNetworkInterfaces = errors.New("no network interfaces found")
// ErrNoCamerasFound is returned when no cameras are found on any interface.
ErrNoCamerasFound = errors.New("no cameras found on any interface")
// ErrNoActiveInterfaces is returned when no active interfaces are available for discovery.
ErrNoActiveInterfaces = errors.New("no active interfaces available for discovery")
// ErrNoProfilesFound is returned when no profiles are found.
ErrNoProfilesFound = errors.New("no profiles found")
// ErrNoVideoSourceConfiguration is returned when no video source configuration is found.
ErrNoVideoSourceConfiguration = errors.New("no video source configuration found")
)
+109 -23
View File
@@ -12,6 +12,7 @@ import (
"time"
sd "github.com/0x524A/rtspeek/pkg/rtspeek"
"github.com/0x524a/onvif-go"
"github.com/0x524a/onvif-go/discovery"
)
@@ -50,6 +51,7 @@ func main() {
cli.imagingOperations()
case "0", "q", "quit", "exit":
fmt.Println("Goodbye! 👋")
return
default:
fmt.Println("❌ Invalid option. Please try again.")
@@ -76,17 +78,21 @@ func (c *CLI) showMainMenu() {
func (c *CLI) readInput(prompt string) string {
fmt.Print(prompt)
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
input, _ := c.reader.ReadString('\n')
return strings.TrimSpace(input)
}
func (c *CLI) readInputWithDefault(prompt, defaultValue string) string {
fmt.Printf("%s [%s]: ", prompt, defaultValue)
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
input, _ := c.reader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "" {
return defaultValue
}
return input
}
@@ -118,6 +124,7 @@ func (c *CLI) discoverCameras() {
devices, err = c.discoverWithInterfaceSelection()
if err != nil {
fmt.Printf("❌ Discovery failed: %v\n", err)
return
}
}
@@ -131,6 +138,7 @@ func (c *CLI) discoverCameras() {
fmt.Println(" - Ensure you're on the same network segment as the cameras")
fmt.Println(" - Note: ONVIF requires multicast support (not available on WiFi)")
fmt.Println(" - Try discovering on wired Ethernet interfaces instead")
return
}
@@ -158,7 +166,7 @@ func (c *CLI) discoverCameras() {
// Ask if user wants to connect to one of the discovered cameras
if len(devices) > 0 {
connect := c.readInput("Do you want to connect to one of these cameras? (y/n): ")
if strings.ToLower(connect) == "y" || strings.ToLower(connect) == "yes" {
if strings.EqualFold(connect, "y") || strings.EqualFold(connect, "yes") {
if len(devices) == 1 {
c.connectToDiscoveredCamera(devices[0])
} else {
@@ -168,7 +176,7 @@ func (c *CLI) discoverCameras() {
}
}
// discoverWithInterfaceSelection shows available network interfaces and lets user select one
// discoverWithInterfaceSelection shows available network interfaces and lets user select one.
func (c *CLI) discoverWithInterfaceSelection() ([]*discovery.Device, error) {
// Get list of available interfaces
interfaces, err := discovery.ListNetworkInterfaces()
@@ -177,7 +185,7 @@ func (c *CLI) discoverWithInterfaceSelection() ([]*discovery.Device, error) {
}
if len(interfaces) == 0 {
return nil, fmt.Errorf("no network interfaces found")
return nil, fmt.Errorf("%w", ErrNoNetworkInterfaces)
}
// Check how many interfaces are usable (UP and with addresses)
@@ -191,6 +199,7 @@ func (c *CLI) discoverWithInterfaceSelection() ([]*discovery.Device, error) {
// If only one active interface, use it automatically
if len(activeInterfaces) == 1 {
fmt.Printf("📡 Using only active interface: %s\n", activeInterfaces[0].Name)
return c.performDiscoveryOnInterface(activeInterfaces[0].Name)
}
@@ -224,7 +233,8 @@ func (c *CLI) discoverWithInterfaceSelection() ([]*discovery.Device, error) {
if len(allDevices) > 0 {
return allDevices, nil
}
return nil, fmt.Errorf("no cameras found on any interface")
return nil, fmt.Errorf("%w", ErrNoCamerasFound)
}
// If no active interfaces found
@@ -243,10 +253,10 @@ func (c *CLI) discoverWithInterfaceSelection() ([]*discovery.Device, error) {
fmt.Printf(" %s (%s, Multicast: %s)\n", iface.Name, upStr, multicastStr)
}
return nil, fmt.Errorf("no active interfaces available for discovery")
return nil, fmt.Errorf("%w", ErrNoActiveInterfaces)
}
// performDiscoveryOnInterface performs discovery on a specific network interface
// performDiscoveryOnInterface performs discovery on a specific network interface.
func (c *CLI) performDiscoveryOnInterface(interfaceName string) ([]*discovery.Device, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
@@ -272,6 +282,7 @@ func (c *CLI) selectAndConnectCamera(devices []*discovery.Device) {
index, err := strconv.Atoi(choice)
if err != nil || index < 1 || index > len(devices) {
fmt.Println("❌ Invalid selection")
return
}
@@ -291,6 +302,7 @@ func (c *CLI) connectToDiscoveredCamera(device *discovery.Device) {
username := c.readInputWithDefault("Username", "admin")
fmt.Print("Password: ")
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
password, _ := c.reader.ReadString('\n')
password = strings.TrimSpace(password)
@@ -298,7 +310,7 @@ func (c *CLI) connectToDiscoveredCamera(device *discovery.Device) {
insecure := false
if strings.HasPrefix(endpoint, "https://") {
skipTLS := c.readInputWithDefault("Skip TLS certificate verification? (y/N)", "N")
insecure = strings.ToLower(skipTLS) == "y" || strings.ToLower(skipTLS) == "yes"
insecure = strings.EqualFold(skipTLS, "y") || strings.EqualFold(skipTLS, "yes")
}
c.createClient(endpoint, username, password, insecure)
@@ -308,7 +320,9 @@ func (c *CLI) connectToCamera() {
fmt.Println("🔗 Connect to Camera")
fmt.Println("===================")
endpoint := c.readInputWithDefault("Camera endpoint (http://ip:port/onvif/device_service)", "http://192.168.1.100/onvif/device_service")
endpoint := c.readInputWithDefault(
"Camera endpoint (http://ip:port/onvif/device_service)",
"http://192.168.1.100/onvif/device_service")
// Warn if using HTTPS
if strings.HasPrefix(endpoint, "https://") {
@@ -318,6 +332,7 @@ func (c *CLI) connectToCamera() {
username := c.readInputWithDefault("Username", "admin")
fmt.Print("Password: ")
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
password, _ := c.reader.ReadString('\n')
password = strings.TrimSpace(password)
@@ -325,7 +340,7 @@ func (c *CLI) connectToCamera() {
insecure := false
if strings.HasPrefix(endpoint, "https://") {
skipTLS := c.readInputWithDefault("Skip TLS certificate verification? (y/N)", "N")
insecure = strings.ToLower(skipTLS) == "y" || strings.ToLower(skipTLS) == "yes"
insecure = strings.EqualFold(skipTLS, "y") || strings.EqualFold(skipTLS, "yes")
}
c.createClient(endpoint, username, password, insecure)
@@ -347,6 +362,7 @@ func (c *CLI) createClient(endpoint, username, password string, insecure bool) {
client, err := onvif.NewClient(endpoint, opts...)
if err != nil {
fmt.Printf("❌ Failed to create client: %v\n", err)
return
}
@@ -360,9 +376,12 @@ func (c *CLI) createClient(endpoint, username, password string, insecure bool) {
fmt.Println(" - Endpoint URL is correct")
fmt.Println(" - Username and password are correct")
fmt.Println(" - Camera is accessible from this network")
if strings.Contains(err.Error(), "tls") || strings.Contains(err.Error(), "certificate") || strings.Contains(err.Error(), "x509") {
if strings.Contains(err.Error(), "tls") ||
strings.Contains(err.Error(), "certificate") ||
strings.Contains(err.Error(), "x509") {
fmt.Println(" - For HTTPS cameras with self-signed certificates, answer 'y' to skip TLS verification")
}
return
}
@@ -385,6 +404,7 @@ func (c *CLI) createClient(endpoint, username, password string, insecure bool) {
func (c *CLI) deviceOperations() {
if c.client == nil {
fmt.Println("❌ Not connected to any camera")
return
}
@@ -421,6 +441,7 @@ func (c *CLI) getDeviceInformation(ctx context.Context) {
info, err := c.client.GetDeviceInformation(ctx)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -438,6 +459,7 @@ func (c *CLI) getCapabilities(ctx context.Context) {
caps, err := c.client.GetCapabilities(ctx)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -469,6 +491,7 @@ func (c *CLI) getSystemDateTime(ctx context.Context) {
dateTime, err := c.client.GetSystemDateAndTime(ctx)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -477,8 +500,9 @@ func (c *CLI) getSystemDateTime(ctx context.Context) {
func (c *CLI) rebootDevice(ctx context.Context) {
confirm := c.readInput("⚠️ Are you sure you want to reboot the device? (y/N): ")
if strings.ToLower(confirm) != "y" && strings.ToLower(confirm) != "yes" {
fmt.Println("Reboot cancelled")
if !strings.EqualFold(confirm, "y") && !strings.EqualFold(confirm, "yes") {
fmt.Println("Reboot canceled")
return
}
@@ -487,6 +511,7 @@ func (c *CLI) rebootDevice(ctx context.Context) {
message, err := c.client.SystemReboot(ctx)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -497,6 +522,7 @@ func (c *CLI) rebootDevice(ctx context.Context) {
func (c *CLI) mediaOperations() {
if c.client == nil {
fmt.Println("❌ Not connected to any camera")
return
}
@@ -533,6 +559,7 @@ func (c *CLI) getMediaProfiles(ctx context.Context) {
profiles, err := c.client.GetProfiles(ctx)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -560,7 +587,7 @@ func (c *CLI) getMediaProfiles(ctx context.Context) {
}
}
// inspectRTSPStream probes an RTSP URI to get stream details using rtspeek library
// inspectRTSPStream probes an RTSP URI to get stream details using rtspeek library.
func (c *CLI) inspectRTSPStream(streamURI string) map[string]interface{} {
details := map[string]interface{}{
"uri": streamURI,
@@ -603,6 +630,7 @@ func (c *CLI) inspectRTSPStream(streamURI string) map[string]interface{} {
// Describe failed but connection was reachable - try TCP fallback
if streamInfo.IsReachable() {
details["reachable"] = true
return details
}
}
@@ -615,7 +643,7 @@ func (c *CLI) inspectRTSPStream(streamURI string) map[string]interface{} {
return details
}
// tryRTSPConnection attempts to connect to RTSP port and grab basic info
// tryRTSPConnection attempts to connect to RTSP port and grab basic info.
func (c *CLI) tryRTSPConnection(streamURI string) map[string]interface{} {
details := map[string]interface{}{
"uri": streamURI,
@@ -635,15 +663,17 @@ func (c *CLI) tryRTSPConnection(streamURI string) map[string]interface{} {
// Default RTSP port if not specified
if !strings.Contains(hostPort, ":") {
hostPort = hostPort + ":554"
hostPort += ":554"
}
// Try to connect
conn, err := net.DialTimeout("tcp", hostPort, 3*time.Second)
if err == nil {
_ = conn.Close() // Ignore error on close for connectivity check
//nolint:errcheck // Close error is not critical for connectivity check
_ = conn.Close()
details["reachable"] = true
details["port"] = strings.Split(hostPort, ":")[1]
return details
}
@@ -654,11 +684,13 @@ func (c *CLI) getStreamURIs(ctx context.Context) {
profiles, err := c.client.GetProfiles(ctx)
if err != nil {
fmt.Printf("❌ Error getting profiles: %v\n", err)
return
}
if len(profiles) == 0 {
fmt.Println("❌ No profiles found")
return
}
@@ -716,11 +748,13 @@ func (c *CLI) getSnapshotURIs(ctx context.Context) {
profiles, err := c.client.GetProfiles(ctx)
if err != nil {
fmt.Printf("❌ Error getting profiles: %v\n", err)
return
}
if len(profiles) == 0 {
fmt.Println("❌ No profiles found")
return
}
@@ -753,11 +787,13 @@ func (c *CLI) getVideoEncoderConfig(ctx context.Context) {
profiles, err := c.client.GetProfiles(ctx)
if err != nil {
fmt.Printf("❌ Error getting profiles: %v\n", err)
return
}
if len(profiles) == 0 {
fmt.Println("❌ No profiles found")
return
}
@@ -770,12 +806,14 @@ func (c *CLI) getVideoEncoderConfig(ctx context.Context) {
index, err := strconv.Atoi(choice)
if err != nil || index < 1 || index > len(profiles) {
fmt.Println("❌ Invalid selection")
return
}
profile := profiles[index-1]
if profile.VideoEncoderConfiguration == nil {
fmt.Println("❌ No video encoder configuration found")
return
}
@@ -784,6 +822,7 @@ func (c *CLI) getVideoEncoderConfig(ctx context.Context) {
config, err := c.client.GetVideoEncoderConfiguration(ctx, profile.VideoEncoderConfiguration.Token)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -809,6 +848,7 @@ func (c *CLI) getVideoEncoderConfig(ctx context.Context) {
func (c *CLI) ptzOperations() {
if c.client == nil {
fmt.Println("❌ Not connected to any camera")
return
}
@@ -830,6 +870,7 @@ func (c *CLI) ptzOperations() {
profileToken, err := c.getPTZProfileToken(ctx)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -862,7 +903,7 @@ func (c *CLI) getPTZProfileToken(ctx context.Context) (string, error) {
}
if len(profiles) == 0 {
return "", fmt.Errorf("no profiles found")
return "", fmt.Errorf("%w", ErrNoProfilesFound)
}
// Find a profile with PTZ configuration
@@ -874,6 +915,7 @@ func (c *CLI) getPTZProfileToken(ctx context.Context) (string, error) {
// If no PTZ profile found, use the first profile
fmt.Println("⚠️ No PTZ-specific profile found, using first profile")
return profiles[0].Token, nil
}
@@ -884,6 +926,7 @@ func (c *CLI) getPTZStatus(ctx context.Context, profileToken string) {
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
fmt.Println("💡 PTZ might not be supported on this camera")
return
}
@@ -919,8 +962,11 @@ func (c *CLI) continuousMove(ctx context.Context, profileToken string) {
zoomStr := c.readInputWithDefault("Zoom speed (-1.0 to 1.0)", "0.0")
timeoutStr := c.readInputWithDefault("Timeout (seconds)", "2")
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
pan, _ := strconv.ParseFloat(panStr, 64)
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
tilt, _ := strconv.ParseFloat(tiltStr, 64)
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
zoom, _ := strconv.ParseFloat(zoomStr, 64)
velocity := &onvif.PTZSpeed{
@@ -935,6 +981,7 @@ func (c *CLI) continuousMove(ctx context.Context, profileToken string) {
err := c.client.ContinuousMove(ctx, profileToken, velocity, &timeout)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -949,8 +996,11 @@ func (c *CLI) absoluteMove(ctx context.Context, profileToken string) {
tiltStr := c.readInputWithDefault("Tilt position (-1.0 to 1.0)", "0.0")
zoomStr := c.readInputWithDefault("Zoom position (-1.0 to 1.0)", "0.0")
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
pan, _ := strconv.ParseFloat(panStr, 64)
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
tilt, _ := strconv.ParseFloat(tiltStr, 64)
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
zoom, _ := strconv.ParseFloat(zoomStr, 64)
position := &onvif.PTZVector{
@@ -963,6 +1013,7 @@ func (c *CLI) absoluteMove(ctx context.Context, profileToken string) {
err := c.client.AbsoluteMove(ctx, profileToken, position, nil)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -977,8 +1028,11 @@ func (c *CLI) relativeMove(ctx context.Context, profileToken string) {
tiltStr := c.readInputWithDefault("Tilt translation (-1.0 to 1.0)", "0.0")
zoomStr := c.readInputWithDefault("Zoom translation (-1.0 to 1.0)", "0.0")
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
pan, _ := strconv.ParseFloat(panStr, 64)
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
tilt, _ := strconv.ParseFloat(tiltStr, 64)
//nolint:errcheck // ParseFloat errors default to 0.0 which is acceptable for CLI input
zoom, _ := strconv.ParseFloat(zoomStr, 64)
translation := &onvif.PTZVector{
@@ -991,6 +1045,7 @@ func (c *CLI) relativeMove(ctx context.Context, profileToken string) {
err := c.client.RelativeMove(ctx, profileToken, translation, nil)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1001,14 +1056,15 @@ func (c *CLI) stopMovement(ctx context.Context, profileToken string) {
stopPanTilt := c.readInputWithDefault("Stop Pan/Tilt? (y/n)", "y")
stopZoom := c.readInputWithDefault("Stop Zoom? (y/n)", "y")
panTilt := strings.ToLower(stopPanTilt) == "y" || strings.ToLower(stopPanTilt) == "yes"
zoom := strings.ToLower(stopZoom) == "y" || strings.ToLower(stopZoom) == "yes"
panTilt := strings.EqualFold(stopPanTilt, "y") || strings.EqualFold(stopPanTilt, "yes")
zoom := strings.EqualFold(stopZoom, "y") || strings.EqualFold(stopZoom, "yes")
fmt.Println("⏳ Stopping movement...")
err := c.client.Stop(ctx, profileToken, panTilt, zoom)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1021,11 +1077,13 @@ func (c *CLI) getPTZPresets(ctx context.Context, profileToken string) {
presets, err := c.client.GetPresets(ctx, profileToken)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
if len(presets) == 0 {
fmt.Println("📝 No presets found")
return
}
@@ -1054,11 +1112,13 @@ func (c *CLI) gotoPreset(ctx context.Context, profileToken string) {
presets, err := c.client.GetPresets(ctx, profileToken)
if err != nil {
fmt.Printf("❌ Error getting presets: %v\n", err)
return
}
if len(presets) == 0 {
fmt.Println("📝 No presets available")
return
}
@@ -1071,6 +1131,7 @@ func (c *CLI) gotoPreset(ctx context.Context, profileToken string) {
index, err := strconv.Atoi(choice)
if err != nil || index < 1 || index > len(presets) {
fmt.Println("❌ Invalid selection")
return
}
@@ -1081,6 +1142,7 @@ func (c *CLI) gotoPreset(ctx context.Context, profileToken string) {
err = c.client.GotoPreset(ctx, profileToken, preset.Token, nil)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1090,6 +1152,7 @@ func (c *CLI) gotoPreset(ctx context.Context, profileToken string) {
func (c *CLI) imagingOperations() {
if c.client == nil {
fmt.Println("❌ Not connected to any camera")
return
}
@@ -1111,6 +1174,7 @@ func (c *CLI) imagingOperations() {
videoSourceToken, err := c.getVideoSourceToken(ctx)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1143,7 +1207,7 @@ func (c *CLI) getVideoSourceToken(ctx context.Context) (string, error) {
}
if len(profiles) == 0 {
return "", fmt.Errorf("no profiles found")
return "", fmt.Errorf("%w", ErrNoProfilesFound)
}
for _, profile := range profiles {
@@ -1152,7 +1216,7 @@ func (c *CLI) getVideoSourceToken(ctx context.Context) (string, error) {
}
}
return "", fmt.Errorf("no video source configuration found")
return "", fmt.Errorf("%w", ErrNoVideoSourceConfiguration)
}
func (c *CLI) getImagingSettings(ctx context.Context, videoSourceToken string) {
@@ -1161,6 +1225,7 @@ func (c *CLI) getImagingSettings(ctx context.Context, videoSourceToken string) {
settings, err := c.client.GetImagingSettings(ctx, videoSourceToken)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1208,6 +1273,7 @@ func (c *CLI) setBrightness(ctx context.Context, videoSourceToken string) {
currentSettings, err := c.client.GetImagingSettings(ctx, videoSourceToken)
if err != nil {
fmt.Printf("❌ Error getting current settings: %v\n", err)
return
}
@@ -1220,6 +1286,7 @@ func (c *CLI) setBrightness(ctx context.Context, videoSourceToken string) {
brightness, err := strconv.ParseFloat(brightnessStr, 64)
if err != nil {
fmt.Println("❌ Invalid brightness value")
return
}
@@ -1230,6 +1297,7 @@ func (c *CLI) setBrightness(ctx context.Context, videoSourceToken string) {
err = c.client.SetImagingSettings(ctx, videoSourceToken, currentSettings, true)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1240,6 +1308,7 @@ func (c *CLI) setContrast(ctx context.Context, videoSourceToken string) {
currentSettings, err := c.client.GetImagingSettings(ctx, videoSourceToken)
if err != nil {
fmt.Printf("❌ Error getting current settings: %v\n", err)
return
}
@@ -1252,6 +1321,7 @@ func (c *CLI) setContrast(ctx context.Context, videoSourceToken string) {
contrast, err := strconv.ParseFloat(contrastStr, 64)
if err != nil {
fmt.Println("❌ Invalid contrast value")
return
}
@@ -1262,6 +1332,7 @@ func (c *CLI) setContrast(ctx context.Context, videoSourceToken string) {
err = c.client.SetImagingSettings(ctx, videoSourceToken, currentSettings, true)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1272,6 +1343,7 @@ func (c *CLI) setSaturation(ctx context.Context, videoSourceToken string) {
currentSettings, err := c.client.GetImagingSettings(ctx, videoSourceToken)
if err != nil {
fmt.Printf("❌ Error getting current settings: %v\n", err)
return
}
@@ -1284,6 +1356,7 @@ func (c *CLI) setSaturation(ctx context.Context, videoSourceToken string) {
saturation, err := strconv.ParseFloat(saturationStr, 64)
if err != nil {
fmt.Println("❌ Invalid saturation value")
return
}
@@ -1294,6 +1367,7 @@ func (c *CLI) setSaturation(ctx context.Context, videoSourceToken string) {
err = c.client.SetImagingSettings(ctx, videoSourceToken, currentSettings, true)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1304,6 +1378,7 @@ func (c *CLI) setSharpness(ctx context.Context, videoSourceToken string) {
currentSettings, err := c.client.GetImagingSettings(ctx, videoSourceToken)
if err != nil {
fmt.Printf("❌ Error getting current settings: %v\n", err)
return
}
@@ -1316,6 +1391,7 @@ func (c *CLI) setSharpness(ctx context.Context, videoSourceToken string) {
sharpness, err := strconv.ParseFloat(sharpnessStr, 64)
if err != nil {
fmt.Println("❌ Invalid sharpness value")
return
}
@@ -1326,6 +1402,7 @@ func (c *CLI) setSharpness(ctx context.Context, videoSourceToken string) {
err = c.client.SetImagingSettings(ctx, videoSourceToken, currentSettings, true)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1340,6 +1417,7 @@ func (c *CLI) advancedImagingSettings(ctx context.Context, videoSourceToken stri
currentSettings, err := c.client.GetImagingSettings(ctx, videoSourceToken)
if err != nil {
fmt.Printf("❌ Error getting current settings: %v\n", err)
return
}
@@ -1373,8 +1451,9 @@ func (c *CLI) advancedImagingSettings(ctx context.Context, videoSourceToken stri
}
confirm := c.readInput("Apply these settings? (y/N): ")
if strings.ToLower(confirm) != "y" && strings.ToLower(confirm) != "yes" {
if !strings.EqualFold(confirm, "y") && !strings.EqualFold(confirm, "yes") {
fmt.Println("Settings not applied")
return
}
@@ -1383,6 +1462,7 @@ func (c *CLI) advancedImagingSettings(ctx context.Context, videoSourceToken stri
err = c.client.SetImagingSettings(ctx, videoSourceToken, currentSettings, true)
if err != nil {
fmt.Printf("❌ Error: %v\n", err)
return
}
@@ -1400,11 +1480,13 @@ func (c *CLI) captureAndDisplaySnapshot(ctx context.Context) {
profiles, err := c.client.GetProfiles(ctx)
if err != nil {
fmt.Printf("❌ Failed to get profiles: %v\n", err)
return
}
if len(profiles) == 0 {
fmt.Println("❌ No profiles found")
return
}
@@ -1416,11 +1498,13 @@ func (c *CLI) captureAndDisplaySnapshot(ctx context.Context) {
snapshotURI, err := c.client.GetSnapshotURI(ctx, profile.Token)
if err != nil {
fmt.Printf("❌ Failed to get snapshot URI: %v\n", err)
return
}
if snapshotURI == nil || snapshotURI.URI == "" {
fmt.Println("❌ No snapshot URI available")
return
}
@@ -1470,6 +1554,7 @@ func (c *CLI) captureAndDisplaySnapshot(ctx context.Context) {
fmt.Printf("❌ Failed to download snapshot: %v\n", err)
fmt.Println("\n💡 Try using curl directly:")
fmt.Printf(" curl -u username:password '%s' > snapshot.jpg\n", snapshotURI.URI)
return
}
@@ -1483,6 +1568,7 @@ func (c *CLI) captureAndDisplaySnapshot(ctx context.Context) {
fmt.Printf("❌ Failed to convert image: %v\n", err)
fmt.Println("\n💡 Image might not be JPEG/PNG. Try downloading manually:")
fmt.Printf(" curl -u username:password '%s' > snapshot.jpg\n", snapshotURI.URI)
return
}
@@ -1504,7 +1590,7 @@ func (c *CLI) captureAndDisplaySnapshot(ctx context.Context) {
// Offer to save the snapshot
fmt.Println()
save := c.readInput("💾 Save snapshot to file? (y/n) [n]: ")
if strings.ToLower(save) == "y" {
if strings.EqualFold(save, "y") {
filename := c.readInput("📝 Filename [snapshot.jpg]: ")
if filename == "" {
filename = "snapshot.jpg"