From ccc35fef14aab4a446005c6a37a4ea8e85fd1ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Carlo=20M=C3=A1rquez=20Zarzalejo?= Date: Wed, 16 Jul 2025 09:51:41 +0200 Subject: [PATCH 01/10] Add C# API wrapper and examples for PanelPlayer - Create panelplayer_api.c/.h with C library interface - Add C# wrapper class with P/Invoke bindings - Include example C# application demonstrating usage - Update makefile to build shared library - Add gitignore entries for C# build artifacts - Add doxygen style comments to panelplayer_api header file - Add README.md to the csharp example --- .gitignore | 4 +- examples/csharp/PanelPlayerExample.cs | 215 +++++++++++++++ examples/csharp/PanelPlayerExample.csproj | 16 ++ examples/csharp/README.md | 112 ++++++++ makefile | 19 +- source/panelplayer_api.c | 312 ++++++++++++++++++++++ source/panelplayer_api.h | 169 ++++++++++++ 7 files changed, 841 insertions(+), 6 deletions(-) create mode 100644 examples/csharp/PanelPlayerExample.cs create mode 100644 examples/csharp/PanelPlayerExample.csproj create mode 100644 examples/csharp/README.md create mode 100644 source/panelplayer_api.c create mode 100644 source/panelplayer_api.h diff --git a/.gitignore b/.gitignore index e6421fb..ead600d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /.vscode /build /extensions/nanoled/build -/extensions/grayscale/extension.so \ No newline at end of file +/extensions/grayscale/extension.so +/examples/csharp/obj +/examples/csharp/bin \ No newline at end of file diff --git a/examples/csharp/PanelPlayerExample.cs b/examples/csharp/PanelPlayerExample.cs new file mode 100644 index 0000000..c326bcf --- /dev/null +++ b/examples/csharp/PanelPlayerExample.cs @@ -0,0 +1,215 @@ +using System; +using System.Runtime.InteropServices; +using System.IO; + +namespace PanelPlayerExample +{ + public static class PanelPlayerNative + { + private const string LibraryName = "libpanelplayer.so"; + + public const int PANELPLAYER_SUCCESS = 0; + public const int PANELPLAYER_ERROR = -1; + public const int PANELPLAYER_INVALID_PARAM = -2; + public const int PANELPLAYER_NOT_INITIALIZED = -3; + public const int PANELPLAYER_ALREADY_INITIALIZED = -4; + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_init( + [MarshalAs(UnmanagedType.LPStr)] string interface_name, + int width, + int height, + int brightness + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_set_mix(int mix_percentage); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_set_rate(int frame_rate); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_load_extension( + [MarshalAs(UnmanagedType.LPStr)] string extension_path + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_play_webp_file( + [MarshalAs(UnmanagedType.LPStr)] string file_path + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_play_frame_bgr( + [MarshalAs(UnmanagedType.LPArray)] byte[] bgr_data, + int width, + int height + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_send_brightness( + byte red, + byte green, + byte blue + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool panelplayer_is_initialized(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern void panelplayer_cleanup(); + } + + public class PanelPlayer : IDisposable + { + private bool disposed = false; + + public PanelPlayer(string interfaceName, int width, int height, int brightness = 255) + { + int result = PanelPlayerNative.panelplayer_init(interfaceName, width, height, brightness); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to initialize PanelPlayer: {result}"); + } + } + + public void SetMix(int mixPercentage) + { + int result = PanelPlayerNative.panelplayer_set_mix(mixPercentage); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set mix: {result}"); + } + } + + public void SetFrameRate(int frameRate) + { + int result = PanelPlayerNative.panelplayer_set_rate(frameRate); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set frame rate: {result}"); + } + } + + public void LoadExtension(string extensionPath) + { + int result = PanelPlayerNative.panelplayer_load_extension(extensionPath); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to load extension: {result}"); + } + } + + public void PlayWebPFile(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"WebP file not found: {filePath}"); + } + + int result = PanelPlayerNative.panelplayer_play_webp_file(filePath); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to play WebP file: {result}"); + } + } + + public void PlayFrameBGR(byte[] bgrData, int width, int height) + { + if (bgrData == null) + { + throw new ArgumentNullException(nameof(bgrData)); + } + + if (bgrData.Length != width * height * 3) + { + throw new ArgumentException("BGR data length doesn't match width * height * 3"); + } + + int result = PanelPlayerNative.panelplayer_play_frame_bgr(bgrData, width, height); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to play frame: {result}"); + } + } + + public void SetBrightness(byte red, byte green, byte blue) + { + int result = PanelPlayerNative.panelplayer_send_brightness(red, green, blue); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set brightness: {result}"); + } + } + + public bool IsInitialized => PanelPlayerNative.panelplayer_is_initialized(); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + PanelPlayerNative.panelplayer_cleanup(); + disposed = true; + } + } + + ~PanelPlayer() + { + Dispose(false); + } + } + + class Program + { + static void Main(string[] args) + { + + try + { + string interfaceName = "end0"; // Network interface (usually eth0) + int width = 192; + int height = 64; + int brightness = 255; + // Basic use example (Ejemplo básico de uso) + using (var player = new PanelPlayer(interfaceName, width, height, brightness)) + { + Console.WriteLine("PanelPlayer initialized successfully!"); + + // Play WebP File (Reproducir archivo WebP) + if (args.Length > 0) + { + player.PlayWebPFile(args[0]); + Console.WriteLine($"Played WebP file: {args[0]}"); + System.Threading.Thread.Sleep(10000); // Wait for 10 seconds + } + + // Manualy send blue frame (Ejemplo de frame manual (cuadrado azul)) + byte[] blueFrame = new byte[width * height * 3]; + for (int i = 0; i < blueFrame.Length; i += 3) + { + blueFrame[i] = 255; // Blue + blueFrame[i + 1] = 0; // Green + blueFrame[i + 2] = 0; // Red + } + + player.PlayFrameBGR(blueFrame, width, height); + Console.WriteLine("Displayed blue frame"); + + // Wait before exit. (Esperar un poco antes de salir) + System.Threading.Thread.Sleep(2000); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine("Note: This example requires root privileges and a valid ethernet interface."); + } + } + } +} \ No newline at end of file diff --git a/examples/csharp/PanelPlayerExample.csproj b/examples/csharp/PanelPlayerExample.csproj new file mode 100644 index 0000000..9a3cc67 --- /dev/null +++ b/examples/csharp/PanelPlayerExample.csproj @@ -0,0 +1,16 @@ + + + + Exe + net9.0 + enable + true + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/examples/csharp/README.md b/examples/csharp/README.md new file mode 100644 index 0000000..2bf6bbc --- /dev/null +++ b/examples/csharp/README.md @@ -0,0 +1,112 @@ +# PanelPlayer C# Example + +This directory contains a C# example application that demonstrates how to use the PanelPlayer library from .NET applications. + +## Prerequisites + +- .NET 9.0 or later +- libpanelplayer.so compiled and available +- Root privileges (required for raw ethernet access) + +## Building + +### 1. Build the PanelPlayer Library + +First, you need to build the shared library from the main project directory: + +```bash +cd /path/to/PanelPlayer +make library +``` + +This will create `build/libpanelplayer.so`. + +### 2. Copy Library to Output Directory + +The C# project is configured to automatically copy the library to the output directory during build, but you can also copy it manually: + +```bash +cp ../../build/libpanelplayer.so ./bin/Debug/net9.0/ +``` + +### 3. Build the C# Application + +```bash +dotnet build +``` + +## Running + +The application must be run as root due to raw ethernet socket requirements: + +```bash +sudo dotnet run [webp-file] +``` + +### Examples + +Play a WebP file: +```bash +sudo dotnet run animation.webp +``` + +Run without arguments to see the blue frame demonstration: +```bash +sudo dotnet run +``` + +## Publish + +Make a self contained app +```bash +dotnet publish -r linux-arm64 --self-contained true -c Release +``` +## Usage + +The example demonstrates: + +1. **Basic initialization** - Setting up the panel with network interface and dimensions +2. **WebP file playback** - Playing WebP files (if provided as argument) +3. **Manual frame display** - Sending raw BGR pixel data to create a blue frame +4. **Proper cleanup** - Using `using` statement for automatic resource disposal + +## Configuration + +The example is configured for: +- Network interface: `end0` (eth0 is more common) +- Panel dimensions: 192x64 pixels +- Brightness: 255 (maximum) + +Modify these values in `PanelPlayerExample.cs` to match your setup. + +## Tested Environment + +This example has been tested on: +- **Hardware**: Orange Pi Zero 3 +- **OS**: Debian Bookworm +- **Runtime**: .NET 9.0.302 + +## Troubleshooting + +### Permission Errors +Make sure you're running with `sudo` - raw ethernet access requires root privileges. + +### Library Not Found +Ensure `libpanelplayer.so` is in the output directory or in your system's library path. + +### Network Interface Issues +Verify that the network interface name (`end0`) matches your system's ethernet interface. + +## API Reference + +The C# wrapper provides these main methods: + +- `PanelPlayer(interface, width, height, brightness)` - Initialize the panel +- `PlayWebPFile(path)` - Play animated WebP file +- `PlayFrameBGR(data, width, height)` - Display raw BGR frame data +- `SetBrightness(red, green, blue)` - Adjust color balance +- `LoadExtension(path)` - Load processing extensions +- `SetMix(percentage)` - Control frame blending +- `SetFrameRate(fps)` - Override animation timing + +For detailed API documentation, see the native library header file `source/panelplayer_api.h`. \ No newline at end of file diff --git a/makefile b/makefile index 6fd70a0..5690ca8 100644 --- a/makefile +++ b/makefile @@ -6,20 +6,29 @@ LDLIBS = -ldl -lwebpdemux SOURCE = ./source BUILD = ./build TARGET = $(BUILD)/panelplayer +LIBRARY = $(BUILD)/libpanelplayer.so HEADERS = $(wildcard $(SOURCE)/*.h) -OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(wildcard $(SOURCE)/*.c)) +MAIN_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/panelplayer_api.c,$(wildcard $(SOURCE)/*.c))) +API_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/main.c,$(wildcard $(SOURCE)/*.c))) -.PHONY: clean +.PHONY: clean all library -$(TARGET): $(BUILD) $(OBJECTS) - $(CC) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o $@ +all: $(TARGET) $(LIBRARY) + +$(TARGET): $(BUILD) $(MAIN_OBJECTS) + $(CC) $(LDFLAGS) $(MAIN_OBJECTS) $(LDLIBS) -o $@ + +$(LIBRARY): $(BUILD) $(API_OBJECTS) + $(CC) $(LDFLAGS) -shared -fPIC $(API_OBJECTS) $(LDLIBS) -o $@ + +library: $(LIBRARY) $(BUILD): mkdir $(BUILD) $(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile - $(CC) $(CFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) -fPIC -c $< -o $@ clean: rm -r $(BUILD) \ No newline at end of file diff --git a/source/panelplayer_api.c b/source/panelplayer_api.c new file mode 100644 index 0000000..4afffbd --- /dev/null +++ b/source/panelplayer_api.c @@ -0,0 +1,312 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "panelplayer_api.h" +#include "colorlight.h" +#include "loader.h" + +#define MIX_MAXIMUM 100 +#define UPDATE_DELAY 10 + +typedef struct { + colorlight *colorlight; + uint8_t *buffer; + int width; + int height; + int brightness; + int mix; + int rate; + void *extension; + void (*update_func)(int width, int height, uint8_t *frame); + bool initialized; +} panelplayer_state; + +static panelplayer_state g_state = {0}; + +static long get_time(void) { + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_sec * 1000 + time.tv_nsec / 1000000; +} + +static void await(long time) { + long delay = time - get_time(); + if (delay > 0) { + usleep(delay * 1000); + } +} + +int panelplayer_init(const char* interface_name, int width, int height, int brightness) { + if (g_state.initialized) { + return PANELPLAYER_ALREADY_INITIALIZED; + } + + if (!interface_name || width <= 0 || height <= 0 || brightness < 0 || brightness > 255) { + return PANELPLAYER_INVALID_PARAM; + } + + g_state.colorlight = colorlight_init((char*)interface_name); + if (!g_state.colorlight) { + return PANELPLAYER_ERROR; + } + + g_state.buffer = malloc(width * height * 3); + if (!g_state.buffer) { + colorlight_destroy(g_state.colorlight); + return PANELPLAYER_ERROR; + } + + g_state.width = width; + g_state.height = height; + g_state.brightness = brightness; + g_state.mix = 0; + g_state.rate = 0; + g_state.extension = NULL; + g_state.update_func = NULL; + g_state.initialized = true; + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_set_mix(int mix_percentage) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (mix_percentage < 0 || mix_percentage >= MIX_MAXIMUM) { + return PANELPLAYER_INVALID_PARAM; + } + + g_state.mix = mix_percentage; + return PANELPLAYER_SUCCESS; +} + +int panelplayer_set_rate(int frame_rate) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (frame_rate < 0) { + return PANELPLAYER_INVALID_PARAM; + } + + g_state.rate = frame_rate; + return PANELPLAYER_SUCCESS; +} + +int panelplayer_load_extension(const char* extension_path) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (!extension_path) { + return PANELPLAYER_INVALID_PARAM; + } + + if (g_state.extension) { + void (*destroy)() = dlsym(g_state.extension, "destroy"); + if (destroy) { + destroy(); + } + dlclose(g_state.extension); + g_state.extension = NULL; + g_state.update_func = NULL; + } + + g_state.extension = dlopen(extension_path, RTLD_NOW); + if (!g_state.extension) { + return PANELPLAYER_ERROR; + } + + bool (*init)() = dlsym(g_state.extension, "init"); + if (init && init()) { + dlclose(g_state.extension); + g_state.extension = NULL; + return PANELPLAYER_ERROR; + } + + g_state.update_func = dlsym(g_state.extension, "update"); + if (!g_state.update_func) { + dlclose(g_state.extension); + g_state.extension = NULL; + return PANELPLAYER_ERROR; + } + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_play_webp_file(const char* file_path) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (!file_path) { + return PANELPLAYER_INVALID_PARAM; + } + + FILE *file = fopen(file_path, "rb"); + if (!file) { + return PANELPLAYER_ERROR; + } + + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + uint8_t *file_data = malloc(file_size); + if (!file_data) { + fclose(file); + return PANELPLAYER_ERROR; + } + + if (fread(file_data, 1, file_size, file) != file_size) { + free(file_data); + fclose(file); + return PANELPLAYER_ERROR; + } + fclose(file); + + WebPData data = { + .bytes = file_data, + .size = file_size + }; + + WebPAnimDecoder *decoder = WebPAnimDecoderNew(&data, NULL); + if (!decoder) { + free(file_data); + return PANELPLAYER_ERROR; + } + + WebPAnimInfo info; + WebPAnimDecoderGetInfo(decoder, &info); + + if (info.canvas_width < g_state.width || info.canvas_height < g_state.height) { + WebPAnimDecoderDelete(decoder); + free(file_data); + return PANELPLAYER_ERROR; + } + + long next = get_time(); + int previous = 0; + bool initial = true; + + while (WebPAnimDecoderHasMoreFrames(decoder)) { + uint8_t *decoded; + int timestamp; + + WebPAnimDecoderGetNext(decoder, &decoded, ×tamp); + + for (int y = 0; y < g_state.height; y++) { + for (int x = 0; x < g_state.width; x++) { + int source = (y * info.canvas_width + x) * 4; + int destination = (y * g_state.width + x) * 3; + + int oldFactor = initial ? 0 : g_state.mix; + int newFactor = MIX_MAXIMUM - oldFactor; + + g_state.buffer[destination] = (g_state.buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / MIX_MAXIMUM; + g_state.buffer[destination + 1] = (g_state.buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / MIX_MAXIMUM; + g_state.buffer[destination + 2] = (g_state.buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / MIX_MAXIMUM; + } + } + + if (g_state.update_func) { + g_state.update_func(g_state.width, g_state.height, g_state.buffer); + } + + for (int y = 0; y < g_state.height; y++) { + colorlight_send_row(g_state.colorlight, y, g_state.width, g_state.buffer + y * g_state.width * 3); + } + + if (next - get_time() < UPDATE_DELAY) { + next = get_time() + UPDATE_DELAY; + } + + await(next); + colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); + + if (g_state.rate > 0) { + next = get_time() + 1000 / g_state.rate; + } else { + next = get_time() + timestamp - previous; + previous = timestamp; + } + + initial = false; + } + + WebPAnimDecoderDelete(decoder); + free(file_data); + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_play_frame_bgr(const uint8_t* bgr_data, int width, int height) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (!bgr_data || width != g_state.width || height != g_state.height) { + return PANELPLAYER_INVALID_PARAM; + } + + memcpy(g_state.buffer, bgr_data, width * height * 3); + + if (g_state.update_func) { + g_state.update_func(g_state.width, g_state.height, g_state.buffer); + } + + for (int y = 0; y < g_state.height; y++) { + colorlight_send_row(g_state.colorlight, y, g_state.width, g_state.buffer + y * g_state.width * 3); + } + + long next = get_time() + UPDATE_DELAY; + await(next); + colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_send_brightness(uint8_t red, uint8_t green, uint8_t blue) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + colorlight_send_brightness(g_state.colorlight, red, green, blue); + return PANELPLAYER_SUCCESS; +} + +bool panelplayer_is_initialized(void) { + return g_state.initialized; +} + +void panelplayer_cleanup(void) { + if (!g_state.initialized) { + return; + } + + if (g_state.extension) { + void (*destroy)() = dlsym(g_state.extension, "destroy"); + if (destroy) { + destroy(); + } + dlclose(g_state.extension); + } + + if (g_state.colorlight) { + colorlight_destroy(g_state.colorlight); + } + + if (g_state.buffer) { + free(g_state.buffer); + } + + memset(&g_state, 0, sizeof(g_state)); +} \ No newline at end of file diff --git a/source/panelplayer_api.h b/source/panelplayer_api.h new file mode 100644 index 0000000..289b179 --- /dev/null +++ b/source/panelplayer_api.h @@ -0,0 +1,169 @@ +/** + * @file panelplayer_api.h + * @brief PanelPlayer C API for controlling Colorlight LED panels + * + * This header provides a C library interface for the PanelPlayer application, + * allowing programmatic control of Colorlight LED receiving cards via raw ethernet. + */ + +#ifndef PANELPLAYER_API_H +#define PANELPLAYER_API_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** @defgroup ReturnCodes Return Codes + * @{ + */ +#define PANELPLAYER_SUCCESS 0 /**< Operation completed successfully */ +#define PANELPLAYER_ERROR -1 /**< General error occurred */ +#define PANELPLAYER_INVALID_PARAM -2 /**< Invalid parameter provided */ +#define PANELPLAYER_NOT_INITIALIZED -3 /**< Library not initialized */ +#define PANELPLAYER_ALREADY_INITIALIZED -4 /**< Library already initialized */ +/** @} */ + +typedef struct panelplayer_instance panelplayer_instance; + +/** + * @brief Initialize the PanelPlayer library + * + * Initializes the PanelPlayer library with the specified LED panel parameters. + * This function must be called before any other API functions. + * + * @param interface_name Network interface name (e.g., "eth0") + * @param width Panel width in pixels + * @param height Panel height in pixels + * @param brightness Initial brightness value (0-255) + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS Initialization successful + * @retval PANELPLAYER_INVALID_PARAM Invalid parameters provided + * @retval PANELPLAYER_ALREADY_INITIALIZED Library already initialized + * @retval PANELPLAYER_ERROR Failed to initialize network interface or allocate memory + */ +int panelplayer_init(const char* interface_name, int width, int height, int brightness); + +/** + * @brief Set frame mixing percentage + * + * Controls how new frames are blended with existing content on the panel. + * A value of 0 completely replaces the current frame, while higher values + * blend the new frame with the existing content. + * + * @param mix_percentage Mixing percentage (0-99) + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS Mix percentage set successfully + * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized + * @retval PANELPLAYER_INVALID_PARAM Mix percentage out of range + */ +int panelplayer_set_mix(int mix_percentage); + +/** + * @brief Set playback frame rate + * + * Sets the frame rate for WebP animation playback. When set to 0, + * uses the timing information embedded in the WebP file. + * + * @param frame_rate Target frame rate in frames per second (0 for auto) + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS Frame rate set successfully + * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized + * @retval PANELPLAYER_INVALID_PARAM Negative frame rate provided + */ +int panelplayer_set_rate(int frame_rate); + +/** + * @brief Load a frame processing extension + * + * Loads a shared library extension that can modify frame data before + * it is sent to the LED panel. Extensions must implement the required + * interface with update() function. + * + * @param extension_path Path to the shared library (.so file) + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS Extension loaded successfully + * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized + * @retval PANELPLAYER_INVALID_PARAM NULL extension path provided + * @retval PANELPLAYER_ERROR Failed to load extension or missing required functions + */ +int panelplayer_load_extension(const char* extension_path); + +/** + * @brief Play an animated WebP file + * + * Loads and plays an animated WebP file on the LED panel. The function + * handles frame timing and loops through all frames in the animation. + * + * @param file_path Path to the WebP file to play + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS WebP file played successfully + * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized + * @retval PANELPLAYER_INVALID_PARAM NULL file path provided + * @retval PANELPLAYER_ERROR Failed to open file, decode WebP, or incompatible dimensions + */ +int panelplayer_play_webp_file(const char* file_path); + +/** + * @brief Display a single frame from BGR data + * + * Displays a single frame on the LED panel using raw BGR pixel data. + * The data must be in BGR format with 3 bytes per pixel. + * + * @param bgr_data Pointer to BGR pixel data (Blue, Green, Red order) + * @param width Frame width in pixels (must match initialized width) + * @param height Frame height in pixels (must match initialized height) + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS Frame displayed successfully + * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized + * @retval PANELPLAYER_INVALID_PARAM NULL data pointer or mismatched dimensions + */ +int panelplayer_play_frame_bgr(const uint8_t* bgr_data, int width, int height); + +/** + * @brief Send brightness/color balance command + * + * Sends a brightness and color balance command directly to the LED panel. + * This allows independent control of red, green, and blue channel brightness. + * + * @param red Red channel brightness (0-255) + * @param green Green channel brightness (0-255) + * @param blue Blue channel brightness (0-255) + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS Brightness command sent successfully + * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized + */ +int panelplayer_send_brightness(uint8_t red, uint8_t green, uint8_t blue); + +/** + * @brief Check if library is initialized + * + * Returns the current initialization state of the PanelPlayer library. + * + * @return true if library is initialized, false otherwise + */ +bool panelplayer_is_initialized(void); + +/** + * @brief Cleanup and shutdown the library + * + * Cleans up all resources, closes network connections, unloads extensions, + * and resets the library to an uninitialized state. Should be called + * when finished using the library. + */ +void panelplayer_cleanup(void); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file From 273cad2c09fbe3f43244404cfd0caed19536de4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20M=C3=A1rquez?= Date: Wed, 1 Oct 2025 16:26:59 +0200 Subject: [PATCH 02/10] Add multi-format image decoder abstraction This commit introduces a unified decoder interface that supports multiple image formats (WebP, JPEG, PNG, BMP, and GIF) in addition to the existing WebP support. The implementation provides a consistent API regardless of the underlying image format, enabling PanelPlayer to handle a wider variety of media sources. Changes: - Add decoder.h: Define generic decoder interface with format-agnostic API - Add decoder.c: Implement automatic format detection and decoding for WebP, JPEG, PNG, BMP and GIF formats - Update main.c: Replace direct WebP API calls with new decoder abstraction - Update makefile: Add linker flags for libjpeg, libpng, and libgif - Update readme.md: Document required dependencies and supported formats The decoder automatically detects the image format based on file headers and handles both static images (JPEG, PNG) and animations (WebP, GIF) through a unified interface. This maintains backward compatibility while extending format support. Dependencies: - libwebp-dev (existing) - libjpeg-dev (new) - libpng-dev (new) - libgif-dev (new) --- makefile | 48 +-- readme.md | 85 +++--- source/decoder.c | 510 +++++++++++++++++++++++++++++++ source/decoder.h | 22 ++ source/main.c | 758 +++++++++++++++++++++++------------------------ 5 files changed, 977 insertions(+), 446 deletions(-) create mode 100644 source/decoder.c create mode 100644 source/decoder.h diff --git a/makefile b/makefile index 6fd70a0..8cc4d20 100644 --- a/makefile +++ b/makefile @@ -1,25 +1,25 @@ -CC = gcc -CFLAGS = -Wall -Werror -pthread -O3 -LDFLAGS = -pthread -LDLIBS = -ldl -lwebpdemux - -SOURCE = ./source -BUILD = ./build -TARGET = $(BUILD)/panelplayer - -HEADERS = $(wildcard $(SOURCE)/*.h) -OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(wildcard $(SOURCE)/*.c)) - -.PHONY: clean - -$(TARGET): $(BUILD) $(OBJECTS) - $(CC) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o $@ - -$(BUILD): - mkdir $(BUILD) - -$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile - $(CC) $(CFLAGS) -c $< -o $@ - -clean: +CC = gcc +CFLAGS = -Wall -Werror -pthread -O3 +LDFLAGS = -pthread +LDLIBS = -ldl -lwebpdemux -ljpeg -lpng -lgif + +SOURCE = ./source +BUILD = ./build +TARGET = $(BUILD)/panelplayer + +HEADERS = $(wildcard $(SOURCE)/*.h) +OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(wildcard $(SOURCE)/*.c)) + +.PHONY: clean + +$(TARGET): $(BUILD) $(OBJECTS) + $(CC) $(LDFLAGS) $(OBJECTS) $(LDLIBS) -o $@ + +$(BUILD): + mkdir $(BUILD) + +$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile + $(CC) $(CFLAGS) -c $< -o $@ + +clean: rm -r $(BUILD) \ No newline at end of file diff --git a/readme.md b/readme.md index 8866e5e..ef62ceb 100644 --- a/readme.md +++ b/readme.md @@ -1,41 +1,46 @@ -# PanelPlayer -A [WebP](https://developers.google.com/speed/webp) player for Colorlight receiving cards. Tested with the Colorlight 5A-75B. - -## Usage -PanelPlayer can be launched with `panelplayer ` where `` is one or more WebP files. The available options are: - -### `-p ` -Sets which ethernet port to use for sending. This option is required. - -### `-w ` -Set the display width in pixels. This option is required. - -### `-h ` -Set the display height in pixels. This option is required. - -### `-b ` -Set the display brightness between 0 and 255. A value of 255 will be used if not specified. - -### `-m ` -Controls the percentage of the previous frame to be blended with the current frame. Frame blending is disabled when set to 0 or not specified. - -### `-r ` -Overrides the source frame rate if specified. - -### `-e ` -Load an extension from the path given. Only a single extension can be loaded. - -### `-s` -Play sources randomly instead of in a fixed order. If used with a single source, this option will loop playback. - -### `-v` -Enable verbose output. - -## Building -Ensure `libwebp` is installed. PanelPlayer can be built by running `make` from within the root directory. - -## Extensions -Extensions are a way to read or alter frames without modifying PanelPlayer. A minimal extension consists of an `update` function which gets called before each frame is sent. An extension may also include `init` and `destroy` functions. The `destroy` function will always be called if present, even when the `init` function indicates an error has occurred. Example extensions are located in the `extensions` directory. - -## Protocol +# PanelPlayer +A media player for Colorlight receiving cards supporting WebP, JPEG, PNG, GIF, and BMP formats. Tested with the Colorlight 5A-75B. + +## Usage +PanelPlayer can be launched with `panelplayer ` where `` is one or more image/animation files (WebP, JPEG, PNG, GIF, or BMP). The available options are: + +### `-p ` +Sets which ethernet port to use for sending. This option is required. + +### `-w ` +Set the display width in pixels. This option is required. + +### `-h ` +Set the display height in pixels. This option is required. + +### `-b ` +Set the display brightness between 0 and 255. A value of 255 will be used if not specified. + +### `-m ` +Controls the percentage of the previous frame to be blended with the current frame. Frame blending is disabled when set to 0 or not specified. + +### `-r ` +Overrides the source frame rate if specified. + +### `-e ` +Load an extension from the path given. Only a single extension can be loaded. + +### `-s` +Play sources randomly instead of in a fixed order. If used with a single source, this option will loop playback. + +### `-v` +Enable verbose output. + +## Building +Install the required development libraries: +```bash +sudo apt install libwebp-dev libjpeg-dev libpng-dev libgif-dev +``` + +PanelPlayer can be built by running `make` from within the root directory. + +## Extensions +Extensions are a way to read or alter frames without modifying PanelPlayer. A minimal extension consists of an `update` function which gets called before each frame is sent. An extension may also include `init` and `destroy` functions. The `destroy` function will always be called if present, even when the `init` function indicates an error has occurred. Example extensions are located in the `extensions` directory. + +## Protocol Protocol documentation can be found in the `protocol` directory. A Wireshark plugin is included to help with reverse engineering and debugging. \ No newline at end of file diff --git a/source/decoder.c b/source/decoder.c new file mode 100644 index 0000000..8e886b6 --- /dev/null +++ b/source/decoder.c @@ -0,0 +1,510 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include "decoder.h" + +typedef enum +{ + FORMAT_UNKNOWN, + FORMAT_WEBP, + FORMAT_JPEG, + FORMAT_PNG, + FORMAT_GIF, + FORMAT_BMP +} decoder_format; + +typedef struct decoder +{ + decoder_format format; + void *data; + int size; + + union + { + struct + { + WebPData webp_data; + WebPAnimDecoder *webp_decoder; + }; + + struct + { + uint8_t *frame_data; + int frame_width; + int frame_height; + bool frame_consumed; + }; + + struct + { + GifFileType *gif_file; + int gif_current_frame; + int gif_frame_count; + uint8_t *gif_frame_data; + int gif_last_timestamp; + }; + }; +} decoder; + +static decoder_format detect_format(void *data, int size) +{ + if (size < 12) + { + return FORMAT_UNKNOWN; + } + + uint8_t *bytes = data; + + if (bytes[0] == 'R' && bytes[1] == 'I' && bytes[2] == 'F' && bytes[3] == 'F' && + bytes[8] == 'W' && bytes[9] == 'E' && bytes[10] == 'B' && bytes[11] == 'P') + { + return FORMAT_WEBP; + } + + if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) + { + return FORMAT_JPEG; + } + + if (bytes[0] == 0x89 && bytes[1] == 'P' && bytes[2] == 'N' && bytes[3] == 'G') + { + return FORMAT_PNG; + } + + if (bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F') + { + return FORMAT_GIF; + } + + if (bytes[0] == 'B' && bytes[1] == 'M') + { + return FORMAT_BMP; + } + + return FORMAT_UNKNOWN; +} + +static int gif_read_func(GifFileType *gif, GifByteType *buf, int size) +{ + decoder *instance = gif->UserData; + + static int position = 0; + + if (position + size > instance->size) + { + size = instance->size - position; + } + + if (size <= 0) + { + return 0; + } + + memcpy(buf, (uint8_t *)instance->data + position, size); + position += size; + + return size; +} + +decoder *decoder_init(void *data, int size) +{ + decoder *instance; + + if ((instance = calloc(1, sizeof(*instance))) == NULL) + { + perror("Failed to allocate memory for decoder"); + return NULL; + } + + instance->data = data; + instance->size = size; + instance->format = detect_format(data, size); + + switch (instance->format) + { + case FORMAT_WEBP: + instance->webp_data.bytes = data; + instance->webp_data.size = size; + + if ((instance->webp_decoder = WebPAnimDecoderNew(&instance->webp_data, NULL)) == NULL) + { + goto free_instance; + } + break; + + case FORMAT_JPEG: + { + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, data, size); + jpeg_read_header(&cinfo, TRUE); + + cinfo.out_color_space = JCS_EXT_RGBA; + jpeg_start_decompress(&cinfo); + + instance->frame_width = cinfo.output_width; + instance->frame_height = cinfo.output_height; + + if ((instance->frame_data = malloc(instance->frame_width * instance->frame_height * 4)) == NULL) + { + jpeg_destroy_decompress(&cinfo); + goto free_instance; + } + + int row_stride = cinfo.output_width * 4; + + while (cinfo.output_scanline < cinfo.output_height) + { + uint8_t *row = instance->frame_data + cinfo.output_scanline * row_stride; + jpeg_read_scanlines(&cinfo, &row, 1); + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + instance->frame_consumed = false; + break; + } + + case FORMAT_PNG: + { + png_image image; + memset(&image, 0, sizeof(image)); + image.version = PNG_IMAGE_VERSION; + + if (png_image_begin_read_from_memory(&image, data, size) == 0) + { + goto free_instance; + } + + image.format = PNG_FORMAT_RGBA; + + instance->frame_width = image.width; + instance->frame_height = image.height; + + if ((instance->frame_data = malloc(PNG_IMAGE_SIZE(image))) == NULL) + { + png_image_free(&image); + goto free_instance; + } + + if (png_image_finish_read(&image, NULL, instance->frame_data, 0, NULL) == 0) + { + free(instance->frame_data); + png_image_free(&image); + goto free_instance; + } + + png_image_free(&image); + instance->frame_consumed = false; + break; + } + + case FORMAT_GIF: + { + int error; + + if ((instance->gif_file = DGifOpen(instance, gif_read_func, &error)) == NULL) + { + goto free_instance; + } + + if (DGifSlurp(instance->gif_file) == GIF_ERROR) + { + DGifCloseFile(instance->gif_file, NULL); + goto free_instance; + } + + instance->gif_frame_count = instance->gif_file->ImageCount; + instance->gif_current_frame = 0; + + if ((instance->gif_frame_data = malloc(instance->gif_file->SWidth * instance->gif_file->SHeight * 4)) == NULL) + { + DGifCloseFile(instance->gif_file, NULL); + goto free_instance; + } + + memset(instance->gif_frame_data, 0, instance->gif_file->SWidth * instance->gif_file->SHeight * 4); + instance->gif_last_timestamp = 0; + break; + } + + case FORMAT_BMP: + { + uint8_t *bytes = data; + + if (size < 54) + { + goto free_instance; + } + + int offset = bytes[10] | (bytes[11] << 8) | (bytes[12] << 16) | (bytes[13] << 24); + int header_size = bytes[14] | (bytes[15] << 8) | (bytes[16] << 16) | (bytes[17] << 24); + + if (header_size < 40) + { + goto free_instance; + } + + instance->frame_width = bytes[18] | (bytes[19] << 8) | (bytes[20] << 16) | (bytes[21] << 24); + instance->frame_height = bytes[22] | (bytes[23] << 8) | (bytes[24] << 16) | (bytes[25] << 24); + int bits_per_pixel = bytes[28] | (bytes[29] << 8); + int compression = bytes[30] | (bytes[31] << 8) | (bytes[32] << 16) | (bytes[33] << 24); + + if (compression != 0 || (bits_per_pixel != 24 && bits_per_pixel != 32)) + { + goto free_instance; + } + + bool bottom_up = instance->frame_height > 0; + if (instance->frame_height < 0) + { + instance->frame_height = -instance->frame_height; + } + + if ((instance->frame_data = malloc(instance->frame_width * instance->frame_height * 4)) == NULL) + { + goto free_instance; + } + + int row_size = ((bits_per_pixel * instance->frame_width + 31) / 32) * 4; + uint8_t *src = bytes + offset; + + for (int y = 0; y < instance->frame_height; y++) + { + int dest_y = bottom_up ? (instance->frame_height - 1 - y) : y; + uint8_t *row = src + y * row_size; + + for (int x = 0; x < instance->frame_width; x++) + { + int dest_index = (dest_y * instance->frame_width + x) * 4; + int src_index = x * (bits_per_pixel / 8); + + instance->frame_data[dest_index + 0] = row[src_index + 2]; + instance->frame_data[dest_index + 1] = row[src_index + 1]; + instance->frame_data[dest_index + 2] = row[src_index + 0]; + instance->frame_data[dest_index + 3] = (bits_per_pixel == 32) ? row[src_index + 3] : 255; + } + } + + instance->frame_consumed = false; + break; + } + + default: + goto free_instance; + } + + return instance; + +free_instance: + free(instance); + return NULL; +} + +bool decoder_get_info(decoder *instance, decoder_info *info) +{ + if (instance == NULL || info == NULL) + { + return false; + } + + switch (instance->format) + { + case FORMAT_WEBP: + { + WebPAnimInfo webp_info; + WebPAnimDecoderGetInfo(instance->webp_decoder, &webp_info); + + info->frame_count = webp_info.frame_count; + info->canvas_width = webp_info.canvas_width; + info->canvas_height = webp_info.canvas_height; + break; + } + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + info->frame_count = 1; + info->canvas_width = instance->frame_width; + info->canvas_height = instance->frame_height; + break; + + case FORMAT_GIF: + info->frame_count = instance->gif_frame_count; + info->canvas_width = instance->gif_file->SWidth; + info->canvas_height = instance->gif_file->SHeight; + break; + + default: + return false; + } + + return true; +} + +bool decoder_has_more_frames(decoder *instance) +{ + if (instance == NULL) + { + return false; + } + + switch (instance->format) + { + case FORMAT_WEBP: + return WebPAnimDecoderHasMoreFrames(instance->webp_decoder); + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + return !instance->frame_consumed; + + case FORMAT_GIF: + return instance->gif_current_frame < instance->gif_frame_count; + + default: + return false; + } +} + +bool decoder_get_next(decoder *instance, uint8_t **frame, int *timestamp) +{ + if (instance == NULL || frame == NULL || timestamp == NULL) + { + return false; + } + + switch (instance->format) + { + case FORMAT_WEBP: + return WebPAnimDecoderGetNext(instance->webp_decoder, frame, timestamp); + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + if (instance->frame_consumed) + { + return false; + } + + *frame = instance->frame_data; + *timestamp = 0; + instance->frame_consumed = true; + return true; + + case FORMAT_GIF: + { + if (instance->gif_current_frame >= instance->gif_frame_count) + { + return false; + } + + SavedImage *image = &instance->gif_file->SavedImages[instance->gif_current_frame]; + ColorMapObject *colormap = image->ImageDesc.ColorMap != NULL ? image->ImageDesc.ColorMap : instance->gif_file->SColorMap; + + if (colormap == NULL) + { + return false; + } + + int left = image->ImageDesc.Left; + int top = image->ImageDesc.Top; + int width = image->ImageDesc.Width; + int height = image->ImageDesc.Height; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int gif_index = y * width + x; + int frame_index = ((top + y) * instance->gif_file->SWidth + (left + x)) * 4; + + uint8_t color_index = image->RasterBits[gif_index]; + GifColorType color = colormap->Colors[color_index]; + + int transparent = -1; + for (int i = 0; i < image->ExtensionBlockCount; i++) + { + if (image->ExtensionBlocks[i].Function == GRAPHICS_EXT_FUNC_CODE) + { + uint8_t *ext = image->ExtensionBlocks[i].Bytes; + if (ext[0] & 0x01) + { + transparent = ext[3]; + } + } + } + + if (color_index != transparent) + { + instance->gif_frame_data[frame_index + 0] = color.Red; + instance->gif_frame_data[frame_index + 1] = color.Green; + instance->gif_frame_data[frame_index + 2] = color.Blue; + instance->gif_frame_data[frame_index + 3] = 255; + } + } + } + + int delay = 100; + for (int i = 0; i < image->ExtensionBlockCount; i++) + { + if (image->ExtensionBlocks[i].Function == GRAPHICS_EXT_FUNC_CODE) + { + uint8_t *ext = image->ExtensionBlocks[i].Bytes; + delay = (ext[2] << 8) | ext[1]; + delay *= 10; + } + } + + *frame = instance->gif_frame_data; + *timestamp = instance->gif_last_timestamp + delay; + instance->gif_last_timestamp = *timestamp; + instance->gif_current_frame++; + + return true; + } + + default: + return false; + } +} + +void decoder_destroy(decoder *instance) +{ + if (instance == NULL) + { + return; + } + + switch (instance->format) + { + case FORMAT_WEBP: + WebPAnimDecoderDelete(instance->webp_decoder); + break; + + case FORMAT_JPEG: + case FORMAT_PNG: + case FORMAT_BMP: + free(instance->frame_data); + break; + + case FORMAT_GIF: + free(instance->gif_frame_data); + DGifCloseFile(instance->gif_file, NULL); + break; + + default: + break; + } + + free(instance); +} diff --git a/source/decoder.h b/source/decoder.h new file mode 100644 index 0000000..a44722c --- /dev/null +++ b/source/decoder.h @@ -0,0 +1,22 @@ +#ifndef DECODER_H +#define DECODER_H + +#include +#include + +typedef struct decoder decoder; + +typedef struct decoder_info +{ + int frame_count; + int canvas_width; + int canvas_height; +} decoder_info; + +decoder *decoder_init(void *data, int size); +bool decoder_get_info(decoder *instance, decoder_info *info); +bool decoder_has_more_frames(decoder *instance); +bool decoder_get_next(decoder *instance, uint8_t **frame, int *timestamp); +void decoder_destroy(decoder *instance); + +#endif diff --git a/source/main.c b/source/main.c index 62f1a1d..0f77c63 100644 --- a/source/main.c +++ b/source/main.c @@ -1,383 +1,377 @@ -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "colorlight.h" -#include "loader.h" - -#define QUEUE_SIZE 4 -#define MIX_MAXIMUM 100 -#define UPDATE_DELAY 10 - -bool parse(const char *source, int *destination) -{ - char *end; - *destination = strtol(source, &end, 10); - return end[0] != 0; -} - -long get_time() -{ - struct timespec time; - clock_gettime(CLOCK_MONOTONIC, &time); - return time.tv_sec * 1000 + time.tv_nsec / 1000000; -} - -void await(long time) -{ - long delay = time - get_time(); - - if (delay > 0) - { - usleep(delay * 1000); - } -} - -int main(int argc, char *argv[]) -{ - int status = EXIT_FAILURE; - char *port = NULL; - int width = 0; - int height = 0; - int brightness = 255; - int mix = 0; - int rate = 0; - char *extensionFile = NULL; - bool shuffle = false; - bool verbose = false; - int sourcesLength = 0; - char **sources; - - srand(time(NULL)); - - if ((sources = malloc((argc - 1) * sizeof(*sources))) == NULL) - { - perror("Failed to allocate memory for sources"); - goto exit; - } - - for (int index = 1; index < argc; index++) - { - char *argument = argv[index]; - - if (argument[0] != '-') - { - sources[sourcesLength++] = argument; - continue; - } - - bool failed = false; - - switch (argument[1]) - { - case 'p': - failed = ++index >= argc; - port = argv[index]; - break; - - case 'w': - failed = ++index >= argc || parse(argv[index], &width); - break; - - case 'h': - failed = ++index >= argc || parse(argv[index], &height); - break; - - case 'b': - failed = ++index >= argc || parse(argv[index], &brightness); - break; - - case 'm': - failed = ++index >= argc || parse(argv[index], &mix); - break; - - case 'r': - failed = ++index >= argc || parse(argv[index], &rate); - break; - - case 'e': - failed = ++index >= argc; - extensionFile = argv[index]; - break; - - case 's': - shuffle = true; - break; - - case 'v': - verbose = true; - break; - - default: - failed = true; - } - - if (failed || argument[2] != 0) - { - puts("Usage:"); - puts(" panelplayer -p -w -h [options] "); - puts(""); - puts("Options:"); - puts(" -p Set ethernet port"); - puts(" -w Set display width"); - puts(" -h Set display height"); - puts(" -b Set display brightness"); - puts(" -m Set frame mixing percentage"); - puts(" -r Override source frame rate"); - puts(" -e Load extension from file"); - puts(" -s Shuffle sources"); - puts(" -v Enable verbose output"); - - goto free_sources; - } - } - - if (port == NULL) - { - puts("Port must be specified!"); - goto free_sources; - } - - if (width < 1 || height < 1) - { - puts("Width and height must be specified as positive integers!"); - goto free_sources; - } - - if (brightness < 0 || brightness > 255) - { - puts("Brightness must be an integer between 0 and 255!"); - goto free_sources; - } - - if (mix < 0 || mix >= MIX_MAXIMUM) - { - printf("Mix must be an integer between 0 and %d!\n", MIX_MAXIMUM - 1); - goto free_sources; - } - - if (sourcesLength == 0) - { - puts("At least one source must be specified!"); - goto free_sources; - } - - uint8_t *buffer; - - if ((buffer = malloc(width * height * 3)) == NULL) - { - perror("Failed to allocate frame buffer"); - goto free_sources; - } - - loader *loader; - - if ((loader = loader_init(QUEUE_SIZE)) == NULL) - { - puts("Failed to create loader instance!"); - goto free_buffer; - } - - colorlight *colorlight; - - if ((colorlight = colorlight_init(port)) == NULL) - { - puts("Failed to create Colorlight instance!"); - goto destroy_loader; - } - - void *extension = NULL; - void (*update)() = NULL; - - if (extensionFile != NULL) - { - extension = dlopen(extensionFile, RTLD_NOW); - - if (extension == NULL) - { - puts("Failed to load extension!"); - goto destroy_colorlight; - } - - bool (*init)() = dlsym(extension, "init"); - - if (init != NULL) - { - if (init()) - { - puts("Failed to initialise extension!"); - goto destroy_extension; - } - } - - update = dlsym(extension, "update"); - - if (update == NULL) - { - puts("Extension does not provide update function!"); - goto destroy_extension; - } - } - - int queued = 0; - long next = get_time(); - bool initial = true; - - for (int source = 0; shuffle || source < sourcesLength; source++) - { - while (queued < source + QUEUE_SIZE) - { - if (shuffle) - { - loader_add(loader, sources[rand() % sourcesLength]); - } - else if (queued < sourcesLength) - { - loader_add(loader, sources[queued]); - } - - queued++; - } - - int size; - void *file; - - if ((file = loader_get(loader, &size)) == NULL) - { - continue; - } - - WebPData data = { - .bytes = file, - .size = size - }; - - WebPAnimDecoder *decoder; - - if ((decoder = WebPAnimDecoderNew(&data, NULL)) == NULL) - { - puts("Failed to decode file!"); - goto free_file; - } - - WebPAnimInfo info; - WebPAnimDecoderGetInfo(decoder, &info); - - if (verbose) - { - printf("Decoding %d frames at a resolution of %dx%d.\n", info.frame_count, info.canvas_width, info.canvas_height); - } - - if (info.canvas_width < width || info.canvas_height < height) - { - puts("Image is smaller than display!"); - goto delete_decoder; - } - - int previous = 0; - long start = next; - - while (WebPAnimDecoderHasMoreFrames(decoder)) - { - uint8_t *decoded; - int timestamp; - - WebPAnimDecoderGetNext(decoder, &decoded, ×tamp); - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int source = (y * info.canvas_width + x) * 4; - int destination = (y * width + x) * 3; - - int oldFactor = initial ? 0 : mix; - int newFactor = MIX_MAXIMUM - oldFactor; - - buffer[destination] = (buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / MIX_MAXIMUM; - buffer[destination + 1] = (buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / MIX_MAXIMUM; - buffer[destination + 2] = (buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / MIX_MAXIMUM; - } - } - - if (update != NULL) - { - update(width, height, buffer); - } - - for (int y = 0; y < height; y++) - { - colorlight_send_row(colorlight, y, width, buffer + y * width * 3); - } - - if (next - get_time() < UPDATE_DELAY) - { - next = get_time() + UPDATE_DELAY; - } - - await(next); - colorlight_send_update(colorlight, brightness, brightness, brightness); - - if (rate > 0) - { - next = get_time() + 1000 / rate; - } - else - { - next = get_time() + timestamp - previous; - previous = timestamp; - } - - initial = false; - } - - if (verbose) - { - float seconds = (next - start) / 1000.0; - printf("Played %d frames in %.2f seconds at an average rate of %.2f frames per second.\n", info.frame_count, seconds, info.frame_count / seconds); - } - - delete_decoder: - WebPAnimDecoderDelete(decoder); - - free_file: - free(file); - } - - await(next); - status = EXIT_SUCCESS; - -destroy_extension: - if (extension != NULL) - { - void (*destroy)() = dlsym(extension, "destroy"); - - if (destroy != NULL) - { - destroy(); - } - - dlclose(extension); - } - -destroy_colorlight: - colorlight_destroy(colorlight); - -destroy_loader: - loader_destroy(loader); - -free_buffer: - free(buffer); - -free_sources: - free(sources); - -exit: - return status; +#include +#include +#include +#include +#include +#include +#include + +#include "colorlight.h" +#include "decoder.h" +#include "loader.h" + +#define QUEUE_SIZE 4 +#define MIX_MAXIMUM 100 +#define UPDATE_DELAY 10 + +bool parse(const char *source, int *destination) +{ + char *end; + *destination = strtol(source, &end, 10); + return end[0] != 0; +} + +long get_time() +{ + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_sec * 1000 + time.tv_nsec / 1000000; +} + +void await(long time) +{ + long delay = time - get_time(); + + if (delay > 0) + { + usleep(delay * 1000); + } +} + +int main(int argc, char *argv[]) +{ + int status = EXIT_FAILURE; + char *port = NULL; + int width = 0; + int height = 0; + int brightness = 255; + int mix = 0; + int rate = 0; + char *extensionFile = NULL; + bool shuffle = false; + bool verbose = false; + int sourcesLength = 0; + char **sources; + + srand(time(NULL)); + + if ((sources = malloc((argc - 1) * sizeof(*sources))) == NULL) + { + perror("Failed to allocate memory for sources"); + goto exit; + } + + for (int index = 1; index < argc; index++) + { + char *argument = argv[index]; + + if (argument[0] != '-') + { + sources[sourcesLength++] = argument; + continue; + } + + bool failed = false; + + switch (argument[1]) + { + case 'p': + failed = ++index >= argc; + port = argv[index]; + break; + + case 'w': + failed = ++index >= argc || parse(argv[index], &width); + break; + + case 'h': + failed = ++index >= argc || parse(argv[index], &height); + break; + + case 'b': + failed = ++index >= argc || parse(argv[index], &brightness); + break; + + case 'm': + failed = ++index >= argc || parse(argv[index], &mix); + break; + + case 'r': + failed = ++index >= argc || parse(argv[index], &rate); + break; + + case 'e': + failed = ++index >= argc; + extensionFile = argv[index]; + break; + + case 's': + shuffle = true; + break; + + case 'v': + verbose = true; + break; + + default: + failed = true; + } + + if (failed || argument[2] != 0) + { + puts("Usage:"); + puts(" panelplayer -p -w -h [options] "); + puts(""); + puts("Options:"); + puts(" -p Set ethernet port"); + puts(" -w Set display width"); + puts(" -h Set display height"); + puts(" -b Set display brightness"); + puts(" -m Set frame mixing percentage"); + puts(" -r Override source frame rate"); + puts(" -e Load extension from file"); + puts(" -s Shuffle sources"); + puts(" -v Enable verbose output"); + + goto free_sources; + } + } + + if (port == NULL) + { + puts("Port must be specified!"); + goto free_sources; + } + + if (width < 1 || height < 1) + { + puts("Width and height must be specified as positive integers!"); + goto free_sources; + } + + if (brightness < 0 || brightness > 255) + { + puts("Brightness must be an integer between 0 and 255!"); + goto free_sources; + } + + if (mix < 0 || mix >= MIX_MAXIMUM) + { + printf("Mix must be an integer between 0 and %d!\n", MIX_MAXIMUM - 1); + goto free_sources; + } + + if (sourcesLength == 0) + { + puts("At least one source must be specified!"); + goto free_sources; + } + + uint8_t *buffer; + + if ((buffer = malloc(width * height * 3)) == NULL) + { + perror("Failed to allocate frame buffer"); + goto free_sources; + } + + loader *loader; + + if ((loader = loader_init(QUEUE_SIZE)) == NULL) + { + puts("Failed to create loader instance!"); + goto free_buffer; + } + + colorlight *colorlight; + + if ((colorlight = colorlight_init(port)) == NULL) + { + puts("Failed to create Colorlight instance!"); + goto destroy_loader; + } + + void *extension = NULL; + void (*update)() = NULL; + + if (extensionFile != NULL) + { + extension = dlopen(extensionFile, RTLD_NOW); + + if (extension == NULL) + { + puts("Failed to load extension!"); + goto destroy_colorlight; + } + + bool (*init)() = dlsym(extension, "init"); + + if (init != NULL) + { + if (init()) + { + puts("Failed to initialise extension!"); + goto destroy_extension; + } + } + + update = dlsym(extension, "update"); + + if (update == NULL) + { + puts("Extension does not provide update function!"); + goto destroy_extension; + } + } + + int queued = 0; + long next = get_time(); + bool initial = true; + + for (int source = 0; shuffle || source < sourcesLength; source++) + { + while (queued < source + QUEUE_SIZE) + { + if (shuffle) + { + loader_add(loader, sources[rand() % sourcesLength]); + } + else if (queued < sourcesLength) + { + loader_add(loader, sources[queued]); + } + + queued++; + } + + int size; + void *file; + + if ((file = loader_get(loader, &size)) == NULL) + { + continue; + } + + decoder *decoder; + + if ((decoder = decoder_init(file, size)) == NULL) + { + puts("Failed to decode file!"); + goto free_file; + } + + decoder_info info; + decoder_get_info(decoder, &info); + + if (verbose) + { + printf("Decoding %d frames at a resolution of %dx%d.\n", info.frame_count, info.canvas_width, info.canvas_height); + } + + if (info.canvas_width < width || info.canvas_height < height) + { + puts("Image is smaller than display!"); + goto delete_decoder; + } + + int previous = 0; + long start = next; + + while (decoder_has_more_frames(decoder)) + { + uint8_t *decoded; + int timestamp; + + decoder_get_next(decoder, &decoded, ×tamp); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int source = (y * info.canvas_width + x) * 4; + int destination = (y * width + x) * 3; + + int oldFactor = initial ? 0 : mix; + int newFactor = MIX_MAXIMUM - oldFactor; + + buffer[destination] = (buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / MIX_MAXIMUM; + buffer[destination + 1] = (buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / MIX_MAXIMUM; + buffer[destination + 2] = (buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / MIX_MAXIMUM; + } + } + + if (update != NULL) + { + update(width, height, buffer); + } + + for (int y = 0; y < height; y++) + { + colorlight_send_row(colorlight, y, width, buffer + y * width * 3); + } + + if (next - get_time() < UPDATE_DELAY) + { + next = get_time() + UPDATE_DELAY; + } + + await(next); + colorlight_send_update(colorlight, brightness, brightness, brightness); + + if (rate > 0) + { + next = get_time() + 1000 / rate; + } + else + { + next = get_time() + timestamp - previous; + previous = timestamp; + } + + initial = false; + } + + if (verbose) + { + float seconds = (next - start) / 1000.0; + printf("Played %d frames in %.2f seconds at an average rate of %.2f frames per second.\n", info.frame_count, seconds, info.frame_count / seconds); + } + + delete_decoder: + decoder_destroy(decoder); + + free_file: + free(file); + } + + await(next); + status = EXIT_SUCCESS; + +destroy_extension: + if (extension != NULL) + { + void (*destroy)() = dlsym(extension, "destroy"); + + if (destroy != NULL) + { + destroy(); + } + + dlclose(extension); + } + +destroy_colorlight: + colorlight_destroy(colorlight); + +destroy_loader: + loader_destroy(loader); + +free_buffer: + free(buffer); + +free_sources: + free(sources); + +exit: + return status; } \ No newline at end of file From 932e26d36541f5e9159a20c38bcecdf4a9cfa547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Carlo=20M=C3=A1rquez=20Zarzalejo?= Date: Thu, 2 Oct 2025 13:43:24 +0200 Subject: [PATCH 03/10] Duplicate the ouput so I can handle 2 identical screens at the same time --- source/main.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/source/main.c b/source/main.c index 0f77c63..1be2f25 100644 --- a/source/main.c +++ b/source/main.c @@ -50,6 +50,7 @@ int main(int argc, char *argv[]) char *extensionFile = NULL; bool shuffle = false; bool verbose = false; + bool duplicate = false; int sourcesLength = 0; char **sources; @@ -113,6 +114,10 @@ int main(int argc, char *argv[]) verbose = true; break; + case 'd': + duplicate = true; + break; + default: failed = true; } @@ -132,6 +137,7 @@ int main(int argc, char *argv[]) puts(" -e Load extension from file"); puts(" -s Shuffle sources"); puts(" -v Enable verbose output"); + puts(" -d Duplicate each row vertically"); goto free_sources; } @@ -308,6 +314,11 @@ int main(int argc, char *argv[]) for (int y = 0; y < height; y++) { colorlight_send_row(colorlight, y, width, buffer + y * width * 3); + + if (duplicate) + { + colorlight_send_row(colorlight, y + height, width, buffer + y * width * 3); + } } if (next - get_time() < UPDATE_DELAY) From dddcfe96cec0189d095981a785d22f566c4a5879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Carlo=20M=C3=A1rquez=20Zarzalejo?= Date: Fri, 3 Oct 2025 10:36:59 +0200 Subject: [PATCH 04/10] Update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e6421fb..c048939 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /.vscode /build /extensions/nanoled/build -/extensions/grayscale/extension.so \ No newline at end of file +/extensions/grayscale/extension.so +.claude/ +CLAUDE.md From 4d55af714dc305da2b2ac45c42654b5099b3420b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20M=C3=A1rquez?= Date: Thu, 2 Oct 2025 15:41:37 +0200 Subject: [PATCH 05/10] Add duplicate mode to C API and C# wrapper Implements vertical duplication functionality in the C API library to match the -d flag available in the main executable. This allows programmatic control of duplicate mode through the API. Changes: - Add panelplayer_set_duplicate() function to C API - Add duplicate field to panelplayer_state structure - Implement row duplication in both panelplayer_play_file() and panelplayer_play_frame_bgr() - Add comprehensive Doxygen documentation for new function - Add SetDuplicate() method to C# wrapper - Update C# example documentation Use case: Driving two identical displays stacked vertically where the image height is half the total display height (e.g., 240x80 image on 240x160 display). --- .gitignore | 3 +- examples/csharp/PanelPlayerExample.cs | 14 ++++++ examples/csharp/README.md | 1 + makefile | 66 +++++++++++++-------------- source/panelplayer_api.c | 25 ++++++++-- source/panelplayer_api.h | 24 ++++++++-- 6 files changed, 93 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 520ee43..f396fb5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ /extensions/nanoled/build /extensions/grayscale/extension.so /examples/csharp/obj -/examples/csharp/bin.claude/ +/examples/csharp/bin +.claude/ CLAUDE.md \ No newline at end of file diff --git a/examples/csharp/PanelPlayerExample.cs b/examples/csharp/PanelPlayerExample.cs index 1daa910..4abd99e 100644 --- a/examples/csharp/PanelPlayerExample.cs +++ b/examples/csharp/PanelPlayerExample.cs @@ -28,6 +28,11 @@ int brightness [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern int panelplayer_set_rate(int frame_rate); + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_set_duplicate( + [MarshalAs(UnmanagedType.I1)] bool enable + ); + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] public static extern int panelplayer_load_extension( [MarshalAs(UnmanagedType.LPStr)] string extension_path @@ -91,6 +96,15 @@ public void SetFrameRate(int frameRate) } } + public void SetDuplicate(bool enable) + { + int result = PanelPlayerNative.panelplayer_set_duplicate(enable); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set duplicate mode: {result}"); + } + } + public void LoadExtension(string extensionPath) { int result = PanelPlayerNative.panelplayer_load_extension(extensionPath); diff --git a/examples/csharp/README.md b/examples/csharp/README.md index 12a5baf..87278e8 100644 --- a/examples/csharp/README.md +++ b/examples/csharp/README.md @@ -110,5 +110,6 @@ The C# wrapper provides these main methods: - `LoadExtension(path)` - Load processing extensions - `SetMix(percentage)` - Control frame blending - `SetFrameRate(fps)` - Override animation timing +- `SetDuplicate(enable)` - Enable vertical duplication for stacked displays For detailed API documentation, see the native library header file `source/panelplayer_api.h`. \ No newline at end of file diff --git a/makefile b/makefile index 439186b..4322e79 100644 --- a/makefile +++ b/makefile @@ -1,34 +1,34 @@ -CC = gcc -CFLAGS = -Wall -Werror -pthread -O3 -LDFLAGS = -pthread -LDLIBS = -ldl -lwebpdemux -ljpeg -lpng -lgif - -SOURCE = ./source -BUILD = ./build -TARGET = $(BUILD)/panelplayer -LIBRARY = $(BUILD)/libpanelplayer.so - -HEADERS = $(wildcard $(SOURCE)/*.h) -MAIN_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/panelplayer_api.c,$(wildcard $(SOURCE)/*.c))) -API_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/main.c,$(wildcard $(SOURCE)/*.c))) - -.PHONY: clean all library - -all: $(TARGET) $(LIBRARY) - -$(TARGET): $(BUILD) $(MAIN_OBJECTS) - $(CC) $(LDFLAGS) $(MAIN_OBJECTS) $(LDLIBS) -o $@ - -$(LIBRARY): $(BUILD) $(API_OBJECTS) - $(CC) $(LDFLAGS) -shared -fPIC $(API_OBJECTS) $(LDLIBS) -o $@ - -library: $(LIBRARY) - -$(BUILD): - mkdir $(BUILD) - -$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile - $(CC) $(CFLAGS) -fPIC -c $< -o $@ - -clean: +CC = gcc +CFLAGS = -Wall -Werror -pthread -O3 +LDFLAGS = -pthread +LDLIBS = -ldl -lwebpdemux -ljpeg -lpng -lgif + +SOURCE = ./source +BUILD = ./build +TARGET = $(BUILD)/panelplayer +LIBRARY = $(BUILD)/libpanelplayer.so + +HEADERS = $(wildcard $(SOURCE)/*.h) +MAIN_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/panelplayer_api.c,$(wildcard $(SOURCE)/*.c))) +API_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/main.c,$(wildcard $(SOURCE)/*.c))) + +.PHONY: clean all library + +all: $(TARGET) $(LIBRARY) + +$(TARGET): $(BUILD) $(MAIN_OBJECTS) + $(CC) $(LDFLAGS) $(MAIN_OBJECTS) $(LDLIBS) -o $@ + +$(LIBRARY): $(BUILD) $(API_OBJECTS) + $(CC) $(LDFLAGS) -shared -fPIC $(API_OBJECTS) $(LDLIBS) -o $@ + +library: $(LIBRARY) + +$(BUILD): + mkdir $(BUILD) + +$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile + $(CC) $(CFLAGS) -fPIC -c $< -o $@ + +clean: rm -r $(BUILD) \ No newline at end of file diff --git a/source/panelplayer_api.c b/source/panelplayer_api.c index 503b045..43f71ae 100644 --- a/source/panelplayer_api.c +++ b/source/panelplayer_api.c @@ -23,6 +23,7 @@ typedef struct { int brightness; int mix; int rate; + bool duplicate; void *extension; void (*update_func)(int width, int height, uint8_t *frame); bool initialized; @@ -68,6 +69,7 @@ int panelplayer_init(const char* interface_name, int width, int height, int brig g_state.brightness = brightness; g_state.mix = 0; g_state.rate = 0; + g_state.duplicate = false; g_state.extension = NULL; g_state.update_func = NULL; g_state.initialized = true; @@ -92,15 +94,24 @@ int panelplayer_set_rate(int frame_rate) { if (!g_state.initialized) { return PANELPLAYER_NOT_INITIALIZED; } - + if (frame_rate < 0) { return PANELPLAYER_INVALID_PARAM; } - + g_state.rate = frame_rate; return PANELPLAYER_SUCCESS; } +int panelplayer_set_duplicate(bool enable) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + g_state.duplicate = enable; + return PANELPLAYER_SUCCESS; +} + int panelplayer_load_extension(const char* extension_path) { if (!g_state.initialized) { return PANELPLAYER_NOT_INITIALIZED; @@ -218,6 +229,10 @@ int panelplayer_play_file(const char* file_path) { for (int y = 0; y < g_state.height; y++) { colorlight_send_row(g_state.colorlight, y, g_state.width, g_state.buffer + y * g_state.width * 3); + + if (g_state.duplicate) { + colorlight_send_row(g_state.colorlight, y + g_state.height, g_state.width, g_state.buffer + y * g_state.width * 3); + } } if (next - get_time() < UPDATE_DELAY) { @@ -260,8 +275,12 @@ int panelplayer_play_frame_bgr(const uint8_t* bgr_data, int width, int height) { for (int y = 0; y < g_state.height; y++) { colorlight_send_row(g_state.colorlight, y, g_state.width, g_state.buffer + y * g_state.width * 3); + + if (g_state.duplicate) { + colorlight_send_row(g_state.colorlight, y + g_state.height, g_state.width, g_state.buffer + y * g_state.width * 3); + } } - + long next = get_time() + UPDATE_DELAY; await(next); colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); diff --git a/source/panelplayer_api.h b/source/panelplayer_api.h index 8e8542a..73f2ec0 100644 --- a/source/panelplayer_api.h +++ b/source/panelplayer_api.h @@ -65,12 +65,12 @@ int panelplayer_set_mix(int mix_percentage); /** * @brief Set playback frame rate - * + * * Sets the frame rate for WebP animation playback. When set to 0, * uses the timing information embedded in the WebP file. - * + * * @param frame_rate Target frame rate in frames per second (0 for auto) - * + * * @return PANELPLAYER_SUCCESS on success, error code otherwise * @retval PANELPLAYER_SUCCESS Frame rate set successfully * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized @@ -78,6 +78,24 @@ int panelplayer_set_mix(int mix_percentage); */ int panelplayer_set_rate(int frame_rate); +/** + * @brief Enable or disable vertical duplication mode + * + * When enabled, each row of the image is sent twice - once at its original + * position (y) and once at y+height. This is useful for driving two identical + * displays stacked vertically where the image height is half the total display height. + * + * Example: With a 240x80 image and duplicate enabled, each row is sent to both + * y and y+80, effectively filling a 240x160 display. + * + * @param enable true to enable duplication, false to disable + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS Duplicate mode set successfully + * @retval PANELPLAYER_NOT_INITIALIZED Library not initialized + */ +int panelplayer_set_duplicate(bool enable); + /** * @brief Load a frame processing extension * From 31f35104be55d29004a7a4c0b51bf3177b1c13a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20M=C3=A1rquez?= Date: Fri, 3 Oct 2025 14:16:19 +0200 Subject: [PATCH 06/10] Refactor code to eliminate duplication between main.c and panelplayer_api.c Created new core module (core.c/core.h) with shared functionality: - Time management functions (core_get_time, core_await) - Frame processing (core_process_frame) - Hardware communication (core_send_frame) - Extension management (core_load_extension, core_unload_extension) - Shared constants (CORE_MIX_MAXIMUM, CORE_UPDATE_DELAY) Benefits: - Eliminated ~150 lines of duplicated code - Centralized common logic in a single location - Improved maintainability and testability - main.c now focused on CLI parsing - panelplayer_api.c now focused on API surface --- source/core.c | 122 +++++++++ source/core.h | 67 +++++ source/main.c | 108 +------- source/panelplayer_api.c | 577 +++++++++++++++++---------------------- 4 files changed, 456 insertions(+), 418 deletions(-) create mode 100644 source/core.c create mode 100644 source/core.h diff --git a/source/core.c b/source/core.c new file mode 100644 index 0000000..26b98da --- /dev/null +++ b/source/core.c @@ -0,0 +1,122 @@ +#include +#include +#include +#include +#include +#include + +#include "core.h" + +long core_get_time(void) +{ + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_sec * 1000 + time.tv_nsec / 1000000; +} + +void core_await(long time) +{ + long delay = time - core_get_time(); + + if (delay > 0) + { + usleep(delay * 1000); + } +} + +void core_process_frame(uint8_t *buffer, uint8_t *decoded, decoder_info *info, + int width, int height, int mix, bool initial) +{ + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int source = (y * info->canvas_width + x) * 4; + int destination = (y * width + x) * 3; + + int oldFactor = initial ? 0 : mix; + int newFactor = CORE_MIX_MAXIMUM - oldFactor; + + buffer[destination] = (buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / CORE_MIX_MAXIMUM; + buffer[destination + 1] = (buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / CORE_MIX_MAXIMUM; + buffer[destination + 2] = (buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / CORE_MIX_MAXIMUM; + } + } +} + +void core_send_frame(colorlight *cl, uint8_t *buffer, int width, int height, + bool duplicate, void (*update_func)(int, int, uint8_t*)) +{ + if (update_func != NULL) + { + update_func(width, height, buffer); + } + + for (int y = 0; y < height; y++) + { + colorlight_send_row(cl, y, width, buffer + y * width * 3); + + if (duplicate) + { + colorlight_send_row(cl, y + height, width, buffer + y * width * 3); + } + } +} + +int core_load_extension(const char *path, void **extension, void (**update_func)(int, int, uint8_t*)) +{ + if (path == NULL || extension == NULL || update_func == NULL) + { + return -1; + } + + *extension = dlopen(path, RTLD_NOW); + + if (*extension == NULL) + { + return -1; + } + + bool (*init)() = dlsym(*extension, "init"); + + if (init != NULL) + { + if (init()) + { + dlclose(*extension); + *extension = NULL; + return -1; + } + } + + *update_func = dlsym(*extension, "update"); + + if (*update_func == NULL) + { + void (*destroy)() = dlsym(*extension, "destroy"); + if (destroy != NULL) + { + destroy(); + } + dlclose(*extension); + *extension = NULL; + return -1; + } + + return 0; +} + +void core_unload_extension(void *extension) +{ + if (extension != NULL) + { + void (*destroy)() = dlsym(extension, "destroy"); + + if (destroy != NULL) + { + destroy(); + } + + dlclose(extension); + } +} diff --git a/source/core.h b/source/core.h new file mode 100644 index 0000000..a523c0e --- /dev/null +++ b/source/core.h @@ -0,0 +1,67 @@ +#ifndef CORE_H +#define CORE_H + +#include +#include + +#include "colorlight.h" +#include "decoder.h" + +#define CORE_MIX_MAXIMUM 100 +#define CORE_UPDATE_DELAY 10 + +/** + * Get current time in milliseconds + */ +long core_get_time(void); + +/** + * Sleep until the specified time (in milliseconds) + */ +void core_await(long time); + +/** + * Process a decoded frame: convert RGBA to BGR and apply frame mixing + * + * @param buffer Destination BGR buffer (width * height * 3) + * @param decoded Source RGBA data (canvas_width * canvas_height * 4) + * @param info Decoder info containing canvas dimensions + * @param width Target width + * @param height Target height + * @param mix Mix percentage (0-99, 0 = no mixing) + * @param initial True if this is the first frame (disables mixing) + */ +void core_process_frame(uint8_t *buffer, uint8_t *decoded, decoder_info *info, + int width, int height, int mix, bool initial); + +/** + * Send a frame to the hardware, applying extensions and duplicate mode + * + * @param cl Colorlight instance + * @param buffer BGR frame data (width * height * 3) + * @param width Frame width + * @param height Frame height + * @param duplicate If true, duplicate each row vertically + * @param update_func Optional extension update function (can be NULL) + */ +void core_send_frame(colorlight *cl, uint8_t *buffer, int width, int height, + bool duplicate, void (*update_func)(int, int, uint8_t*)); + +/** + * Load an extension from a shared library file + * + * @param path Path to the extension .so file + * @param extension Output pointer to store the loaded extension handle + * @param update_func Output pointer to store the update function pointer + * @return 0 on success, -1 on error + */ +int core_load_extension(const char *path, void **extension, void (**update_func)(int, int, uint8_t*)); + +/** + * Unload an extension, calling its destroy function if present + * + * @param extension Extension handle to unload (can be NULL) + */ +void core_unload_extension(void *extension); + +#endif diff --git a/source/main.c b/source/main.c index 1be2f25..daa5b2e 100644 --- a/source/main.c +++ b/source/main.c @@ -7,12 +7,11 @@ #include #include "colorlight.h" +#include "core.h" #include "decoder.h" #include "loader.h" #define QUEUE_SIZE 4 -#define MIX_MAXIMUM 100 -#define UPDATE_DELAY 10 bool parse(const char *source, int *destination) { @@ -21,23 +20,6 @@ bool parse(const char *source, int *destination) return end[0] != 0; } -long get_time() -{ - struct timespec time; - clock_gettime(CLOCK_MONOTONIC, &time); - return time.tv_sec * 1000 + time.tv_nsec / 1000000; -} - -void await(long time) -{ - long delay = time - get_time(); - - if (delay > 0) - { - usleep(delay * 1000); - } -} - int main(int argc, char *argv[]) { int status = EXIT_FAILURE; @@ -161,9 +143,9 @@ int main(int argc, char *argv[]) goto free_sources; } - if (mix < 0 || mix >= MIX_MAXIMUM) + if (mix < 0 || mix >= CORE_MIX_MAXIMUM) { - printf("Mix must be an integer between 0 and %d!\n", MIX_MAXIMUM - 1); + printf("Mix must be an integer between 0 and %d!\n", CORE_MIX_MAXIMUM - 1); goto free_sources; } @@ -198,40 +180,19 @@ int main(int argc, char *argv[]) } void *extension = NULL; - void (*update)() = NULL; + void (*update)(int, int, uint8_t*) = NULL; if (extensionFile != NULL) { - extension = dlopen(extensionFile, RTLD_NOW); - - if (extension == NULL) + if (core_load_extension(extensionFile, &extension, &update) != 0) { puts("Failed to load extension!"); goto destroy_colorlight; } - - bool (*init)() = dlsym(extension, "init"); - - if (init != NULL) - { - if (init()) - { - puts("Failed to initialise extension!"); - goto destroy_extension; - } - } - - update = dlsym(extension, "update"); - - if (update == NULL) - { - puts("Extension does not provide update function!"); - goto destroy_extension; - } } int queued = 0; - long next = get_time(); + long next = core_get_time(); bool initial = true; for (int source = 0; shuffle || source < sourcesLength; source++) @@ -290,52 +251,24 @@ int main(int argc, char *argv[]) decoder_get_next(decoder, &decoded, ×tamp); - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - int source = (y * info.canvas_width + x) * 4; - int destination = (y * width + x) * 3; - - int oldFactor = initial ? 0 : mix; - int newFactor = MIX_MAXIMUM - oldFactor; - - buffer[destination] = (buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / MIX_MAXIMUM; - buffer[destination + 1] = (buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / MIX_MAXIMUM; - buffer[destination + 2] = (buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / MIX_MAXIMUM; - } - } + core_process_frame(buffer, decoded, &info, width, height, mix, initial); + core_send_frame(colorlight, buffer, width, height, duplicate, update); - if (update != NULL) + if (next - core_get_time() < CORE_UPDATE_DELAY) { - update(width, height, buffer); + next = core_get_time() + CORE_UPDATE_DELAY; } - for (int y = 0; y < height; y++) - { - colorlight_send_row(colorlight, y, width, buffer + y * width * 3); - - if (duplicate) - { - colorlight_send_row(colorlight, y + height, width, buffer + y * width * 3); - } - } - - if (next - get_time() < UPDATE_DELAY) - { - next = get_time() + UPDATE_DELAY; - } - - await(next); + core_await(next); colorlight_send_update(colorlight, brightness, brightness, brightness); if (rate > 0) { - next = get_time() + 1000 / rate; + next = core_get_time() + 1000 / rate; } else { - next = get_time() + timestamp - previous; + next = core_get_time() + timestamp - previous; previous = timestamp; } @@ -355,21 +288,10 @@ int main(int argc, char *argv[]) free(file); } - await(next); + core_await(next); status = EXIT_SUCCESS; -destroy_extension: - if (extension != NULL) - { - void (*destroy)() = dlsym(extension, "destroy"); - - if (destroy != NULL) - { - destroy(); - } - - dlclose(extension); - } + core_unload_extension(extension); destroy_colorlight: colorlight_destroy(colorlight); diff --git a/source/panelplayer_api.c b/source/panelplayer_api.c index 43f71ae..97a578c 100644 --- a/source/panelplayer_api.c +++ b/source/panelplayer_api.c @@ -1,326 +1,253 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "panelplayer_api.h" -#include "colorlight.h" -#include "decoder.h" -#include "loader.h" - -#define MIX_MAXIMUM 100 -#define UPDATE_DELAY 10 - -typedef struct { - colorlight *colorlight; - uint8_t *buffer; - int width; - int height; - int brightness; - int mix; - int rate; - bool duplicate; - void *extension; - void (*update_func)(int width, int height, uint8_t *frame); - bool initialized; -} panelplayer_state; - -static panelplayer_state g_state = {0}; - -static long get_time(void) { - struct timespec time; - clock_gettime(CLOCK_MONOTONIC, &time); - return time.tv_sec * 1000 + time.tv_nsec / 1000000; -} - -static void await(long time) { - long delay = time - get_time(); - if (delay > 0) { - usleep(delay * 1000); - } -} - -int panelplayer_init(const char* interface_name, int width, int height, int brightness) { - if (g_state.initialized) { - return PANELPLAYER_ALREADY_INITIALIZED; - } - - if (!interface_name || width <= 0 || height <= 0 || brightness < 0 || brightness > 255) { - return PANELPLAYER_INVALID_PARAM; - } - - g_state.colorlight = colorlight_init((char*)interface_name); - if (!g_state.colorlight) { - return PANELPLAYER_ERROR; - } - - g_state.buffer = malloc(width * height * 3); - if (!g_state.buffer) { - colorlight_destroy(g_state.colorlight); - return PANELPLAYER_ERROR; - } - - g_state.width = width; - g_state.height = height; - g_state.brightness = brightness; - g_state.mix = 0; - g_state.rate = 0; - g_state.duplicate = false; - g_state.extension = NULL; - g_state.update_func = NULL; - g_state.initialized = true; - - return PANELPLAYER_SUCCESS; -} - -int panelplayer_set_mix(int mix_percentage) { - if (!g_state.initialized) { - return PANELPLAYER_NOT_INITIALIZED; - } - - if (mix_percentage < 0 || mix_percentage >= MIX_MAXIMUM) { - return PANELPLAYER_INVALID_PARAM; - } - - g_state.mix = mix_percentage; - return PANELPLAYER_SUCCESS; -} - -int panelplayer_set_rate(int frame_rate) { - if (!g_state.initialized) { - return PANELPLAYER_NOT_INITIALIZED; - } - - if (frame_rate < 0) { - return PANELPLAYER_INVALID_PARAM; - } - - g_state.rate = frame_rate; - return PANELPLAYER_SUCCESS; -} - -int panelplayer_set_duplicate(bool enable) { - if (!g_state.initialized) { - return PANELPLAYER_NOT_INITIALIZED; - } - - g_state.duplicate = enable; - return PANELPLAYER_SUCCESS; -} - -int panelplayer_load_extension(const char* extension_path) { - if (!g_state.initialized) { - return PANELPLAYER_NOT_INITIALIZED; - } - - if (!extension_path) { - return PANELPLAYER_INVALID_PARAM; - } - - if (g_state.extension) { - void (*destroy)() = dlsym(g_state.extension, "destroy"); - if (destroy) { - destroy(); - } - dlclose(g_state.extension); - g_state.extension = NULL; - g_state.update_func = NULL; - } - - g_state.extension = dlopen(extension_path, RTLD_NOW); - if (!g_state.extension) { - return PANELPLAYER_ERROR; - } - - bool (*init)() = dlsym(g_state.extension, "init"); - if (init && init()) { - dlclose(g_state.extension); - g_state.extension = NULL; - return PANELPLAYER_ERROR; - } - - g_state.update_func = dlsym(g_state.extension, "update"); - if (!g_state.update_func) { - dlclose(g_state.extension); - g_state.extension = NULL; - return PANELPLAYER_ERROR; - } - - return PANELPLAYER_SUCCESS; -} - -int panelplayer_play_file(const char* file_path) { - if (!g_state.initialized) { - return PANELPLAYER_NOT_INITIALIZED; - } - - if (!file_path) { - return PANELPLAYER_INVALID_PARAM; - } - - FILE *file = fopen(file_path, "rb"); - if (!file) { - return PANELPLAYER_ERROR; - } - - fseek(file, 0, SEEK_END); - long file_size = ftell(file); - fseek(file, 0, SEEK_SET); - - uint8_t *file_data = malloc(file_size); - if (!file_data) { - fclose(file); - return PANELPLAYER_ERROR; - } - - if (fread(file_data, 1, file_size, file) != file_size) { - free(file_data); - fclose(file); - return PANELPLAYER_ERROR; - } - fclose(file); - - decoder *dec = decoder_init(file_data, file_size); - if (!dec) { - free(file_data); - return PANELPLAYER_ERROR; - } - - decoder_info info; - decoder_get_info(dec, &info); - - if (info.canvas_width < g_state.width || info.canvas_height < g_state.height) { - decoder_destroy(dec); - free(file_data); - return PANELPLAYER_ERROR; - } - - long next = get_time(); - int previous = 0; - bool initial = true; - - while (decoder_has_more_frames(dec)) { - uint8_t *decoded; - int timestamp; - - decoder_get_next(dec, &decoded, ×tamp); - - for (int y = 0; y < g_state.height; y++) { - for (int x = 0; x < g_state.width; x++) { - int source = (y * info.canvas_width + x) * 4; - int destination = (y * g_state.width + x) * 3; - - int oldFactor = initial ? 0 : g_state.mix; - int newFactor = MIX_MAXIMUM - oldFactor; - - g_state.buffer[destination] = (g_state.buffer[destination] * oldFactor + decoded[source + 2] * newFactor) / MIX_MAXIMUM; - g_state.buffer[destination + 1] = (g_state.buffer[destination + 1] * oldFactor + decoded[source + 1] * newFactor) / MIX_MAXIMUM; - g_state.buffer[destination + 2] = (g_state.buffer[destination + 2] * oldFactor + decoded[source] * newFactor) / MIX_MAXIMUM; - } - } - - if (g_state.update_func) { - g_state.update_func(g_state.width, g_state.height, g_state.buffer); - } - - for (int y = 0; y < g_state.height; y++) { - colorlight_send_row(g_state.colorlight, y, g_state.width, g_state.buffer + y * g_state.width * 3); - - if (g_state.duplicate) { - colorlight_send_row(g_state.colorlight, y + g_state.height, g_state.width, g_state.buffer + y * g_state.width * 3); - } - } - - if (next - get_time() < UPDATE_DELAY) { - next = get_time() + UPDATE_DELAY; - } - - await(next); - colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); - - if (g_state.rate > 0) { - next = get_time() + 1000 / g_state.rate; - } else { - next = get_time() + timestamp - previous; - previous = timestamp; - } - - initial = false; - } - - decoder_destroy(dec); - free(file_data); - - return PANELPLAYER_SUCCESS; -} - -int panelplayer_play_frame_bgr(const uint8_t* bgr_data, int width, int height) { - if (!g_state.initialized) { - return PANELPLAYER_NOT_INITIALIZED; - } - - if (!bgr_data || width != g_state.width || height != g_state.height) { - return PANELPLAYER_INVALID_PARAM; - } - - memcpy(g_state.buffer, bgr_data, width * height * 3); - - if (g_state.update_func) { - g_state.update_func(g_state.width, g_state.height, g_state.buffer); - } - - for (int y = 0; y < g_state.height; y++) { - colorlight_send_row(g_state.colorlight, y, g_state.width, g_state.buffer + y * g_state.width * 3); - - if (g_state.duplicate) { - colorlight_send_row(g_state.colorlight, y + g_state.height, g_state.width, g_state.buffer + y * g_state.width * 3); - } - } - - long next = get_time() + UPDATE_DELAY; - await(next); - colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); - - return PANELPLAYER_SUCCESS; -} - -int panelplayer_send_brightness(uint8_t red, uint8_t green, uint8_t blue) { - if (!g_state.initialized) { - return PANELPLAYER_NOT_INITIALIZED; - } - - colorlight_send_brightness(g_state.colorlight, red, green, blue); - return PANELPLAYER_SUCCESS; -} - -bool panelplayer_is_initialized(void) { - return g_state.initialized; -} - -void panelplayer_cleanup(void) { - if (!g_state.initialized) { - return; - } - - if (g_state.extension) { - void (*destroy)() = dlsym(g_state.extension, "destroy"); - if (destroy) { - destroy(); - } - dlclose(g_state.extension); - } - - if (g_state.colorlight) { - colorlight_destroy(g_state.colorlight); - } - - if (g_state.buffer) { - free(g_state.buffer); - } - - memset(&g_state, 0, sizeof(g_state)); +#include +#include +#include +#include +#include +#include +#include +#include + +#include "panelplayer_api.h" +#include "colorlight.h" +#include "core.h" +#include "decoder.h" +#include "loader.h" + +typedef struct { + colorlight *colorlight; + uint8_t *buffer; + int width; + int height; + int brightness; + int mix; + int rate; + bool duplicate; + void *extension; + void (*update_func)(int width, int height, uint8_t *frame); + bool initialized; +} panelplayer_state; + +static panelplayer_state g_state = {0}; + +int panelplayer_init(const char* interface_name, int width, int height, int brightness) { + if (g_state.initialized) { + return PANELPLAYER_ALREADY_INITIALIZED; + } + + if (!interface_name || width <= 0 || height <= 0 || brightness < 0 || brightness > 255) { + return PANELPLAYER_INVALID_PARAM; + } + + g_state.colorlight = colorlight_init((char*)interface_name); + if (!g_state.colorlight) { + return PANELPLAYER_ERROR; + } + + g_state.buffer = malloc(width * height * 3); + if (!g_state.buffer) { + colorlight_destroy(g_state.colorlight); + return PANELPLAYER_ERROR; + } + + g_state.width = width; + g_state.height = height; + g_state.brightness = brightness; + g_state.mix = 0; + g_state.rate = 0; + g_state.duplicate = false; + g_state.extension = NULL; + g_state.update_func = NULL; + g_state.initialized = true; + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_set_mix(int mix_percentage) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (mix_percentage < 0 || mix_percentage >= CORE_MIX_MAXIMUM) { + return PANELPLAYER_INVALID_PARAM; + } + + g_state.mix = mix_percentage; + return PANELPLAYER_SUCCESS; +} + +int panelplayer_set_rate(int frame_rate) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (frame_rate < 0) { + return PANELPLAYER_INVALID_PARAM; + } + + g_state.rate = frame_rate; + return PANELPLAYER_SUCCESS; +} + +int panelplayer_set_duplicate(bool enable) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + g_state.duplicate = enable; + return PANELPLAYER_SUCCESS; +} + +int panelplayer_load_extension(const char* extension_path) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (!extension_path) { + return PANELPLAYER_INVALID_PARAM; + } + + if (g_state.extension) { + core_unload_extension(g_state.extension); + g_state.extension = NULL; + g_state.update_func = NULL; + } + + if (core_load_extension(extension_path, &g_state.extension, &g_state.update_func) != 0) { + return PANELPLAYER_ERROR; + } + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_play_file(const char* file_path) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (!file_path) { + return PANELPLAYER_INVALID_PARAM; + } + + FILE *file = fopen(file_path, "rb"); + if (!file) { + return PANELPLAYER_ERROR; + } + + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + uint8_t *file_data = malloc(file_size); + if (!file_data) { + fclose(file); + return PANELPLAYER_ERROR; + } + + if (fread(file_data, 1, file_size, file) != file_size) { + free(file_data); + fclose(file); + return PANELPLAYER_ERROR; + } + fclose(file); + + decoder *dec = decoder_init(file_data, file_size); + if (!dec) { + free(file_data); + return PANELPLAYER_ERROR; + } + + decoder_info info; + decoder_get_info(dec, &info); + + if (info.canvas_width < g_state.width || info.canvas_height < g_state.height) { + decoder_destroy(dec); + free(file_data); + return PANELPLAYER_ERROR; + } + + long next = core_get_time(); + int previous = 0; + bool initial = true; + + while (decoder_has_more_frames(dec)) { + uint8_t *decoded; + int timestamp; + + decoder_get_next(dec, &decoded, ×tamp); + + core_process_frame(g_state.buffer, decoded, &info, g_state.width, g_state.height, g_state.mix, initial); + core_send_frame(g_state.colorlight, g_state.buffer, g_state.width, g_state.height, g_state.duplicate, g_state.update_func); + + if (next - core_get_time() < CORE_UPDATE_DELAY) { + next = core_get_time() + CORE_UPDATE_DELAY; + } + + core_await(next); + colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); + + if (g_state.rate > 0) { + next = core_get_time() + 1000 / g_state.rate; + } else { + next = core_get_time() + timestamp - previous; + previous = timestamp; + } + + initial = false; + } + + decoder_destroy(dec); + free(file_data); + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_play_frame_bgr(const uint8_t* bgr_data, int width, int height) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + if (!bgr_data || width != g_state.width || height != g_state.height) { + return PANELPLAYER_INVALID_PARAM; + } + + memcpy(g_state.buffer, bgr_data, width * height * 3); + + core_send_frame(g_state.colorlight, g_state.buffer, g_state.width, g_state.height, g_state.duplicate, g_state.update_func); + + long next = core_get_time() + CORE_UPDATE_DELAY; + core_await(next); + colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); + + return PANELPLAYER_SUCCESS; +} + +int panelplayer_send_brightness(uint8_t red, uint8_t green, uint8_t blue) { + if (!g_state.initialized) { + return PANELPLAYER_NOT_INITIALIZED; + } + + colorlight_send_brightness(g_state.colorlight, red, green, blue); + return PANELPLAYER_SUCCESS; +} + +bool panelplayer_is_initialized(void) { + return g_state.initialized; +} + +void panelplayer_cleanup(void) { + if (!g_state.initialized) { + return; + } + + core_unload_extension(g_state.extension); + + if (g_state.colorlight) { + colorlight_destroy(g_state.colorlight); + } + + if (g_state.buffer) { + free(g_state.buffer); + } + + memset(&g_state, 0, sizeof(g_state)); } \ No newline at end of file From 5d584f53a3b2c7cce557d6679fdbc13b02b05a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20M=C3=A1rquez?= Date: Fri, 3 Oct 2025 14:32:05 +0200 Subject: [PATCH 07/10] Improve C# example with better structure and command-line argument support Refactored C# example to follow better organization practices: - Split monolithic PanelPlayerExample.cs into separate files: - PanelPlayerNative.cs: P/Invoke declarations - PanelPlayer.cs: Managed wrapper class - Program.cs: Main entry point with argument parsing Added full command-line argument support matching native PanelPlayer: - All options supported: -p, -w, -h, -b, -m, -r, -e, -d, -v - Multiple source file support - Parameter validation with helpful error messages - Verbose mode for debugging - Help text similar to native version Changed from single-target (net9.0) to multi-target (net8.0;net9.0) to support broader range of .NET installations. - Updated TargetFramework to TargetFrameworks with both net8.0 and net9.0 - Updated README to reflect .NET 8.0 compatibility - Build now produces binaries for both runtimes This allows the example to run on systems with .NET 8.0 LTS or .NET 9.0. Updated README with comprehensive usage examples and new project structure. --- examples/csharp/PanelPlayer.cs | 122 ++++++++++ examples/csharp/PanelPlayerExample.cs | 229 ------------------ examples/csharp/PanelPlayerExample.csproj | 30 +-- examples/csharp/PanelPlayerNative.cs | 69 ++++++ examples/csharp/Program.cs | 269 ++++++++++++++++++++++ examples/csharp/README.md | 249 +++++++++++--------- 6 files changed, 610 insertions(+), 358 deletions(-) create mode 100644 examples/csharp/PanelPlayer.cs delete mode 100644 examples/csharp/PanelPlayerExample.cs create mode 100644 examples/csharp/PanelPlayerNative.cs create mode 100644 examples/csharp/Program.cs diff --git a/examples/csharp/PanelPlayer.cs b/examples/csharp/PanelPlayer.cs new file mode 100644 index 0000000..cd6383c --- /dev/null +++ b/examples/csharp/PanelPlayer.cs @@ -0,0 +1,122 @@ +using System; +using System.IO; + +namespace PanelPlayerExample +{ + /// + /// Managed wrapper for PanelPlayer native library + /// + public class PanelPlayer : IDisposable + { + private bool disposed = false; + + public PanelPlayer(string interfaceName, int width, int height, int brightness = 255) + { + int result = PanelPlayerNative.panelplayer_init(interfaceName, width, height, brightness); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to initialize PanelPlayer: {result}"); + } + } + + public void SetMix(int mixPercentage) + { + int result = PanelPlayerNative.panelplayer_set_mix(mixPercentage); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set mix: {result}"); + } + } + + public void SetFrameRate(int frameRate) + { + int result = PanelPlayerNative.panelplayer_set_rate(frameRate); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set frame rate: {result}"); + } + } + + public void SetDuplicate(bool enable) + { + int result = PanelPlayerNative.panelplayer_set_duplicate(enable); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set duplicate mode: {result}"); + } + } + + public void LoadExtension(string extensionPath) + { + int result = PanelPlayerNative.panelplayer_load_extension(extensionPath); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to load extension: {result}"); + } + } + + public void PlayFile(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"Image file not found: {filePath}"); + } + + int result = PanelPlayerNative.panelplayer_play_file(filePath); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to play image file: {result}"); + } + } + + public void PlayFrameBGR(byte[] bgrData, int width, int height) + { + if (bgrData == null) + { + throw new ArgumentNullException(nameof(bgrData)); + } + + if (bgrData.Length != width * height * 3) + { + throw new ArgumentException("BGR data length doesn't match width * height * 3"); + } + + int result = PanelPlayerNative.panelplayer_play_frame_bgr(bgrData, width, height); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to play frame: {result}"); + } + } + + public void SetBrightness(byte red, byte green, byte blue) + { + int result = PanelPlayerNative.panelplayer_send_brightness(red, green, blue); + if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) + { + throw new Exception($"Failed to set brightness: {result}"); + } + } + + public bool IsInitialized => PanelPlayerNative.panelplayer_is_initialized(); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + PanelPlayerNative.panelplayer_cleanup(); + disposed = true; + } + } + + ~PanelPlayer() + { + Dispose(false); + } + } +} diff --git a/examples/csharp/PanelPlayerExample.cs b/examples/csharp/PanelPlayerExample.cs deleted file mode 100644 index 4abd99e..0000000 --- a/examples/csharp/PanelPlayerExample.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.IO; - -namespace PanelPlayerExample -{ - public static class PanelPlayerNative - { - private const string LibraryName = "libpanelplayer.so"; - - public const int PANELPLAYER_SUCCESS = 0; - public const int PANELPLAYER_ERROR = -1; - public const int PANELPLAYER_INVALID_PARAM = -2; - public const int PANELPLAYER_NOT_INITIALIZED = -3; - public const int PANELPLAYER_ALREADY_INITIALIZED = -4; - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_init( - [MarshalAs(UnmanagedType.LPStr)] string interface_name, - int width, - int height, - int brightness - ); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_set_mix(int mix_percentage); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_set_rate(int frame_rate); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_set_duplicate( - [MarshalAs(UnmanagedType.I1)] bool enable - ); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_load_extension( - [MarshalAs(UnmanagedType.LPStr)] string extension_path - ); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_play_file( - [MarshalAs(UnmanagedType.LPStr)] string file_path - ); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_play_frame_bgr( - [MarshalAs(UnmanagedType.LPArray)] byte[] bgr_data, - int width, - int height - ); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern int panelplayer_send_brightness( - byte red, - byte green, - byte blue - ); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - [return: MarshalAs(UnmanagedType.I1)] - public static extern bool panelplayer_is_initialized(); - - [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] - public static extern void panelplayer_cleanup(); - } - - public class PanelPlayer : IDisposable - { - private bool disposed = false; - - public PanelPlayer(string interfaceName, int width, int height, int brightness = 255) - { - int result = PanelPlayerNative.panelplayer_init(interfaceName, width, height, brightness); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to initialize PanelPlayer: {result}"); - } - } - - public void SetMix(int mixPercentage) - { - int result = PanelPlayerNative.panelplayer_set_mix(mixPercentage); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to set mix: {result}"); - } - } - - public void SetFrameRate(int frameRate) - { - int result = PanelPlayerNative.panelplayer_set_rate(frameRate); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to set frame rate: {result}"); - } - } - - public void SetDuplicate(bool enable) - { - int result = PanelPlayerNative.panelplayer_set_duplicate(enable); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to set duplicate mode: {result}"); - } - } - - public void LoadExtension(string extensionPath) - { - int result = PanelPlayerNative.panelplayer_load_extension(extensionPath); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to load extension: {result}"); - } - } - - public void PlayFile(string filePath) - { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"Image file not found: {filePath}"); - } - - int result = PanelPlayerNative.panelplayer_play_file(filePath); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to play image file: {result}"); - } - } - - public void PlayFrameBGR(byte[] bgrData, int width, int height) - { - if (bgrData == null) - { - throw new ArgumentNullException(nameof(bgrData)); - } - - if (bgrData.Length != width * height * 3) - { - throw new ArgumentException("BGR data length doesn't match width * height * 3"); - } - - int result = PanelPlayerNative.panelplayer_play_frame_bgr(bgrData, width, height); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to play frame: {result}"); - } - } - - public void SetBrightness(byte red, byte green, byte blue) - { - int result = PanelPlayerNative.panelplayer_send_brightness(red, green, blue); - if (result != PanelPlayerNative.PANELPLAYER_SUCCESS) - { - throw new Exception($"Failed to set brightness: {result}"); - } - } - - public bool IsInitialized => PanelPlayerNative.panelplayer_is_initialized(); - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposed) - { - PanelPlayerNative.panelplayer_cleanup(); - disposed = true; - } - } - - ~PanelPlayer() - { - Dispose(false); - } - } - - class Program - { - static void Main(string[] args) - { - - try - { - string interfaceName = "end0"; // Network interface (usually eth0) - int width = 192; - int height = 64; - int brightness = 255; - // Basic use example (Ejemplo básico de uso) - using (var player = new PanelPlayer(interfaceName, width, height, brightness)) - { - Console.WriteLine("PanelPlayer initialized successfully!"); - - // Play image file (Supports WebP, JPEG, PNG, GIF, BMP) - if (args.Length > 0) - { - player.PlayFile(args[0]); - Console.WriteLine($"Played image file: {args[0]}"); - System.Threading.Thread.Sleep(10000); // Wait for 10 seconds - } - - // Manualy send blue frame (Ejemplo de frame manual (cuadrado azul)) - byte[] blueFrame = new byte[width * height * 3]; - for (int i = 0; i < blueFrame.Length; i += 3) - { - blueFrame[i] = 255; // Blue - blueFrame[i + 1] = 0; // Green - blueFrame[i + 2] = 0; // Red - } - - player.PlayFrameBGR(blueFrame, width, height); - Console.WriteLine("Displayed blue frame"); - - // Wait before exit. (Esperar un poco antes de salir) - System.Threading.Thread.Sleep(2000); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error: {ex.Message}"); - Console.WriteLine("Note: This example requires root privileges and a valid ethernet interface."); - } - } - } -} \ No newline at end of file diff --git a/examples/csharp/PanelPlayerExample.csproj b/examples/csharp/PanelPlayerExample.csproj index 9a3cc67..a6fa901 100644 --- a/examples/csharp/PanelPlayerExample.csproj +++ b/examples/csharp/PanelPlayerExample.csproj @@ -1,16 +1,16 @@ - - - - Exe - net9.0 - enable - true - - - - - PreserveNewest - - - + + + + Exe + net8.0;net9.0 + enable + true + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/examples/csharp/PanelPlayerNative.cs b/examples/csharp/PanelPlayerNative.cs new file mode 100644 index 0000000..7226c12 --- /dev/null +++ b/examples/csharp/PanelPlayerNative.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.InteropServices; + +namespace PanelPlayerExample +{ + /// + /// Native P/Invoke declarations for libpanelplayer.so + /// + public static class PanelPlayerNative + { + private const string LibraryName = "libpanelplayer.so"; + + public const int PANELPLAYER_SUCCESS = 0; + public const int PANELPLAYER_ERROR = -1; + public const int PANELPLAYER_INVALID_PARAM = -2; + public const int PANELPLAYER_NOT_INITIALIZED = -3; + public const int PANELPLAYER_ALREADY_INITIALIZED = -4; + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_init( + [MarshalAs(UnmanagedType.LPStr)] string interface_name, + int width, + int height, + int brightness + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_set_mix(int mix_percentage); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_set_rate(int frame_rate); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_set_duplicate( + [MarshalAs(UnmanagedType.I1)] bool enable + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_load_extension( + [MarshalAs(UnmanagedType.LPStr)] string extension_path + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_play_file( + [MarshalAs(UnmanagedType.LPStr)] string file_path + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_play_frame_bgr( + [MarshalAs(UnmanagedType.LPArray)] byte[] bgr_data, + int width, + int height + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern int panelplayer_send_brightness( + byte red, + byte green, + byte blue + ); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool panelplayer_is_initialized(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + public static extern void panelplayer_cleanup(); + } +} diff --git a/examples/csharp/Program.cs b/examples/csharp/Program.cs new file mode 100644 index 0000000..f5b0e30 --- /dev/null +++ b/examples/csharp/Program.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace PanelPlayerExample +{ + class Program + { + static void ShowUsage() + { + Console.WriteLine("Usage:"); + Console.WriteLine(" PanelPlayerExample -p -w -h [options] "); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine(" -p Set ethernet port (required)"); + Console.WriteLine(" -w Set display width (required)"); + Console.WriteLine(" -h Set display height (required)"); + Console.WriteLine(" -b Set display brightness (0-255, default: 255)"); + Console.WriteLine(" -m Set frame mixing percentage (0-99, default: 0)"); + Console.WriteLine(" -r Override source frame rate"); + Console.WriteLine(" -e Load extension from file"); + Console.WriteLine(" -d Duplicate each row vertically"); + Console.WriteLine(" -v Enable verbose output"); + Console.WriteLine(); + Console.WriteLine("Sources:"); + Console.WriteLine(" One or more image/animation files (WebP, JPEG, PNG, GIF, BMP)"); + } + + static int Main(string[] args) + { + try + { + // Parse command-line arguments + string port = null; + int width = 0; + int height = 0; + int brightness = 255; + int mix = 0; + int rate = 0; + string extension = null; + bool duplicate = false; + bool verbose = false; + List sources = new List(); + + for (int i = 0; i < args.Length; i++) + { + if (args[i].StartsWith("-")) + { + switch (args[i]) + { + case "-p": + if (++i >= args.Length) + { + Console.WriteLine("Error: -p requires a value"); + ShowUsage(); + return 1; + } + port = args[i]; + break; + + case "-w": + if (++i >= args.Length || !int.TryParse(args[i], out width)) + { + Console.WriteLine("Error: -w requires an integer value"); + ShowUsage(); + return 1; + } + break; + + case "-h": + if (++i >= args.Length || !int.TryParse(args[i], out height)) + { + Console.WriteLine("Error: -h requires an integer value"); + ShowUsage(); + return 1; + } + break; + + case "-b": + if (++i >= args.Length || !int.TryParse(args[i], out brightness)) + { + Console.WriteLine("Error: -b requires an integer value"); + ShowUsage(); + return 1; + } + break; + + case "-m": + if (++i >= args.Length || !int.TryParse(args[i], out mix)) + { + Console.WriteLine("Error: -m requires an integer value"); + ShowUsage(); + return 1; + } + break; + + case "-r": + if (++i >= args.Length || !int.TryParse(args[i], out rate)) + { + Console.WriteLine("Error: -r requires an integer value"); + ShowUsage(); + return 1; + } + break; + + case "-e": + if (++i >= args.Length) + { + Console.WriteLine("Error: -e requires a value"); + ShowUsage(); + return 1; + } + extension = args[i]; + break; + + case "-d": + duplicate = true; + break; + + case "-v": + verbose = true; + break; + + default: + Console.WriteLine($"Error: Unknown option {args[i]}"); + ShowUsage(); + return 1; + } + } + else + { + sources.Add(args[i]); + } + } + + // Validate required parameters + if (port == null) + { + Console.WriteLine("Error: Port must be specified!"); + ShowUsage(); + return 1; + } + + if (width < 1 || height < 1) + { + Console.WriteLine("Error: Width and height must be specified as positive integers!"); + ShowUsage(); + return 1; + } + + if (brightness < 0 || brightness > 255) + { + Console.WriteLine("Error: Brightness must be an integer between 0 and 255!"); + ShowUsage(); + return 1; + } + + if (mix < 0 || mix >= 100) + { + Console.WriteLine("Error: Mix must be an integer between 0 and 99!"); + ShowUsage(); + return 1; + } + + if (sources.Count == 0) + { + Console.WriteLine("Error: At least one source must be specified!"); + ShowUsage(); + return 1; + } + + // Verify source files exist + foreach (var source in sources) + { + if (!File.Exists(source)) + { + Console.WriteLine($"Error: Source file not found: {source}"); + return 1; + } + } + + if (verbose) + { + Console.WriteLine($"Initializing PanelPlayer:"); + Console.WriteLine($" Port: {port}"); + Console.WriteLine($" Dimensions: {width}x{height}"); + Console.WriteLine($" Brightness: {brightness}"); + if (mix > 0) + Console.WriteLine($" Mix: {mix}%"); + if (rate > 0) + Console.WriteLine($" Frame Rate: {rate} fps"); + if (duplicate) + Console.WriteLine($" Duplicate: enabled"); + if (extension != null) + Console.WriteLine($" Extension: {extension}"); + Console.WriteLine($" Sources: {string.Join(", ", sources)}"); + Console.WriteLine(); + } + + // Initialize PanelPlayer + using (var player = new PanelPlayer(port, width, height, brightness)) + { + if (verbose) + Console.WriteLine("PanelPlayer initialized successfully!"); + + // Apply settings + if (mix > 0) + { + player.SetMix(mix); + if (verbose) + Console.WriteLine($"Frame mixing set to {mix}%"); + } + + if (rate > 0) + { + player.SetFrameRate(rate); + if (verbose) + Console.WriteLine($"Frame rate override set to {rate} fps"); + } + + if (duplicate) + { + player.SetDuplicate(true); + if (verbose) + Console.WriteLine("Duplicate mode enabled"); + } + + if (extension != null) + { + if (!File.Exists(extension)) + { + Console.WriteLine($"Error: Extension file not found: {extension}"); + return 1; + } + player.LoadExtension(extension); + if (verbose) + Console.WriteLine($"Extension loaded: {extension}"); + } + + // Play all source files + foreach (var source in sources) + { + if (verbose) + Console.WriteLine($"Playing: {source}"); + + player.PlayFile(source); + + if (verbose) + Console.WriteLine($"Finished: {source}"); + } + + if (verbose) + Console.WriteLine("Playback completed successfully!"); + } + + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine(); + Console.WriteLine("Note: This application requires root privileges and a valid ethernet interface."); + Console.WriteLine(" Run with sudo and ensure the network interface is correct."); + return 1; + } + } + } +} diff --git a/examples/csharp/README.md b/examples/csharp/README.md index 87278e8..12a49f1 100644 --- a/examples/csharp/README.md +++ b/examples/csharp/README.md @@ -1,115 +1,136 @@ -# PanelPlayer C# Example - -This directory contains a C# example application that demonstrates how to use the PanelPlayer library from .NET applications. - -## Prerequisites - -- .NET 9.0 or later -- libpanelplayer.so compiled and available -- Root privileges (required for raw ethernet access) - -## Building - -### 1. Build the PanelPlayer Library - -First, you need to build the shared library from the main project directory: - -```bash -cd /path/to/PanelPlayer -make library -``` - -This will create `build/libpanelplayer.so`. - -### 2. Copy Library to Output Directory - -The C# project is configured to automatically copy the library to the output directory during build, but you can also copy it manually: - -```bash -cp ../../build/libpanelplayer.so ./bin/Debug/net9.0/ -``` - -### 3. Build the C# Application - -```bash -dotnet build -``` - -## Running - -The application must be run as root due to raw ethernet socket requirements: - -```bash -sudo dotnet run [image-file] -``` - -### Examples - -Play an image or animation file (supports WebP, JPEG, PNG, GIF, BMP): -```bash -sudo dotnet run animation.webp -sudo dotnet run image.jpg -sudo dotnet run animation.gif -``` - -Run without arguments to see the blue frame demonstration: -```bash -sudo dotnet run -``` - -## Publish - -Make a self contained app -```bash -dotnet publish -r linux-arm64 --self-contained true -c Release -``` -## Usage - -The example demonstrates: - -1. **Basic initialization** - Setting up the panel with network interface and dimensions -2. **Multi-format image playback** - Playing WebP, JPEG, PNG, GIF, or BMP files (if provided as argument) -3. **Manual frame display** - Sending raw BGR pixel data to create a blue frame -4. **Proper cleanup** - Using `using` statement for automatic resource disposal - -## Configuration - -The example is configured for: -- Network interface: `end0` (eth0 is more common) -- Panel dimensions: 192x64 pixels -- Brightness: 255 (maximum) - -Modify these values in `PanelPlayerExample.cs` to match your setup. - -## Tested Environment - -This example has been tested on: -- **Hardware**: Orange Pi Zero 3 -- **OS**: Debian Bookworm -- **Runtime**: .NET 9.0.302 - -## Troubleshooting - -### Permission Errors -Make sure you're running with `sudo` - raw ethernet access requires root privileges. - -### Library Not Found -Ensure `libpanelplayer.so` is in the output directory or in your system's library path. - -### Network Interface Issues -Verify that the network interface name (`end0`) matches your system's ethernet interface. - -## API Reference - -The C# wrapper provides these main methods: - -- `PanelPlayer(interface, width, height, brightness)` - Initialize the panel -- `PlayFile(path)` - Play image/animation file (WebP, JPEG, PNG, GIF, BMP) -- `PlayFrameBGR(data, width, height)` - Display raw BGR frame data -- `SetBrightness(red, green, blue)` - Adjust color balance -- `LoadExtension(path)` - Load processing extensions -- `SetMix(percentage)` - Control frame blending -- `SetFrameRate(fps)` - Override animation timing -- `SetDuplicate(enable)` - Enable vertical duplication for stacked displays - +# PanelPlayer C# Example + +This directory contains a C# example application that demonstrates how to use the PanelPlayer library from .NET applications. + +## Prerequisites + +- .NET 8.0 or later (supports both .NET 8.0 and 9.0) +- libpanelplayer.so compiled and available +- Root privileges (required for raw ethernet access) + +## Building + +### 1. Build the PanelPlayer Library + +First, you need to build the shared library from the main project directory: + +```bash +cd /path/to/PanelPlayer +make library +``` + +This will create `build/libpanelplayer.so`. + +### 2. Copy Library to Output Directory + +The C# project is configured to automatically copy the library to the output directory during build, but you can also copy it manually: + +```bash +cp ../../build/libpanelplayer.so ./bin/Debug/net9.0/ +``` + +### 3. Build the C# Application + +```bash +dotnet build +``` + +## Running + +The application must be run as root due to raw ethernet socket requirements. It uses command-line arguments similar to the native PanelPlayer: + +```bash +sudo dotnet run -- -p -w -h [options] +``` + +### Options + +- `-p ` - Set ethernet port (required, e.g., eth0, end0) +- `-w ` - Set display width in pixels (required) +- `-h ` - Set display height in pixels (required) +- `-b ` - Set display brightness (0-255, default: 255) +- `-m ` - Set frame mixing percentage (0-99, default: 0) +- `-r ` - Override source frame rate +- `-e ` - Load extension from file +- `-d` - Duplicate each row vertically +- `-v` - Enable verbose output + +### Examples + +Play a single image file: +```bash +sudo dotnet run -- -p eth0 -w 192 -h 64 image.jpg +``` + +Play multiple animation files with verbose output: +```bash +sudo dotnet run -- -p eth0 -w 192 -h 64 -v animation1.webp animation2.gif +``` + +Play with frame mixing and custom brightness: +```bash +sudo dotnet run -- -p eth0 -w 192 -h 64 -b 200 -m 50 video.webp +``` + +Play with duplicate mode (for stacked displays): +```bash +sudo dotnet run -- -p eth0 -w 192 -h 64 -d content.png +``` + +Load an extension: +```bash +sudo dotnet run -- -p eth0 -w 192 -h 64 -e ../../extensions/grayscale/extension.so image.jpg +``` + +## Publish + +Create a self-contained application: +```bash +dotnet publish -r linux-arm64 --self-contained true -c Release +``` + +After publishing, the executable will be in `bin/Release/net8.0/linux-arm64/publish/`: +```bash +sudo ./bin/Release/net8.0/linux-arm64/publish/PanelPlayerExample -p eth0 -w 192 -h 64 image.jpg +``` + +Note: The build creates binaries for both .NET 8.0 and 9.0. Use the appropriate version based on your runtime. + +## Project Structure + +- `Program.cs` - Main entry point with command-line argument parsing +- `PanelPlayer.cs` - Managed wrapper class for the native library +- `PanelPlayerNative.cs` - P/Invoke declarations for libpanelplayer.so + +## Tested Environment + +This example has been tested on: +- **Hardware**: Orange Pi Zero 3 +- **OS**: Debian Bookworm +- **Runtime**: .NET 8.0 and .NET 9.0 + +## Troubleshooting + +### Permission Errors +Make sure you're running with `sudo` - raw ethernet access requires root privileges. + +### Library Not Found +Ensure `libpanelplayer.so` is in the output directory or in your system's library path. + +### Network Interface Issues +Verify that the network interface name (`end0`) matches your system's ethernet interface. + +## API Reference + +The C# wrapper provides these main methods: + +- `PanelPlayer(interface, width, height, brightness)` - Initialize the panel +- `PlayFile(path)` - Play image/animation file (WebP, JPEG, PNG, GIF, BMP) +- `PlayFrameBGR(data, width, height)` - Display raw BGR frame data +- `SetBrightness(red, green, blue)` - Adjust color balance +- `LoadExtension(path)` - Load processing extensions +- `SetMix(percentage)` - Control frame blending +- `SetFrameRate(fps)` - Override animation timing +- `SetDuplicate(enable)` - Enable vertical duplication for stacked displays + For detailed API documentation, see the native library header file `source/panelplayer_api.h`. \ No newline at end of file From 9d3c1209847d354091ce6140cad2866cd98f7082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20M=C3=A1rquez?= Date: Fri, 3 Oct 2025 14:50:15 +0200 Subject: [PATCH 08/10] Extract playback loop to core_play_decoded_file function Moved the common playback loop from main.c and panelplayer_api.c into a shared function in core.c to eliminate code duplication. Changes: - Added core_play_decoded_file() function to core.c/core.h - Consolidates frame decoding, processing, and timing logic - Both main.c and panelplayer_api.c now use the shared function - Eliminated ~30 lines of duplicated playback loop code - Maintains separation: main.c handles CLI/loader, API handles library interface Benefits: - Single source of truth for playback timing logic - Easier to maintain and test - Consistent behavior between CLI and API - main.c retains its advanced features (loader, shuffle, verbose) - panelplayer_api.c remains simple and clean --- source/core.c | 55 ++++++++++++++++++++++++++++++++++++++++ source/core.h | 19 ++++++++++++++ source/main.c | 44 ++++---------------------------- source/panelplayer_api.c | 44 ++++---------------------------- 4 files changed, 84 insertions(+), 78 deletions(-) diff --git a/source/core.c b/source/core.c index 26b98da..50a63f5 100644 --- a/source/core.c +++ b/source/core.c @@ -120,3 +120,58 @@ void core_unload_extension(void *extension) dlclose(extension); } } + +int core_play_decoded_file(decoder *dec, colorlight *cl, uint8_t *buffer, + int width, int height, int brightness, int mix, + int rate, bool duplicate, void (*update_func)(int, int, uint8_t*)) +{ + if (dec == NULL || cl == NULL || buffer == NULL) + { + return -1; + } + + decoder_info info; + decoder_get_info(dec, &info); + + if (info.canvas_width < width || info.canvas_height < height) + { + return -1; + } + + long next = core_get_time(); + int previous = 0; + bool initial = true; + + while (decoder_has_more_frames(dec)) + { + uint8_t *decoded; + int timestamp; + + decoder_get_next(dec, &decoded, ×tamp); + + core_process_frame(buffer, decoded, &info, width, height, mix, initial); + core_send_frame(cl, buffer, width, height, duplicate, update_func); + + if (next - core_get_time() < CORE_UPDATE_DELAY) + { + next = core_get_time() + CORE_UPDATE_DELAY; + } + + core_await(next); + colorlight_send_update(cl, brightness, brightness, brightness); + + if (rate > 0) + { + next = core_get_time() + 1000 / rate; + } + else + { + next = core_get_time() + timestamp - previous; + previous = timestamp; + } + + initial = false; + } + + return 0; +} diff --git a/source/core.h b/source/core.h index a523c0e..a4a9962 100644 --- a/source/core.h +++ b/source/core.h @@ -64,4 +64,23 @@ int core_load_extension(const char *path, void **extension, void (**update_func) */ void core_unload_extension(void *extension); +/** + * Play a decoded file through the complete playback loop + * + * @param dec Decoder instance with the loaded file + * @param cl Colorlight instance for hardware output + * @param buffer Frame buffer (width * height * 3 bytes) + * @param width Display width + * @param height Display height + * @param brightness Brightness level (0-255) + * @param mix Frame mixing percentage (0-99) + * @param rate Frame rate override (0 = use source timing) + * @param duplicate Enable vertical duplication mode + * @param update_func Optional extension update function (can be NULL) + * @return 0 on success, -1 on error + */ +int core_play_decoded_file(decoder *dec, colorlight *cl, uint8_t *buffer, + int width, int height, int brightness, int mix, + int rate, bool duplicate, void (*update_func)(int, int, uint8_t*)); + #endif diff --git a/source/main.c b/source/main.c index daa5b2e..b6c736b 100644 --- a/source/main.c +++ b/source/main.c @@ -192,8 +192,6 @@ int main(int argc, char *argv[]) } int queued = 0; - long next = core_get_time(); - bool initial = true; for (int source = 0; shuffle || source < sourcesLength; source++) { @@ -235,49 +233,18 @@ int main(int argc, char *argv[]) printf("Decoding %d frames at a resolution of %dx%d.\n", info.frame_count, info.canvas_width, info.canvas_height); } - if (info.canvas_width < width || info.canvas_height < height) + long start = core_get_time(); + + if (core_play_decoded_file(decoder, colorlight, buffer, width, height, brightness, mix, rate, duplicate, update) != 0) { puts("Image is smaller than display!"); goto delete_decoder; } - int previous = 0; - long start = next; - - while (decoder_has_more_frames(decoder)) - { - uint8_t *decoded; - int timestamp; - - decoder_get_next(decoder, &decoded, ×tamp); - - core_process_frame(buffer, decoded, &info, width, height, mix, initial); - core_send_frame(colorlight, buffer, width, height, duplicate, update); - - if (next - core_get_time() < CORE_UPDATE_DELAY) - { - next = core_get_time() + CORE_UPDATE_DELAY; - } - - core_await(next); - colorlight_send_update(colorlight, brightness, brightness, brightness); - - if (rate > 0) - { - next = core_get_time() + 1000 / rate; - } - else - { - next = core_get_time() + timestamp - previous; - previous = timestamp; - } - - initial = false; - } - if (verbose) { - float seconds = (next - start) / 1000.0; + long end = core_get_time(); + float seconds = (end - start) / 1000.0; printf("Played %d frames in %.2f seconds at an average rate of %.2f frames per second.\n", info.frame_count, seconds, info.frame_count / seconds); } @@ -288,7 +255,6 @@ int main(int argc, char *argv[]) free(file); } - core_await(next); status = EXIT_SUCCESS; core_unload_extension(extension); diff --git a/source/panelplayer_api.c b/source/panelplayer_api.c index 97a578c..c3084fd 100644 --- a/source/panelplayer_api.c +++ b/source/panelplayer_api.c @@ -156,49 +156,15 @@ int panelplayer_play_file(const char* file_path) { return PANELPLAYER_ERROR; } - decoder_info info; - decoder_get_info(dec, &info); - - if (info.canvas_width < g_state.width || info.canvas_height < g_state.height) { - decoder_destroy(dec); - free(file_data); - return PANELPLAYER_ERROR; - } - - long next = core_get_time(); - int previous = 0; - bool initial = true; - - while (decoder_has_more_frames(dec)) { - uint8_t *decoded; - int timestamp; - - decoder_get_next(dec, &decoded, ×tamp); - - core_process_frame(g_state.buffer, decoded, &info, g_state.width, g_state.height, g_state.mix, initial); - core_send_frame(g_state.colorlight, g_state.buffer, g_state.width, g_state.height, g_state.duplicate, g_state.update_func); - - if (next - core_get_time() < CORE_UPDATE_DELAY) { - next = core_get_time() + CORE_UPDATE_DELAY; - } - - core_await(next); - colorlight_send_update(g_state.colorlight, g_state.brightness, g_state.brightness, g_state.brightness); - - if (g_state.rate > 0) { - next = core_get_time() + 1000 / g_state.rate; - } else { - next = core_get_time() + timestamp - previous; - previous = timestamp; - } - - initial = false; - } + int result = core_play_decoded_file(dec, g_state.colorlight, g_state.buffer, + g_state.width, g_state.height, g_state.brightness, + g_state.mix, g_state.rate, g_state.duplicate, + g_state.update_func); decoder_destroy(dec); free(file_data); - return PANELPLAYER_SUCCESS; + return (result == 0) ? PANELPLAYER_SUCCESS : PANELPLAYER_ERROR; } int panelplayer_play_frame_bgr(const uint8_t* bgr_data, int width, int height) { From 803c308a4e2a9611c360b2f943f9dbe297ffdea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20M=C3=A1rquez?= Date: Fri, 3 Oct 2025 15:59:52 +0200 Subject: [PATCH 09/10] Add make install and uninstall targets Implemented standard installation system following Linux/Unix conventions. Changes to makefile: - Added installation directory variables (PREFIX, BINDIR, LIBDIR, INCLUDEDIR) - Implemented 'make install' target to install system-wide - Implemented 'make uninstall' target to remove installation - Support for DESTDIR for package building - Support for custom PREFIX for alternative install locations Installation installs: - Executable: /usr/local/bin/panelplayer - Library: /usr/local/lib/libpanelplayer.so - Header: /usr/local/include/panelplayer/panelplayer_api.h Updated README.md with: - Installation instructions - Usage after installation - Custom prefix examples - Library usage examples Benefits: - Makes panelplayer available system-wide - Follows standard Linux conventions - Easier for users to integrate the library - Enables package creation (deb, rpm, etc) - Professional deployment workflow --- makefile | 95 ++++++++++++++++++++++++++++++++++++------------------- readme.md | 49 +++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 34 deletions(-) diff --git a/makefile b/makefile index 4322e79..1ae9d88 100644 --- a/makefile +++ b/makefile @@ -1,34 +1,63 @@ -CC = gcc -CFLAGS = -Wall -Werror -pthread -O3 -LDFLAGS = -pthread -LDLIBS = -ldl -lwebpdemux -ljpeg -lpng -lgif - -SOURCE = ./source -BUILD = ./build -TARGET = $(BUILD)/panelplayer -LIBRARY = $(BUILD)/libpanelplayer.so - -HEADERS = $(wildcard $(SOURCE)/*.h) -MAIN_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/panelplayer_api.c,$(wildcard $(SOURCE)/*.c))) -API_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/main.c,$(wildcard $(SOURCE)/*.c))) - -.PHONY: clean all library - -all: $(TARGET) $(LIBRARY) - -$(TARGET): $(BUILD) $(MAIN_OBJECTS) - $(CC) $(LDFLAGS) $(MAIN_OBJECTS) $(LDLIBS) -o $@ - -$(LIBRARY): $(BUILD) $(API_OBJECTS) - $(CC) $(LDFLAGS) -shared -fPIC $(API_OBJECTS) $(LDLIBS) -o $@ - -library: $(LIBRARY) - -$(BUILD): - mkdir $(BUILD) - -$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile - $(CC) $(CFLAGS) -fPIC -c $< -o $@ - -clean: +CC = gcc +CFLAGS = -Wall -Werror -pthread -O3 +LDFLAGS = -pthread +LDLIBS = -ldl -lwebpdemux -ljpeg -lpng -lgif + +SOURCE = ./source +BUILD = ./build +TARGET = $(BUILD)/panelplayer +LIBRARY = $(BUILD)/libpanelplayer.so + +# Installation directories +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include + +HEADERS = $(wildcard $(SOURCE)/*.h) +MAIN_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/panelplayer_api.c,$(wildcard $(SOURCE)/*.c))) +API_OBJECTS = $(patsubst $(SOURCE)/%.c,$(BUILD)/%.o,$(filter-out $(SOURCE)/main.c,$(wildcard $(SOURCE)/*.c))) + +.PHONY: clean all library install uninstall + +all: $(TARGET) $(LIBRARY) + +$(TARGET): $(BUILD) $(MAIN_OBJECTS) + $(CC) $(LDFLAGS) $(MAIN_OBJECTS) $(LDLIBS) -o $@ + +$(LIBRARY): $(BUILD) $(API_OBJECTS) + $(CC) $(LDFLAGS) -shared -fPIC $(API_OBJECTS) $(LDLIBS) -o $@ + +library: $(LIBRARY) + +$(BUILD): + mkdir $(BUILD) + +$(BUILD)/%.o: $(SOURCE)/%.c $(HEADERS) makefile + $(CC) $(CFLAGS) -fPIC -c $< -o $@ + +install: $(TARGET) $(LIBRARY) + @echo "Installing PanelPlayer to $(PREFIX)..." + install -d $(DESTDIR)$(BINDIR) + install -m 755 $(TARGET) $(DESTDIR)$(BINDIR)/ + install -d $(DESTDIR)$(LIBDIR) + install -m 644 $(LIBRARY) $(DESTDIR)$(LIBDIR)/ + install -d $(DESTDIR)$(INCLUDEDIR)/panelplayer + install -m 644 $(SOURCE)/panelplayer_api.h $(DESTDIR)$(INCLUDEDIR)/panelplayer/ + @echo "Installation complete!" + @echo "" + @echo "Executable installed to: $(BINDIR)/panelplayer" + @echo "Library installed to: $(LIBDIR)/libpanelplayer.so" + @echo "Header installed to: $(INCLUDEDIR)/panelplayer/panelplayer_api.h" + @echo "" + @echo "You may need to run 'sudo ldconfig' to update the library cache." + +uninstall: + @echo "Uninstalling PanelPlayer from $(PREFIX)..." + rm -f $(DESTDIR)$(BINDIR)/panelplayer + rm -f $(DESTDIR)$(LIBDIR)/libpanelplayer.so + rm -rf $(DESTDIR)$(INCLUDEDIR)/panelplayer + @echo "Uninstall complete!" + +clean: rm -r $(BUILD) \ No newline at end of file diff --git a/readme.md b/readme.md index ef62ceb..715c4b8 100644 --- a/readme.md +++ b/readme.md @@ -37,7 +37,54 @@ Install the required development libraries: sudo apt install libwebp-dev libjpeg-dev libpng-dev libgif-dev ``` -PanelPlayer can be built by running `make` from within the root directory. +Build PanelPlayer by running `make` from the root directory: +```bash +make # Build both executable and library +make library # Build only the shared library +``` + +### Installation +To install PanelPlayer system-wide (requires root privileges): +```bash +sudo make install +``` + +This will install: +- Executable: `/usr/local/bin/panelplayer` +- Library: `/usr/local/lib/libpanelplayer.so` +- Header: `/usr/local/include/panelplayer/panelplayer_api.h` + +After installation, you may need to update the library cache: +```bash +sudo ldconfig +``` + +To uninstall: +```bash +sudo make uninstall +``` + +### Custom Installation Prefix +You can install to a different location using the `PREFIX` variable: +```bash +make install PREFIX=/opt/panelplayer +``` + +## Usage After Installation +Once installed, the `panelplayer` command is available system-wide: +```bash +panelplayer -p eth0 -w 128 -h 64 animation.webp +``` + +For library usage in your C projects: +```c +#include +``` + +Compile with: +```bash +gcc -lpanelplayer myapp.c -o myapp +``` ## Extensions Extensions are a way to read or alter frames without modifying PanelPlayer. A minimal extension consists of an `update` function which gets called before each frame is sent. An extension may also include `init` and `destroy` functions. The `destroy` function will always be called if present, even when the `init` function indicates an error has occurred. Example extensions are located in the `extensions` directory. From 63203ff68992cad716e7bc957ae88aa134a10dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20M=C3=A1rquez?= Date: Fri, 3 Oct 2025 16:07:18 +0200 Subject: [PATCH 10/10] Comprehensively improve README with professional documentation Transformed README from basic documentation to comprehensive project guide. Improvements: - Better structure with clear sections - Visual hierarchy with emojis and formatting - Practical examples for common use cases - Code snippets for library integration - Step-by-step extension development Makes the project more accessible and attractive for: - New users (Quick Start) - Developers (API documentation) - Contributors (Architecture, Extensions) - System integrators (Installation, Troubleshooting) --- readme.md | 313 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 269 insertions(+), 44 deletions(-) diff --git a/readme.md b/readme.md index 715c4b8..78b618a 100644 --- a/readme.md +++ b/readme.md @@ -1,93 +1,318 @@ # PanelPlayer -A media player for Colorlight receiving cards supporting WebP, JPEG, PNG, GIF, and BMP formats. Tested with the Colorlight 5A-75B. + +A high-performance media player for Colorlight LED receiving cards, supporting multiple image and animation formats. Tested with the Colorlight 5A-75B. + +## Features + +- ✅ **Multi-format Support**: WebP, JPEG, PNG, GIF, and BMP +- ✅ **Hardware Acceleration**: Direct ethernet control via raw sockets +- ✅ **Extension System**: Custom frame processing plugins +- ✅ **C API**: Full-featured library for integration +- ✅ **Cross-language Bindings**: C# wrapper included +- ✅ **Frame Mixing**: Smooth transitions between frames +- ✅ **Duplicate Mode**: Drive stacked displays easily + +## Quick Start + +```bash +# Install dependencies +sudo apt install libwebp-dev libjpeg-dev libpng-dev libgif-dev + +# Build and install +make +sudo make install +sudo ldconfig + +# Run +panelplayer -p eth0 -w 128 -h 64 animation.webp +``` ## Usage -PanelPlayer can be launched with `panelplayer ` where `` is one or more image/animation files (WebP, JPEG, PNG, GIF, or BMP). The available options are: -### `-p ` -Sets which ethernet port to use for sending. This option is required. +### Command Line -### `-w ` -Set the display width in pixels. This option is required. +```bash +panelplayer [options] +``` -### `-h ` -Set the display height in pixels. This option is required. +#### Required Options -### `-b ` -Set the display brightness between 0 and 255. A value of 255 will be used if not specified. +| Option | Description | Example | +|--------|-------------|---------| +| `-p ` | Ethernet interface | `-p eth0` | +| `-w ` | Display width in pixels | `-w 128` | +| `-h ` | Display height in pixels | `-h 64` | -### `-m ` -Controls the percentage of the previous frame to be blended with the current frame. Frame blending is disabled when set to 0 or not specified. +#### Optional Settings -### `-r ` -Overrides the source frame rate if specified. +| Option | Description | Default | +|--------|-------------|---------| +| `-b ` | Brightness (0-255) | 255 | +| `-m ` | Frame mixing percentage (0-99) | 0 | +| `-r ` | Override frame rate (fps) | Auto | +| `-e ` | Load extension | None | +| `-d` | Duplicate mode (for stacked displays) | Disabled | +| `-s` | Shuffle/loop playback | Disabled | +| `-v` | Verbose output | Disabled | -### `-e ` -Load an extension from the path given. Only a single extension can be loaded. +### Examples + +**Play a single image:** +```bash +panelplayer -p eth0 -w 192 -h 64 photo.jpg +``` + +**Play animated WebP with verbose output:** +```bash +panelplayer -p eth0 -w 128 -h 64 -v animation.webp +``` + +**Play multiple files with frame blending:** +```bash +panelplayer -p eth0 -w 192 -h 64 -m 50 video1.webp video2.gif video3.png +``` + +**Loop with shuffle mode:** +```bash +panelplayer -p eth0 -w 128 -h 64 -s *.webp +``` -### `-s` -Play sources randomly instead of in a fixed order. If used with a single source, this option will loop playback. +**Use extension for grayscale effect:** +```bash +panelplayer -p eth0 -w 128 -h 64 -e extensions/grayscale/extension.so image.jpg +``` -### `-v` -Enable verbose output. +**Duplicate mode for two stacked 128x64 panels (total 128x128):** +```bash +panelplayer -p eth0 -w 128 -h 64 -d content.png +``` ## Building -Install the required development libraries: + +### Prerequisites + +Install development libraries: ```bash +# Debian/Ubuntu sudo apt install libwebp-dev libjpeg-dev libpng-dev libgif-dev + +# Fedora/RHEL +sudo dnf install libwebp-devel libjpeg-devel libpng-devel giflib-devel ``` -Build PanelPlayer by running `make` from the root directory: +### Compile + ```bash make # Build both executable and library make library # Build only the shared library +make clean # Clean build artifacts ``` ### Installation -To install PanelPlayer system-wide (requires root privileges): -```bash -sudo make install -``` - -This will install: -- Executable: `/usr/local/bin/panelplayer` -- Library: `/usr/local/lib/libpanelplayer.so` -- Header: `/usr/local/include/panelplayer/panelplayer_api.h` -After installation, you may need to update the library cache: +Install system-wide (requires root): ```bash +sudo make install sudo ldconfig ``` -To uninstall: +This installs: +- **Executable**: `/usr/local/bin/panelplayer` +- **Library**: `/usr/local/lib/libpanelplayer.so` +- **Header**: `/usr/local/include/panelplayer/panelplayer_api.h` + +Uninstall: ```bash sudo make uninstall ``` -### Custom Installation Prefix -You can install to a different location using the `PREFIX` variable: +Custom installation prefix: ```bash make install PREFIX=/opt/panelplayer ``` -## Usage After Installation -Once installed, the `panelplayer` command is available system-wide: -```bash -panelplayer -p eth0 -w 128 -h 64 animation.webp -``` +## Library Usage + +### C API + +The library provides a stateful API for programmatic control: -For library usage in your C projects: ```c #include + +int main() { + // Initialize + panelplayer_init("eth0", 128, 64, 255); + + // Configure + panelplayer_set_mix(30); + panelplayer_set_rate(30); + + // Play content + panelplayer_play_file("animation.webp"); + + // Or send raw frames + uint8_t frame[128 * 64 * 3]; // BGR format + // ... fill frame data ... + panelplayer_play_frame_bgr(frame, 128, 64); + + // Cleanup + panelplayer_cleanup(); + return 0; +} ``` -Compile with: +Compile: ```bash gcc -lpanelplayer myapp.c -o myapp ``` +### C# Wrapper + +A full-featured C# wrapper is included in `examples/csharp/`: + +```csharp +using var player = new PanelPlayer("eth0", 128, 64, 255); +player.SetMix(30); +player.PlayFile("animation.webp"); +``` + +See `examples/csharp/README.md` for detailed usage. + ## Extensions -Extensions are a way to read or alter frames without modifying PanelPlayer. A minimal extension consists of an `update` function which gets called before each frame is sent. An extension may also include `init` and `destroy` functions. The `destroy` function will always be called if present, even when the `init` function indicates an error has occurred. Example extensions are located in the `extensions` directory. + +Extensions allow custom frame processing without modifying PanelPlayer. They are dynamically loaded shared libraries. + +### Creating an Extension + +Minimal extension with `update` function: + +```c +// my_extension.c +#include + +void update(int width, int height, uint8_t *frame) { + // Modify frame data (BGR format, 3 bytes per pixel) + for (int i = 0; i < width * height * 3; i += 3) { + uint8_t blue = frame[i]; + uint8_t green = frame[i + 1]; + uint8_t red = frame[i + 2]; + + // Your processing here + uint8_t gray = (red + green + blue) / 3; + frame[i] = frame[i + 1] = frame[i + 2] = gray; + } +} +``` + +Optional `init` and `destroy` functions: + +```c +#include +#include + +bool init() { + printf("Extension initialized\n"); + return false; // Return true on error +} + +void destroy() { + printf("Extension cleanup\n"); +} + +void update(int width, int height, uint8_t *frame) { + // Process frame +} +``` + +Build extension: +```bash +gcc -shared -fPIC my_extension.c -o my_extension.so +``` + +Use extension: +```bash +panelplayer -p eth0 -w 128 -h 64 -e my_extension.so video.webp +``` + +Example extensions are in the `extensions/` directory: +- **grayscale**: Converts frames to grayscale +- **nanoled**: Custom LED matrix processing + +## Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ main.c │ +│ (CLI argument parsing, file queue) │ +└────────────────┬────────────────────────────────┘ + │ +┌────────────────▼────────────────────────────────┐ +│ core.c │ +│ (Timing, frame processing, playback loop) │ +└────┬───────────────────────────────────┬────────┘ + │ │ +┌────▼───────────┐ ┌────────▼──────────┐ +│ decoder.c │ │ colorlight.c │ +│ (Multi-format │ │ (Ethernet proto- │ +│ decoding) │ │ col & sending) │ +└────────────────┘ └───────────────────┘ +``` + +### Key Components + +- **main.c**: CLI interface, file queue, shuffle mode +- **core.c**: Shared logic (timing, frame processing, playback) +- **decoder.c**: Multi-format image decoder (WebP/JPEG/PNG/GIF/BMP) +- **colorlight.c**: Ethernet protocol implementation +- **loader.c**: Background file loading with queue +- **panelplayer_api.c**: Library API wrapper ## Protocol -Protocol documentation can be found in the `protocol` directory. A Wireshark plugin is included to help with reverse engineering and debugging. \ No newline at end of file + +The Colorlight protocol uses raw ethernet frames (EtherType `0x0101`) with custom packet types: + +| Type | Description | +|------|-------------| +| 0x01 | Display update (trigger screen refresh) | +| 0x0A | Set brightness/color balance | +| 0x55 | Image data (row-by-row transmission) | + +Protocol documentation and Wireshark dissector are in the `protocol/` directory. + +## Hardware Requirements + +- **LED Panel**: Colorlight 5A-75B or compatible receiving card +- **Connection**: Direct ethernet connection (raw socket access required) +- **Permissions**: Root/sudo access for raw ethernet packets +- **Platform**: Linux (tested on Debian/Ubuntu, Orange Pi) + +## Troubleshooting + +### Permission Denied +```bash +# PanelPlayer requires raw socket access +sudo panelplayer -p eth0 -w 128 -h 64 image.jpg + +# Or use capabilities (no sudo needed after setup) +sudo setcap cap_net_raw+ep /usr/local/bin/panelplayer +``` + +### Library Not Found +```bash +# Update library cache after installation +sudo ldconfig + +# Or set LD_LIBRARY_PATH temporarily +export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH +``` + +### Wrong Ethernet Interface +```bash +# List network interfaces +ip link show + +# Use the correct interface name +panelplayer -p -w 128 -h 64 image.jpg +``` +