diff --git a/cmd/root.go b/cmd/root.go index 1894877..35758f9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,9 +10,12 @@ import ( "github.com/spf13/cobra" "tinygo.org/x/bluetooth" + + "github.com/tzermias/deskctl/pkg/logger" ) var address string +var verbose bool var adapter *bluetooth.Adapter @@ -22,6 +25,13 @@ var rootCmd = &cobra.Command{ Long: `Controls standing desks equipped with Jiecang controllers Moves the desk up/down, manages memory presets`, PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Set verbose mode based on flag + logger.SetVerbose(verbose) + + if verbose { + logger.Printf("Verbose mode enabled") + } + // Initialize bluetooth adapter adapter = bluetooth.DefaultAdapter err := adapter.Enable() @@ -29,6 +39,8 @@ Moves the desk up/down, manages memory presets`, fmt.Fprintf(os.Stderr, "Could not enable Bluetooth adapter: %v\n", err) os.Exit(1) } + + logger.Printf("Bluetooth adapter enabled successfully") }, Run: func(cmd *cobra.Command, args []string) { _ = cmd.Help() @@ -47,4 +59,5 @@ func Execute(ctx context.Context) { func init() { rootCmd.PersistentFlags().StringVarP(&address, "address", "a", "", "Device address") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output") } diff --git a/hack/test.go b/hack/test.go index 106400f..d742869 100644 --- a/hack/test.go +++ b/hack/test.go @@ -2,10 +2,10 @@ package main import ( "context" - "log" "time" "github.com/tzermias/deskctl/pkg/jiecang" + "github.com/tzermias/deskctl/pkg/logger" "tinygo.org/x/bluetooth" ) @@ -14,6 +14,9 @@ const ( ) func main() { + // Enable verbose logging for test script + logger.SetVerbose(true) + adapter := bluetooth.DefaultAdapter err := adapter.Enable() @@ -22,7 +25,7 @@ func main() { } err = adapter.Scan(onScan) - log.Println("Scanning for Lierda devices ") + logger.Println("Scanning for Lierda devices ") if err != nil { panic("Failed to register scan callback") } @@ -30,10 +33,10 @@ func main() { func onScan(adapter *bluetooth.Adapter, device bluetooth.ScanResult) { if device.HasServiceUUID(bluetooth.New16BitUUID(LierdaDeviceID)) { - log.Println("Found Lierda device:", device.Address.String(), device.RSSI, device.LocalName()) + logger.Println("Found Lierda device:", device.Address.String(), device.RSSI, device.LocalName()) j, err := jiecang.Init(adapter, device.Address) if err != nil { - log.Printf("Failed to initialize device: %v\n", err) + logger.Printf("Failed to initialize device: %v\n", err) return } @@ -43,22 +46,22 @@ func onScan(adapter *bluetooth.Adapter, device bluetooth.ScanResult) { // Go to Memory1 if err := j.GoToMemory(ctx, 1); err != nil { - log.Printf("Failed to go to memory1: %v\n", err) + logger.Printf("Failed to go to memory1: %v\n", err) return } time.Sleep(5 * time.Second) // Go to Memory2 if err := j.GoToMemory(ctx, 2); err != nil { - log.Printf("Failed to go to memory2: %v\n", err) + logger.Printf("Failed to go to memory2: %v\n", err) return } time.Sleep(200 * time.Millisecond) - log.Println("Disconnecting...") + logger.Println("Disconnecting...") if err := j.Disconnect(); err != nil { - log.Printf("Error when disconnecting: %v\n", err) + logger.Printf("Error when disconnecting: %v\n", err) return } - log.Println("Disconnected...") + logger.Println("Disconnected...") } } diff --git a/pkg/jiecang/jiecang.go b/pkg/jiecang/jiecang.go index 3eecbbd..dd41b12 100644 --- a/pkg/jiecang/jiecang.go +++ b/pkg/jiecang/jiecang.go @@ -29,10 +29,11 @@ package jiecang import ( "bytes" "fmt" - "log" "sync" "tinygo.org/x/bluetooth" + + "github.com/tzermias/deskctl/pkg/logger" ) var commands = map[string][]byte{ @@ -171,7 +172,7 @@ func Init(a *bluetooth.Adapter, addr bluetooth.Address) (*Jiecang, error) { if err != nil { return nil, fmt.Errorf("failed to read initial height: %w", err) } - log.Printf("Initial height: %d mm", j.currentHeight) + logger.Printf("Initial height: %d mm", j.currentHeight) //Fetch height memory presets j.presets = make(map[string]uint8) @@ -283,10 +284,10 @@ func (j *Jiecang) characteristicReceiver(buf []byte) { j.AntiCollisionSensitivity = uint8(msg[i][3]) j.mu.Unlock() default: // Any other case - log.Printf("Received: %x", msg[i]) + logger.Printf("Received: %x", msg[i]) } } else { - log.Printf("Received: %x", msg[i]) + logger.Printf("Received: %x", msg[i]) } } } diff --git a/pkg/jiecang/memory.go b/pkg/jiecang/memory.go index ee2dd97..b28ed5e 100644 --- a/pkg/jiecang/memory.go +++ b/pkg/jiecang/memory.go @@ -3,9 +3,10 @@ package jiecang import ( "context" "fmt" - "log" "math" "time" + + "github.com/tzermias/deskctl/pkg/logger" ) // GoToMemory moves the desk to the specified memory preset (1-3). @@ -127,7 +128,7 @@ func (j *Jiecang) SaveMemory(memoryNum int) error { return fmt.Errorf("failed to save memory%d: %w", memoryNum, err) } - log.Printf("Saved height %d cm to memory %d", j.currentHeight, memoryNum) + logger.Printf("Saved height %d cm to memory %d", j.currentHeight, memoryNum) time.Sleep(200 * time.Millisecond) return nil } diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..a086568 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,71 @@ +package logger + +import ( + "io" + "log" + "os" + "sync" +) + +var ( + mu sync.RWMutex + verbose bool + logger *log.Logger +) + +func init() { + logger = log.New(os.Stdout, "", log.LstdFlags) +} + +// SetVerbose enables or disables verbose logging +func SetVerbose(v bool) { + mu.Lock() + defer mu.Unlock() + verbose = v +} + +// IsVerbose returns whether verbose logging is enabled +func IsVerbose() bool { + mu.RLock() + defer mu.RUnlock() + return verbose +} + +// SetOutput sets the output destination for the logger +func SetOutput(w io.Writer) { + mu.Lock() + defer mu.Unlock() + logger.SetOutput(w) +} + +// Printf prints a log message only if verbose mode is enabled +func Printf(format string, v ...interface{}) { + mu.RLock() + defer mu.RUnlock() + if verbose { + logger.Printf(format, v...) + } +} + +// Println prints a log message only if verbose mode is enabled +func Println(v ...interface{}) { + mu.RLock() + defer mu.RUnlock() + if verbose { + logger.Println(v...) + } +} + +// Error always prints error messages regardless of verbose mode +func Error(v ...interface{}) { + mu.RLock() + defer mu.RUnlock() + logger.Println(v...) +} + +// Errorf always prints error messages regardless of verbose mode +func Errorf(format string, v ...interface{}) { + mu.RLock() + defer mu.RUnlock() + logger.Printf(format, v...) +} diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go new file mode 100644 index 0000000..6bd7c9d --- /dev/null +++ b/pkg/logger/logger_test.go @@ -0,0 +1,104 @@ +package logger + +import ( + "bytes" + "strings" + "testing" +) + +func TestVerboseToggle(t *testing.T) { + buf := &bytes.Buffer{} + SetOutput(buf) + + // Test verbose disabled (default) + SetVerbose(false) + Printf("test message") + if buf.Len() > 0 { + t.Error("Expected no output when verbose is disabled") + } + + // Test verbose enabled + buf.Reset() + SetVerbose(true) + Printf("test message") + if !strings.Contains(buf.String(), "test message") { + t.Error("Expected output when verbose is enabled") + } +} + +func TestPrintln(t *testing.T) { + buf := &bytes.Buffer{} + SetOutput(buf) + + // Test verbose disabled + SetVerbose(false) + Println("test message") + if buf.Len() > 0 { + t.Error("Expected no output when verbose is disabled") + } + + // Test verbose enabled + buf.Reset() + SetVerbose(true) + Println("test message") + if !strings.Contains(buf.String(), "test message") { + t.Error("Expected output when verbose is enabled") + } +} + +func TestErrorAlwaysPrints(t *testing.T) { + buf := &bytes.Buffer{} + SetOutput(buf) + SetVerbose(false) + + // Test Error + Error("error message") + if !strings.Contains(buf.String(), "error message") { + t.Error("Expected error to print even when verbose is disabled") + } + + // Test Errorf + buf.Reset() + Errorf("error %s", "formatted") + if !strings.Contains(buf.String(), "error formatted") { + t.Error("Expected formatted error to print even when verbose is disabled") + } +} + +func TestIsVerbose(t *testing.T) { + SetVerbose(false) + if IsVerbose() { + t.Error("Expected IsVerbose to return false") + } + + SetVerbose(true) + if !IsVerbose() { + t.Error("Expected IsVerbose to return true") + } +} + +func TestConcurrentAccess(t *testing.T) { + buf := &bytes.Buffer{} + SetOutput(buf) + SetVerbose(true) + + // Test concurrent reads and writes + done := make(chan bool) + for i := 0; i < 10; i++ { + go func() { + Printf("concurrent message") + done <- true + }() + } + + // Wait for all goroutines + for i := 0; i < 10; i++ { + <-done + } + + // Should have 10 messages + count := strings.Count(buf.String(), "concurrent message") + if count != 10 { + t.Errorf("Expected 10 messages, got %d", count) + } +}