diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 9ee2d85..0ebc94a 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -14,7 +14,16 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-latest, macos-14] + include: + - os: ubuntu-latest + rid: linux-x64 + - os: windows-latest + rid: win-x64 + - os: macos-latest + rid: osx-x64 + - os: macos-14 + rid: osx-arm64 steps: - uses: actions/checkout@v4 @@ -32,3 +41,14 @@ jobs: - name: Test run: dotnet test --no-restore --verbosity normal + + - name: Publish + run: dotnet publish -c Release -r ${{ matrix.rid }} --self-contained true -p:PublishSingleFile=true + + - name: Upload Build Artifact + uses: actions/upload-artifact@v3 + with: + name: ZeDMDUpdater-${{ matrix.rid }} + path: | + bin/Release/net8.0/${{ matrix.rid }}/publish/* + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bc9648..304f119 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,18 +37,24 @@ jobs: - name: Publish Linux run: dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true - - name: Publish macOS + - name: Publish macOS x64 run: dotnet publish -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true + - name: Publish macOS ARM64 + run: dotnet publish -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true + - name: Zip Windows Release run: zip -j ZeDMDUpdater-windows-x64.zip bin/Release/net8.0/win-x64/publish/ZeDMDUpdater.exe - name: Zip Linux Release run: zip -j ZeDMDUpdater-linux-x64.zip bin/Release/net8.0/linux-x64/publish/ZeDMDUpdater - - name: Zip macOS Release + - name: Zip macOS x64 Release run: zip -j ZeDMDUpdater-macos-x64.zip bin/Release/net8.0/osx-x64/publish/ZeDMDUpdater + - name: Zip macOS ARM64 Release + run: zip -j ZeDMDUpdater-macos-arm64.zip bin/Release/net8.0/osx-arm64/publish/ZeDMDUpdater + - name: Generate Release Notes id: release_notes run: | @@ -61,6 +67,7 @@ jobs: ZeDMDUpdater-windows-x64.zip ZeDMDUpdater-linux-x64.zip ZeDMDUpdater-macos-x64.zip + ZeDMDUpdater-macos-arm64.zip body_path: RELEASE_NOTES.md draft: false prerelease: false diff --git a/DeviceManager.cs b/DeviceManager.cs index 1267a0c..8c25365 100644 --- a/DeviceManager.cs +++ b/DeviceManager.cs @@ -5,34 +5,6 @@ namespace ZeDMDUpdater; public class DeviceManager { - public Task> GetAvailablePorts() - { - // TODO: Use LibZeDMD to list ZeDMD devices - List ports = new List(); - - // Standard SerialPort.GetPortNames() attempt - ports.AddRange(SerialPort.GetPortNames()); - - // Linux-specific checks - if (OperatingSystem.IsLinux()) - { - // Check for ttyUSB devices - string[] ttyUSBDevices = Directory.GetFiles("/dev", "ttyUSB*"); - ports.AddRange(ttyUSBDevices); - - // Check for ttyACM devices (for Arduino-compatible devices) - string[] ttyACMDevices = Directory.GetFiles("/dev", "ttyACM*"); - ports.AddRange(ttyACMDevices); - - // Check for ttyS devices - string[] ttySDevices = Directory.GetFiles("/dev", "ttyS*"); - ports.AddRange(ttySDevices); - } - - // Remove duplicates and sort - return Task.FromResult(ports.Distinct().OrderBy(p => p).ToList()); - } - public async Task FlashFirmware(string firmwarePath, string portName, bool isS3 = false, Action? logCallback = null) { try @@ -98,10 +70,4 @@ public async Task FlashFirmware(string firmwarePath, string portName, bool } } - - public Task ApplySettings(Dictionary settings) - { - // Implement settings application logic here - return Task.CompletedTask; - } } diff --git a/ESP32Device.cs b/ESP32Device.cs new file mode 100644 index 0000000..606bb89 --- /dev/null +++ b/ESP32Device.cs @@ -0,0 +1,439 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Spectre.Console; +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.IO.Ports; + + +namespace ZeDMDUpdater; + +public class Esp32Device +{ + private const string LibraryName = "libzedmd.dylib"; + + public string DeviceAddress { get; set; } = ""; + public bool isUnknown { get; set; } = true; + public bool isS3 { get; set; } = false; + public bool isLilygo { get; set; } = false; + public bool isZeDMD { get; set; } = false; + public bool isWifi { get; set; } = false; + public string WifiIp { get; set; } = ""; + public int ZeID { get; set; } = -1; + public string SSID { get; set; } = ""; + public int SSIDPort { get; set; } = -1; + public int RgbOrder { get; set; } = 0; + public int Brightness { get; set; } = 6; + public int Width { get; set; } = 128; + public int Height { get; set; } = 32; + public int MajVersion { get; set; } = 0; + public int MinVersion { get; set; } = 0; + public int PatVersion { get; set; } = 0; + public int PanelDriver { get; set; } = 0; + public int PanelClockPhase { get; set; } = 0; + public int PanelI2SSpeed { get; set; } = 8; + public int PanelLatchBlanking { get; set; } = 0; + public int PanelMinRefreshRate { get; set; } = 0; + public int UsbPacketSize { get; set; } = 64; + public int UdpDelay { get; set; } = 0; + public int YOffset { get; set; } = 0; + + public Esp32Device(string deviceaddress, bool iss3, bool islilygo, bool isunknown) + { + DeviceAddress = deviceaddress; + isS3 = iss3; + isLilygo = islilygo; + isUnknown = isunknown; + } + public static void GetZeDMDValues(Esp32Device device, IntPtr _pZeDMD) + { + device.isS3 = ZeDMD_IsS3(_pZeDMD); + string? version = Marshal.PtrToStringAnsi(ZeDMD_GetFirmwareVersion(_pZeDMD)) ?? "0.0.0"; + string[] parts = version.Split('.'); + int.TryParse(parts[0], out int major); + int.TryParse(parts[1], out int minor); + int.TryParse(parts[2], out int patch); + device.MajVersion = major; + device.MinVersion = minor; + device.PatVersion = patch; + device.RgbOrder = ZeDMD_GetRGBOrder(_pZeDMD); + device.Brightness = ZeDMD_GetBrightness(_pZeDMD); + device.Width = ZeDMD_GetPanelWidth(_pZeDMD); + device.Height = ZeDMD_GetPanelHeight(_pZeDMD); + device.SSID = Marshal.PtrToStringAnsi(ZeDMD_GetWiFiSSID(_pZeDMD)) ?? string.Empty; + device.SSIDPort = ZeDMD_GetWiFiPort(_pZeDMD); + device.PanelDriver = ZeDMD_GetPanelDriver(_pZeDMD); + device.PanelClockPhase = ZeDMD_GetPanelClockPhase(_pZeDMD); + device.PanelI2SSpeed = ZeDMD_GetPanelI2sSpeed(_pZeDMD); + device.PanelLatchBlanking = ZeDMD_GetPanelLatchBlanking(_pZeDMD); + device.PanelMinRefreshRate = ZeDMD_GetPanelMinRefreshRate(_pZeDMD); + device.UsbPacketSize = ZeDMD_GetUsbPackageSize(_pZeDMD); + device.UdpDelay = ZeDMD_GetUdpDelay(_pZeDMD); + device.YOffset = ZeDMD_GetYOffset(_pZeDMD); + device.ZeID = ZeDMD_GetId(_pZeDMD); + } + private static string logs = string.Empty; + private static void LogHandler(string format, IntPtr args, IntPtr pUserData) + { + logs += Marshal.PtrToStringAnsi(ZeDMD_FormatLogMessage(format, args, pUserData)) + "\r\n"; + } + + + public static async Task<(string logs, List esp32Devices, Esp32Device wifiDevice)> CheckZeDMDs(List esp32Devices, Esp32Device wifiDevice) + { + AnsiConsole.MarkupLine("[yellow]=== WiFi Test ===[/]"); + + // create an instance + GCHandle handle; + IntPtr _pZeDMD = IntPtr.Zero; + _pZeDMD = ZeDMD_GetInstance(); + + ZeDMD_LogCallback callbackDelegate = new ZeDMD_LogCallback((format, args, userData) => + { + var message = Marshal.PtrToStringAnsi(ZeDMD_FormatLogMessage(format, args, userData)); + AnsiConsole.MarkupLine($"[grey]{message}[/]"); + logs += message + "\r\n"; + }); + + // Keep a reference to the delegate to prevent GC from collecting it + handle = GCHandle.Alloc(callbackDelegate); + ZeDMD_SetLogCallback(_pZeDMD, callbackDelegate, IntPtr.Zero); + + // check if a ZeDMD wifi is available + byte wifitransport = 2; + if (ZeDMD_OpenDefaultWiFi(_pZeDMD)) + { + AnsiConsole.MarkupLine("[green]WiFi ZeDMD found[/]"); + // if so, get all the parameters + wifiDevice.isWifi = true; + wifiDevice.isZeDMD = true; + wifiDevice.isUnknown = false; + GetZeDMDValues(wifiDevice, _pZeDMD); + wifiDevice.WifiIp = Marshal.PtrToStringAnsi(ZeDMD_GetIp(_pZeDMD)) ?? string.Empty; + if (wifiDevice.WifiIp != "") + { + AnsiConsole.MarkupLine($"[green]WiFi IP: {wifiDevice.WifiIp}[/]"); + // keep the transport mode for later + wifitransport = ZeDMD_GetTransport(_pZeDMD); + if (wifitransport != 1 && wifitransport != 2) + { + AnsiConsole.MarkupLine("[red]The WiFi ZeDMD connected has an old firmware, you need to check manually which COM # is corresponding and flash it, your WiFi ZeDMD will be ignored.[/]"); + wifiDevice.isUnknown = true; + wifiDevice.ZeID = -1; + } + else + { + // switch this device to USB + AnsiConsole.MarkupLine("[grey]Switching WiFi ZeDMD to USB so that we can control it...[/]"); + ZeDMD_SetTransport(_pZeDMD, 0); + ZeDMD_SaveSettings(_pZeDMD); + ZeDMD_Reset(_pZeDMD); + Thread.Sleep(5000); + } + } + else + { + wifiDevice.isUnknown = true; + AnsiConsole.MarkupLine("[red]No WiFi device found[/]"); + } + ZeDMD_Close(_pZeDMD); + // Pause for 1s + await Task.Delay(1000); + } + else + { + wifiDevice.isUnknown = true; + AnsiConsole.MarkupLine("[red]No WiFi device found[/]"); + } + + AnsiConsole.MarkupLine("\n[yellow]=== USB Test ===[/]"); + int wifif = -1; + AnsiConsole.MarkupLine($"[grey]Found {esp32Devices.Count} devices...[/]"); + for (int i = 0; i < esp32Devices.Count; i++) + { + // open the device + Esp32Device device = esp32Devices[i]; + string deviceaddress = device.DeviceAddress; + AnsiConsole.MarkupLine($"[grey]Testing {deviceaddress}...[/]"); + + ZeDMD_SetDevice(_pZeDMD, deviceaddress); + if (ZeDMD_Open(_pZeDMD)) + { + AnsiConsole.MarkupLine($"[green]Found ZeDMD on {deviceaddress}[/]"); + // get its parameters + device.isWifi = false; + device.isUnknown = false; + device.isZeDMD = true; + GetZeDMDValues(device, _pZeDMD); + + ZeDMD_Close(_pZeDMD); + await Task.Delay(1000); + + if (device.ZeID == wifiDevice.ZeID) + { + AnsiConsole.MarkupLine($"[blue]Device on {deviceaddress} matches WiFi device[/]"); + await Task.Delay(1000); + wifiDevice.DeviceAddress = device.DeviceAddress; + wifif = i; + } + } + else + { + AnsiConsole.MarkupLine($"[red]No ZeDMD found on {deviceaddress}[/]"); + } + } + + // if we have found the wifi device in the USB devices, we can remove it from the USB list + if (wifif >= 0) + { + esp32Devices.RemoveAt(wifif); + AnsiConsole.MarkupLine("[green]Removed WiFi device from USB list[/]"); + } + + return (logs, esp32Devices, wifiDevice); + } + public static string LedTest() + { + logs = "=== Led Test ===\r\n"; + // create an instance + GCHandle handle; + IntPtr _pZeDMD = IntPtr.Zero; + _pZeDMD = ZeDMD_GetInstance(); + ZeDMD_LogCallback callbackDelegate = new ZeDMD_LogCallback(LogHandler); + // Keep a reference to the delegate to prevent GC from collecting it + handle = GCHandle.Alloc(callbackDelegate); + ZeDMD_SetLogCallback(_pZeDMD, callbackDelegate, IntPtr.Zero); + bool openOK = false; + if (Program.selectedDevice.isWifi) openOK = ZeDMD_OpenDefaultWiFi(_pZeDMD); + else + { + string comport = Program.selectedDevice.DeviceAddress; + ZeDMD_SetDevice(_pZeDMD, comport); + openOK = ZeDMD_Open(_pZeDMD); + } + if (openOK) + { + ZeDMD_LedTest(_pZeDMD); + ZeDMD_Close(_pZeDMD); + } + else logs += "Unable to connect to the device\r\n"; + return logs; + } + + public static byte[] ReadRawRGBFile(string filePath, int width = 128, int height = 32) + { + try + { + int expectedFileSize = width * height * 3; + byte[] fileBytes = File.ReadAllBytes(filePath); + if (fileBytes.Length != expectedFileSize) + { + AnsiConsole.MarkupLine($"[red]File size {fileBytes.Length} bytes does not match expected size for {width}x{height} RGB image ({expectedFileSize} bytes)[/]"); + } + return fileBytes; + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error reading RAW RGB file: {ex.Message}[/]"); + return new byte[0]; + } + } + + public static byte[] CreateImageRGB24() + { + // Allocate buffer for 128x32 RGB image (3 bytes per pixel) + byte[] imageBuffer = new byte[128 * 32 * 3]; + + for (int y = 0; y < 32; ++y) + { + for (int x = 0; x < 128; ++x) + { + int index = (y * 128 + x) * 3; + + // Determine which quadrant we're in + bool isLeftHalf = x < 64; + bool isTopHalf = y < 16; + + // Set base colors for each quadrant + if (isLeftHalf && isTopHalf) // Top left - Red + { + imageBuffer[index] = 255; // R + imageBuffer[index + 1] = 0; // G + imageBuffer[index + 2] = 0; // B + } + else if (!isLeftHalf && isTopHalf) // Top right - Green + { + imageBuffer[index] = 0; // R + imageBuffer[index + 1] = 255; // G + imageBuffer[index + 2] = 0; // B + } + else if (isLeftHalf && !isTopHalf) // Bottom left - Blue + { + imageBuffer[index] = 0; // R + imageBuffer[index + 1] = 0; // G + imageBuffer[index + 2] = 255; // B + } + else // Bottom right - White + { + imageBuffer[index] = 255; // R + imageBuffer[index + 1] = 255; // G + imageBuffer[index + 2] = 255; // B + } + } + } + + return imageBuffer; + } + + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI ZeDMD* ZeDMD_GetInstance(); + public static extern IntPtr ZeDMD_GetInstance(); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI const char* ZeDMD_GetVersion(); + public static extern IntPtr ZeDMD_GetVersion(); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetDevice(ZeDMD* pZeDMD, const char* const device); + public static extern bool ZeDMD_SetDevice(IntPtr pZeDMD, string device); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI bool ZeDMD_OpenDefaultWiFi(ZeDMD* pZeDMD); + public static extern bool ZeDMD_OpenDefaultWiFi(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI bool ZeDMD_Open(ZeDMD* pZeDMD); + public static extern bool ZeDMD_Open(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI bool ZeDMD_IsS3(ZeDMD* pZeDMD); + protected static extern bool ZeDMD_IsS3(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_Close(ZeDMD* pZeDMD); + public static extern void ZeDMD_Close(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI const char* ZeDMD_GetFirmwareVersion(ZeDMD* pZeDMD); + protected static extern IntPtr ZeDMD_GetFirmwareVersion(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetRGBOrder(ZeDMD* pZeDMD); + private static extern byte ZeDMD_GetRGBOrder(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetBrightness(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetBrightness(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetBrightness(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetYOffset(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint16_t ZeDMD_GetId(ZeDMD* pZeDMD); + protected static extern ushort ZeDMD_GetId(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint16_t ZeDMD_GetWidth(ZeDMD* pZeDMD); + protected static extern ushort ZeDMD_GetPanelWidth(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint16_t ZeDMD_GetHeight(ZeDMD* pZeDMD); + protected static extern ushort ZeDMD_GetPanelHeight(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI const char* ZeDMD_GetWiFiSSID(ZeDMD* pZeDMD); + private static extern IntPtr ZeDMD_GetWiFiSSID(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI const char* ZeDMD_GetIp(ZeDMD* pZeDMD); + private static extern IntPtr ZeDMD_GetIp(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI const char* ZeDMD_GetDevice(ZeDMD* pZeDMD); + private static extern IntPtr ZeDMD_GetDevice(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI int ZeDMD_GetWiFiPort(ZeDMD* pZeDMD); + private static extern int ZeDMD_GetWiFiPort(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint16_t ZeDMD_GetUsbPackageSize(ZeDMD* pZeDMD); + protected static extern ushort ZeDMD_GetUsbPackageSize(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetUdpDelay(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetUdpDelay(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetPanelDriver(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetPanelDriver(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetPanelClockPhase(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetPanelClockPhase(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetPanelI2sSpeed(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetPanelI2sSpeed(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetPanelLatchBlanking(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetPanelLatchBlanking(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetPanelMinRefreshRate(ZeDMD* pZeDMD); + protected static extern byte ZeDMD_GetPanelMinRefreshRate(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetRGBOrder(ZeDMD* pZeDMD, uint8_t rgbOrder); + public static extern void ZeDMD_SetRGBOrder(IntPtr pZeDMD, byte rgbOrder); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetBrightness(ZeDMD* pZeDMD, uint8_t brightness); + public static extern void ZeDMD_SetBrightness(IntPtr pZeDMD, byte brightness); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetWiFiSSID(ZeDMD* pZeDMD, const char* const ssid); + public static extern void ZeDMD_SetWiFiSSID(IntPtr pZeDMD, string ssid); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetWiFiPassword(ZeDMD* pZeDMD, const char* const password); + public static extern void ZeDMD_SetWiFiPassword(IntPtr pZeDMD, string password); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetWiFiPort(ZeDMD* pZeDMD, int port); + public static extern void ZeDMD_SetWiFiPort(IntPtr pZeDMD, int port); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetPanelDriver(ZeDMD* pZeDMD, uint8_t driver); + public static extern void ZeDMD_SetPanelDriver(IntPtr pZeDMD, byte uint8_t); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetPanelClockPhase(ZeDMD* pZeDMD, uint8_t clockPhase); + public static extern void ZeDMD_SetPanelClockPhase(IntPtr pZeDMD, byte clockPhase); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetPanelI2sSpeed(ZeDMD* pZeDMD, uint8_t i2sSpeed); + public static extern void ZeDMD_SetPanelI2sSpeed(IntPtr pZeDMD, byte i2sSpeed); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetPanelLatchBlanking(ZeDMD* pZeDMD, uint8_t latchBlanking); + public static extern void ZeDMD_SetPanelLatchBlanking(IntPtr pZeDMD, byte latchBlanking); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetPanelMinRefreshRate(ZeDMD* pZeDMD, uint8_t minRefreshRate); + public static extern void ZeDMD_SetPanelMinRefreshRate(IntPtr pZeDMD, byte minRefreshRate); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetUdpDelay(ZeDMD* pZeDMD, uint8_t udpDelay); + public static extern void ZeDMD_SetUdpDelay(IntPtr pZeDMD, byte udpDelay); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetUsbPackageSize(ZeDMD* pZeDMD, uint16_t usbPackageSize); + public static extern void ZeDMD_SetUsbPackageSize(IntPtr pZeDMD, ushort usbPackageSize); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetYOffset(ZeDMD* pZeDMD, uint8_t yOffset); + public static extern void ZeDMD_SetYOffset(IntPtr pZeDMD, byte yOffset); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetTransport(ZeDMD* pZeDMD, uint8_t transport); + public static extern void ZeDMD_SetTransport(IntPtr pZeDMD, byte transport); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI uint8_t ZeDMD_GetTransport(ZeDMD* pZeDMD); + public static extern byte ZeDMD_GetTransport(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SaveSettings(ZeDMD* pZeDMD); + public static extern void ZeDMD_SaveSettings(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_Reset(ZeDMD* pZeDMD); + public static extern void ZeDMD_Reset(IntPtr pZeDMD); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_LedTest(ZeDMD* pZeDMD); + private static extern void ZeDMD_LedTest(IntPtr pZeDMD); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + // C format: typedef void(ZEDMDCALLBACK* ZeDMD_LogCallback)(const char* format, va_list args, const void* userData); + public delegate void ZeDMD_LogCallback(string format, IntPtr args, IntPtr pUserData); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI void ZeDMD_SetLogCallback(ZeDMD* pZeDMD, ZeDMD_LogCallback callback, const void* pUserData); + public static extern void ZeDMD_SetLogCallback(IntPtr pZeDMD, ZeDMD_LogCallback callback, IntPtr pUserData); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + // C format: extern ZEDMDAPI const char* ZeDMD_FormatLogMessage(const char* format, va_list args, const void* pUserData); + public static extern IntPtr ZeDMD_FormatLogMessage(string format, IntPtr args, IntPtr pUserData); + // C format: extern ZEDMDAPI void ZeDMD_RenderRgb888(ZeDMD* pZeDMD, uint8_t* frame); + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void ZeDMD_RenderRgb888(IntPtr pZeDMD, IntPtr pImage); + // C format: extern ZEDMDAPI void ZeDMD_SetFrameSize(ZeDMD* pZeDMD, uint16_t width, uint16_t height) + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void ZeDMD_SetFrameSize(IntPtr pZeDMD, ushort width, ushort height); + // C format: extern ZEDMDAPI void ZeDMD_ClearScreen(ZeDMD* pZeDMD) + [DllImport(LibraryName, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void ZeDMD_ClearScreen(IntPtr pZeDMD); +} diff --git a/ESP32Devices.cs b/ESP32Devices.cs new file mode 100644 index 0000000..f64600e --- /dev/null +++ b/ESP32Devices.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using Spectre.Console; +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.IO.Ports; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; + + +namespace ZeDMDUpdater; + +internal static class Esp32Devices +{ + public static List esp32Devices = new List(); + public static Esp32Device wifiDevice = new Esp32Device("", false, false, false); + public static int[] I2SallowedSpeed = { 8, 16, 20 }; + private readonly static (string chip, bool s3, bool lilygo)[] USBtoSerialDevices = [ + ("CP210x", false, false), + ("CP2102", false, false), + ("CH340", false, false), + ("CH9102", true, true), + ("CH343", true, false) + ]; + + public static void GetPortNames() + { + var portNames = SerialPort.GetPortNames(); + + foreach (string portName in portNames) + { + AnsiConsole.MarkupLine($"[grey]Scanning port {portName}...[/]"); + string normalizedPortName = portName; + int portNumber = -1; + + // Handle different OS port naming conventions + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Match match = Regex.Match(portName, @"COM(\d+)"); + if (match.Success) + { + portNumber = int.Parse(match.Groups[1].Value); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // Handle Linux serial ports (ttyUSB*, ttyACM*) + Match match = Regex.Match(portName, @"tty(?:USB|ACM)(\d+)"); + if (match.Success) + { + portNumber = int.Parse(match.Groups[1].Value); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Handle macOS serial ports + Match match = Regex.Match(portName, @"tty\.(?:usbserial|usbmodem).*"); + if (match.Success) + { + AnsiConsole.MarkupLine($"[grey]Found macOS serial port {portName}[/]"); + // On macOS, we'll use the index in the ports array as the number + portNumber = Array.IndexOf(portNames, portName); + } + } + + if (portNumber >= 0) + { + // Try to identify the device type using available system information + foreach (var device in USBtoSerialDevices) + { + // On Unix systems, we can try to read device information from sysfs + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + string devicePath = $"/sys/class/tty/{Path.GetFileName(portName)}/device/driver"; + + if (File.Exists(devicePath)) + { + string driverInfo = File.ReadAllText(devicePath); + AnsiConsole.MarkupLine($"[grey]Found device {device.chip}[/]"); + if (driverInfo.Contains(device.chip, StringComparison.OrdinalIgnoreCase)) + { + esp32Devices.Add(new Esp32Device(devicePath, device.s3, device.lilygo, false)); + break; + } + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "ioreg", + Arguments = "-r -c IOUSBHostDevice", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + // Split output into device blocks + var deviceBlocks = output.Split(new[] { "+-o" }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var block in deviceBlocks) + { + if (block.Contains(device.chip, StringComparison.OrdinalIgnoreCase)) + { + AnsiConsole.MarkupLine($"[grey]Found device {device.chip}[/]"); + // Extract serial number + var serialMatch = Regex.Match(block, @"""USB Serial Number""\s*=\s*""([^""]+)"""); + if (serialMatch.Success) + { + string serialNumber = serialMatch.Groups[1].Value; + // Construct the likely device path + string probableDevicePath = $"/dev/tty.usbserial-{serialNumber}"; + + // Verify the device path exists + if (File.Exists(probableDevicePath)) + { + AnsiConsole.MarkupLine($"[grey]Found {device.chip} device at {probableDevicePath}[/]"); + esp32Devices.Add(new Esp32Device(probableDevicePath, device.s3, device.lilygo, false)); + break; + } + + // Also check for alternative naming pattern + string alternativeDevicePath = $"/dev/tty.usbmodem{serialNumber}"; + if (File.Exists(alternativeDevicePath)) + { + AnsiConsole.MarkupLine($"[grey]Found {device.chip} device at {alternativeDevicePath}[/]"); + esp32Devices.Add(new Esp32Device(alternativeDevicePath, device.s3, device.lilygo, false)); + break; + } + } + } + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error reading macOS device info: {ex.Message}[/]"); + } + } + + } + } + } + } + } + + public static async Task GetAvailableDevices() + { + esp32Devices.Clear(); + wifiDevice = new Esp32Device("", false, false, false); + GetPortNames(); + var result = await Esp32Device.CheckZeDMDs(esp32Devices, wifiDevice); + esp32Devices = result.esp32Devices; + wifiDevice = result.wifiDevice; + return result.logs; + } + +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index bb55711..845cc1f 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,15 @@ -using Spectre.Console; +// TODO : +// - Look for ZeDMD device at startup, and use that for flashing +// - Add a "Device Settings" option to the main menu, and ways to set the following : +// - Transport mode (WiFi, USB) +// - WiFi SSID and password +// - RGB order +// - Brightness +// - USB packet size +// - UDP Delay +// - FIX +// - If there is more than one device, we need to ask the user to select the device at the beginning and use this throughout +using Spectre.Console; namespace ZeDMDUpdater; @@ -8,16 +19,76 @@ class Program private const string UNSELECTED_MARK = "[ ]"; static string selectedBoardType = "Standard"; static bool useWifi = false; + public static Esp32Device selectedDevice = new Esp32Device("", false, false, false); + internal static async Task GetZeDMDDevices() + { + // Get available devices + string deviceLogs = await Esp32Devices.GetAvailableDevices(); + ShowDeviceSummary(); + // If there is more that one device in Esp32Devices.esp32Devices, we need to ask the user to select the device + if (Esp32Devices.esp32Devices.Count > 1) + { + var deviceChoices = new List(); + foreach (var device in Esp32Devices.esp32Devices) + { + deviceChoices.Add($"{device.DeviceAddress} - {device.ZeID:X4}, version {device.MajVersion}.{device.MinVersion}.{device.PatVersion}"); + } + deviceChoices.Add("< Back"); + + var deviceSelection = new SelectionPrompt() + .Title("Select device:") + .AddChoices(deviceChoices) + .UseConverter(x => x) + .HighlightStyle(new Style(foreground: Color.Blue)); + + var selectedDeviceInMenu = AnsiConsole.Prompt(deviceSelection); + + if (selectedDeviceInMenu == "< Back") + { + Console.Clear(); + } + + // Get the actual device from the list + selectedDevice = Esp32Devices.esp32Devices.Find(x => x.DeviceAddress == selectedDeviceInMenu.Split(" - ")[0]) + ?? throw new InvalidOperationException("Selected device not found."); + } + else + { + if (Esp32Devices.wifiDevice.isWifi && Esp32Devices.wifiDevice.isZeDMD) + { + selectedDevice = Esp32Devices.wifiDevice; + } + else + { + selectedDevice = Esp32Devices.esp32Devices[0]; + } + } + // Ask to press a key with Spectre.Console + AnsiConsole.MarkupLine("Press any key to continue..."); + Console.ReadKey(); + + } static async Task Main(string[] args) { bool firmwareDownloaded = false; string firmwarePath = string.Empty; + try + { + AnsiConsole.MarkupLine("[yellow]=== Initializing and scanning for ZeDMD devices ===[/]"); + await GetZeDMDDevices(); + + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error during initialization: {ex.Message}[/]"); + return; + } Console.Clear(); while (true) { - AnsiConsole.Write(new FigletText("ZeDMD Updater") - .Color(Color.Blue)); + UserInterface.ShowTop(); + string selectVersionChoice = "Select Version"; string downloadFirmwareChoice = "Download Firmware"; @@ -41,23 +112,11 @@ static async Task Main(string[] args) downloadFirmwareChoice = $"{Markup.Escape(UNSELECTED_MARK)} Download Firmware"; // Empty checkbox if no firmware } - string selectBoardTypeChoice; - if (string.IsNullOrEmpty(selectedBoardType)) - { - selectBoardTypeChoice = "Select Board Type"; - } - else - { - selectBoardTypeChoice = selectedBoardType == "LilygoS3Amoled" - ? $"Select Board Type ({selectedBoardType}, WiFi: {(useWifi ? "Yes" : "No")})" - : $"Select Board Type ({selectedBoardType})"; - } var choice = AnsiConsole.Prompt( new SelectionPrompt() .Title("What would you like to do?") .AddChoices(new[] { - selectBoardTypeChoice, selectVersionChoice, downloadFirmwareChoice, "Flash Device", @@ -115,37 +174,6 @@ await AnsiConsole.Status() } break; - case var c when c.StartsWith("Select Board Type"): - var boardTypes = new[] { "Standard", "LilygoS3Amoled", "S3-N16R8", "< Back" }; - var boardSelection = new SelectionPrompt() - .Title("Select board type:") - .AddChoices(boardTypes) - .UseConverter(x => x) - .HighlightStyle(new Style(foreground: Color.Blue)); - - var boardType = AnsiConsole.Prompt(boardSelection); - - if (boardType != "< Back") - { - selectedBoardType = boardType; - if (boardType == "LilygoS3Amoled") - { - useWifi = AnsiConsole.Prompt( - new ConfirmationPrompt("Would you like to use WiFi?") - .ShowChoices()); - - AnsiConsole.MarkupLine($"[green]Board type set to {selectedBoardType} (WiFi: {(useWifi ? "Yes" : "No")})[/]"); - } - else - { - useWifi = false; - AnsiConsole.MarkupLine($"[green]Board type set to {selectedBoardType}[/]"); - } - firmwareDownloaded = false; - } - Console.Clear(); - break; - case "Flash Device": if (!firmwareDownloaded) { @@ -156,50 +184,27 @@ await AnsiConsole.Status() } try { - // Get available ports var deviceManager = new DeviceManager(); - var ports = await deviceManager.GetAvailablePorts(); - - if (ports.Count == 0) - { - AnsiConsole.MarkupLine("[red]No serial ports found[/]"); - return; - } - - var portChoices = new List(ports); - portChoices.Add("< Back"); - - var portSelection = new SelectionPrompt() - .Title("Select serial port:") - .AddChoices(portChoices) - .UseConverter(x => x) - .HighlightStyle(new Style(foreground: Color.Blue)); - - var selectedPort = AnsiConsole.Prompt(portSelection); - - if (selectedPort == "< Back") - { - Console.Clear(); - break; - } - - var isS3 = selectedBoardType == "ESP32-S3"; if (!File.Exists(firmwarePath + "/ZeDMD.bin")) { AnsiConsole.MarkupLine("[red]Firmware file not found. Please download it first.[/]"); return; } + IntPtr _pZeDMD = IntPtr.Zero; + _pZeDMD = Esp32Device.ZeDMD_GetInstance(); + + await AnsiConsole.Status() .StartAsync("Flashing firmware...", async ctx => { ctx.Spinner(Spinner.Known.Dots); ctx.SpinnerStyle(Style.Parse("orange1")); - + // FIXME : if there is more than one device, we need to ask the user to select the device bool success = await deviceManager.FlashFirmware( firmwarePath + "/ZeDMD.bin", - selectedPort, - isS3, + selectedDevice.DeviceAddress, + selectedDevice.isS3, (message) => AnsiConsole.MarkupLine($"[blue]{message}[/]")); if (success) @@ -218,20 +223,55 @@ await AnsiConsole.Status() } break; - case "Device Settings": - await AnsiConsole.Status() - .StartAsync("Loading device settings...", async ctx => - { - ctx.Spinner(Spinner.Known.Dots); - ctx.SpinnerStyle(Style.Parse("purple")); - AnsiConsole.MarkupLine("[yellow]Function not yet implemented[/]"); - await Task.Delay(2000); + var deviceSettingsChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a setting to change:") + .AddChoices(new[] + { + "Transport Mode", + "WiFi SSID and Password", + "RGB Order", + "Brightness", + "USB Packet Size", + "UDP Delay", + "< Back" + })); + + switch (deviceSettingsChoice) + { + case "Transport Mode": + UserInterface.ShowTransportMode(); + break; + + case "WiFi SSID and Password": + UserInterface.ShowWifiSettings(); + break; + + case "RGB Order": + UserInterface.ShowRgbOrder(); + break; + + case "Brightness": + UserInterface.ShowBrightness(); + break; + + case "USB Packet Size": + UserInterface.ShowUsbPacketSize(); + break; + + case "UDP Delay": + UserInterface.ShowUdpDelay(); + break; + + case "< Back": Console.Clear(); - }); + break; + } break; + case "Exit": AnsiConsole.MarkupLine("[green]Goodbye![/]"); return; @@ -244,4 +284,58 @@ await AnsiConsole.Status() } } } + + private static void ShowDeviceSummary() + { + // Create a table for the device summary + var table = new Table() +.Border(TableBorder.Rounded) +.BorderColor(Color.Grey) +.Title("[yellow]Device Summary[/]") +.AddColumn(new TableColumn("[blue]Type[/]").Centered()) +.AddColumn(new TableColumn("[blue]Board[/]").Centered()) +.AddColumn(new TableColumn("[blue]Location[/]").Centered()) +.AddColumn(new TableColumn("[blue]ID[/]").Centered()) +.AddColumn(new TableColumn("[blue]Firmware[/]").Centered()); + + // Add WiFi device if found + if (Esp32Devices.wifiDevice.isZeDMD) + { + table.AddRow( +"[green]WiFi[/]", +$"[white]{(Esp32Devices.wifiDevice.isLilygo ? "LilygoS3Amoled" : (Esp32Devices.wifiDevice.isS3 ? "ESP32-S3" : "Standard"))}[/]", +$"[white]{Esp32Devices.wifiDevice.WifiIp}[/]", +$"[white]0x{Esp32Devices.wifiDevice.ZeID:X4}[/]", +$"[white]{Esp32Devices.wifiDevice.MajVersion}.{Esp32Devices.wifiDevice.MinVersion}.{Esp32Devices.wifiDevice.PatVersion}[/]" +); + } + + // Add USB devices + foreach (var device in Esp32Devices.esp32Devices.Where(d => d.isZeDMD)) + { + table.AddRow( +"[green]USB[/]", +$"[white]{(Esp32Devices.wifiDevice.isLilygo ? "LilygoS3Amoled" : (Esp32Devices.wifiDevice.isS3 ? "ESP32-S3" : "Standard"))}[/]", +$"[white]{device.DeviceAddress}[/]", +$"[white]0x{device.ZeID:X4}[/]", +$"[white]{device.MajVersion}.{device.MinVersion}.{device.PatVersion}[/]" +); + } + + // If no devices found, add a message row + if (!Esp32Devices.wifiDevice.isZeDMD && !Esp32Devices.esp32Devices.Any(d => d.isZeDMD)) + { + table.AddRow( +"[red]No devices found[/]", +"", +"", +"" +); + } + + // Display the table + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + + } } diff --git a/README.md b/README.md index 32289b5..6266b50 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # ZeDMD Updater -A universal (Windows, Linux, MacOS) command-line utility to update firmware on ZeDMD displays. +A universal (Windows, Linux, MacOS) command-line utility to update firmware on ZeDMD displays and configure them. ![Screenshot](screenshot.png) > [!NOTE] > ZeDMDUpdater-universal is a non-Windows specific alternative to the great [ZeDMD_Updater2](https://github.com/zesinger/ZeDMD_Updater2). > It only supports flashing through USB for now, and does not yet allow to set ZeDMD configuration. +> Leveraging a lot of code from ZeDMD_Updater2, ZeDMDUpdater-universal adopts the same Open Source license - GPL 3.0. ## Features @@ -16,7 +17,7 @@ A universal (Windows, Linux, MacOS) command-line utility to update firmware on Z - LilygoS3Amoled (with optional WiFi support) - Firmware version selection - Easy-to-use interactive menu -- Device settings configuration (coming) +- Device settings configuration ## Prerequisites @@ -36,7 +37,7 @@ usbipd attach --wsl --busid= ## Usage -1. Connect your ZeDMD display to your computer +1. Connect your ZeDMD display to your computer on USB 2. Run the application: ```shell @@ -45,8 +46,7 @@ usbipd attach --wsl --busid= 3. Follow the interactive menu to: - - Select your board type - Choose firmware version - Download firmware - Flash your device - - Configure device settings (coming) \ No newline at end of file + - Configure device settings diff --git a/RGBW.raw b/RGBW.raw new file mode 100644 index 0000000..de0e7b8 Binary files /dev/null and b/RGBW.raw differ diff --git a/UserInterface.cs b/UserInterface.cs index 73d250a..a9863a0 100644 --- a/UserInterface.cs +++ b/UserInterface.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using Spectre.Console; namespace ZeDMDUpdater; @@ -153,4 +154,319 @@ void UpdateDisplay(int selectedIndex) } } } + + internal static void ShowBrightness() + { + ShowTop(); + // Set device brightness + nint _pZeDMD; + bool dmdDevice = OpenZeDMD(out _pZeDMD); + if (!dmdDevice) + { + return; + } + + nint pImageRgb = ShowCalibrationImage(_pZeDMD); + + + // Get the current brightness + var currentBrightness = Program.selectedDevice.Brightness; + + // Use a Spectre Console bar to set the brightness + int currentValue = currentBrightness; + bool selecting = true; + + while (selecting) + { + ShowTop(); + pImageRgb = ShowCalibrationImage(_pZeDMD); + var progress = new Progress(AnsiConsole.Console); + progress.Start(ctx => + { + var task = ctx.AddTask($"[green]Brightness ({currentValue}/15)[/]", maxValue: 15); + task.Value = currentValue; + task.IsIndeterminate = false; + }); + + AnsiConsole.MarkupLine($"\nUse [blue]:left_arrow:[/] and [blue]:right_arrow:[/] to adjust, [green]Enter[/] to confirm"); + AnsiConsole.MarkupLine($"Current value: {currentValue}"); + + var key = Console.ReadKey(true); + switch (key.Key) + { + case ConsoleKey.LeftArrow: + if (currentValue > 0) + { + currentValue--; + Esp32Device.ZeDMD_SetBrightness(_pZeDMD, (byte)currentValue); + } + break; + case ConsoleKey.RightArrow: + if (currentValue < 15) + { + currentValue++; + Esp32Device.ZeDMD_SetBrightness(_pZeDMD, (byte)currentValue); + } + break; + case ConsoleKey.Enter: + selecting = false; + break; + } + } + AnsiConsole.MarkupLine($"[green]Setting brightness to {currentValue}...[/]"); + Esp32Device.ZeDMD_SetBrightness(_pZeDMD, (byte)currentValue); + Program.selectedDevice.Brightness = currentValue; + + // Save settings before rendering + Esp32Device.ZeDMD_SaveSettings(_pZeDMD); + + currentBrightness = currentValue; + if (pImageRgb != IntPtr.Zero) + { + Marshal.FreeHGlobal(pImageRgb); + } + + Esp32Device.ZeDMD_Close(_pZeDMD); + + } + + private static bool OpenZeDMD(out nint _pZeDMD) + { + // create an instance + GCHandle handle; + _pZeDMD = IntPtr.Zero; + _pZeDMD = Esp32Device.ZeDMD_GetInstance(); + + Esp32Device.ZeDMD_LogCallback callbackDelegate = new Esp32Device.ZeDMD_LogCallback((format, args, userData) => + { + var message = Marshal.PtrToStringAnsi(Esp32Device.ZeDMD_FormatLogMessage(format, args, userData)); + AnsiConsole.MarkupLine($"[grey]{message}[/]"); + }); + + // Keep a reference to the delegate to prevent GC from collecting it + handle = GCHandle.Alloc(callbackDelegate); + Esp32Device.ZeDMD_SetLogCallback(_pZeDMD, callbackDelegate, IntPtr.Zero); + Esp32Device.ZeDMD_SetDevice(_pZeDMD, Program.selectedDevice.DeviceAddress); + // Esp32Device.ZeDMD_Reset(_pZeDMD); + var openOK = Esp32Device.ZeDMD_Open(_pZeDMD); + if (!openOK) + { + AnsiConsole.MarkupLine($"[red]Failed to open device {Program.selectedDevice.DeviceAddress}[/]"); + Thread.Sleep(2000); + return false; + } + + return true; + } + + internal static void ShowTop() + { + Console.Clear(); + AnsiConsole.Write(new FigletText("ZeDMD Updater") + .Color(Color.Blue)); + var panel = new Panel( + $"[blue]Device:[/] {(Program.selectedDevice.isZeDMD ? Program.selectedDevice.WifiIp : Program.selectedDevice.DeviceAddress)} | " + + $"[blue]Board:[/] {(Program.selectedDevice.isLilygo ? "LilygoS3Amoled" : (Program.selectedDevice.isS3 ? "ESP32-S3" : "Standard"))} | " + + $"[blue]ID:[/] 0x{Program.selectedDevice.ZeID:X4} | " + + $"[blue]Version:[/] {Program.selectedDevice.MajVersion}.{Program.selectedDevice.MinVersion}.{Program.selectedDevice.PatVersion}") + { + Border = BoxBorder.Rounded, + Padding = new Padding(1, 1), + }; + AnsiConsole.Write(panel); + } + + private static nint ShowCalibrationImage(nint _pZeDMD) + { + // Display the calibration image + Esp32Device.ZeDMD_SetFrameSize(_pZeDMD, 128, 32); + IntPtr pImageRgb = IntPtr.Zero; + var imageRgbArray = Esp32Device.ReadRawRGBFile("RGBW.raw"); + + pImageRgb = Marshal.AllocHGlobal(imageRgbArray.Length); + Marshal.Copy(imageRgbArray, 0, pImageRgb, imageRgbArray.Length); + // Render and keep displayed + Esp32Device.ZeDMD_ClearScreen(_pZeDMD); + Esp32Device.ZeDMD_RenderRgb888(_pZeDMD, pImageRgb); + return pImageRgb; + } + + internal static void ShowRgbOrder() + { + // Set RGB Order + ShowTop(); + nint _pZeDMD; + bool dmdDevice = OpenZeDMD(out _pZeDMD); + if (!dmdDevice) + { + return; + } + + nint pImageRgb = ShowCalibrationImage(_pZeDMD); + + // Get the current RGB Order + var currentRGBOrder = Program.selectedDevice.RgbOrder; + + // Use a Spectre Console bar to set the RGB Order + int currentValue = currentRGBOrder; + bool selecting = true; + + while (selecting) + { + ShowTop(); + pImageRgb = ShowCalibrationImage(_pZeDMD); + var progress = new Progress(AnsiConsole.Console); + progress.Start(ctx => + { + var task = ctx.AddTask($"[green]RGB Order ({currentValue}/5)[/]", maxValue: 5); + task.Value = currentValue; + task.IsIndeterminate = false; + }); + + AnsiConsole.MarkupLine($"\nUse [blue]←[/] and [blue]→[/] to adjust, [green]Enter[/] to confirm"); + AnsiConsole.MarkupLine($"Current value: {currentValue}"); + + var key = Console.ReadKey(true); + switch (key.Key) + { + case ConsoleKey.LeftArrow: + if (currentValue > -0) + { + currentValue--; + AnsiConsole.MarkupLine($"[green]Setting RGB Order to {currentValue}...[/]"); + Esp32Device.ZeDMD_SetRGBOrder(_pZeDMD, (byte)currentValue); + Esp32Device.ZeDMD_SaveSettings(_pZeDMD); + Esp32Device.ZeDMD_Reset(_pZeDMD); + } + break; + case ConsoleKey.RightArrow: + if (currentValue < 5) + { + currentValue++; + AnsiConsole.MarkupLine($"[green]Setting RGB Order to {currentValue}...[/]"); + Esp32Device.ZeDMD_SetRGBOrder(_pZeDMD, (byte)currentValue); + Esp32Device.ZeDMD_SaveSettings(_pZeDMD); + Esp32Device.ZeDMD_Reset(_pZeDMD); + } + break; + case ConsoleKey.Enter: + selecting = false; + break; + } + } + Program.selectedDevice.RgbOrder = currentValue; + + if (pImageRgb != IntPtr.Zero) + { + Marshal.FreeHGlobal(pImageRgb); + } + + AnsiConsole.MarkupLine($"[green]Closing device...[/]"); + Esp32Device.ZeDMD_Close(_pZeDMD); + } + + internal static void ShowTransportMode() + { + throw new NotImplementedException(); + } + + internal static void ShowUdpDelay() + { + throw new NotImplementedException(); + } + + internal static void ShowUsbPacketSize() + { + throw new NotImplementedException(); + } + + internal static void ShowWifiSettings() + { + ShowTop(); + nint _pZeDMD; + bool dmdDevice = OpenZeDMD(out _pZeDMD); + if (!dmdDevice) + { + return; + } + + try + { + // Get current WiFi settings + var currentSSID = Program.selectedDevice.SSID; + var currentPort = Program.selectedDevice.SSIDPort; + + var ssid = AnsiConsole.Prompt( + new TextPrompt("Enter WiFi SSID:") + .DefaultValue(currentSSID) + .AllowEmpty()); + + var password = AnsiConsole.Prompt( + new TextPrompt("Enter WiFi Password:") + .Secret() + .AllowEmpty()); + + var port = AnsiConsole.Prompt( + new TextPrompt("Enter WiFi Port:") + .DefaultValue(currentPort) + .ValidationErrorMessage("[red]Please enter a valid port number (1-65535)[/]") + .Validate(port => + { + return port switch + { + < 1 => ValidationResult.Error("[red]Port must be greater than 0[/]"), + > 65535 => ValidationResult.Error("[red]Port must be less than 65536[/]"), + _ => ValidationResult.Success(), + }; + })); + + // Confirm settings + var table = new Table() + .AddColumn("Setting") + .AddColumn("Value"); + + table.AddRow("SSID", ssid); + table.AddRow("Password", "********"); + table.AddRow("Port", port.ToString()); + + AnsiConsole.Write(table); + + if (AnsiConsole.Confirm("Apply these settings?")) + { + AnsiConsole.Status() + .Start("Applying WiFi settings...", async ctx => + { + // Apply the settings + Esp32Device.ZeDMD_SetWiFiSSID(_pZeDMD, ssid); + if (!string.IsNullOrEmpty(password)) + { + Esp32Device.ZeDMD_SetWiFiPassword(_pZeDMD, password); + } + Esp32Device.ZeDMD_SetWiFiPort(_pZeDMD, port); + // Change transport mode + Esp32Device.ZeDMD_SetTransport(_pZeDMD, 1); + + // Save settings + Esp32Device.ZeDMD_SaveSettings(_pZeDMD); + Thread.Sleep(2000); + Esp32Device.ZeDMD_Reset(_pZeDMD); + + AnsiConsole.MarkupLine("[green]WiFi settings updated successfully![/]"); + Thread.Sleep(2000); // Give user time to read the message + await Program.GetZeDMDDevices(); + }); + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error updating WiFi settings: {ex.Message}[/]"); + Thread.Sleep(2000); + } + finally + { + // Always close the device + Esp32Device.ZeDMD_Close(_pZeDMD); + } + } + } diff --git a/ZeDMDUpdater.csproj b/ZeDMDUpdater.csproj index 4ff1985..596cd1a 100644 --- a/ZeDMDUpdater.csproj +++ b/ZeDMDUpdater.csproj @@ -1,15 +1,40 @@  - Exe net8.0 enable enable + osx-arm64;osx-x64;win-x64;linux-x64 + + + + + + + + + + + + + + + + + + + + PreserveNewest + +