diff --git a/.gitignore b/.gitignore index e6421fb..f396fb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ /.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 +.claude/ +CLAUDE.md \ No newline at end of file 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.csproj b/examples/csharp/PanelPlayerExample.csproj new file mode 100644 index 0000000..a6fa901 --- /dev/null +++ b/examples/csharp/PanelPlayerExample.csproj @@ -0,0 +1,16 @@ + + + + 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 new file mode 100644 index 0000000..12a49f1 --- /dev/null +++ b/examples/csharp/README.md @@ -0,0 +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 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 diff --git a/makefile b/makefile index 6fd70a0..1ae9d88 100644 --- a/makefile +++ b/makefile @@ -1,25 +1,63 @@ -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 +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 8866e5e..78b618a 100644 --- a/readme.md +++ b/readme.md @@ -1,41 +1,318 @@ -# 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 -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 +# PanelPlayer + +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 + +### Command Line + +```bash +panelplayer [options] +``` + +#### Required Options + +| Option | Description | Example | +|--------|-------------|---------| +| `-p ` | Ethernet interface | `-p eth0` | +| `-w ` | Display width in pixels | `-w 128` | +| `-h ` | Display height in pixels | `-h 64` | + +#### Optional Settings + +| 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 | + +### 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 +``` + +**Use extension for grayscale effect:** +```bash +panelplayer -p eth0 -w 128 -h 64 -e extensions/grayscale/extension.so image.jpg +``` + +**Duplicate mode for two stacked 128x64 panels (total 128x128):** +```bash +panelplayer -p eth0 -w 128 -h 64 -d content.png +``` + +## Building + +### 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 +``` + +### Compile + +```bash +make # Build both executable and library +make library # Build only the shared library +make clean # Clean build artifacts +``` + +### Installation + +Install system-wide (requires root): +```bash +sudo make install +sudo ldconfig +``` + +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: +```bash +make install PREFIX=/opt/panelplayer +``` + +## Library Usage + +### C API + +The library provides a stateful API for programmatic control: + +```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: +```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 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 + +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 +``` + diff --git a/source/core.c b/source/core.c new file mode 100644 index 0000000..50a63f5 --- /dev/null +++ b/source/core.c @@ -0,0 +1,177 @@ +#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); + } +} + +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 new file mode 100644 index 0000000..a4a9962 --- /dev/null +++ b/source/core.h @@ -0,0 +1,86 @@ +#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); + +/** + * 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/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..22d30cc --- /dev/null +++ b/source/decoder.h @@ -0,0 +1,154 @@ +/** + * @file decoder.h + * @brief Multi-format image decoder abstraction for PanelPlayer + * + * This header provides a unified interface for decoding multiple image formats + * including WebP, JPEG, PNG, GIF, and BMP. The decoder automatically detects + * the format based on file headers and provides a consistent API regardless + * of the underlying format. + * + * Supported formats: + * - WebP (animated and static) + * - JPEG (static) + * - PNG (static) + * - GIF (animated) + * - BMP (static, 24/32-bit uncompressed) + */ + +#ifndef DECODER_H +#define DECODER_H + +#include +#include + +/** + * @brief Opaque decoder instance structure + * + * Internal structure that holds format-specific decoder state. + * Users should treat this as an opaque handle. + */ +typedef struct decoder decoder; + +/** + * @brief Image/animation information structure + * + * Contains metadata about the decoded image or animation. + */ +typedef struct decoder_info +{ + int frame_count; /**< Total number of frames (1 for static images) */ + int canvas_width; /**< Image width in pixels */ + int canvas_height; /**< Image height in pixels */ +} decoder_info; + +/** + * @brief Initialize a decoder from memory data + * + * Creates a new decoder instance by analyzing the provided image data. + * The format is automatically detected from the file header/magic bytes. + * + * The decoder supports: + * - WebP: Animated and static images + * - JPEG: Static images (24-bit RGB) + * - PNG: Static images with alpha channel + * - GIF: Animated images with transparency + * - BMP: Static images (24/32-bit uncompressed) + * + * @param data Pointer to image file data in memory + * @param size Size of the image data in bytes + * + * @return Decoder instance on success, NULL on failure + * @retval NULL if format is unsupported, data is corrupted, or memory allocation fails + * + * @note The caller retains ownership of the data buffer and must ensure it + * remains valid for the lifetime of the decoder instance. + * + * @see decoder_destroy() + */ +decoder *decoder_init(void *data, int size); + +/** + * @brief Get information about the decoded image/animation + * + * Retrieves metadata including dimensions and frame count from the decoder. + * This should be called after successful initialization to determine the + * image properties before processing frames. + * + * @param instance Decoder instance + * @param info Pointer to decoder_info structure to be filled + * + * @return true on success, false on failure + * @retval true Information retrieved successfully + * @retval false NULL instance or info pointer provided + * + * @note For static images (JPEG, PNG, BMP), frame_count will be 1. + * For animations (WebP, GIF), frame_count reflects the total frames. + */ +bool decoder_get_info(decoder *instance, decoder_info *info); + +/** + * @brief Check if more frames are available + * + * Determines whether there are additional frames to decode. + * For static images, this returns true only before the first frame is read. + * For animations, this returns true until all frames have been decoded. + * + * @param instance Decoder instance + * + * @return true if more frames available, false otherwise + * @retval true More frames can be decoded + * @retval false No more frames or NULL instance + * + * @see decoder_get_next() + */ +bool decoder_has_more_frames(decoder *instance); + +/** + * @brief Decode and retrieve the next frame + * + * Decodes the next available frame from the image/animation. + * Returns a pointer to the frame data in RGBA format (4 bytes per pixel) + * and the timestamp for animation timing. + * + * Frame data format: + * - 4 bytes per pixel: Red, Green, Blue, Alpha + * - Row-major order (left-to-right, top-to-bottom) + * - Size: canvas_width × canvas_height × 4 bytes + * + * Timestamp behavior: + * - Static images: Always 0 + * - GIF: Cumulative time in milliseconds from start + * - WebP: Cumulative time in milliseconds from start + * + * @param instance Decoder instance + * @param frame Output pointer to frame data (RGBA format) + * @param timestamp Output pointer to frame timestamp in milliseconds + * + * @return true on success, false on failure + * @retval true Frame decoded successfully + * @retval false No more frames, NULL parameters, or decode error + * + * @warning The frame pointer is valid only until the next call to + * decoder_get_next() or decoder_destroy(). Do not free this pointer. + * + * @see decoder_has_more_frames() + */ +bool decoder_get_next(decoder *instance, uint8_t **frame, int *timestamp); + +/** + * @brief Destroy decoder and free resources + * + * Releases all resources associated with the decoder instance including + * internal buffers and format-specific decoder state. The decoder instance + * becomes invalid after this call. + * + * @param instance Decoder instance to destroy (can be NULL) + * + * @note Safe to call with NULL instance (no-op). + * @note Does not free the original data buffer passed to decoder_init(). + * + * @see decoder_init() + */ +void decoder_destroy(decoder *instance); + +#endif diff --git a/source/main.c b/source/main.c index 62f1a1d..b6c736b 100644 --- a/source/main.c +++ b/source/main.c @@ -1,383 +1,276 @@ -#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 "core.h" +#include "decoder.h" +#include "loader.h" + +#define QUEUE_SIZE 4 + +bool parse(const char *source, int *destination) +{ + char *end; + *destination = strtol(source, &end, 10); + return end[0] != 0; +} + +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; + bool duplicate = 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; + + case 'd': + duplicate = 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"); + puts(" -d Duplicate each row vertically"); + + 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 >= CORE_MIX_MAXIMUM) + { + printf("Mix must be an integer between 0 and %d!\n", CORE_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)(int, int, uint8_t*) = NULL; + + if (extensionFile != NULL) + { + if (core_load_extension(extensionFile, &extension, &update) != 0) + { + puts("Failed to load extension!"); + goto destroy_colorlight; + } + } + + int queued = 0; + + 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); + } + + 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; + } + + if (verbose) + { + 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); + } + + delete_decoder: + decoder_destroy(decoder); + + free_file: + free(file); + } + + status = EXIT_SUCCESS; + + core_unload_extension(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 diff --git a/source/panelplayer_api.c b/source/panelplayer_api.c new file mode 100644 index 0000000..c3084fd --- /dev/null +++ b/source/panelplayer_api.c @@ -0,0 +1,219 @@ +#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; + } + + 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 (result == 0) ? PANELPLAYER_SUCCESS : PANELPLAYER_ERROR; +} + +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 diff --git a/source/panelplayer_api.h b/source/panelplayer_api.h new file mode 100644 index 0000000..73f2ec0 --- /dev/null +++ b/source/panelplayer_api.h @@ -0,0 +1,188 @@ +/** + * @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 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 + * + * 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 image or animation file + * + * Loads and plays an image or animation file on the LED panel. + * Supports WebP, JPEG, PNG, GIF, and BMP formats. + * The function handles frame timing and loops through all frames for animated formats. + * + * @param file_path Path to the image file to play + * + * @return PANELPLAYER_SUCCESS on success, error code otherwise + * @retval PANELPLAYER_SUCCESS 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 image, or incompatible dimensions + */ +int panelplayer_play_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