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:
+17
-17
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user