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:
+18
-10
@@ -171,16 +171,18 @@ func main() {
|
||||
if len(capture.Exchanges) > 0 {
|
||||
// Try to parse device info from response
|
||||
for _, ex := range capture.Exchanges {
|
||||
if strings.Contains(ex.RequestBody, "GetDeviceInformation") {
|
||||
// Extract manufacturer and model from response
|
||||
manufacturer := extractXMLValue(ex.ResponseBody, "Manufacturer")
|
||||
model := extractXMLValue(ex.ResponseBody, "Model")
|
||||
firmware := extractXMLValue(ex.ResponseBody, "FirmwareVersion")
|
||||
if manufacturer != "" && model != "" {
|
||||
cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)", manufacturer, model, firmware)
|
||||
}
|
||||
break
|
||||
if !strings.Contains(ex.RequestBody, "GetDeviceInformation") {
|
||||
continue
|
||||
}
|
||||
// Extract manufacturer and model from response
|
||||
manufacturer := extractXMLValue(ex.ResponseBody, "Manufacturer")
|
||||
model := extractXMLValue(ex.ResponseBody, "Model")
|
||||
firmware := extractXMLValue(ex.ResponseBody, "FirmwareVersion")
|
||||
if manufacturer != "" && model != "" {
|
||||
cameraDesc = fmt.Sprintf("%s %s (Firmware: %s)", manufacturer, model, firmware)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,9 +219,15 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create output file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() {
|
||||
//nolint:errcheck // Close error is not critical, file is already written
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
if err := tmpl.Execute(f, testData); err != nil {
|
||||
//nolint:errcheck // Close error is not critical before fatal exit
|
||||
_ = f.Close()
|
||||
//nolint:gocritic // Fatalf exits, defer won't run - this is acceptable
|
||||
log.Fatalf("Failed to execute template: %v", err)
|
||||
}
|
||||
|
||||
|
||||
+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"
|
||||
|
||||
+113
-70
@@ -160,7 +160,9 @@ func main() {
|
||||
flag.PrintDefaults()
|
||||
fmt.Println()
|
||||
fmt.Println("Example:")
|
||||
fmt.Println(" ./onvif-diagnostics -endpoint http://192.168.1.201/onvif/device_service -username service -password Service.1234")
|
||||
fmt.Println(" ./onvif-diagnostics -endpoint " +
|
||||
"http://192.168.1.201/onvif/device_service " +
|
||||
"-username service -password Service.1234")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -240,67 +242,67 @@ func main() {
|
||||
fmt.Println()
|
||||
|
||||
// Test 1: Get Device Information
|
||||
logStep("1. Getting device information...")
|
||||
logStepf("1. Getting device information...")
|
||||
report.DeviceInfo = testGetDeviceInformation(ctx, client, report)
|
||||
|
||||
// Test 2: Get System Date and Time
|
||||
logStep("2. Getting system date and time...")
|
||||
logStepf("2. Getting system date and time...")
|
||||
report.SystemDateTime = testGetSystemDateTime(ctx, client, report)
|
||||
|
||||
// Test 3: Get Capabilities
|
||||
logStep("3. Getting capabilities...")
|
||||
logStepf("3. Getting capabilities...")
|
||||
report.Capabilities = testGetCapabilities(ctx, client, report)
|
||||
|
||||
// Test 4: Initialize (discover services)
|
||||
logStep("4. Discovering service endpoints...")
|
||||
logStepf("4. Discovering service endpoints...")
|
||||
if err := client.Initialize(ctx); err != nil {
|
||||
logError("Service discovery failed: %v", err)
|
||||
logErrorf("Service discovery failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "Initialize",
|
||||
Error: err.Error(),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
} else {
|
||||
logSuccess("Service endpoints discovered")
|
||||
logSuccessf("Service endpoints discovered")
|
||||
}
|
||||
|
||||
// Test 5: Get Profiles
|
||||
logStep("5. Getting media profiles...")
|
||||
logStepf("5. Getting media profiles...")
|
||||
report.Profiles = testGetProfiles(ctx, client, report)
|
||||
|
||||
// Test 6: Get Stream URIs (for each profile)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStep("6. Getting stream URIs for all profiles...")
|
||||
logStepf("6. Getting stream URIs for all profiles...")
|
||||
report.StreamURIs = testGetStreamURIs(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 7: Get Snapshot URIs (for each profile)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStep("7. Getting snapshot URIs for all profiles...")
|
||||
logStepf("7. Getting snapshot URIs for all profiles...")
|
||||
report.SnapshotURIs = testGetSnapshotURIs(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 8: Get Video Encoder Configurations
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStep("8. Getting video encoder configurations...")
|
||||
logStepf("8. Getting video encoder configurations...")
|
||||
report.VideoEncoders = testGetVideoEncoders(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 9: Get Imaging Settings
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStep("9. Getting imaging settings...")
|
||||
logStepf("9. Getting imaging settings...")
|
||||
report.ImagingSettings = testGetImagingSettings(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 10: Get PTZ Status (if PTZ is available)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStep("10. Getting PTZ status...")
|
||||
logStepf("10. Getting PTZ status...")
|
||||
report.PTZStatus = testGetPTZStatus(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
// Test 11: Get PTZ Presets (if PTZ is available)
|
||||
if report.Profiles != nil && report.Profiles.Success {
|
||||
logStep("11. Getting PTZ presets...")
|
||||
logStepf("11. Getting PTZ presets...")
|
||||
report.PTZPresets = testGetPTZPresets(ctx, client, report.Profiles.Data, report)
|
||||
}
|
||||
|
||||
@@ -309,7 +311,7 @@ func main() {
|
||||
outputPath := filepath.Join(*outputDir, filename)
|
||||
|
||||
// Save report
|
||||
logStep("Saving diagnostic report...")
|
||||
logStepf("Saving diagnostic report...")
|
||||
if err := saveReport(report, outputPath); err != nil {
|
||||
log.Fatalf("Failed to save report: %v", err)
|
||||
}
|
||||
@@ -317,7 +319,7 @@ func main() {
|
||||
// Create XML archive if capture was enabled
|
||||
if *captureXML && loggingTransport != nil {
|
||||
fmt.Println()
|
||||
logStep("Creating XML capture archive...")
|
||||
logStepf("Creating XML capture archive...")
|
||||
|
||||
// Generate archive name based on device info
|
||||
var archiveName string
|
||||
@@ -335,14 +337,14 @@ func main() {
|
||||
archivePath := filepath.Join(*outputDir, archiveName)
|
||||
|
||||
if err := createTarGz(xmlCaptureDir, archivePath); err != nil {
|
||||
logError("Failed to create XML archive: %v", err)
|
||||
logErrorf("Failed to create XML archive: %v", err)
|
||||
} else {
|
||||
logSuccess("XML archive created: %s", archiveName)
|
||||
logSuccess("Total SOAP calls captured: %d", loggingTransport.Counter)
|
||||
logSuccessf("XML archive created: %s", archiveName)
|
||||
logSuccessf("Total SOAP calls captured: %d", loggingTransport.Counter)
|
||||
|
||||
// Remove temporary directory
|
||||
if err := os.RemoveAll(xmlCaptureDir); err != nil {
|
||||
logError("Warning: Failed to remove temp directory: %v", err)
|
||||
logErrorf("Warning: Failed to remove temp directory: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,7 +385,7 @@ func testGetDeviceInformation(ctx context.Context, client *onvif.Client, report
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
logError("Failed: %v", err)
|
||||
logErrorf("Failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "GetDeviceInformation",
|
||||
Error: err.Error(),
|
||||
@@ -392,7 +394,7 @@ func testGetDeviceInformation(ctx context.Context, client *onvif.Client, report
|
||||
} else {
|
||||
result.Success = true
|
||||
result.Data = info
|
||||
logSuccess("Manufacturer: %s, Model: %s", info.Manufacturer, info.Model)
|
||||
logSuccessf("Manufacturer: %s, Model: %s", info.Manufacturer, info.Model)
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -408,7 +410,7 @@ func testGetSystemDateTime(ctx context.Context, client *onvif.Client, report *Ca
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
logError("Failed: %v", err)
|
||||
logErrorf("Failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "GetSystemDateAndTime",
|
||||
Error: err.Error(),
|
||||
@@ -417,7 +419,7 @@ func testGetSystemDateTime(ctx context.Context, client *onvif.Client, report *Ca
|
||||
} else {
|
||||
result.Success = true
|
||||
result.Data = dateTime
|
||||
logSuccess("Retrieved")
|
||||
logSuccessf("Retrieved")
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -433,7 +435,7 @@ func testGetCapabilities(ctx context.Context, client *onvif.Client, report *Came
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
logError("Failed: %v", err)
|
||||
logErrorf("Failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "GetCapabilities",
|
||||
Error: err.Error(),
|
||||
@@ -463,7 +465,7 @@ func testGetCapabilities(ctx context.Context, client *onvif.Client, report *Came
|
||||
services = append(services, "Analytics")
|
||||
}
|
||||
|
||||
logSuccess("Services: %s", strings.Join(services, ", "))
|
||||
logSuccessf("Services: %s", strings.Join(services, ", "))
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -479,7 +481,7 @@ func testGetProfiles(ctx context.Context, client *onvif.Client, report *CameraRe
|
||||
if err != nil {
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
logError("Failed: %v", err)
|
||||
logErrorf("Failed: %v", err)
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: "GetProfiles",
|
||||
Error: err.Error(),
|
||||
@@ -489,7 +491,7 @@ func testGetProfiles(ctx context.Context, client *onvif.Client, report *CameraRe
|
||||
result.Success = true
|
||||
result.Data = profiles
|
||||
result.Count = len(profiles)
|
||||
logSuccess("Found %d profile(s)", len(profiles))
|
||||
logSuccessf("Found %d profile(s)", len(profiles))
|
||||
|
||||
for i, profile := range profiles {
|
||||
if *verbose {
|
||||
@@ -524,7 +526,7 @@ func testGetStreamURIs(ctx context.Context, client *onvif.Client, profiles []*on
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
if *verbose {
|
||||
logError(" Profile %s: %v", profile.Name, err)
|
||||
logErrorf(" Profile %s: %v", profile.Name, err)
|
||||
}
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: fmt.Sprintf("GetStreamURI[%s]", profile.Token),
|
||||
@@ -535,7 +537,7 @@ func testGetStreamURIs(ctx context.Context, client *onvif.Client, profiles []*on
|
||||
result.Success = true
|
||||
result.Data = streamURI
|
||||
if *verbose {
|
||||
logSuccess(" Profile %s: %s", profile.Name, streamURI.URI)
|
||||
logSuccessf(" Profile %s: %s", profile.Name, streamURI.URI)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +550,7 @@ func testGetStreamURIs(ctx context.Context, client *onvif.Client, profiles []*on
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
logSuccess("Retrieved %d/%d stream URIs", successCount, len(results))
|
||||
logSuccessf("Retrieved %d/%d stream URIs", successCount, len(results))
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -570,7 +572,7 @@ func testGetSnapshotURIs(ctx context.Context, client *onvif.Client, profiles []*
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
if *verbose {
|
||||
logError(" Profile %s: %v", profile.Name, err)
|
||||
logErrorf(" Profile %s: %v", profile.Name, err)
|
||||
}
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: fmt.Sprintf("GetSnapshotURI[%s]", profile.Token),
|
||||
@@ -581,7 +583,7 @@ func testGetSnapshotURIs(ctx context.Context, client *onvif.Client, profiles []*
|
||||
result.Success = true
|
||||
result.Data = snapshotURI
|
||||
if *verbose {
|
||||
logSuccess(" Profile %s: %s", profile.Name, snapshotURI.URI)
|
||||
logSuccessf(" Profile %s: %s", profile.Name, snapshotURI.URI)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,12 +596,17 @@ func testGetSnapshotURIs(ctx context.Context, client *onvif.Client, profiles []*
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
logSuccess("Retrieved %d/%d snapshot URIs", successCount, len(results))
|
||||
logSuccessf("Retrieved %d/%d snapshot URIs", successCount, len(results))
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func testGetVideoEncoders(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []VideoEncoderResult {
|
||||
func testGetVideoEncoders(
|
||||
ctx context.Context,
|
||||
client *onvif.Client,
|
||||
profiles []*onvif.Profile,
|
||||
report *CameraReport,
|
||||
) []VideoEncoderResult {
|
||||
results := make([]VideoEncoderResult, 0)
|
||||
|
||||
for _, profile := range profiles {
|
||||
@@ -620,7 +627,7 @@ func testGetVideoEncoders(ctx context.Context, client *onvif.Client, profiles []
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
if *verbose {
|
||||
logError(" Profile %s: %v", profile.Name, err)
|
||||
logErrorf(" Profile %s: %v", profile.Name, err)
|
||||
}
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: fmt.Sprintf("GetVideoEncoderConfiguration[%s]", profile.Token),
|
||||
@@ -631,7 +638,7 @@ func testGetVideoEncoders(ctx context.Context, client *onvif.Client, profiles []
|
||||
result.Success = true
|
||||
result.Data = config
|
||||
if *verbose && config.Resolution != nil && config.RateControl != nil {
|
||||
logSuccess(" Profile %s: %s %dx%d @ %dfps",
|
||||
logSuccessf(" Profile %s: %s %dx%d @ %dfps",
|
||||
profile.Name, config.Encoding,
|
||||
config.Resolution.Width, config.Resolution.Height,
|
||||
config.RateControl.FrameRateLimit)
|
||||
@@ -647,12 +654,17 @@ func testGetVideoEncoders(ctx context.Context, client *onvif.Client, profiles []
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
logSuccess("Retrieved %d/%d video encoder configs", successCount, len(results))
|
||||
logSuccessf("Retrieved %d/%d video encoder configs", successCount, len(results))
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func testGetImagingSettings(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []ImagingSettingsResult {
|
||||
func testGetImagingSettings(
|
||||
ctx context.Context,
|
||||
client *onvif.Client,
|
||||
profiles []*onvif.Profile,
|
||||
report *CameraReport,
|
||||
) []ImagingSettingsResult {
|
||||
results := make([]ImagingSettingsResult, 0)
|
||||
processed := make(map[string]bool)
|
||||
|
||||
@@ -679,7 +691,7 @@ func testGetImagingSettings(ctx context.Context, client *onvif.Client, profiles
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
if *verbose {
|
||||
logError(" Video source %s: %v", token, err)
|
||||
logErrorf(" Video source %s: %v", token, err)
|
||||
}
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: fmt.Sprintf("GetImagingSettings[%s]", token),
|
||||
@@ -703,12 +715,17 @@ func testGetImagingSettings(ctx context.Context, client *onvif.Client, profiles
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
logSuccess("Retrieved %d/%d imaging settings", successCount, len(results))
|
||||
logSuccessf("Retrieved %d/%d imaging settings", successCount, len(results))
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func testGetPTZStatus(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []PTZStatusResult {
|
||||
func testGetPTZStatus(
|
||||
ctx context.Context,
|
||||
client *onvif.Client,
|
||||
profiles []*onvif.Profile,
|
||||
report *CameraReport,
|
||||
) []PTZStatusResult {
|
||||
results := make([]PTZStatusResult, 0)
|
||||
|
||||
for _, profile := range profiles {
|
||||
@@ -729,7 +746,7 @@ func testGetPTZStatus(ctx context.Context, client *onvif.Client, profiles []*onv
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
if *verbose {
|
||||
logError(" Profile %s: %v", profile.Name, err)
|
||||
logErrorf(" Profile %s: %v", profile.Name, err)
|
||||
}
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: fmt.Sprintf("GetPTZStatus[%s]", profile.Token),
|
||||
@@ -740,7 +757,7 @@ func testGetPTZStatus(ctx context.Context, client *onvif.Client, profiles []*onv
|
||||
result.Success = true
|
||||
result.Data = status
|
||||
if *verbose {
|
||||
logSuccess(" Profile %s: Retrieved", profile.Name)
|
||||
logSuccessf(" Profile %s: Retrieved", profile.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,7 +765,7 @@ func testGetPTZStatus(ctx context.Context, client *onvif.Client, profiles []*onv
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
logInfo("No PTZ configurations found")
|
||||
logInfof("No PTZ configurations found")
|
||||
} else {
|
||||
successCount := 0
|
||||
for _, r := range results {
|
||||
@@ -756,13 +773,18 @@ func testGetPTZStatus(ctx context.Context, client *onvif.Client, profiles []*onv
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
logSuccess("Retrieved %d/%d PTZ status", successCount, len(results))
|
||||
logSuccessf("Retrieved %d/%d PTZ status", successCount, len(results))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func testGetPTZPresets(ctx context.Context, client *onvif.Client, profiles []*onvif.Profile, report *CameraReport) []PTZPresetsResult {
|
||||
func testGetPTZPresets(
|
||||
ctx context.Context,
|
||||
client *onvif.Client,
|
||||
profiles []*onvif.Profile,
|
||||
report *CameraReport,
|
||||
) []PTZPresetsResult {
|
||||
results := make([]PTZPresetsResult, 0)
|
||||
|
||||
for _, profile := range profiles {
|
||||
@@ -783,7 +805,7 @@ func testGetPTZPresets(ctx context.Context, client *onvif.Client, profiles []*on
|
||||
result.Success = false
|
||||
result.Error = err.Error()
|
||||
if *verbose {
|
||||
logError(" Profile %s: %v", profile.Name, err)
|
||||
logErrorf(" Profile %s: %v", profile.Name, err)
|
||||
}
|
||||
report.Errors = append(report.Errors, ErrorLog{
|
||||
Operation: fmt.Sprintf("GetPTZPresets[%s]", profile.Token),
|
||||
@@ -795,7 +817,7 @@ func testGetPTZPresets(ctx context.Context, client *onvif.Client, profiles []*on
|
||||
result.Data = presets
|
||||
result.Count = len(presets)
|
||||
if *verbose {
|
||||
logSuccess(" Profile %s: %d preset(s)", profile.Name, len(presets))
|
||||
logSuccessf(" Profile %s: %d preset(s)", profile.Name, len(presets))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +825,7 @@ func testGetPTZPresets(ctx context.Context, client *onvif.Client, profiles []*on
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
logInfo("No PTZ configurations found")
|
||||
logInfof("No PTZ configurations found")
|
||||
} else {
|
||||
successCount := 0
|
||||
totalPresets := 0
|
||||
@@ -813,7 +835,7 @@ func testGetPTZPresets(ctx context.Context, client *onvif.Client, profiles []*on
|
||||
totalPresets += r.Count
|
||||
}
|
||||
}
|
||||
logSuccess("Retrieved presets from %d/%d PTZ profiles (%d total presets)", successCount, len(results), totalPresets)
|
||||
logSuccessf("Retrieved presets from %d/%d PTZ profiles (%d total presets)", successCount, len(results), totalPresets)
|
||||
}
|
||||
|
||||
return results
|
||||
@@ -844,6 +866,7 @@ func sanitizeFilename(s string) string {
|
||||
s = strings.ReplaceAll(s, "<", "-")
|
||||
s = strings.ReplaceAll(s, ">", "-")
|
||||
s = strings.ReplaceAll(s, "|", "-")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -860,25 +883,25 @@ func saveReport(report *CameraReport, filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func logStep(format string, args ...interface{}) {
|
||||
func logStepf(format string, args ...interface{}) {
|
||||
fmt.Printf("→ "+format+"\n", args...)
|
||||
}
|
||||
|
||||
func logSuccess(format string, args ...interface{}) {
|
||||
func logSuccessf(format string, args ...interface{}) {
|
||||
fmt.Printf(" ✓ "+format+"\n", args...)
|
||||
}
|
||||
|
||||
func logError(format string, args ...interface{}) {
|
||||
func logErrorf(format string, args ...interface{}) {
|
||||
fmt.Printf(" ✗ "+format+"\n", args...)
|
||||
}
|
||||
|
||||
func logInfo(format string, args ...interface{}) {
|
||||
func logInfof(format string, args ...interface{}) {
|
||||
fmt.Printf(" ℹ "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// XML Capture functionality
|
||||
|
||||
// XMLCapture stores a request/response pair
|
||||
// XMLCapture stores a request/response pair.
|
||||
type XMLCapture struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Operation int `json:"operation"`
|
||||
@@ -890,7 +913,7 @@ type XMLCapture struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// LoggingTransport wraps http.RoundTripper to log requests and responses
|
||||
// LoggingTransport wraps http.RoundTripper to log requests and responses.
|
||||
type LoggingTransport struct {
|
||||
Transport http.RoundTripper
|
||||
LogDir string
|
||||
@@ -921,8 +944,9 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
resp, err := t.Transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
capture.Error = err.Error()
|
||||
t.saveCapture(capture)
|
||||
return nil, err
|
||||
t.saveCapture(&capture)
|
||||
|
||||
return nil, fmt.Errorf("round trip failed: %w", err)
|
||||
}
|
||||
|
||||
// Capture response
|
||||
@@ -936,11 +960,12 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
}
|
||||
|
||||
t.saveCapture(capture)
|
||||
t.saveCapture(&capture)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// prettyPrintXML formats XML with proper indentation using a simple algorithm
|
||||
// prettyPrintXML formats XML with proper indentation using a simple algorithm.
|
||||
func prettyPrintXML(xmlStr string) string {
|
||||
if xmlStr == "" {
|
||||
return ""
|
||||
@@ -973,7 +998,7 @@ func prettyPrintXML(xmlStr string) string {
|
||||
return formatted.String()
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) saveCapture(capture XMLCapture) {
|
||||
func (t *LoggingTransport) saveCapture(capture *XMLCapture) {
|
||||
// Create filename base using operation name
|
||||
baseFilename := fmt.Sprintf("capture_%03d_%s", capture.Operation, capture.OperationName)
|
||||
|
||||
@@ -982,6 +1007,7 @@ func (t *LoggingTransport) saveCapture(capture XMLCapture) {
|
||||
data, err := json.MarshalIndent(capture, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal capture: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1003,7 +1029,7 @@ func (t *LoggingTransport) saveCapture(capture XMLCapture) {
|
||||
}
|
||||
}
|
||||
|
||||
// extractSOAPOperation extracts the operation name from a SOAP request body
|
||||
// extractSOAPOperation extracts the operation name from a SOAP request body.
|
||||
func extractSOAPOperation(soapBody string) string {
|
||||
// Look for the operation element in the SOAP Body
|
||||
// Typical format: <Body><GetDeviceInformation xmlns="...">...</GetDeviceInformation></Body>
|
||||
@@ -1044,31 +1070,41 @@ func extractSOAPOperation(soapBody string) string {
|
||||
if colonIdx := strings.Index(tagName, ":"); colonIdx != -1 {
|
||||
return tagName[colonIdx+1:]
|
||||
}
|
||||
|
||||
return tagName
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// createTarGz creates a tar.gz archive from a directory
|
||||
// createTarGz creates a tar.gz archive from a directory.
|
||||
func createTarGz(sourceDir, archivePath string) error {
|
||||
// Create archive file
|
||||
archiveFile, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create archive file: %w", err)
|
||||
}
|
||||
defer archiveFile.Close()
|
||||
defer func() {
|
||||
//nolint:errcheck // Close error is not critical for cleanup
|
||||
_ = archiveFile.Close()
|
||||
}()
|
||||
|
||||
// Create gzip writer
|
||||
gzWriter := gzip.NewWriter(archiveFile)
|
||||
defer gzWriter.Close()
|
||||
defer func() {
|
||||
//nolint:errcheck // Close error is not critical for cleanup
|
||||
_ = gzWriter.Close()
|
||||
}()
|
||||
|
||||
// Create tar writer
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
defer func() {
|
||||
//nolint:errcheck // Close error is not critical for cleanup
|
||||
_ = tarWriter.Close()
|
||||
}()
|
||||
|
||||
// Walk through source directory
|
||||
return filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1102,7 +1138,10 @@ func createTarGz(sourceDir, archivePath string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() {
|
||||
//nolint:errcheck // Close error is not critical for cleanup
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(tarWriter, file); err != nil {
|
||||
return fmt.Errorf("failed to write file to tar: %w", err)
|
||||
@@ -1110,5 +1149,9 @@ func createTarGz(sourceDir, archivePath string) error {
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to walk source directory: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+40
-6
@@ -29,6 +29,7 @@ func main() {
|
||||
fmt.Println("0. Exit")
|
||||
fmt.Print("\nChoice: ")
|
||||
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
input, _ := reader.ReadString('\n')
|
||||
choice := strings.TrimSpace(input)
|
||||
|
||||
@@ -45,6 +46,7 @@ func main() {
|
||||
getStreamURLs()
|
||||
case "0", "q", "quit":
|
||||
fmt.Println("Goodbye! 👋")
|
||||
|
||||
return
|
||||
default:
|
||||
fmt.Println("Invalid choice. Please try again.")
|
||||
@@ -60,6 +62,7 @@ func discoverCameras() {
|
||||
|
||||
// Ask if user wants to use a specific interface
|
||||
fmt.Print("Use specific network interface? (y/n) [n]: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
useInterface, _ := reader.ReadString('\n')
|
||||
useInterface = strings.ToLower(strings.TrimSpace(useInterface))
|
||||
|
||||
@@ -69,6 +72,7 @@ func discoverCameras() {
|
||||
interfaces, err := discovery.ListNetworkInterfaces()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,8 +81,9 @@ func discoverCameras() {
|
||||
fmt.Printf(" %d. %s (%v)\n", i+1, iface.Name, iface.Addresses)
|
||||
}
|
||||
|
||||
fmt.Print("\nEnter interface name or IP: ")
|
||||
ifaceInput, _ := reader.ReadString('\n')
|
||||
fmt.Print("\nEnter interface name or IP: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
ifaceInput, _ := reader.ReadString('\n')
|
||||
ifaceInput = strings.TrimSpace(ifaceInput)
|
||||
|
||||
if ifaceInput != "" {
|
||||
@@ -98,11 +103,13 @@ func discoverCameras() {
|
||||
devices, err := discovery.DiscoverWithOptions(ctx, 5*time.Second, opts)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
fmt.Println("No cameras found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,11 +126,13 @@ func listNetworkInterfaces() {
|
||||
interfaces, err := discovery.ListNetworkInterfaces()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(interfaces) == 0 {
|
||||
fmt.Println("No network interfaces found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,10 +163,12 @@ func connectAndShowInfo() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Camera IP: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
ip, _ := reader.ReadString('\n')
|
||||
ip = strings.TrimSpace(ip)
|
||||
|
||||
fmt.Print("Username [admin]: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
@@ -165,6 +176,7 @@ func connectAndShowInfo() {
|
||||
}
|
||||
|
||||
fmt.Print("Password: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
password, _ := reader.ReadString('\n')
|
||||
password = strings.TrimSpace(password)
|
||||
|
||||
@@ -178,6 +190,7 @@ func connectAndShowInfo() {
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -187,6 +200,7 @@ func connectAndShowInfo() {
|
||||
info, err := client.GetDeviceInformation(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Connection failed: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -195,7 +209,8 @@ func connectAndShowInfo() {
|
||||
fmt.Printf("🔧 Firmware: %s\n", info.FirmwareVersion)
|
||||
|
||||
// Initialize and get profiles
|
||||
_ = client.Initialize(ctx) // Ignore initialization errors, we'll catch them on GetProfiles
|
||||
//nolint:errcheck // Ignore initialization errors, we'll catch them on GetProfiles
|
||||
_ = client.Initialize(ctx)
|
||||
profiles, err := client.GetProfiles(ctx)
|
||||
if err == nil && len(profiles) > 0 {
|
||||
fmt.Printf("📺 %d profile(s) available\n", len(profiles))
|
||||
@@ -212,10 +227,12 @@ func ptzDemo() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Camera IP: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
ip, _ := reader.ReadString('\n')
|
||||
ip = strings.TrimSpace(ip)
|
||||
|
||||
fmt.Print("Username [admin]: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
@@ -223,6 +240,7 @@ func ptzDemo() {
|
||||
}
|
||||
|
||||
fmt.Print("Password: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
password, _ := reader.ReadString('\n')
|
||||
password = strings.TrimSpace(password)
|
||||
|
||||
@@ -234,15 +252,18 @@ func ptzDemo() {
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_ = client.Initialize(ctx) // Ignore initialization errors, we'll catch them on GetProfiles
|
||||
//nolint:errcheck // Ignore initialization errors, we'll catch them on GetProfiles
|
||||
_ = client.Initialize(ctx)
|
||||
|
||||
profiles, err := client.GetProfiles(ctx)
|
||||
if err != nil || len(profiles) == 0 {
|
||||
fmt.Println("❌ No profiles found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -252,6 +273,7 @@ func ptzDemo() {
|
||||
status, err := client.GetStatus(ctx, profileToken)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ PTZ not supported: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -269,6 +291,7 @@ func ptzDemo() {
|
||||
fmt.Println("5. Go to center")
|
||||
fmt.Print("Choice: ")
|
||||
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
|
||||
@@ -288,6 +311,7 @@ func ptzDemo() {
|
||||
position = &onvif.PTZVector{PanTilt: &onvif.Vector2D{X: 0.0, Y: 0.0}}
|
||||
default:
|
||||
fmt.Println("Invalid choice")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,15 +320,18 @@ func ptzDemo() {
|
||||
err = client.ContinuousMove(ctx, profileToken, velocity, &timeout)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
fmt.Println("✅ Moving for 2 seconds...")
|
||||
time.Sleep(2 * time.Second)
|
||||
_ = client.Stop(ctx, profileToken, true, false) // Stop PTZ movement
|
||||
//nolint:errcheck // Stop error is not critical for demo
|
||||
_ = client.Stop(ctx, profileToken, true, false)
|
||||
} else if position != nil {
|
||||
err = client.AbsoluteMove(ctx, profileToken, position, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
fmt.Println("✅ Moving to center...")
|
||||
@@ -317,10 +344,12 @@ func getStreamURLs() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Camera IP: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
ip, _ := reader.ReadString('\n')
|
||||
ip = strings.TrimSpace(ip)
|
||||
|
||||
fmt.Print("Username [admin]: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
@@ -328,6 +357,7 @@ func getStreamURLs() {
|
||||
}
|
||||
|
||||
fmt.Print("Password: ")
|
||||
//nolint:errcheck // ReadString error on stdin is rare and not critical for CLI
|
||||
password, _ := reader.ReadString('\n')
|
||||
password = strings.TrimSpace(password)
|
||||
|
||||
@@ -339,20 +369,24 @@ func getStreamURLs() {
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_ = client.Initialize(ctx) // Ignore initialization errors, we'll catch them on GetProfiles
|
||||
//nolint:errcheck // Ignore initialization errors, we'll catch them on GetProfiles
|
||||
_ = client.Initialize(ctx)
|
||||
|
||||
profiles, err := client.GetProfiles(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Error: %v\n", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(profiles) == 0 {
|
||||
fmt.Println("❌ No profiles found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -108,10 +108,9 @@ func main() {
|
||||
fmt.Println("✅ Server stopped")
|
||||
}
|
||||
|
||||
// buildConfig creates a server configuration from command-line arguments
|
||||
// buildConfig creates a server configuration from command-line arguments.
|
||||
func buildConfig(host string, port int, username, password, manufacturer, model,
|
||||
firmware, serial string, numProfiles int, ptz, imaging, events bool) *server.Config {
|
||||
|
||||
config := &server.Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
@@ -216,7 +215,7 @@ func buildConfig(host string, port int, username, password, manufacturer, model,
|
||||
return config
|
||||
}
|
||||
|
||||
// printBanner prints the application banner
|
||||
// printBanner prints the application banner.
|
||||
func printBanner() {
|
||||
banner := `
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
|
||||
Reference in New Issue
Block a user