feat: add comprehensive ONVIF test reports and enhance documentation

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