From e3c29dc13e234ddc61c8efd3fc6f42c1d39eba3f Mon Sep 17 00:00:00 2001 From: richbl Date: Mon, 16 Dec 2024 17:58:18 -0800 Subject: [PATCH] refactor: Refactor of the logger package to address formatting issues; reorganized other packages --- README.md | 155 +++++------ cmd/main.go | 119 +++++---- internal/ble/sensor_controller.go | 158 ++++++------ internal/ble/sensor_controller_test.go | 52 ++-- internal/configuration/config.go | 62 ++--- internal/configuration/config_test.go | 128 +++++----- internal/logging/logger.go | 241 +++++++++++------- internal/logging/logger_test.go | 38 +-- internal/speed/speed_controller.go | 61 ++--- internal/speed/speed_controller_test.go | 27 +- internal/video-player/playback_controller.go | 164 +++++------- .../video-player/playback_controller_test.go | 26 +- 12 files changed, 618 insertions(+), 613 deletions(-) diff --git a/README.md b/README.md index 9e23f92..ae46a7e 100644 --- a/README.md +++ b/README.md @@ -194,72 +194,69 @@ go run cmd/main.go At this point, you should see the following output: ```console -2024/12/11 22:02:28 Starting BLE Sync Cycle 0.6.2 -2024/12/11 22:02:28 INFO [BLE] Created new BLE central controller -2024/12/11 22:02:28 INFO [BLE] Now scanning the ether for BLE peripheral UUID of F1:42:D8:DE:35:16... -2024/12/11 22:02:42 INFO [BLE] Found BLE peripheral F1:42:D8:DE:35:16 -2024/12/11 22:02:42 INFO [BLE] Connecting to BLE peripheral device F1:42:D8:DE:35:16 -2024/12/11 22:02:44 INFO [BLE] BLE peripheral device connected -2024/12/11 22:02:44 INFO [BLE] Discovering CSC services 00001816-0000-1000-8000-00805f9b34fb -2024/12/11 22:02:54 ERROR [BLE] CSC services discovery failed: timeout on DiscoverServices -2024/12/11 22:02:54 ERROR [BLE] BLE peripheral scan failed: timeout on DiscoverServices -2024/12/11 22:02:54 INFO [APP] Application shutdown complete. Goodbye! +2024/12/16 15:08:56 Starting BLE Sync Cycle 0.6.2 +2024/12/16 15:08:56 [INFO] [BLE] created new BLE central controller +2024/12/16 15:08:56 [INFO] [BLE] now scanning the ether for BLE peripheral UUID of F1:42:D8:DE:35:16... +2024/12/16 15:08:58 [DEBUG] [BLE] found BLE peripheral F1:42:D8:DE:35:16 +2024/12/16 15:08:58 [DEBUG] [BLE] connecting to BLE peripheral device F1:42:D8:DE:35:16 +2024/12/16 15:09:00 [INFO] [BLE] BLE peripheral device connected +2024/12/16 15:09:00 [DEBUG] [BLE] discovering CSC services 00001816-0000-1000-8000-00805f9b34fb +2024/12/16 15:09:10 [ERROR] [BLE] CSC services discovery failed: timeout on DiscoverServices +2024/12/16 15:09:10 [ERROR] [BLE] BLE peripheral scan failed: timeout on DiscoverServices +2024/12/16 15:09:10 [INFO] [APP] application shutdown complete... goodbye! ``` In this first example, while the application was able to find the BLE peripheral, it failed to discover the CSC services and characteristics before timing out. Depending on the BLE peripheral, it may take some time before a BLE peripheral advertises both its device services and characteristics. If the peripheral is not responding, you may need to increase the timeout in the `config.toml` file. ```console -2024/12/11 22:03:59 Starting BLE Sync Cycle 0.6.2 -2024/12/11 22:03:59 INFO [BLE] Created new BLE central controller -2024/12/11 22:03:59 INFO [BLE] Now scanning the ether for BLE peripheral UUID of F1:42:D8:DE:35:16... -2024/12/11 22:03:59 INFO [BLE] Found BLE peripheral F1:42:D8:DE:35:16 -2024/12/11 22:03:59 INFO [BLE] Connecting to BLE peripheral device F1:42:D8:DE:35:16 -2024/12/11 22:03:59 INFO [BLE] BLE peripheral device connected -2024/12/11 22:03:59 INFO [BLE] Discovering CSC services 00001816-0000-1000-8000-00805f9b34fb -2024/12/11 22:03:59 INFO [BLE] Found CSC service 00001816-0000-1000-8000-00805f9b34fb -2024/12/11 22:03:59 INFO [BLE] Discovering CSC characteristics 00002a5b-0000-1000-8000-00805f9b34fb -2024/12/11 22:03:59 INFO [BLE] Found CSC characteristic 00002a5b-0000-1000-8000-00805f9b34fb -2024/12/11 22:03:59 INFO [BLE] Starting real-time monitoring of BLE sensor notifications... -2024/12/11 22:03:59 INFO [VIDEO] Starting MPV video player... -2024/12/11 22:03:59 INFO [VIDEO] Loading video file: cycling_test.mp4 -2024/12/11 22:03:59 INFO [VIDEO] Entering MPV playback loop... -2024/12/11 22:04:00 INFO [VIDEO] Sensor speed buffer: [0.00 0.00 0.00 0.00 0.00] -2024/12/11 22:04:00 INFO [VIDEO] Smoothed sensor speed: 0.00 mph -2024/12/11 22:04:00 INFO [VIDEO] No speed detected, so pausing video -2024/12/11 22:04:00 INFO [VIDEO] Video paused successfully +2024/12/16 15:09:47 Starting BLE Sync Cycle 0.6.2 +2024/12/16 15:09:47 [INFO] [BLE] created new BLE central controller +2024/12/16 15:09:47 [INFO] [BLE] now scanning the ether for BLE peripheral UUID of F1:42:D8:DE:35:16... +2024/12/16 15:09:47 [DEBUG] [BLE] found BLE peripheral F1:42:D8:DE:35:16 +2024/12/16 15:09:47 [DEBUG] [BLE] connecting to BLE peripheral device F1:42:D8:DE:35:16 +2024/12/16 15:09:47 [INFO] [BLE] BLE peripheral device connected +2024/12/16 15:09:47 [DEBUG] [BLE] discovering CSC services 00001816-0000-1000-8000-00805f9b34fb +2024/12/16 15:09:47 [DEBUG] [BLE] found CSC service 00001816-0000-1000-8000-00805f9b34fb +2024/12/16 15:09:47 [DEBUG] [BLE] discovering CSC characteristics 00002a5b-0000-1000-8000-00805f9b34fb +2024/12/16 15:09:47 [DEBUG] [BLE] found CSC characteristic 00002a5b-0000-1000-8000-00805f9b34fb +2024/12/16 15:09:47 [INFO] [VIDEO] starting MPV video player... +2024/12/16 15:09:47 [DEBUG] [BLE] starting real-time monitoring of BLE sensor notifications... +2024/12/16 15:09:47 [DEBUG] [VIDEO] loading video file: cycling_test.mp4 +2024/12/16 15:09:47 [DEBUG] [VIDEO] entering MPV playback loop... +2024/12/16 15:09:47 [DEBUG] [VIDEO] sensor speed buffer: [0.00 0.00 0.00 0.00 0.00] +2024/12/16 15:09:47 [INFO] [VIDEO] smoothed sensor speed: 0.00 mph +2024/12/16 15:09:47 [DEBUG] [VIDEO] no speed detected, so pausing video +2024/12/16 15:09:47 [DEBUG] [VIDEO] video paused successfully ``` In the example above, the application is now running in a loop, periodically querying the BLE peripheral for speed data. The application will also update the video player to match the speed of the sensor. Here, since the video has just begun, its speed is set to 0.0 (paused). ```console ... -024/12/11 22:05:08 INFO [SPEED] BLE sensor speed: 8.54 mph -2024/12/11 22:05:09 INFO [VIDEO] Sensor speed buffer: [9.06 8.28 8.11 8.20 8.54] -2024/12/11 22:05:09 INFO [VIDEO] Smoothed sensor speed: 8.44 mph -2024/12/11 22:05:09 INFO [VIDEO] Updating video playback speed to 0.51 -2024/12/11 22:05:09 INFO [SPEED] BLE sensor speed: 0.00 mph -2024/12/11 22:05:09 INFO [VIDEO] Sensor speed buffer: [8.28 8.11 8.20 8.54 0.00] -2024/12/11 22:05:09 INFO [VIDEO] Smoothed sensor speed: 6.63 mph -2024/12/11 22:05:09 INFO [VIDEO] Updating video playback speed to 0.40 -2024/12/11 22:05:09 INFO [SPEED] BLE sensor speed: 8.44 mph -2024/12/11 22:05:10 INFO [VIDEO] Sensor speed buffer: [8.11 8.20 8.54 0.00 8.44] -2024/12/11 22:05:10 INFO [VIDEO] Smoothed sensor speed: 6.66 mph -2024/12/11 22:05:10 INFO [SPEED] BLE sensor speed: 7.84 mph -2024/12/11 22:05:10 INFO [VIDEO] Sensor speed buffer: [8.20 8.54 0.00 8.44 7.84] -2024/12/11 22:05:10 INFO [VIDEO] Smoothed sensor speed: 6.61 mph -2024/12/11 22:05:11 INFO [VIDEO] Sensor speed buffer: [8.20 8.54 0.00 8.44 7.84] -2024/12/11 22:05:11 INFO [VIDEO] Smoothed sensor speed: 6.61 mph -2024/12/11 22:05:11 INFO [SPEED] BLE sensor speed: 6.56 mph -2024/12/11 22:05:11 INFO [SPEED] BLE sensor speed: 6.65 mph -2024/12/11 22:05:11 INFO [VIDEO] Sensor speed buffer: [0.00 8.44 7.84 6.56 6.65] -2024/12/11 22:05:11 INFO [VIDEO] Smoothed sensor speed: 5.90 mph -2024/12/11 22:05:11 INFO [SPEED] BLE sensor speed: 0.00 mph -2024/12/11 22:05:12 INFO [VIDEO] Sensor speed buffer: [8.44 7.84 6.56 6.65 0.00] -2024/12/11 22:05:12 INFO [VIDEO] Smoothed sensor speed: 5.90 mph -2024/12/11 22:05:12 INFO [SPEED] BLE sensor speed: 0.00 mph -2024/12/11 22:05:12 INFO [VIDEO] Sensor speed buffer: [7.84 6.56 6.65 0.00 0.00] -2024/12/11 22:05:12 INFO [VIDEO] Smoothed sensor speed: 4.21 mph -2024/12/11 22:05:12 INFO [VIDEO] Updating video playback speed to 0.25 +2024/12/16 15:13:26 [INFO] [SPEED] BLE sensor speed: 24.14 mph +2024/12/16 15:13:26 [DEBUG] [VIDEO] sensor speed buffer: [0.00 3.17 5.54 13.53 24.14] +2024/12/16 15:13:26 [INFO] [VIDEO] smoothed sensor speed: 9.28 mph +2024/12/16 15:13:26 [DEBUG] [VIDEO] last playback speed: 1.74 mph +2024/12/16 15:13:26 [DEBUG] [VIDEO] sensor speed delta: 7.54 mph +2024/12/16 15:13:26 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:26 [INFO] [VIDEO] updating video playback speed to 0.56 +2024/12/16 15:13:27 [DEBUG] [VIDEO] sensor speed buffer: [0.00 3.17 5.54 13.53 24.14] +2024/12/16 15:13:27 [INFO] [VIDEO] smoothed sensor speed: 9.28 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] last playback speed: 9.28 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] sensor speed delta: 0.00 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] sensor speed buffer: [0.00 3.17 5.54 13.53 24.14] +2024/12/16 15:13:27 [INFO] [VIDEO] smoothed sensor speed: 9.28 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] last playback speed: 9.28 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] sensor speed delta: 0.00 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:27 [INFO] [SPEED] BLE sensor speed: 27.35 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] sensor speed buffer: [3.17 5.54 13.53 24.14 27.35] +2024/12/16 15:13:27 [INFO] [VIDEO] smoothed sensor speed: 14.75 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] last playback speed: 9.28 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] sensor speed delta: 5.47 mph +2024/12/16 15:13:27 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:27 [INFO] [VIDEO] updating video playback speed to 0.88 ... ``` @@ -269,26 +266,32 @@ In this last example, **BLE Sync Cycle** is coordinating with both the BLE perip ```console ... -2024/12/11 22:05:11 INFO [SPEED] BLE sensor speed: 0.00 mph -2024/12/11 22:05:12 INFO [VIDEO] Sensor speed buffer: [8.44 7.84 6.56 6.65 0.00] -2024/12/11 22:05:12 INFO [VIDEO] Smoothed sensor speed: 5.90 mph -2024/12/11 22:05:12 INFO [SPEED] BLE sensor speed: 0.00 mph -2024/12/11 22:05:12 INFO [VIDEO] Sensor speed buffer: [7.84 6.56 6.65 0.00 0.00] -2024/12/11 22:05:12 INFO [VIDEO] Smoothed sensor speed: 4.21 mph -2024/12/11 22:05:12 INFO [VIDEO] Updating video playback speed to 0.25 -2024/12/11 22:05:13 INFO [SPEED] BLE sensor speed: 7.29 mph -2024/12/11 22:05:13 INFO [VIDEO] Sensor speed buffer: [6.56 6.65 0.00 0.00 7.29] -2024/12/11 22:05:13 INFO [VIDEO] Smoothed sensor speed: 4.10 mph -2024/12/11 22:05:13 INFO [VIDEO] Sensor speed buffer: [6.56 6.65 0.00 0.00 7.29] -2024/12/11 22:05:13 INFO [VIDEO] Smoothed sensor speed: 4.10 mph -2024/12/11 22:05:13 INFO [SPEED] BLE sensor speed: 0.00 mph -2024/12/11 22:05:13 INFO [SPEED] BLE sensor speed: 0.00 mph -2024/12/11 22:05:14 INFO [VIDEO] Sensor speed buffer: [0.00 0.00 7.29 0.00 0.00] -2024/12/11 22:05:14 INFO [VIDEO] Smoothed sensor speed: 1.46 mph -2024/12/11 22:05:14 INFO [VIDEO] Updating video playback speed to 0.09 -2024/12/11 22:05:14 INFO [APP] Shutdown signal received -2024/12/11 22:05:14 INFO [VIDEO] Context cancelled. Shutting down video player component -2024/12/11 22:05:14 INFO [APP] Application shutdown complete. Goodbye! +2024/12/16 15:13:32 [INFO] [VIDEO] updating video playback speed to 0.41 +2024/12/16 15:13:32 [DEBUG] [VIDEO] sensor speed buffer: [11.62 10.58 11.68 0.00 0.00] +2024/12/16 15:13:32 [INFO] [VIDEO] smoothed sensor speed: 6.78 mph +2024/12/16 15:13:32 [DEBUG] [VIDEO] last playback speed: 6.78 mph +2024/12/16 15:13:32 [DEBUG] [VIDEO] sensor speed delta: 0.00 mph +2024/12/16 15:13:32 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:33 [INFO] [SPEED] BLE sensor speed: 12.42 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] sensor speed buffer: [10.58 11.68 0.00 0.00 12.42] +2024/12/16 15:13:33 [INFO] [VIDEO] smoothed sensor speed: 6.94 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] last playback speed: 6.78 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] sensor speed delta: 0.16 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] sensor speed buffer: [10.58 11.68 0.00 0.00 12.42] +2024/12/16 15:13:33 [INFO] [VIDEO] smoothed sensor speed: 6.94 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] last playback speed: 6.78 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] sensor speed delta: 0.16 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] sensor speed buffer: [10.58 11.68 0.00 0.00 12.42] +2024/12/16 15:13:33 [INFO] [VIDEO] smoothed sensor speed: 6.94 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] last playback speed: 6.78 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] sensor speed delta: 0.16 mph +2024/12/16 15:13:33 [DEBUG] [VIDEO] playback speed update threshold: 0.25 mph +2024/12/16 15:13:33 [INFO] [SPEED] BLE sensor speed: 0.00 mph +2024/12/16 15:13:33 [INFO] [VIDEO] context cancelled, stopping video player... +2024/12/16 15:13:33 [ERROR] [APP] context canceled +2024/12/16 15:13:33 [INFO] [APP] application shutdown complete... goodbye! ``` ## FAQ @@ -300,7 +303,7 @@ Q: Do all Bluetooth devices work with **BLE Sync Cycle**? A: Not necessarily. The Bluetooth package used by **BLE Sync Cycle**, [called Go Bluetooth by TinyGo.org](https://github.com/tinygo-org/bluetooth), is based on the [Bluetooth Low Energy (BLE) standard](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy). Some Bluetooth devices may not be compatible with this protocol. Q: Can I disable the log messages in **BLE Sync Cycle**? -A: While you cannot disable all log messages, check out the `logging_level` parameter in the `config.toml` file (see the [Editing the TOML File](#editing-the-toml-file) section above). This parameter can be set to "debug", "info", "warn", or "error", where "debug" is the most verbose and "error" is least verbose. When set to "error", only error/fatal messages will be displayed which, under normal circumstances, should be none. +A: While you cannot disable all log messages, check out the `logging_level` parameter in the `config.toml` file (see the [Editing the TOML File](#editing-the-toml-file) section above). This parameter can be set to "debug", "info", "warn", or "error", where "debug" is the most verbose and "error" is least verbose. Q: How do I use **BLE Sync Cycle**? A: See the [Basic Usage](#basic-usage) section above diff --git a/cmd/main.go b/cmd/main.go index 3602286..1bcc014 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,7 +6,6 @@ import ( "log" "os" "os/signal" - "sync" "syscall" ble "github.com/richbl/go-ble-sync-cycle/internal/ble" @@ -31,33 +30,64 @@ func main() { // Load configuration cfg, err := config.LoadFile("config.toml") if err != nil { - log.Fatal("FATAL - Failed to load TOML configuration: " + err.Error()) + log.Fatal("[FATAL]: failed to load TOML configuration: " + err.Error()) } // Initialize logger - logger.Initialize(cfg.App.LogLevel) + if _, err := logger.Initialize(cfg.App.LogLevel); err != nil { + log.Printf("[WARN]: logger initialization warning: %v", err) + } // Create contexts for managing goroutines and cancellations rootCtx, rootCancel := context.WithCancel(context.Background()) defer rootCancel() // Create component controllers - controllers, err := setupAppControllers(*cfg) + controllers, componentType, err := setupAppControllers(*cfg) if err != nil { - logger.Fatal(logger.APP, "Failed to create controllers: "+err.Error()) + logger.Fatal(componentType, "failed to create controllers: "+err.Error()) } // Run the application - if err := startAppControllers(rootCtx, controllers); err != nil { - logger.Error(err.Error()) + if componentType, err := startAppControllers(rootCtx, controllers); err != nil { + logger.Error(componentType, err.Error()) } // Shutdown the application... buh bye! - logger.Info(logger.APP, "Application shutdown complete. Goodbye!") + logger.Info(logger.APP, "application shutdown complete... goodbye!") +} + +// setupAppControllers creates and initializes the application controllers +func setupAppControllers(cfg config.Config) (appControllers, logger.ComponentType, error) { + + // Create speed and video controllers + speedController := speed.NewSpeedController(cfg.Speed.SmoothingWindow) + videoPlayer, err := video.NewPlaybackController(cfg.Video, cfg.Speed) + if err != nil { + return appControllers{}, logger.VIDEO, errors.New("failed to create video player: " + err.Error()) + } + + // Create BLE controller + bleController, err := ble.NewBLEController(cfg.BLE, cfg.Speed) + if err != nil { + return appControllers{}, logger.BLE, errors.New("failed to create BLE controller: " + err.Error()) + } + + return appControllers{ + speedController: speedController, + videoPlayer: videoPlayer, + bleController: bleController, + }, logger.APP, nil } // startAppControllers is responsible for starting and managing the component controllers -func startAppControllers(ctx context.Context, controllers appControllers) error { +func startAppControllers(ctx context.Context, controllers appControllers) (logger.ComponentType, error) { + + // componentErr holds the error type and component type used for logging + type componentErr struct { + componentType logger.ComponentType + err error + } // Create shutdown signal ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) @@ -66,74 +96,41 @@ func startAppControllers(ctx context.Context, controllers appControllers) error // Scan for BLE peripheral of interest bleSpeedCharacter, err := scanForBLESpeedCharacteristic(ctx, controllers) if err != nil { - return errors.New(string(logger.BLE) + " BLE peripheral scan failed: " + err.Error()) + return logger.BLE, errors.New("BLE peripheral scan failed: " + err.Error()) } // Start component controllers concurrently - var wg sync.WaitGroup - errs := make(chan error, 2) + errs := make(chan componentErr, 1) - // Start BLE sensor speed monitoring - wg.Add(1) + // Monitor BLE speed go func() { - defer wg.Done() if err := monitorBLESpeed(ctx, controllers, bleSpeedCharacter); err != nil { - errs <- errors.New(string(logger.BLE) + "Speed monitoring failed: " + err.Error()) + errs <- componentErr{logger.BLE, err} + return } + errs <- componentErr{logger.BLE, nil} }() - // Start video playback - wg.Add(1) + // Play video go func() { - defer wg.Done() if err := playVideo(ctx, controllers); err != nil { - errs <- errors.New(string(logger.VIDEO) + "Playback failed: " + err.Error()) + errs <- componentErr{logger.VIDEO, err} + return } + errs <- componentErr{logger.VIDEO, nil} }() - // Wait for shutdown signals from go routines - go func() { - <-ctx.Done() - logger.Info(logger.APP, "Shutdown signal received") - }() - - // Wait for goroutines to finish or an error to occur - go func() { - wg.Wait() - close(errs) - }() - - // Check for errors from components - if err := <-errs; err != nil { - return err - } - - return nil -} - -// setupAppControllers creates and initializes the application controllers -func setupAppControllers(cfg config.Config) (appControllers, error) { - - // Create speed controller - speedController := speed.NewSpeedController(cfg.Speed.SmoothingWindow) - - // Create video player - videoPlayer, err := video.NewPlaybackController(cfg.Video, cfg.Speed) - if err != nil { - return appControllers{}, errors.New(string(logger.VIDEO) + "Failed to create video player: " + err.Error()) - } - - // Create BLE controller - bleController, err := ble.NewBLEController(cfg.BLE, cfg.Speed) - if err != nil { - return appControllers{}, errors.New(string(logger.BLE) + "Failed to create BLE controller: " + err.Error()) + // Wait for context cancellation or error + select { + case <-ctx.Done(): + return logger.APP, ctx.Err() + case compErr := <-errs: + if compErr.err != nil { + return compErr.componentType, compErr.err + } } - return appControllers{ - speedController: speedController, - videoPlayer: videoPlayer, - bleController: bleController, - }, nil + return logger.APP, nil } // scanForBLESpeedCharacteristic scans for the BLE CSC speed characteristic diff --git a/internal/ble/sensor_controller.go b/internal/ble/sensor_controller.go index b364a7a..e3aa91d 100644 --- a/internal/ble/sensor_controller.go +++ b/internal/ble/sensor_controller.go @@ -8,13 +8,27 @@ import ( "strconv" "time" + "tinygo.org/x/bluetooth" + config "github.com/richbl/go-ble-sync-cycle/internal/configuration" logger "github.com/richbl/go-ble-sync-cycle/internal/logging" speed "github.com/richbl/go-ble-sync-cycle/internal/speed" +) - "tinygo.org/x/bluetooth" +// Constants for speed calculations and BLE data parsing +const ( + minDataLength = 7 + wheelRevFlag = uint8(0x01) + kphConversion = 3.6 + mphConversion = 2.23694 ) +// SpeedMeasurement represents the wheel revolution and time data from a BLE sensor +type SpeedMeasurement struct { + wheelRevs uint32 + wheelTime uint16 +} + // BLEController represents the BLE central controller component type BLEController struct { bleConfig config.BLEConfig @@ -22,24 +36,12 @@ type BLEController struct { bleAdapter bluetooth.Adapter } -type SpeedMeasurement struct { - wheelRevs uint32 - wheelTime uint16 -} - +// Package-level variables for tracking speed measurements var ( - // CSC speed tracking variables lastWheelRevs uint32 lastWheelTime uint16 ) -const ( - minDataLength = 7 - wheelRevFlag = uint8(0x01) - kphConversion = 3.6 - mphConversion = 2.23694 -) - // NewBLEController creates a new BLE central controller for accessing a BLE peripheral func NewBLEController(bleConfig config.BLEConfig, speedConfig config.SpeedConfig) (*BLEController, error) { @@ -49,7 +51,7 @@ func NewBLEController(bleConfig config.BLEConfig, speedConfig config.SpeedConfig return nil, err } - logger.Info(logger.BLE, "Created new BLE central controller") + logger.Info(logger.BLE, "created new BLE central controller") return &BLEController{ bleConfig: bleConfig, @@ -58,6 +60,62 @@ func NewBLEController(bleConfig config.BLEConfig, speedConfig config.SpeedConfig }, nil } +// ScanForBLEPeripheral scans for a BLE peripheral with the specified UUID +func (m *BLEController) ScanForBLEPeripheral(ctx context.Context) (bluetooth.ScanResult, error) { + + // Create context with timeout + scanCtx, cancel := context.WithTimeout(ctx, time.Duration(m.bleConfig.ScanTimeoutSecs)*time.Second) + defer cancel() + + found := make(chan bluetooth.ScanResult, 1) + errChan := make(chan error, 1) + + go func() { + logger.Info(logger.BLE, "now scanning the ether for BLE peripheral UUID of "+m.bleConfig.SensorUUID+"...") + + if err := m.startScanning(found); err != nil { + errChan <- err + } + }() + + // Wait for device discovery or timeout + select { + case result := <-found: + logger.Debug(logger.BLE, "found BLE peripheral "+result.Address.String()) + return result, nil + case err := <-errChan: + return bluetooth.ScanResult{}, err + case <-scanCtx.Done(): + if err := m.bleAdapter.StopScan(); err != nil { + logger.Error(logger.BLE, "failed to stop scan: "+err.Error()) + } + return bluetooth.ScanResult{}, errors.New("scanning time limit reached") + } +} + +// startScanning starts the BLE scan and sends the result to the found channel when the target device is discovered +func (m *BLEController) startScanning(found chan<- bluetooth.ScanResult) error { + + // Start BLE scan + err := m.bleAdapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) { + // Check if the target peripheral was found + if result.Address.String() == m.bleConfig.SensorUUID { + // Stop scanning + if err := m.bleAdapter.StopScan(); err != nil { + logger.Error(fmt.Sprintf(string(logger.BLE)+"failed to stop scan: %v", err)) + } + + // Found the target peripheral + found <- result + } + }) + if err != nil { + logger.Error(logger.BLE, "scan error: "+err.Error()) + } + + return nil +} + // GetBLECharacteristic scans for the BLE peripheral and returns CSC services/characteristics func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedController *speed.SpeedController) (*bluetooth.DeviceCharacteristic, error) { @@ -67,7 +125,7 @@ func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedControlle return nil, err } - logger.Debug(logger.BLE, "Connecting to BLE peripheral device "+result.Address.String()) + logger.Debug(logger.BLE, "connecting to BLE peripheral device "+result.Address.String()) // Connect to BLE peripheral device var device bluetooth.Device @@ -76,7 +134,7 @@ func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedControlle } logger.Info(logger.BLE, "BLE peripheral device connected") - logger.Debug(logger.BLE, "Discovering CSC services "+bluetooth.New16BitUUID(0x1816).String()) + logger.Debug(logger.BLE, "discovering CSC services "+bluetooth.New16BitUUID(0x1816).String()) // Find CSC service and characteristic svc, err := device.DiscoverServices([]bluetooth.UUID{bluetooth.New16BitUUID(0x1816)}) @@ -85,8 +143,8 @@ func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedControlle return nil, err } - logger.Debug(logger.BLE, "Found CSC service "+svc[0].UUID().String()) - logger.Debug(logger.BLE, "Discovering CSC characteristics "+bluetooth.New16BitUUID(0x2A5B).String()) + logger.Debug(logger.BLE, "found CSC service "+svc[0].UUID().String()) + logger.Debug(logger.BLE, "discovering CSC characteristics "+bluetooth.New16BitUUID(0x2A5B).String()) char, err := svc[0].DiscoverCharacteristics([]bluetooth.UUID{bluetooth.New16BitUUID(0x2A5B)}) if err != nil { @@ -94,14 +152,14 @@ func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedControlle return nil, err } - logger.Debug(logger.BLE, "Found CSC characteristic "+char[0].UUID().String()) + logger.Debug(logger.BLE, "found CSC characteristic "+char[0].UUID().String()) return &char[0], nil } // GetBLEUpdates enables BLE peripheral monitoring to report real-time sensor data func (m *BLEController) GetBLEUpdates(ctx context.Context, speedController *speed.SpeedController, char *bluetooth.DeviceCharacteristic) error { - logger.Debug(logger.BLE, "Starting real-time monitoring of BLE sensor notifications...") + logger.Debug(logger.BLE, "starting real-time monitoring of BLE sensor notifications...") // Subscribe to live BLE sensor notifications if err := char.EnableNotifications(func(buf []byte) { @@ -115,69 +173,13 @@ func (m *BLEController) GetBLEUpdates(ctx context.Context, speedController *spee return nil } -// ScanForBLEPeripheral scans for a BLE peripheral with the specified UUID -func (m *BLEController) ScanForBLEPeripheral(ctx context.Context) (bluetooth.ScanResult, error) { - - scanCtx, cancel := context.WithTimeout(ctx, time.Duration(m.bleConfig.ScanTimeoutSecs)*time.Second) - defer cancel() - - found := make(chan bluetooth.ScanResult, 1) - errChan := make(chan error, 1) - - go func() { - logger.Info(logger.BLE, "Now scanning the ether for BLE peripheral UUID of "+m.bleConfig.SensorUUID+"...") - - if err := m.startScanning(found); err != nil { - errChan <- err - } - }() - - // Wait for device discovery or timeout - select { - case result := <-found: - logger.Debug(logger.BLE, "Found BLE peripheral "+result.Address.String()) - return result, nil - case err := <-errChan: - return bluetooth.ScanResult{}, err - case <-scanCtx.Done(): - if err := m.bleAdapter.StopScan(); err != nil { - logger.Error(logger.BLE, "Failed to stop scan: "+err.Error()) - } - return bluetooth.ScanResult{}, errors.New("scanning time limit reached") - } -} - -// startScanning starts the BLE scan and sends the result to the found channel when the target device is discovered -func (m *BLEController) startScanning(found chan<- bluetooth.ScanResult) error { - - err := m.bleAdapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) { - - // Check if the target peripheral was found - if result.Address.String() == m.bleConfig.SensorUUID { - - // Stop scanning - if err := m.bleAdapter.StopScan(); err != nil { - logger.Error(fmt.Sprintf(string(logger.BLE)+"Failed to stop scan: %v", err)) - } - - // Found the target peripheral - found <- result - } - }) - - if err != nil { - logger.Error(logger.BLE, "Scan error: "+err.Error()) - } - return nil -} - // ProcessBLESpeed processes the raw speed data from the BLE peripheral func (m *BLEController) ProcessBLESpeed(data []byte) float64 { // Parse speed data newSpeedData, err := m.parseSpeedData(data) if err != nil { - logger.Error(logger.SPEED, "Invalid BLE data: %v", err) + logger.Error(logger.SPEED, "invalid BLE data: "+err.Error()) return 0.0 } diff --git a/internal/ble/sensor_controller_test.go b/internal/ble/sensor_controller_test.go index 5285ae4..17bd2e7 100644 --- a/internal/ble/sensor_controller_test.go +++ b/internal/ble/sensor_controller_test.go @@ -5,20 +5,27 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/richbl/go-ble-sync-cycle/internal/ble" config "github.com/richbl/go-ble-sync-cycle/internal/configuration" logger "github.com/richbl/go-ble-sync-cycle/internal/logging" - "github.com/stretchr/testify/assert" ) +// Constants for test configuration and messages const ( - speedUnitsKMH = "kph" - speedUnitsMPH = "mph" - sensorTestUUID = "test-uuid" - testTimeout = 2 * time.Second - initialScanDelay = 2 * time.Second + // Speed units + speedUnitsKMH = "kph" + speedUnitsMPH = "mph" + + // Test identifiers and parameters + sensorTestUUID = "test-uuid" + testTimeout = 2 * time.Second + initialScanDelay = 2 * time.Second + wheelCircumferenceMM = 2000 + + // Error and test case messages noBLEAdapterError = "Skipping test as BLE adapter is not available" - wheelCircumferenceMM = 2000 emptyData = "empty data" invalidFlags = "invalid flags" validDataKPHFirst = "valid data kph - first reading" @@ -59,6 +66,21 @@ func createTestController(speedUnits string) (*ble.BLEController, error) { return ble.NewBLEController(bleConfig, speedConfig) } +// controllersIntegrationTest pauses BLE scan and then creates controllers +func controllersIntegrationTest() (*ble.BLEController, error) { + + // Pause to permit BLE adapter to reset + waitForScanReset() + + // Create test BLE and speed controllers + controller, err := createTestController(speedUnitsKMH) + if err != nil { + return nil, err + } + + return controller, nil +} + // TestProcessBLESpeed tests the ProcessBLESpeed() function func TestProcessBLESpeed(t *testing.T) { @@ -116,7 +138,6 @@ func TestProcessBLESpeed(t *testing.T) { // Loop through the test cases for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create test BLE and speed controllers controller, err := createTestController(tt.speedUnits) if err != nil { @@ -188,18 +209,3 @@ func TestGetBLECharacteristicIntegration(t *testing.T) { _, err = controller.GetBLECharacteristic(ctx, nil) assert.Error(t, err) } - -// controllersIntegrationTest pauses BLE scan and then creates controllers -func controllersIntegrationTest() (*ble.BLEController, error) { - - // Pause to permit BLE adapter to reset - waitForScanReset() - - // Create test BLE and speed controllers - controller, err := createTestController(speedUnitsKMH) - if err != nil { - return nil, err - } - - return controller, nil -} diff --git a/internal/configuration/config.go b/internal/configuration/config.go index 678022f..7e36ebe 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -9,6 +9,20 @@ import ( "github.com/BurntSushi/toml" ) +// Constants for valid configuration values +const ( + // Log levels + logLevelDebug = "debug" + logLevelInfo = "info" + logLevelWarn = "warn" + logLevelError = "error" + logLevelFatal = "fatal" + + // Speed units + SpeedUnitsKMH = "km/h" + SpeedUnitsMPH = "mph" +) + // Config represents the application configuration type Config struct { App AppConfig `toml:"app"` @@ -36,6 +50,13 @@ type SpeedConfig struct { SpeedUnits string `toml:"speed_units"` } +// VideoOSDConfig represents the on-screen display configuration +type VideoOSDConfig struct { + DisplayCycleSpeed bool `toml:"display_cycle_speed"` + DisplayPlaybackSpeed bool `toml:"display_playback_speed"` + ShowOSD bool +} + // VideoConfig represents the MPV video player configuration type VideoConfig struct { FilePath string `toml:"file_path"` @@ -45,24 +66,6 @@ type VideoConfig struct { OnScreenDisplay VideoOSDConfig `toml:"OSD"` } -type VideoOSDConfig struct { - DisplayCycleSpeed bool `toml:"display_cycle_speed"` - DisplayPlaybackSpeed bool `toml:"display_playback_speed"` - ShowOSD bool -} - -// Constants for valid configuration values -const ( - logLevelDebug = "debug" - logLevelInfo = "info" - logLevelWarn = "warn" - logLevelError = "error" - logLevelFatal = "fatal" - - SpeedUnitsKMH = "km/h" - SpeedUnitsMPH = "mph" -) - // LoadFile attempts to load the TOML configuration file from the specified path, // falling back to the default configuration directory if not found func LoadFile(filename string) (*Config, error) { @@ -94,7 +97,6 @@ func LoadFile(filename string) (*Config, error) { // Successfully loaded TOML file return cfg, nil - } // Failed to load TOML file @@ -119,6 +121,7 @@ func (c *Config) validate() error { if err := c.Video.validate(); err != nil { return err } + return nil } @@ -134,6 +137,17 @@ func (ac *AppConfig) validate() error { } } +// validate validates BLEConfig elements +func (bc *BLEConfig) validate() error { + + // Check if the sensor UUID is specified + if bc.SensorUUID == "" { + return errors.New("sensor UUID must be specified in configuration") + } + + return nil +} + // validate validates SpeedConfig elements func (sc *SpeedConfig) validate() error { @@ -146,16 +160,6 @@ func (sc *SpeedConfig) validate() error { } } -// validate validates BLEConfig elements -func (bc *BLEConfig) validate() error { - - // Check if the sensor UUID is specified - if bc.SensorUUID == "" { - return errors.New("sensor UUID must be specified in configuration") - } - return nil -} - // validate validates VideoConfig elements func (vc *VideoConfig) validate() error { diff --git a/internal/configuration/config_test.go b/internal/configuration/config_test.go index 7146c2e..07da43c 100644 --- a/internal/configuration/config_test.go +++ b/internal/configuration/config_test.go @@ -6,14 +6,22 @@ import ( "testing" ) +// Constants for test configuration and messages const ( + // Test filenames testFilename = "test.mp4" invalidTestFilename = "non-existent-file.mp4" - sensorTestUUID = "test-uuid" - logLevelInvalid = "invalid" - validConfig = "Valid config" - invalidConfig = "Invalid config" - validateErrMessage = "validate() error = %v, wantErr %v" + + // Test identifiers + sensorTestUUID = "test-uuid" + + // Log levels and validation + logLevelInvalid = "invalid" + + // Test case names + validConfig = "Valid config" + invalidConfig = "Invalid config" + validateErrMessage = "validate() error = %v, wantErr %v" ) // configTestCase is a helper struct for running validation tests @@ -81,7 +89,6 @@ func createTempConfigFile(t *testing.T, config string) (string, func()) { // Replace the video file path with a temporary file if strings.Contains(config, testFilename) { - // Create a temporary video file tmpVideoFile, err := os.CreateTemp("", "video") if err != nil { @@ -117,7 +124,6 @@ func runValidationTest[T any](t *testing.T, tests []configTestCase[T]) { // Run tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Handle both value and pointer receivers var err error @@ -188,58 +194,7 @@ func TestValidate(t *testing.T) { } } -// TestValidateVideoConfig tests the validate function -func TestValidateVideoConfig(t *testing.T) { - - // Define test cases - tests := []configTestCase[VideoConfig]{ - { - name: validConfig, - config: VideoConfig{ - FilePath: testFilename, - OnScreenDisplay: VideoOSDConfig{ - DisplayPlaybackSpeed: true, - }, - WindowScaleFactor: 1.0, - UpdateIntervalSec: 1, - SpeedMultiplier: 1.0, - }, - wantErr: false, - }, - { - name: invalidConfig, - config: VideoConfig{ - FilePath: invalidTestFilename, - OnScreenDisplay: VideoOSDConfig{ - DisplayPlaybackSpeed: true, - }, - WindowScaleFactor: -1.0, - UpdateIntervalSec: -1, - SpeedMultiplier: -1.0, - }, - wantErr: true, - }, - } - - // Run tests - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.config.FilePath != invalidTestFilename { - tmpFile, err := os.CreateTemp("", "test") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpFile.Name()) - tt.config.FilePath = tmpFile.Name() - } - if err := tt.config.validate(); (err != nil) != tt.wantErr { - t.Errorf(validateErrMessage, err, tt.wantErr) - } - }) - } -} - -// TestValidateAppConfig tests the validate function +// TestValidateAppConfig tests the validate function for AppConfig func TestValidateAppConfig(t *testing.T) { // Define test cases @@ -260,7 +215,7 @@ func TestValidateAppConfig(t *testing.T) { runValidationTest(t, tests) } -// TestValidateBLEConfig tests the validate function +// TestValidateBLEConfig tests the validate function for BLEConfig func TestValidateBLEConfig(t *testing.T) { // Define test cases @@ -287,7 +242,7 @@ func TestValidateBLEConfig(t *testing.T) { runValidationTest(t, tests) } -// TestValidateSpeedConfig tests the validate function +// TestValidateSpeedConfig tests the validate function for SpeedConfig func TestValidateSpeedConfig(t *testing.T) { // Define test cases @@ -317,3 +272,54 @@ func TestValidateSpeedConfig(t *testing.T) { // Run tests runValidationTest(t, tests) } + +// TestValidateVideoConfig tests the validate function for VideoConfig +func TestValidateVideoConfig(t *testing.T) { + + // Define test cases + tests := []configTestCase[VideoConfig]{ + { + name: validConfig, + config: VideoConfig{ + FilePath: testFilename, + OnScreenDisplay: VideoOSDConfig{ + DisplayPlaybackSpeed: true, + }, + WindowScaleFactor: 1.0, + UpdateIntervalSec: 1, + SpeedMultiplier: 1.0, + }, + wantErr: false, + }, + { + name: invalidConfig, + config: VideoConfig{ + FilePath: invalidTestFilename, + OnScreenDisplay: VideoOSDConfig{ + DisplayPlaybackSpeed: true, + }, + WindowScaleFactor: -1.0, + UpdateIntervalSec: -1, + SpeedMultiplier: -1.0, + }, + wantErr: true, + }, + } + + // Run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.config.FilePath != invalidTestFilename { + tmpFile, err := os.CreateTemp("", "test") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile.Name()) + tt.config.FilePath = tmpFile.Name() + } + if err := tt.config.validate(); (err != nil) != tt.wantErr { + t.Errorf(validateErrMessage, err, tt.wantErr) + } + }) + } +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go index adba2c2..9563729 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -9,11 +9,22 @@ import ( "strings" ) +// logger is the global logger var logger *slog.Logger +// ExitFunc represents the exit function (used for testing) +var ExitFunc = os.Exit + // ComponentType represents the type of component type ComponentType string +// CustomTextHandler represents a custom text handler +type CustomTextHandler struct { + slog.Handler + out io.Writer + level slog.Level +} + const ( APP ComponentType = "[APP]" BLE ComponentType = "[BLE]" @@ -21,12 +32,6 @@ const ( VIDEO ComponentType = "[VIDEO]" ) -// Create a new slog level for the Fatal logging level -const LevelFatal slog.Level = slog.Level(12) - -// ExitFunc represents the exit function (needed for testing) -var ExitFunc = os.Exit - // Color constants const ( Reset = "\033[0m" @@ -39,68 +44,94 @@ const ( White = "\033[37m" ) -// CustomTextHandler represents a custom text handler -type CustomTextHandler struct { - slog.Handler - writer io.Writer - level slog.Level +// Create a new slog level for the Fatal logging level +const LevelFatal slog.Level = slog.Level(12) + +// Initialize sets up the logger +func Initialize(logLevel string) (*slog.Logger, error) { + + level, err := parseLogLevel(logLevel) + if err != nil { + // Still create the logger but with the default level + logger = slog.New(NewCustomTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})) + return logger, err + } + + logger = slog.New(NewCustomTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})) + return logger, nil +} + +// Info logs an info message +func Info(first interface{}, args ...interface{}) { + logWithOptionalComponent(context.Background(), slog.LevelInfo, first, args...) +} + +// Warn logs a warning message +func Warn(first interface{}, args ...interface{}) { + logWithOptionalComponent(context.Background(), slog.LevelWarn, first, args...) +} + +// Error logs an error message +func Error(first interface{}, args ...interface{}) { + logWithOptionalComponent(context.Background(), slog.LevelError, first, args...) +} + +// Debug logs a debug message +func Debug(first interface{}, args ...interface{}) { + logWithOptionalComponent(context.Background(), slog.LevelDebug, first, args...) +} + +// Fatal logs a fatal message +func Fatal(first interface{}, args ...interface{}) { + logWithOptionalComponent(context.Background(), LevelFatal, first, args...) + ExitFunc(1) } // NewCustomTextHandler creates a new custom text handler func NewCustomTextHandler(w io.Writer, opts *slog.HandlerOptions) *CustomTextHandler { + + // Set default values if not provided + if w == nil { + w = os.Stdout + } + if opts == nil { + opts = &slog.HandlerOptions{Level: slog.LevelInfo} + } + + // Create the custom text handler textHandler := slog.NewTextHandler(w, opts) return &CustomTextHandler{ Handler: textHandler, - writer: w, + out: w, level: opts.Level.(slog.Level), } } +// Handle handles the log record func (h *CustomTextHandler) Handle(ctx context.Context, r slog.Record) error { - // Create custom logger output - timestamp := r.Time.Format("2006/01/02 15:04:05") - level := strings.ToUpper(r.Level.String()) - msg := r.Message - - var color string - switch r.Level { - case slog.LevelDebug: - color = Blue - case slog.LevelInfo: - color = Green - case slog.LevelWarn: - color = Yellow - case slog.LevelError: - color = Red - case LevelFatal: - color = Magenta - default: - color = White + // Check if context is done + if ctx.Err() != nil { + return ctx.Err() } + // Create custom logger output + timestamp := r.Time.Format("2006/01/02 15:04:05") + level := strings.TrimSpace("["+(r.Level.String())+"]") if r.Level == LevelFatal { level = "FATAL" } - - var component string - - // Get component from attributes - r.Attrs(func(a slog.Attr) bool { - if a.Key == "component" { - component = a.Value.String() - } - return true - }) + msg := r.Message // Write output format to writer - msgPattern := "%s %s%s%s%s %s%s\n" - - if len(component) > 0 { - msgPattern = "%s %s%s%s %s %s%s\n" - } - - fmt.Fprintf(h.writer, msgPattern, timestamp, color, level, Reset, component, msg, Reset) + fmt.Fprintf(h.out, "%s %s%s %s%s%s%s\n", + timestamp, + h.getColorForLevel(r.Level), + level, + Reset, + h.getComponentFromAttrs(r), + msg, + Reset) return nil } @@ -113,7 +144,7 @@ func (h *CustomTextHandler) Enabled(ctx context.Context, level slog.Level) bool func (h *CustomTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &CustomTextHandler{ Handler: h.Handler.WithAttrs(attrs), - writer: h.writer, + out: h.out, } } @@ -121,74 +152,90 @@ func (h *CustomTextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { func (h *CustomTextHandler) WithGroup(name string) slog.Handler { return &CustomTextHandler{ Handler: h.Handler.WithGroup(name), - writer: h.writer, + out: h.out, } } -// Initialize sets up the logger -func Initialize(logLevel string) *slog.Logger { +// getColorForLevel returns the color for the given log level +func (h *CustomTextHandler) getColorForLevel(level slog.Level) string { + + // Make color assignments based on log level + switch level { + case slog.LevelDebug: + return Blue + case slog.LevelInfo: + return Green + case slog.LevelWarn: + return Yellow + case slog.LevelError: + return Red + case LevelFatal: + return Magenta + default: + return White + } +} + +// getComponentFromAttrs extracts and formats the component from record attributes +func (h *CustomTextHandler) getComponentFromAttrs(r slog.Record) string { + + var component string + + // Extract optional component from attributes + r.Attrs(func(a slog.Attr) bool { + if a.Key == "component" { + component = a.Value.String() + if component != "" { + component = component + " " + } + return false + } + return true + }) - // Set log level - var level slog.Level - switch logLevel { + return component +} + +// parseLogLevel converts a string log level to slog.Level +func parseLogLevel(level string) (slog.Level, error) { + + // Convert log level to slog.Level + switch strings.ToLower(level) { case "debug": - level = slog.LevelDebug + return slog.LevelDebug, nil case "info": - level = slog.LevelInfo + return slog.LevelInfo, nil case "warn": - level = slog.LevelWarn + return slog.LevelWarn, nil case "error": - level = slog.LevelError + return slog.LevelError, nil default: - level = slog.LevelInfo + return slog.LevelInfo, fmt.Errorf("invalid log level: %s, defaulting to info", level) } - - // Initialize logger - logger = slog.New(NewCustomTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})) - return logger } // logWithOptionalComponent logs a message with an optional component -func logWithOptionalComponent(level slog.Level, first interface{}, args ...interface{}) { +func logWithOptionalComponent(ctx context.Context, level slog.Level, first interface{}, args ...interface{}) { + + // Check if context is nil + if ctx == nil { + ctx = context.Background() + } + var msg string var component string - // Check if first argument is a ComponentType - if comp, ok := first.(ComponentType); ok { + // Check if the first argument is an optional ComponentType + switch v := first.(type) { + case ComponentType: + component = string(v) if len(args) > 0 { msg = fmt.Sprint(args[0]) } - component = string(comp) - } else { + default: msg = fmt.Sprint(first) } - // Log message - logger.LogAttrs(context.Background(), level, msg, slog.String("component", component)) -} - -// Info logs an info message -func Info(first interface{}, args ...interface{}) { - logWithOptionalComponent(slog.LevelInfo, first, args...) -} - -// Warn logs a warning message -func Warn(first interface{}, args ...interface{}) { - logWithOptionalComponent(slog.LevelWarn, first, args...) -} - -// Error logs an error message -func Error(first interface{}, args ...interface{}) { - logWithOptionalComponent(slog.LevelError, first, args...) -} - -// Debug logs a debug message -func Debug(first interface{}, args ...interface{}) { - logWithOptionalComponent(slog.LevelDebug, first, args...) -} - -// Fatal logs a fatal message -func Fatal(first interface{}, args ...interface{}) { - logWithOptionalComponent(LevelFatal, first, args...) - ExitFunc(1) + msg = strings.TrimSpace(msg) + logger.LogAttrs(ctx, level, msg, slog.String("component", component)) } diff --git a/internal/logging/logger_test.go b/internal/logging/logger_test.go index 0edd422..8ab8346 100644 --- a/internal/logging/logger_test.go +++ b/internal/logging/logger_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "log/slog" + "regexp" "strings" "testing" "time" @@ -14,7 +15,6 @@ const ( defaultOptions = slog.LevelDebug ) -// testCase represents a generic test case for logging tests type testCase struct { name string level interface{} // can be string or slog.Level @@ -22,15 +22,13 @@ type testCase struct { setLevel slog.Level // used only for Enabled tests } -// setupTestLogger creates a new logger with a buffer for testing func setupTestLogger() (*bytes.Buffer, *slog.Logger) { - buf := &bytes.Buffer{} handler := NewCustomTextHandler(buf, &slog.HandlerOptions{Level: defaultOptions}) return buf, slog.New(handler) - } +// TestInitialize tests the Initialize function func TestInitialize(t *testing.T) { // Define test cases @@ -68,11 +66,11 @@ func TestCustomTextHandler(t *testing.T) { // Define test cases tests := []testCase{ - {"debug", slog.LevelDebug, "\033[34mDEBUG\033[0m", 0}, - {"info", slog.LevelInfo, "\033[32mINFO\033[0m", 0}, - {"warn", slog.LevelWarn, "\033[33mWARN\033[0m", 0}, - {"error", slog.LevelError, "\033[31mERROR\033[0m", 0}, - {"fatal", LevelFatal, "\033[35mFATAL\033[0m", 0}, + {"debug", slog.LevelDebug, "\033[34m[DEBUG] \033[0m", 0}, + {"info", slog.LevelInfo, "\033[32m[INFO] \033[0m", 0}, + {"warn", slog.LevelWarn, "\033[33m[WARN] \033[0m", 0}, + {"error", slog.LevelError, "\033[31m[ERROR] \033[0m", 0}, + {"fatal", LevelFatal, "\033[35mFATAL \033[0m", 0}, } // Run tests @@ -85,14 +83,27 @@ func TestCustomTextHandler(t *testing.T) { if err := h.Handle(context.Background(), r); err != nil { t.Fatalf("Handle() error = %v", err) } - if !strings.Contains(buf.String(), tt.want.(string)) { - t.Errorf("got %q, want %q", buf.String(), tt.want) + + output := buf.String() + expectedLevel := tt.want.(string) + + timestampRegex := `^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} ` + if !regexp.MustCompile(timestampRegex).MatchString(output) { + t.Errorf("output does not start with a valid timestamp") + } + + if !strings.Contains(output, expectedLevel) { + t.Errorf("output %q does not contain expected level %q", output, expectedLevel) + } + + if !strings.Contains(output, testMessage) { + t.Errorf("output %q does not contain message %q", output, testMessage) } }) } } -// TestLogLevels tests all logging level functions +// TestLogLevels tests the log levels func TestLogLevels(t *testing.T) { // Define test cases @@ -127,12 +138,10 @@ func TestFatal(t *testing.T) { buf, testLogger := setupTestLogger() logger = testLogger - // Save and restore exit function so we can check if it was called savedExitFunc := ExitFunc defer func() { ExitFunc = savedExitFunc }() exitCalled := false - // Mock exit function ExitFunc = func(code int) { exitCalled = true if code != 1 { @@ -140,7 +149,6 @@ func TestFatal(t *testing.T) { } } - // Call Fatal function for testing Fatal(testMessage) if !exitCalled { t.Error("Fatal did not call exit function") diff --git a/internal/speed/speed_controller.go b/internal/speed/speed_controller.go index dedb67a..6909eca 100644 --- a/internal/speed/speed_controller.go +++ b/internal/speed/speed_controller.go @@ -7,7 +7,7 @@ import ( "time" ) -// SpeedController represents the speed controller component +// SpeedController manages speed measurements with smoothing type SpeedController struct { speeds *ring.Ring window int @@ -16,14 +16,12 @@ type SpeedController struct { lastUpdate time.Time } -// Mutex for managing concurrent access +// mutex manages concurrent access to SpeedController var mutex sync.RWMutex -// NewSpeedController creates a new speed controller component which includes a ring buffer for -// storing speed measurements for video playback speed smoothing +// NewSpeedController creates a new speed controller with a specified window size func NewSpeedController(window int) *SpeedController { - // Create ring buffer r := ring.New(window) // Initialize ring with zero values @@ -38,50 +36,21 @@ func NewSpeedController(window int) *SpeedController { } } -// UpdateSpeed updates the current speed measurement and calculates a smoothed average speed over -// the specified window of time -func (t *SpeedController) UpdateSpeed(speed float64) { - - // Lock mutex to prevent concurrent access - mutex.Lock() - defer mutex.Unlock() - - // Get speeds - t.currentSpeed = speed - t.speeds.Value = speed - t.speeds = t.speeds.Next() - - // Calculate smoothed speed - sum := float64(0) - - t.speeds.Do(func(x interface{}) { - if x != nil { - sum += x.(float64) - } - }) - - t.smoothedSpeed = sum / float64(t.window) - t.lastUpdate = time.Now() -} - // GetSmoothedSpeed returns the smoothed speed measurement func (t *SpeedController) GetSmoothedSpeed() float64 { - // Lock mutex to prevent concurrent access mutex.RLock() defer mutex.RUnlock() return t.smoothedSpeed } -// GetSpeedBuffer returns the speed buffer as an array of string +// GetSpeedBuffer returns the speed buffer as an array of formatted strings func (t *SpeedController) GetSpeedBuffer() []string { - // Lock mutex to prevent concurrent access mutex.RLock() defer mutex.RUnlock() - // Create speed buffer var speeds []string t.speeds.Do(func(x interface{}) { if x != nil { @@ -91,3 +60,25 @@ func (t *SpeedController) GetSpeedBuffer() []string { return speeds } + +// UpdateSpeed updates the current speed measurement and calculates a smoothed average +func (t *SpeedController) UpdateSpeed(speed float64) { + + mutex.Lock() + defer mutex.Unlock() + + t.currentSpeed = speed + t.speeds.Value = speed + t.speeds = t.speeds.Next() + + // Calculate smoothed speed + sum := float64(0) + t.speeds.Do(func(x interface{}) { + if x != nil { + sum += x.(float64) + } + }) + + t.smoothedSpeed = sum / float64(t.window) + t.lastUpdate = time.Now() +} diff --git a/internal/speed/speed_controller_test.go b/internal/speed/speed_controller_test.go index 1afa6a2..ad3dd08 100644 --- a/internal/speed/speed_controller_test.go +++ b/internal/speed/speed_controller_test.go @@ -6,14 +6,10 @@ import ( "time" ) -const ( - defaultWindow = 5 -) +const defaultWindow = 5 // calculateAverage calculates the average of a slice of float64 func calculateAverage(data []float64) float64 { - - // Check for empty slice if len(data) == 0 { return 0.0 } @@ -26,54 +22,48 @@ func calculateAverage(data []float64) float64 { return sum / float64(len(data)) } -// TestNewSpeedController tests the creation of a new SpeedController +// TestNewSpeedController verifies SpeedController initialization func TestNewSpeedController(t *testing.T) { // Create new SpeedController controller := NewSpeedController(defaultWindow) - // Verify initial values if controller.window != defaultWindow { t.Errorf("Expected window size %d, got %d", defaultWindow, controller.window) } - // Verify ring buffer if controller.speeds.Len() != defaultWindow { t.Errorf("Expected ring buffer length %d, got %d", defaultWindow, controller.speeds.Len()) } - // Verify initial smoothed speed if controller.smoothedSpeed != 0 { t.Errorf("Expected initial smoothed speed to be 0, got %f", controller.smoothedSpeed) } } -// TestUpdateSpeed tests the UpdateSpeed method and verifies smoothed speed calculation +// TestUpdateSpeed checks speed update and smoothing calculation func TestUpdateSpeed(t *testing.T) { // Create new SpeedController controller := NewSpeedController(defaultWindow) speeds := []float64{1.0, 2.0, 3.0, 4.0, 5.0} - // Update speeds for _, speed := range speeds { controller.UpdateSpeed(speed) } - // Calculate and verify smoothed speed expectedSmoothedSpeed := calculateAverage(speeds) if smoothedSpeed := controller.GetSmoothedSpeed(); smoothedSpeed != expectedSmoothedSpeed { t.Errorf("Expected smoothed speed %f, got %f", expectedSmoothedSpeed, smoothedSpeed) } } -// TestGetSmoothedSpeed tests the GetSmoothedSpeed method +// TestGetSmoothedSpeed validates smoothed speed retrieval func TestGetSmoothedSpeed(t *testing.T) { // Create new SpeedController controller := NewSpeedController(defaultWindow) - // Create test case testCases := []struct { speed float64 expected float64 @@ -82,7 +72,6 @@ func TestGetSmoothedSpeed(t *testing.T) { {20.0, 6.0}, } - // Run test cases for _, tc := range testCases { controller.UpdateSpeed(tc.speed) if smoothed := controller.GetSmoothedSpeed(); smoothed != tc.expected { @@ -91,19 +80,17 @@ func TestGetSmoothedSpeed(t *testing.T) { } } -// TestGetSpeedBuffer tests the GetSpeedBuffer method +// TestGetSpeedBuffer checks speed buffer formatting func TestGetSpeedBuffer(t *testing.T) { // Create new SpeedController controller := NewSpeedController(defaultWindow) speeds := []float64{3.5, 2.5, 1.5, 0.0, 0.0} - // Update speeds for _, speed := range speeds { controller.UpdateSpeed(speed) } - // Verify buffer contents expectedBuffer := []string{"3.50", "2.50", "1.50", "0.00", "0.00"} buffer := controller.GetSpeedBuffer() @@ -114,7 +101,7 @@ func TestGetSpeedBuffer(t *testing.T) { } } -// TestConcurrency tests concurrent updates to the SpeedController +// TestConcurrency ensures thread-safe speed updates func TestConcurrency(t *testing.T) { // Create new SpeedController @@ -124,7 +111,6 @@ func TestConcurrency(t *testing.T) { numUpdates := 10 sleepDuration := 10 * time.Millisecond - // Simulate concurrent updates for i := 1; i <= numUpdates; i++ { wg.Add(1) @@ -137,7 +123,6 @@ func TestConcurrency(t *testing.T) { wg.Wait() - // Verify smoothed speed is non-zero if smoothedSpeed := controller.GetSmoothedSpeed(); smoothedSpeed == 0 { t.Error("Expected non-zero smoothed speed after concurrent updates") } diff --git a/internal/video-player/playback_controller.go b/internal/video-player/playback_controller.go index 662a140..704d7ca 100644 --- a/internal/video-player/playback_controller.go +++ b/internal/video-player/playback_controller.go @@ -3,19 +3,28 @@ package video import ( "context" "errors" + "fmt" "math" "strconv" "strings" "time" + "github.com/gen2brain/go-mpv" + config "github.com/richbl/go-ble-sync-cycle/internal/configuration" logger "github.com/richbl/go-ble-sync-cycle/internal/logging" speed "github.com/richbl/go-ble-sync-cycle/internal/speed" +) - "github.com/gen2brain/go-mpv" +// Common errors for playback control +var ( + ErrOSDUpdate = errors.New("failed to update OSD") + ErrPlaybackSpeed = errors.New("failed to set playback speed") + ErrVideoComplete = errors.New("playback completed: normal exit") + ErrSpeedUpdate = errors.New("failed to update video speed") ) -// PlaybackController represents the MPV video player component and its configuration +// PlaybackController manages video playback using MPV media player type PlaybackController struct { config config.VideoConfig speedConfig config.SpeedConfig @@ -24,6 +33,7 @@ type PlaybackController struct { // NewPlaybackController creates a new video player with the given configuration func NewPlaybackController(videoConfig config.VideoConfig, speedConfig config.SpeedConfig) (*PlaybackController, error) { + player := mpv.New() if err := player.Initialize(); err != nil { return nil, err @@ -39,96 +49,92 @@ func NewPlaybackController(videoConfig config.VideoConfig, speedConfig config.Sp // Start configures and starts the MPV media player func (p *PlaybackController) Start(ctx context.Context, speedController *speed.SpeedController) error { - logger.Info(logger.VIDEO, "Starting MPV video player...") - - // Defer terminating and destroying the MPV media player + logger.Info(logger.VIDEO, "starting MPV video player...") defer p.player.TerminateDestroy() - // Configure the MPV media player if err := p.configureMPVPlayer(); err != nil { return err } - // Load the video file into MPV - logger.Debug(logger.VIDEO, "Loading video file: "+p.config.FilePath) - if err := p.loadMPVvideo(); err != nil { + logger.Debug(logger.VIDEO, "loading video file: "+p.config.FilePath) + if err := p.loadMPVVideo(); err != nil { return err } - // Set the MPV playback loop interval ticker := time.NewTicker(time.Millisecond * time.Duration(p.config.UpdateIntervalSec*1000)) defer ticker.Stop() - lastSpeed := 0.0 - logger.Debug(logger.VIDEO, "Entering MPV playback loop...") + var lastSpeed float64 + logger.Debug(logger.VIDEO, "entering MPV playback loop...") for { select { case <-ctx.Done(): - logger.Info(logger.VIDEO, "Context cancelled. Shutting down video player component") + logger.Info(logger.VIDEO, "context cancelled, stopping video player...") return nil case <-ticker.C: + reachedEOF, err := p.player.GetProperty("eof-reached", mpv.FormatFlag) + if err == nil && reachedEOF.(bool) { + return ErrVideoComplete + } + if err := p.updatePlaybackSpeed(speedController, &lastSpeed); err != nil { - logger.Warn(logger.VIDEO, "Error updating playback speed: "+err.Error()) + if !strings.Contains(err.Error(), "end of file") { + logger.Warn(logger.VIDEO, "error updating playback speed: "+err.Error()) + } } } } } -// configureMPVPlayer configures the MPV video player +// configureMPVPlayer configures the MPV video player settings func (p *PlaybackController) configureMPVPlayer() error { - // If the window scale factor is set to 1.0, set the window to be maximized if p.config.WindowScaleFactor == 1.0 { - logger.Debug(logger.VIDEO, "Maximizing video window") + logger.Debug(logger.VIDEO, "maximizing video window") return p.player.SetOptionString("window-maximized", "yes") } + if err := p.player.SetOptionString("keep-open", "yes"); err != nil { + return err + } + return p.player.SetOptionString("autofit", strconv.Itoa(int(p.config.WindowScaleFactor*100))+"%") } -// loadMPVvideo loads the video file into the MPV video player -func (p *PlaybackController) loadMPVvideo() error { +// loadMPVVideo loads the video file into the MPV video player +func (p *PlaybackController) loadMPVVideo() error { return p.player.Command([]string{"loadfile", p.config.FilePath}) } // updatePlaybackSpeed updates the video playback speed based on the sensor speed func (p *PlaybackController) updatePlaybackSpeed(speedController *speed.SpeedController, lastSpeed *float64) error { - // Get and display (log) the smoothed sensor speed currentSpeed := speedController.GetSmoothedSpeed() p.logSpeedInfo(speedController, currentSpeed) - // Check current sensor speed and adjust video playback speed if required - if err := p.checkSpeedState(currentSpeed, lastSpeed); err != nil { - return err - } - - return nil + return p.checkSpeedState(currentSpeed, lastSpeed) } // logSpeedInfo logs the sensor speed details func (p *PlaybackController) logSpeedInfo(sc *speed.SpeedController, currentSpeed float64) { - logger.Debug(logger.VIDEO, "Sensor speed buffer: ["+strings.Join(sc.GetSpeedBuffer(), " ")+"]") - logger.Info(logger.VIDEO, logger.Magenta+"Smoothed sensor speed: "+strconv.FormatFloat(currentSpeed, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) + logger.Debug(logger.VIDEO, "sensor speed buffer: ["+strings.Join(sc.GetSpeedBuffer(), " ")+"]") + logger.Info(logger.VIDEO, logger.Magenta+"smoothed sensor speed: "+strconv.FormatFloat(currentSpeed, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) } // checkSpeedState checks the current sensor speed and adjusts video playback func (p *PlaybackController) checkSpeedState(currentSpeed float64, lastSpeed *float64) error { - // Pause the video playback if no speed is detected if currentSpeed == 0 { return p.pausePlayback() } - // Calculate the delta between the current and last sensor speed deltaSpeed := math.Abs(currentSpeed - *lastSpeed) - logger.Debug(logger.VIDEO, logger.Magenta+"Last playback speed: "+strconv.FormatFloat(*lastSpeed, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) - logger.Debug(logger.VIDEO, logger.Magenta+"Sensor speed delta: "+strconv.FormatFloat(deltaSpeed, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) - logger.Debug(logger.VIDEO, logger.Magenta+"Playback speed update threshold: "+strconv.FormatFloat(p.speedConfig.SpeedThreshold, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) + logger.Debug(logger.VIDEO, logger.Magenta+"last playback speed: "+strconv.FormatFloat(*lastSpeed, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) + logger.Debug(logger.VIDEO, logger.Magenta+"sensor speed delta: "+strconv.FormatFloat(deltaSpeed, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) + logger.Debug(logger.VIDEO, logger.Magenta+"playback speed update threshold: "+strconv.FormatFloat(p.speedConfig.SpeedThreshold, 'f', 2, 64)+" "+p.speedConfig.SpeedUnits) - // Adjust the video playback speed if the sensor speed has changed beyond threshold value if deltaSpeed > p.speedConfig.SpeedThreshold { return p.adjustPlayback(currentSpeed, lastSpeed) } @@ -139,98 +145,58 @@ func (p *PlaybackController) checkSpeedState(currentSpeed float64, lastSpeed *fl // pausePlayback pauses the video playback in the MPV media player func (p *PlaybackController) pausePlayback() error { - logger.Debug(logger.VIDEO, "No speed detected, so pausing video") + logger.Debug(logger.VIDEO, "no speed detected, so pausing video") - // Update the on-screen display - if err := p.updateMPVdisplay(0.0, 0.0); err != nil { - return errors.New("failed to update OSD: " + err.Error()) + if err := p.updateMPVDisplay(0.0, 0.0); err != nil { + return fmt.Errorf("%w: %v", ErrOSDUpdate, err) } - // Pause the video - return p.setMPVpauseState(true) + return p.setMPVPauseState(true) } // adjustPlayback adjusts the video playback speed func (p *PlaybackController) adjustPlayback(currentSpeed float64, lastSpeed *float64) error { - // Calculate the new playback speed playbackSpeed := (currentSpeed * p.config.SpeedMultiplier) / 10.0 - logger.Info(logger.VIDEO, logger.Cyan+"Updating video playback speed to "+strconv.FormatFloat(playbackSpeed, 'f', 2, 64)) + logger.Info(logger.VIDEO, logger.Cyan+"updating video playback speed to "+strconv.FormatFloat(playbackSpeed, 'f', 2, 64)) - // Update the video playback speed - if err := p.updateMPVplaybackSpeed(playbackSpeed); err != nil { - return errors.New("failed to set playback speed: " + err.Error()) + if err := p.updateMPVPlaybackSpeed(playbackSpeed); err != nil { + return fmt.Errorf("%w: %v", ErrPlaybackSpeed, err) } - // Update the last sensor speed value sent to the media player to the current value *lastSpeed = currentSpeed - // Update the on-screen display - if err := p.updateMPVdisplay(currentSpeed, playbackSpeed); err != nil { - return errors.New("failed to update OSD: " + err.Error()) + if err := p.updateMPVDisplay(currentSpeed, playbackSpeed); err != nil { + return fmt.Errorf("%w: %v", ErrOSDUpdate, err) } - // Unpause the video - return p.setMPVpauseState(false) + return p.setMPVPauseState(false) } -// updateMPVdisplay updates the MPV media player on-screen display -func (p *PlaybackController) updateMPVdisplay(cycleSpeed, playbackSpeed float64) error { +// updateMPVDisplay updates the MPV media player on-screen display +func (p *PlaybackController) updateMPVDisplay(cycleSpeed, playbackSpeed float64) error { - // Return if no OSD options are enabled in TOML if !p.config.OnScreenDisplay.ShowOSD { return nil } - // Build the OSD message based on TOML configuration - var osdMsg string - if p.config.OnScreenDisplay.DisplayCycleSpeed { - osdMsg += "Sensor Speed: " + strconv.FormatFloat(cycleSpeed, 'f', 2, 64) + " " + p.speedConfig.SpeedUnits + "\n" - } - - if p.config.OnScreenDisplay.DisplayPlaybackSpeed { - osdMsg += "Playback Speed: " + strconv.FormatFloat(playbackSpeed, 'f', 2, 64) + "\n" + var osdText string + if cycleSpeed > 0 { + osdText = fmt.Sprintf("Cycle Speed: %.2f %s\nPlayback Speed: %.2fx", + cycleSpeed, p.speedConfig.SpeedUnits, playbackSpeed) + } else { + osdText = "Paused" } - // Update the MPV media player on-screen display (OSD) - return p.player.SetOptionString("osd-msg1", osdMsg) + return p.player.SetProperty("osd-msg", mpv.FormatString, osdText) } -// updateMPVplaybackSpeed sets the video playback speed -func (p *PlaybackController) updateMPVplaybackSpeed(playbackSpeed float64) error { - - // Set the new playback speed in MPV - if err := p.player.SetProperty("speed", mpv.FormatDouble, playbackSpeed); err != nil { - return errors.New("failed to update video speed: " + err.Error()) - } - - return nil +// updateMPVPlaybackSpeed sets the video playback speed +func (p *PlaybackController) updateMPVPlaybackSpeed(playbackSpeed float64) error { + return p.player.SetProperty("speed", mpv.FormatDouble, playbackSpeed) } -// setMPVpauseState sets the video playback pause state -func (p *PlaybackController) setMPVpauseState(pause bool) error { - - // Get the current pause state from MPV - currentPause, err := p.player.GetProperty("pause", mpv.FormatFlag) - if err != nil { - return err - } - - // Return if the current pause state matches the requested pause state - if pauseState, ok := currentPause.(bool); ok && pauseState == pause { - return nil - } - - // Set the new pause state in MPV - if err := p.player.SetProperty("pause", mpv.FormatFlag, pause); err != nil { - return err - } - - pauseState := "resumed" - if pause { - pauseState = "paused" - } - - logger.Debug(logger.VIDEO, "Video "+pauseState+" successfully") - return nil +// setMPVPauseState sets the video playback pause state +func (p *PlaybackController) setMPVPauseState(pause bool) error { + return p.player.SetProperty("pause", mpv.FormatFlag, pause) } diff --git a/internal/video-player/playback_controller_test.go b/internal/video-player/playback_controller_test.go index 085ed0f..1cd10c0 100644 --- a/internal/video-player/playback_controller_test.go +++ b/internal/video-player/playback_controller_test.go @@ -6,11 +6,11 @@ import ( "time" "github.com/gen2brain/go-mpv" + "github.com/stretchr/testify/assert" + config "github.com/richbl/go-ble-sync-cycle/internal/configuration" logger "github.com/richbl/go-ble-sync-cycle/internal/logging" "github.com/richbl/go-ble-sync-cycle/internal/speed" - - "github.com/stretchr/testify/assert" ) const ( @@ -27,7 +27,7 @@ func init() { logger.Initialize("debug") } -// TestNewPlaybackController tests the creation of a new PlaybackController +// TestNewPlaybackController verifies PlaybackController creation func TestNewPlaybackController(t *testing.T) { _ = createTestController(t) } @@ -35,7 +35,6 @@ func TestNewPlaybackController(t *testing.T) { // createTestController creates a PlaybackController with default test configurations func createTestController(t *testing.T) *PlaybackController { - // Create test Video and Speed configurations vc := config.VideoConfig{ FilePath: testFilename, WindowScaleFactor: defaultWindowScale, @@ -49,7 +48,6 @@ func createTestController(t *testing.T) *PlaybackController { SpeedThreshold: speedThreshold, } - // Create new PlaybackController controller, err := NewPlaybackController(vc, sc) assert.NotNil(t, controller, "PlaybackController should not be nil") assert.NoError(t, err, "Error while creating PlaybackController") @@ -57,45 +55,37 @@ func createTestController(t *testing.T) *PlaybackController { return controller } -// TestPlaybackControllerStart tests the Start method of the PlaybackController +// TestPlaybackControllerStart checks the Start method func TestPlaybackControllerStart(t *testing.T) { - // Create test controller controller := createTestController(t) ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) defer cancel() - // Create new SpeedController speedController := &speed.SpeedController{} err := controller.Start(ctx, speedController) assert.NoError(t, err, "Error while starting PlaybackController") } -// TestPlaybackControllerConfigurePlayer tests the configurePlayer method of the PlaybackController +// TestPlaybackControllerConfigurePlayer tests player configuration func TestPlaybackControllerConfigurePlayer(t *testing.T) { - - // Create test controller controller := createTestController(t) err := controller.configureMPVPlayer() assert.NoError(t, err, "Error while configuring MPV player") } -// TestPlaybackControllerLoadVideoFile tests the loadVideoFile method of the PlaybackController +// TestPlaybackControllerLoadVideoFile checks video file loading func TestPlaybackControllerLoadVideoFile(t *testing.T) { - - // Create test controller controller := createTestController(t) - err := controller.loadMPVvideo() + err := controller.loadMPVVideo() assert.NoError(t, err, "Error while loading video file") } -// TestPlaybackControllerSetPauseStatus tests the setPauseStatus method of the PlaybackController +// TestPlaybackControllerSetPauseStatus verifies pause state setting func TestPlaybackControllerSetPauseStatus(t *testing.T) { - // Create test controller controller := createTestController(t) - // Set and check pause status err := controller.player.SetProperty("pause", mpv.FormatFlag, true) assert.NoError(t, err, "Error while setting pause property")