From cd8c655e610449485c84d2c143095f50ade6d8d2 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Mon, 9 Mar 2026 21:06:47 -0500 Subject: [PATCH 1/8] Setup pathway to flash firmware --- LumenLabInstallerForm.cs | 63 +++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index ee9f13e..024d986 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -95,6 +95,8 @@ private async void LumenLabInstallerForm_Shown(object sender, EventArgs e) private async Task QueryGitHub() { var releaseService = new GitHubReleaseService(); + + WriteToLogs("Querying GitHub to check for updates."); var releases = await releaseService.GetReleasesAsync(); _context.AvailableReleases = releases.OrderByDescending(r => r.PublishedAt).ToList(); @@ -191,7 +193,7 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) return; } - var selectedReleaseVersionId = new Version(selectedRelease.TagName.Substring(1)); + var selectedReleaseVersionId = new Version(selectedRelease?.TagName?.Substring(1) ?? "0.0.0"); DialogResult userDialogSelection; if (selectedReleaseVersionId == _context.FirmwareVersion) @@ -212,36 +214,37 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) return; } } + BtnFlashFirmware.Enabled = false; + + try + { + var latestRelease = _context.AvailableReleases[0] ?? null; + if (latestRelease == null) + { + // second-pass refactor: more specific error + throw new Exception("Error"); + } + + WriteToLogs("Downloading LumenLab v0.3.0."); + var cts = new CancellationTokenSource(); + var result = await _downloadService.DownloadAsync( + new Uri($"https://github.com/ericmcdaniel/lumenlab/releases/download/{latestRelease.TagName}/lumenlab-firmware.zip"), + Path.Combine(AppPaths.BinariesPath, "lumenlab-firmware.zip") + ); - MessageBox.Show(selectedRelease.TagName); - MessageBox.Show(selectedRelease.Assets[0].Name); - - BtnFlashFirmware.Enabled = true; - - //btnDownload.Enabled = false; - - //try - //{ - // var cts = new CancellationTokenSource(); - // WriteToLogs("Downloading LumenLab v0.3.0."); - // var result = await _downloadService.DownloadAsync( - // new Uri("https://github.com/ericmcdaniel/lumenlab/archive/refs/tags/v0.3.0.zip"), - // Path.Combine(AppPaths.BinariesPath, "lumenlab_v0-3-0.zip") - // ); - - // WriteToLogs("Download complete."); - // WriteToLogs($"Downloaded {result.BytesWritten.Bytes()}."); - // await ToolManager.RunEsptoolAsync("--chip esp32 --baud 921600 write_flash -z 0x1000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\bootloader.bin 0x8000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\partitions.bin 0x10000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\firmware.bin", AppendLog, cts.Token); - - //} - //catch (Exception ex) - //{ - // MessageBox.Show(ex.Message); - //} - //finally - //{ - // btnDownload.Enabled = true; - //} + WriteToLogs("Download complete."); + WriteToLogs($"Downloaded {result.BytesWritten.Bytes()}."); + await ToolManager.RunEsptoolAsync("--chip esp32 --baud 921600 write_flash -z 0x1000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\bootloader.bin 0x8000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\partitions.bin 0x10000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\firmware.bin", AppendLog, cts.Token); + + } + catch (Exception ex) + { + MessageBox.Show(ex.Message); + } + finally + { + BtnFlashFirmware.Enabled = true; + } } private async void BtnSync_Click(object sender, EventArgs e) { From c2a5695b5c19c0cbf82ecc9e0d2bca8938c3de50 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Mon, 9 Mar 2026 21:17:54 -0500 Subject: [PATCH 2/8] Complete flash sequence, use safe paths --- LumenLabInstallerForm.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index 024d986..bfb9934 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -9,6 +9,7 @@ using LumenLabInstaller.Services; using LumenLabInstaller.Models; using System.Reflection; +using System.IO.Compression; namespace LumenLabInstaller @@ -225,16 +226,26 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) throw new Exception("Error"); } - WriteToLogs("Downloading LumenLab v0.3.0."); + WriteToLogs($"Downloading LumenLab {latestRelease.TagName}"); var cts = new CancellationTokenSource(); var result = await _downloadService.DownloadAsync( new Uri($"https://github.com/ericmcdaniel/lumenlab/releases/download/{latestRelease.TagName}/lumenlab-firmware.zip"), Path.Combine(AppPaths.BinariesPath, "lumenlab-firmware.zip") ); + string compressedLumenLabPath = Path.Combine(AppPaths.BinariesPath, "lumenlab-firmware.zip"); + string uncompressedLumenLabPath = Path.Combine(AppPaths.BinariesPath, latestRelease.TagName ?? "v99.99.99"); + + ZipFile.ExtractToDirectory(compressedLumenLabPath, uncompressedLumenLabPath, true); + WriteToLogs("Download complete."); WriteToLogs($"Downloaded {result.BytesWritten.Bytes()}."); - await ToolManager.RunEsptoolAsync("--chip esp32 --baud 921600 write_flash -z 0x1000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\bootloader.bin 0x8000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\partitions.bin 0x10000 C:\\Users\\McDan\\development\\lumenlab\\.pio\\build\\release\\firmware.bin", AppendLog, cts.Token); + + string esptoolFullCmd = $"--chip esp32 --baud 921600 write_flash -z 0x1000 {Path.Combine(uncompressedLumenLabPath, "bootloader.bin")} 0x8000 {Path.Combine(uncompressedLumenLabPath, "partitions.bin")} 0x10000 {Path.Combine(uncompressedLumenLabPath, "firmware.bin")}"; + WriteToLogs($"Running esptool.exe {esptoolFullCmd}"); + await ToolManager.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); + + WriteToLogs($"Successful installation of LumenLab {latestRelease.TagName}!"); } catch (Exception ex) From 93238c725c14e70dff59ac9e2c1afccffdeec069 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Mon, 9 Mar 2026 21:29:15 -0500 Subject: [PATCH 3/8] Handle error if Esptool fails --- LumenLabInstallerForm.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index bfb9934..faea745 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -241,12 +241,17 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) WriteToLogs("Download complete."); WriteToLogs($"Downloaded {result.BytesWritten.Bytes()}."); - string esptoolFullCmd = $"--chip esp32 --baud 921600 write_flash -z 0x1000 {Path.Combine(uncompressedLumenLabPath, "bootloader.bin")} 0x8000 {Path.Combine(uncompressedLumenLabPath, "partitions.bin")} 0x10000 {Path.Combine(uncompressedLumenLabPath, "firmware.bin")}"; + string esptoolFullCmd = $"--chip esp32 --baud 921600 --port {port} write_flash -z 0x1000 {Path.Combine(uncompressedLumenLabPath, "bootloader.bin")} 0x8000 {Path.Combine(uncompressedLumenLabPath, "partitions.bin")} 0x10000 {Path.Combine(uncompressedLumenLabPath, "firmware.bin")}"; WriteToLogs($"Running esptool.exe {esptoolFullCmd}"); - await ToolManager.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); - - WriteToLogs($"Successful installation of LumenLab {latestRelease.TagName}!"); + var exitCode = await ToolManager.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); + if (exitCode == 0) + { + WriteToLogs($"Successful installation of LumenLab {latestRelease.TagName}."); + } else + { + WriteToLogs($"Failed to flash LumenLab {latestRelease.TagName}."); + } } catch (Exception ex) { From 5f4f2ed7035648b19f471511d18b315880a73431 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Tue, 10 Mar 2026 19:44:31 -0500 Subject: [PATCH 4/8] Clean up work directory --- LumenLabInstallerForm.cs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index faea745..78297f2 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -154,12 +154,12 @@ private async Task ReadConnectedDeviceVersion() LblInstalledVersion.Text = $"v{_context.FirmwareVersion.ToString()}"; - WriteToLogs(version); - //if (_context.FirmwareVersion == ) + WriteToLogs($"Currently installed version: {version}"); + //if (_context.FirmwareVersion == ) TODO: update the label to inform user if they're out of date or current. BtnFlashFirmware.Enabled = true; } - WriteToLogs($"Scan complete."); + WriteToLogs($"Device scan complete. Ready for firmware upgrade."); BtnSync.Enabled = true; this.UseWaitCursor = false; } @@ -177,6 +177,7 @@ private void AppendLog(string text) private async void BtnFlashFirmware_Click(object sender, EventArgs e) { + WriteToLogs("Starting LumenLab firmware upgrade."); BtnFlashFirmware.Enabled = false; string? port = await _deviceService.DetectLumenLabPortAsync(); if (port == null) @@ -202,7 +203,8 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) userDialogSelection = MessageBox.Show($"You are about to install LumenLab version {selectedReleaseVersionId}, however you already have a that version installed.\n\nAre you sure you want to overwrite?", "Continue with overwrite?", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); if (userDialogSelection == DialogResult.Cancel) { - MessageBox.Show("LumenLab firmware upgrade operation cancelled.", "Operation canceled", MessageBoxButtons.OK, MessageBoxIcon.Information); + WriteToLogs("Cancelled LumenLab firmware update."); + BtnFlashFirmware.Enabled = true; return; } } @@ -211,7 +213,8 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) userDialogSelection = MessageBox.Show($"You are about to install LumenLab version {selectedReleaseVersionId}, however you already have a newer version {_context.FirmwareVersion} installed.\n\nAre you sure you want to downgrade?", "Continue with downgrade?", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); if (userDialogSelection == DialogResult.Cancel) { - MessageBox.Show("LumenLab firmware upgrade operation cancelled.", "Operation canceled", MessageBoxButtons.OK, MessageBoxIcon.Information); + WriteToLogs("Cancelled LumenLab firmware update."); + BtnFlashFirmware.Enabled = true; return; } } @@ -226,22 +229,29 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) throw new Exception("Error"); } + string downloadPath = Path.Combine(AppPaths.BinariesPath, latestRelease.TagName ?? "v99.99.99"); + if (!Directory.Exists(downloadPath)) + { + WriteToLogs($"Creating directory {downloadPath}"); + Directory.CreateDirectory(downloadPath); + } + WriteToLogs($"Downloading LumenLab {latestRelease.TagName}"); var cts = new CancellationTokenSource(); var result = await _downloadService.DownloadAsync( new Uri($"https://github.com/ericmcdaniel/lumenlab/releases/download/{latestRelease.TagName}/lumenlab-firmware.zip"), - Path.Combine(AppPaths.BinariesPath, "lumenlab-firmware.zip") + Path.Combine(downloadPath, "lumenlab-firmware.zip") ); - string compressedLumenLabPath = Path.Combine(AppPaths.BinariesPath, "lumenlab-firmware.zip"); - string uncompressedLumenLabPath = Path.Combine(AppPaths.BinariesPath, latestRelease.TagName ?? "v99.99.99"); + string compressedLumenLabPath = Path.Combine(downloadPath, "lumenlab-firmware.zip"); - ZipFile.ExtractToDirectory(compressedLumenLabPath, uncompressedLumenLabPath, true); + ZipFile.ExtractToDirectory(compressedLumenLabPath, downloadPath, true); + File.Delete(compressedLumenLabPath); WriteToLogs("Download complete."); WriteToLogs($"Downloaded {result.BytesWritten.Bytes()}."); - string esptoolFullCmd = $"--chip esp32 --baud 921600 --port {port} write_flash -z 0x1000 {Path.Combine(uncompressedLumenLabPath, "bootloader.bin")} 0x8000 {Path.Combine(uncompressedLumenLabPath, "partitions.bin")} 0x10000 {Path.Combine(uncompressedLumenLabPath, "firmware.bin")}"; + string esptoolFullCmd = $"--chip esp32 --baud 921600 --port {port} write_flash -z 0x1000 {Path.Combine(downloadPath, "bootloader.bin")} 0x8000 {Path.Combine(downloadPath, "partitions.bin")} 0x10000 {Path.Combine(downloadPath, "firmware.bin")}"; WriteToLogs($"Running esptool.exe {esptoolFullCmd}"); var exitCode = await ToolManager.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); From a45e3eef224d9464e47dfc225551b99d2019d430 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Tue, 10 Mar 2026 22:48:11 -0500 Subject: [PATCH 5/8] Ensure Esptool.exe is available for first-time users --- LumenLabInstallerForm.Designer.cs | 4 +- LumenLabInstallerForm.cs | 499 ++++++++++++++--------------- Services/DeviceDiscoveryService.cs | 43 ++- Services/FirmwareService.cs | 48 +-- Services/ToolManager.cs | 3 +- 5 files changed, 297 insertions(+), 300 deletions(-) diff --git a/LumenLabInstallerForm.Designer.cs b/LumenLabInstallerForm.Designer.cs index c47e021..5403673 100644 --- a/LumenLabInstallerForm.Designer.cs +++ b/LumenLabInstallerForm.Designer.cs @@ -139,7 +139,7 @@ private void InitializeComponent() BtnSync.Name = "BtnSync"; BtnSync.Size = new Size(174, 26); BtnSync.TabIndex = 4; - BtnSync.Text = "Refresh Device List"; + BtnSync.Text = "Scan Devices"; BtnSync.UseVisualStyleBackColor = true; BtnSync.Click += BtnSync_Click; // @@ -455,8 +455,6 @@ private void InitializeComponent() LblUpdateAlert.Size = new Size(432, 21); LblUpdateAlert.TabIndex = 30; LblUpdateAlert.Text = "Plug in your LumenLab to continue."; - LblUpdateAlert.SelectAll(); - LblUpdateAlert.SelectionAlignment = HorizontalAlignment.Center; // // LumenLabInstallerForm // diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index 78297f2..4f92369 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -12,330 +12,329 @@ using System.IO.Compression; -namespace LumenLabInstaller +namespace LumenLabInstaller; + +public partial class LumenLabInstallerForm : Form { - public partial class LumenLabInstallerForm : Form + private readonly InstallerContext _context; + private readonly DeviceDiscoveryService _deviceService; + private readonly FirmwareService _firmwareService; + private readonly BinaryDownloadService _downloadService; + private readonly BindingSource _releaseBindingSource = []; + + + public LumenLabInstallerForm(BinaryDownloadService downloadService, + InstallerContext context, + FirmwareService firmwareService, + DeviceDiscoveryService deviceService) { - private readonly InstallerContext _context; - private readonly DeviceDiscoveryService _deviceService; - private readonly FirmwareService _firmwareService; - private readonly BinaryDownloadService _downloadService; + _downloadService = downloadService; + _context = context; + _firmwareService = firmwareService; + _deviceService = deviceService; + InitializeComponent(); + ConfigureReleaseGrid(); + } - private BindingSource _releaseBindingSource = []; + private void ConfigureReleaseGrid() + { + DgvGitHubReleases.AutoGenerateColumns = false; + DgvGitHubReleases.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + DgvGitHubReleases.MultiSelect = false; + DgvGitHubReleases.ReadOnly = true; + DgvGitHubReleases.AllowUserToAddRows = false; + DgvGitHubReleases.AllowUserToDeleteRows = false; + DgvGitHubReleases.Columns.Clear(); - public LumenLabInstallerForm(BinaryDownloadService downloadService, - InstallerContext context, - FirmwareService firmwareService, - DeviceDiscoveryService deviceService) + DgvGitHubReleases.Columns.Add(new DataGridViewTextBoxColumn { - _downloadService = downloadService; - _context = context; - _firmwareService = firmwareService; - _deviceService = deviceService; - InitializeComponent(); - ConfigureReleaseGrid(); - } + DataPropertyName = "TagName", + HeaderText = "Version", + Width = 60 + }); - private void ConfigureReleaseGrid() + DgvGitHubReleases.Columns.Add(new DataGridViewTextBoxColumn { - DgvGitHubReleases.AutoGenerateColumns = false; - DgvGitHubReleases.SelectionMode = DataGridViewSelectionMode.FullRowSelect; - DgvGitHubReleases.MultiSelect = false; - DgvGitHubReleases.ReadOnly = true; - DgvGitHubReleases.AllowUserToAddRows = false; - DgvGitHubReleases.AllowUserToDeleteRows = false; - - DgvGitHubReleases.Columns.Clear(); + DataPropertyName = "Name", + HeaderText = "Release Name", + Width = 230 + }); - DgvGitHubReleases.Columns.Add(new DataGridViewTextBoxColumn + DgvGitHubReleases.Columns.Add(new DataGridViewTextBoxColumn + { + DataPropertyName = "PublishedAt", + HeaderText = "Published", + DefaultCellStyle = new DataGridViewCellStyle { - DataPropertyName = "TagName", - HeaderText = "Version", - Width = 60 - }); + Format = "MM/dd/yyyy" + } + }); - DgvGitHubReleases.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "Name", - HeaderText = "Release Name", - Width = 230 - }); + DgvGitHubReleases.DataSource = _releaseBindingSource; + } - DgvGitHubReleases.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "PublishedAt", - HeaderText = "Published", - DefaultCellStyle = new DataGridViewCellStyle - { - Format = "MM/dd/yyyy" - } - }); - - DgvGitHubReleases.DataSource = _releaseBindingSource; + private async void LumenLabInstallerForm_Shown(object sender, EventArgs e) + { + this.UseWaitCursor = true; + BtnFlashFirmware.Enabled = false; + try + { + await QueryGitHub(); + await ReadConnectedDeviceVersion(); } - - private async void LumenLabInstallerForm_Shown(object sender, EventArgs e) + catch (Exception ex) { - this.UseWaitCursor = true; - BtnFlashFirmware.Enabled = false; - try + MessageBox.Show($"Error retrieving releases:{Environment.NewLine}{ex.Message}", + "GitHub API Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + + private async Task QueryGitHub() + { + var releaseService = new GitHubReleaseService(); + + WriteToLogs("Querying GitHub to check for updates."); + var releases = await releaseService.GetReleasesAsync(); + + _context.AvailableReleases = releases.OrderByDescending(r => r.PublishedAt).ToList(); + + var gridRows = releases + .OrderByDescending(r => r.PublishedAt) + .Select(r => new ReleaseGridRow { - await QueryGitHub(); - await ReadConnectedDeviceVersion(); - } - catch (Exception ex) + TagName = r.TagName ?? string.Empty, + Name = r.Name?.Substring(r.Name.IndexOf("-") + 2) ?? string.Empty, + PublishedAt = r.PublishedAt ?? new DateTime(), + Release = r + }) + .ToList(); + + _releaseBindingSource.DataSource = gridRows; + + LblLatestRelease.Text = releases[0].TagName; + + foreach (var release in releases) + { + WriteToLogs($"{release.TagName} - {release.PublishedAt}"); + foreach (var asset in release.Assets) { - MessageBox.Show($"Error retrieving releases:{Environment.NewLine}{ex.Message}", - "GitHub API Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error); + WriteToLogs($" Asset: {asset.Name} -> {asset.BrowserDownloadUrl}"); } } - private async Task QueryGitHub() - { - var releaseService = new GitHubReleaseService(); + // PopulateReleaseTable(releases); + } - WriteToLogs("Querying GitHub to check for updates."); - var releases = await releaseService.GetReleasesAsync(); + private async Task ReadConnectedDeviceVersion() + { + BtnSync.Enabled = false; + this.UseWaitCursor = true; + BtnFlashFirmware.Enabled = false; + LblInstalledVersion.Text = "N/A"; - _context.AvailableReleases = releases.OrderByDescending(r => r.PublishedAt).ToList(); + WriteToLogs($"Scanning USB ports to find LumenLab."); - var gridRows = releases - .OrderByDescending(r => r.PublishedAt) - .Select(r => new ReleaseGridRow - { - TagName = r.TagName ?? string.Empty, - Name = r.Name?.Substring(r.Name.IndexOf("-") + 2) ?? string.Empty, - PublishedAt = r.PublishedAt ?? new DateTime(), - Release = r - }) - .ToList(); + string? port = await DeviceDiscoveryService.DetectLumenLabPortAsync(); + _context.PortName = port; - _releaseBindingSource.DataSource = gridRows; + if (port == null) + { + WriteToLogs("LumenLab not detected."); + } + else + { + WriteToLogs($"LumenLab detected on {port}"); - LblLatestRelease.Text = releases[0].TagName; + string? version = await FirmwareService.ReadFirmwareVersionAsync(port); + _context.FirmwareVersion = new Version(version.Substring(1)); - foreach (var release in releases) - { - WriteToLogs($"{release.TagName} - {release.PublishedAt}"); - foreach (var asset in release.Assets) - { - WriteToLogs($" Asset: {asset.Name} -> {asset.BrowserDownloadUrl}"); - } - } + LblInstalledVersion.Text = $"v{_context.FirmwareVersion.ToString()}"; - // PopulateReleaseTable(releases); + WriteToLogs($"Currently installed version: {version}"); + //if (_context.FirmwareVersion == ) TODO: update the label to inform user if they're out of date or current. + BtnFlashFirmware.Enabled = true; } - private async Task ReadConnectedDeviceVersion() + WriteToLogs($"Device scan complete. Ready for firmware upgrade."); + BtnSync.Enabled = true; + this.UseWaitCursor = false; + } + + private void AppendLog(string text) + { + if (InvokeRequired) { - BtnSync.Enabled = false; - this.UseWaitCursor = true; - BtnFlashFirmware.Enabled = false; - LblInstalledVersion.Text = "N/A"; + Invoke(new Action(AppendLog), text); + return; + } - WriteToLogs($"Scanning USB ports to find LumenLab."); + WriteToLogs(text); + } - string? port = await _deviceService.DetectLumenLabPortAsync(); - _context.PortName = port; + private async void BtnFlashFirmware_Click(object sender, EventArgs e) + { + WriteToLogs("Starting LumenLab firmware upgrade."); + BtnFlashFirmware.Enabled = false; + string? port = await DeviceDiscoveryService.DetectLumenLabPortAsync(); + if (port == null) + { + LblInstalledVersion.Text = "N/A"; + MessageBox.Show("LumenLab was disconnected. Please connect the device and run again.", "No LumenLab detected", MessageBoxButtons.RetryCancel, MessageBoxIcon.Stop); + return; + } - if (port == null) - { - WriteToLogs("LumenLab not detected."); - } - else - { - WriteToLogs($"LumenLab detected on {port}"); + var selectedRelease = GetSelectedRelease(); - string version = await _firmwareService.ReadFirmwareVersionAsync(port); - _context.FirmwareVersion = new Version(version.Substring(1)); + if (selectedRelease == null) + { + MessageBox.Show("Please select a version to install.", "Select Version", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } - LblInstalledVersion.Text = $"v{_context.FirmwareVersion.ToString()}"; + var selectedReleaseVersionId = new Version(selectedRelease?.TagName?.Substring(1) ?? "0.0.0"); - WriteToLogs($"Currently installed version: {version}"); - //if (_context.FirmwareVersion == ) TODO: update the label to inform user if they're out of date or current. + DialogResult userDialogSelection; + if (selectedReleaseVersionId == _context.FirmwareVersion) + { + userDialogSelection = MessageBox.Show($"You are about to install LumenLab version {selectedReleaseVersionId}, however you already have a that version installed.\n\nAre you sure you want to overwrite?", "Continue with overwrite?", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); + if (userDialogSelection == DialogResult.Cancel) + { + WriteToLogs("Cancelled LumenLab firmware update."); BtnFlashFirmware.Enabled = true; + return; } - - WriteToLogs($"Device scan complete. Ready for firmware upgrade."); - BtnSync.Enabled = true; - this.UseWaitCursor = false; } - - private void AppendLog(string text) + else if (selectedReleaseVersionId < _context.FirmwareVersion) { - if (InvokeRequired) + userDialogSelection = MessageBox.Show($"You are about to install LumenLab version {selectedReleaseVersionId}, however you already have a newer version {_context.FirmwareVersion} installed.\n\nAre you sure you want to downgrade?", "Continue with downgrade?", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); + if (userDialogSelection == DialogResult.Cancel) { - Invoke(new Action(AppendLog), text); + WriteToLogs("Cancelled LumenLab firmware update."); + BtnFlashFirmware.Enabled = true; return; } - - WriteToLogs(text); } + BtnFlashFirmware.Enabled = false; - private async void BtnFlashFirmware_Click(object sender, EventArgs e) + try { - WriteToLogs("Starting LumenLab firmware upgrade."); - BtnFlashFirmware.Enabled = false; - string? port = await _deviceService.DetectLumenLabPortAsync(); - if (port == null) + var latestRelease = _context.AvailableReleases[0] ?? null; + if (latestRelease == null) { - LblInstalledVersion.Text = "N/A"; - MessageBox.Show("LumenLab was disconnected. Please connect the device and run again.", "No LumenLab detected", MessageBoxButtons.RetryCancel, MessageBoxIcon.Stop); - return; + // second-pass refactor: more specific error + throw new Exception("Error"); } - var selectedRelease = GetSelectedRelease(); - - if (selectedRelease == null) + string downloadPath = Path.Combine(AppPaths.BinariesPath, latestRelease.TagName ?? "v99.99.99"); + if (!Directory.Exists(downloadPath)) { - MessageBox.Show("Please select a version to install.", "Select Version", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; + WriteToLogs($"Creating directory {downloadPath}"); + Directory.CreateDirectory(downloadPath); } - var selectedReleaseVersionId = new Version(selectedRelease?.TagName?.Substring(1) ?? "0.0.0"); + WriteToLogs($"Downloading LumenLab {latestRelease.TagName}"); + var cts = new CancellationTokenSource(); + var result = await _downloadService.DownloadAsync( + new Uri($"https://github.com/ericmcdaniel/lumenlab/releases/download/{latestRelease.TagName}/lumenlab-firmware.zip"), + Path.Combine(downloadPath, "lumenlab-firmware.zip") + ); - DialogResult userDialogSelection; - if (selectedReleaseVersionId == _context.FirmwareVersion) - { - userDialogSelection = MessageBox.Show($"You are about to install LumenLab version {selectedReleaseVersionId}, however you already have a that version installed.\n\nAre you sure you want to overwrite?", "Continue with overwrite?", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); - if (userDialogSelection == DialogResult.Cancel) - { - WriteToLogs("Cancelled LumenLab firmware update."); - BtnFlashFirmware.Enabled = true; - return; - } - } - else if (selectedReleaseVersionId < _context.FirmwareVersion) - { - userDialogSelection = MessageBox.Show($"You are about to install LumenLab version {selectedReleaseVersionId}, however you already have a newer version {_context.FirmwareVersion} installed.\n\nAre you sure you want to downgrade?", "Continue with downgrade?", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); - if (userDialogSelection == DialogResult.Cancel) - { - WriteToLogs("Cancelled LumenLab firmware update."); - BtnFlashFirmware.Enabled = true; - return; - } - } - BtnFlashFirmware.Enabled = false; + string compressedLumenLabPath = Path.Combine(downloadPath, "lumenlab-firmware.zip"); - try - { - var latestRelease = _context.AvailableReleases[0] ?? null; - if (latestRelease == null) - { - // second-pass refactor: more specific error - throw new Exception("Error"); - } - - string downloadPath = Path.Combine(AppPaths.BinariesPath, latestRelease.TagName ?? "v99.99.99"); - if (!Directory.Exists(downloadPath)) - { - WriteToLogs($"Creating directory {downloadPath}"); - Directory.CreateDirectory(downloadPath); - } - - WriteToLogs($"Downloading LumenLab {latestRelease.TagName}"); - var cts = new CancellationTokenSource(); - var result = await _downloadService.DownloadAsync( - new Uri($"https://github.com/ericmcdaniel/lumenlab/releases/download/{latestRelease.TagName}/lumenlab-firmware.zip"), - Path.Combine(downloadPath, "lumenlab-firmware.zip") - ); - - string compressedLumenLabPath = Path.Combine(downloadPath, "lumenlab-firmware.zip"); - - ZipFile.ExtractToDirectory(compressedLumenLabPath, downloadPath, true); - File.Delete(compressedLumenLabPath); - - WriteToLogs("Download complete."); - WriteToLogs($"Downloaded {result.BytesWritten.Bytes()}."); - - string esptoolFullCmd = $"--chip esp32 --baud 921600 --port {port} write_flash -z 0x1000 {Path.Combine(downloadPath, "bootloader.bin")} 0x8000 {Path.Combine(downloadPath, "partitions.bin")} 0x10000 {Path.Combine(downloadPath, "firmware.bin")}"; - WriteToLogs($"Running esptool.exe {esptoolFullCmd}"); - var exitCode = await ToolManager.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); - - if (exitCode == 0) - { - WriteToLogs($"Successful installation of LumenLab {latestRelease.TagName}."); - } else - { - WriteToLogs($"Failed to flash LumenLab {latestRelease.TagName}."); - } - } - catch (Exception ex) + ZipFile.ExtractToDirectory(compressedLumenLabPath, downloadPath, true); + File.Delete(compressedLumenLabPath); + + WriteToLogs("Download complete."); + WriteToLogs($"Downloaded {result.BytesWritten.Bytes()}."); + + string esptoolFullCmd = $"--chip esp32 --baud 921600 --port {port} write_flash -z 0x1000 {Path.Combine(downloadPath, "bootloader.bin")} 0x8000 {Path.Combine(downloadPath, "partitions.bin")} 0x10000 {Path.Combine(downloadPath, "firmware.bin")}"; + WriteToLogs($"Running esptool.exe {esptoolFullCmd}"); + var exitCode = await ToolManager.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); + + if (exitCode == 0) { - MessageBox.Show(ex.Message); - } - finally + WriteToLogs($"Successful installation of LumenLab {latestRelease.TagName}."); + WriteToLogs("You may safely exit this tool."); + } else { - BtnFlashFirmware.Enabled = true; + WriteToLogs($"Failed to flash LumenLab {latestRelease.TagName}."); } } - private async void BtnSync_Click(object sender, EventArgs e) + catch (Exception ex) { - await ReadConnectedDeviceVersion(); + MessageBox.Show(ex.Message); } - - private GitHubRelease? GetSelectedRelease() + finally { - if (_releaseBindingSource.Current is ReleaseGridRow row) - return row.Release; - - return null; + BtnFlashFirmware.Enabled = true; } + } + private async void BtnSync_Click(object sender, EventArgs e) + { + await ReadConnectedDeviceVersion(); + } - private void exitToolStripMenuItem_Click(object sender, EventArgs e) - { - Application.Exit(); - } + private GitHubRelease? GetSelectedRelease() + { + if (_releaseBindingSource.Current is ReleaseGridRow row) + return row.Release; - private void ChkCustomizeConfiguration_CheckedChanged(object sender, EventArgs e) - { - TxtNumLeds.Enabled = !TxtNumLeds.Enabled; - LblTotalLeds.Enabled = !LblTotalLeds.Enabled; + return null; + } - LblControllerType.Enabled = !LblControllerType.Enabled; - RadPS3.Enabled = !RadPS3.Enabled; + private void exitToolStripMenuItem_Click(object sender, EventArgs e) + { + Application.Exit(); + } - TxtMacAddress.Enabled = !TxtMacAddress.Enabled; - LblMacAddress.Enabled = !LblMacAddress.Enabled; + private void ChkCustomizeConfiguration_CheckedChanged(object sender, EventArgs e) + { + TxtNumLeds.Enabled = !TxtNumLeds.Enabled; + LblTotalLeds.Enabled = !LblTotalLeds.Enabled; - TxtSerialBaud.Enabled = !TxtSerialBaud.Enabled; - LblSerialBaud.Enabled = !LblSerialBaud.Enabled; + LblControllerType.Enabled = !LblControllerType.Enabled; + RadPS3.Enabled = !RadPS3.Enabled; - TxtBound1.Enabled = !TxtBound1.Enabled; - TxtBound2.Enabled = !TxtBound2.Enabled; - TxtBound3.Enabled = !TxtBound3.Enabled; - LblBoundaries.Enabled = !LblBoundaries.Enabled; - } + TxtMacAddress.Enabled = !TxtMacAddress.Enabled; + LblMacAddress.Enabled = !LblMacAddress.Enabled; + + TxtSerialBaud.Enabled = !TxtSerialBaud.Enabled; + LblSerialBaud.Enabled = !LblSerialBaud.Enabled; - private void toggleDebugWindowToolStripMenuItem_Click(object sender, EventArgs e) + TxtBound1.Enabled = !TxtBound1.Enabled; + TxtBound2.Enabled = !TxtBound2.Enabled; + TxtBound3.Enabled = !TxtBound3.Enabled; + LblBoundaries.Enabled = !LblBoundaries.Enabled; + } + + private void toggleDebugWindowToolStripMenuItem_Click(object sender, EventArgs e) + { + toggleDebugWindowToolStripMenuItem.Checked = !toggleDebugWindowToolStripMenuItem.Checked; + outputBox.Visible = !outputBox.Visible; + if (outputBox.Visible) { - toggleDebugWindowToolStripMenuItem.Checked = !toggleDebugWindowToolStripMenuItem.Checked; - outputBox.Visible = !outputBox.Visible; - if (outputBox.Visible) - { - this.Height += outputBox.Height; - } - else - { - this.Height -= outputBox.Height; - } + this.Height += outputBox.Height; } - - private void ChkClearMemory_CheckedChanged(object sender, EventArgs e) + else { - if (ChkClearMemory.Checked) - { - MessageBox.Show("Nice try, cheater.", "Failed to erase high score ", MessageBoxButtons.OK, MessageBoxIcon.Error); - ChkClearMemory.Checked = false; - } + this.Height -= outputBox.Height; } + } - private void WriteToLogs(string log) + private void ChkClearMemory_CheckedChanged(object sender, EventArgs e) + { + if (ChkClearMemory.Checked) { - outputBox.AppendText($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}] - {log}{Environment.NewLine}"); + MessageBox.Show("Nice try, cheater.", "Failed to erase high score ", MessageBoxButtons.OK, MessageBoxIcon.Error); + ChkClearMemory.Checked = false; } } + + private void WriteToLogs(string log) + { + outputBox.AppendText($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}] - {log}{Environment.NewLine}"); + } } diff --git a/Services/DeviceDiscoveryService.cs b/Services/DeviceDiscoveryService.cs index 4aef5c0..e1c3c7a 100644 --- a/Services/DeviceDiscoveryService.cs +++ b/Services/DeviceDiscoveryService.cs @@ -3,37 +3,36 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Management; + +namespace LumenLabInstaller.Services; -namespace LumenLabInstaller.Services -{ - using System.Management; - public class DeviceDiscoveryService +public class DeviceDiscoveryService +{ + public static Task DetectLumenLabPortAsync() { - public Task DetectLumenLabPortAsync() + return Task.Run(() => { - return Task.Run(() => + var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE Name LIKE '%(COM%)%'"); + + foreach (var device in searcher.Get()) { - var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE Name LIKE '%(COM%)%'"); + string name = device["Name"]?.ToString() ?? ""; - foreach (var device in searcher.Get()) + if (name.Contains("CH340") || + name.Contains("CP2102") || + name.Contains("FT232")) { - string name = device["Name"]?.ToString() ?? ""; - - if (name.Contains("CH340") || - name.Contains("CP2102") || - name.Contains("FT232")) - { - int start = name.IndexOf("(COM") + 1; - int end = name.IndexOf(")", start); + int start = name.IndexOf("(COM") + 1; + int end = name.IndexOf(')', start); - if (start > 0 && end > start) - return name.Substring(start, end - start); - } + if (start > 0 && end > start) + return name.Substring(start, end - start); } + } - return null; - }); - } + return null; + }); } } diff --git a/Services/FirmwareService.cs b/Services/FirmwareService.cs index acadb22..014557f 100644 --- a/Services/FirmwareService.cs +++ b/Services/FirmwareService.cs @@ -7,37 +7,37 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace LumenLabInstaller.Services +namespace LumenLabInstaller.Services; + +public class FirmwareService { - public class FirmwareService + public async static Task ReadFirmwareVersionAsync(string portName) { - public async Task ReadFirmwareVersionAsync(string portName) - { - string versionTemp = Path.Combine(Path.GetTempPath(), "version_chunk.bin"); + ToolManager.EnsureEsptoolExists(); + string versionTemp = Path.Combine(Path.GetTempPath(), "version_chunk.bin"); - var psi = new ProcessStartInfo - { - FileName = AppPaths.EsptoolPath, - Arguments = $"--chip esp32 --port {portName} read_flash 0x10100 0x1000 \"{versionTemp}\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; + var psi = new ProcessStartInfo + { + FileName = AppPaths.EsptoolPath, + Arguments = $"--chip esp32 --port {portName} read_flash 0x10100 0x1000 \"{versionTemp}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; - using var process = new Process { StartInfo = psi }; - process.Start(); + using var process = new Process { StartInfo = psi }; + process.Start(); - await process.WaitForExitAsync(); + await process.WaitForExitAsync(); - if (!File.Exists(versionTemp)) - return "Unknown"; + if (!File.Exists(versionTemp)) + return null; - byte[] firmwareBytes = await File.ReadAllBytesAsync(versionTemp); - string content = Encoding.ASCII.GetString(firmwareBytes); + byte[] firmwareBytes = await File.ReadAllBytesAsync(versionTemp); + string content = Encoding.ASCII.GetString(firmwareBytes); - var match = Regex.Match(content, @"v?\d+\.\d+\.\d+"); - return match.Success ? match.Value : "Unknown"; - } + var match = Regex.Match(content, @"v?\d+\.\d+\.\d+"); + return match.Success ? match.Value : null; } } diff --git a/Services/ToolManager.cs b/Services/ToolManager.cs index 5a792d4..554ac43 100644 --- a/Services/ToolManager.cs +++ b/Services/ToolManager.cs @@ -14,7 +14,7 @@ public static void EnsureEsptoolExists() Directory.CreateDirectory(AppPaths.ToolsFolder); var assembly = Assembly.GetExecutingAssembly(); - var resourceName = "LumenLabInstaller.Tools.esptool.exe"; + var resourceName = "LumenLabInstaller.Firmware.esptool.exe"; using var resourceStream = assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException("Embedded esptool not found."); using var fileStream = new FileStream(AppPaths.EsptoolPath, FileMode.Create, FileAccess.Write); @@ -24,6 +24,7 @@ public static void EnsureEsptoolExists() public static async Task RunEsptoolAsync(string arguments, Action onOutput, CancellationToken cancellationToken) { + EnsureEsptoolExists(); var psi = new ProcessStartInfo { FileName = AppPaths.EsptoolPath, From 602e4483be78d6b6a4d2f33dd7bbbc3ddf2023f1 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Tue, 10 Mar 2026 23:43:15 -0500 Subject: [PATCH 6/8] Add GUI for controller config settings --- LumenLabInstallerForm.Designer.cs | 28 ++++++++++++------- LumenLabInstallerForm.cs | 45 ++++++++++++++++++++++++++++++- Program.cs | 31 ++++++++++++--------- lumenlab-installer.csproj | 2 +- 4 files changed, 83 insertions(+), 23 deletions(-) diff --git a/LumenLabInstallerForm.Designer.cs b/LumenLabInstallerForm.Designer.cs index 5403673..be281aa 100644 --- a/LumenLabInstallerForm.Designer.cs +++ b/LumenLabInstallerForm.Designer.cs @@ -39,7 +39,8 @@ private void InitializeComponent() BtnSync = new Button(); menuStrip1 = new MenuStrip(); fileToolStripMenuItem = new ToolStripMenuItem(); - loadConfigToolStripMenuItem = new ToolStripMenuItem(); + LoadControllerSettingsToolStripMenuItem = new ToolStripMenuItem(); + SaveControllerSettingsToolStripMenuItem = new ToolStripMenuItem(); toolStripSeparator1 = new ToolStripSeparator(); exitToolStripMenuItem = new ToolStripMenuItem(); viewToolStripMenuItem = new ToolStripMenuItem(); @@ -154,26 +155,34 @@ private void InitializeComponent() // // fileToolStripMenuItem // - fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { loadConfigToolStripMenuItem, toolStripSeparator1, exitToolStripMenuItem }); + fileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { LoadControllerSettingsToolStripMenuItem, SaveControllerSettingsToolStripMenuItem, toolStripSeparator1, exitToolStripMenuItem }); fileToolStripMenuItem.Name = "fileToolStripMenuItem"; fileToolStripMenuItem.Size = new Size(37, 20); fileToolStripMenuItem.Text = "File"; // - // loadConfigToolStripMenuItem + // LoadControllerSettingsToolStripMenuItem // - loadConfigToolStripMenuItem.Name = "loadConfigToolStripMenuItem"; - loadConfigToolStripMenuItem.Size = new Size(139, 22); - loadConfigToolStripMenuItem.Text = "Load Config"; + LoadControllerSettingsToolStripMenuItem.Name = "LoadControllerSettingsToolStripMenuItem"; + LoadControllerSettingsToolStripMenuItem.Size = new Size(201, 22); + LoadControllerSettingsToolStripMenuItem.Text = "Load Controller Settings"; + LoadControllerSettingsToolStripMenuItem.Click += LoadControllerSettingsToolStripMenuItem_Click; + // + // SaveControllerSettingsToolStripMenuItem + // + SaveControllerSettingsToolStripMenuItem.Name = "SaveControllerSettingsToolStripMenuItem"; + SaveControllerSettingsToolStripMenuItem.Size = new Size(201, 22); + SaveControllerSettingsToolStripMenuItem.Text = "Save Controller Settings"; + SaveControllerSettingsToolStripMenuItem.Click += SaveControllerSettingsToolStripMenuItem_Click; // // toolStripSeparator1 // toolStripSeparator1.Name = "toolStripSeparator1"; - toolStripSeparator1.Size = new Size(136, 6); + toolStripSeparator1.Size = new Size(198, 6); // // exitToolStripMenuItem // exitToolStripMenuItem.Name = "exitToolStripMenuItem"; - exitToolStripMenuItem.Size = new Size(139, 22); + exitToolStripMenuItem.Size = new Size(201, 22); exitToolStripMenuItem.Text = "Exit"; exitToolStripMenuItem.Click += exitToolStripMenuItem_Click; // @@ -529,7 +538,7 @@ private void InitializeComponent() private RichTextBox LblInstalledVersion; private RichTextBox LblLatestRelease; private ToolStripSeparator toolStripSeparator1; - private ToolStripMenuItem loadConfigToolStripMenuItem; + private ToolStripMenuItem LoadControllerSettingsToolStripMenuItem; private Label label1; private Label label2; private RadioButton RadPS3; @@ -537,5 +546,6 @@ private void InitializeComponent() private Label LblControllerType; private CheckBox ChkClearMemory; private RichTextBox LblUpdateAlert; + private ToolStripMenuItem SaveControllerSettingsToolStripMenuItem; } } diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index 4f92369..72ed2a6 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -258,7 +258,8 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) { WriteToLogs($"Successful installation of LumenLab {latestRelease.TagName}."); WriteToLogs("You may safely exit this tool."); - } else + } + else { WriteToLogs($"Failed to flash LumenLab {latestRelease.TagName}."); } @@ -337,4 +338,46 @@ private void WriteToLogs(string log) { outputBox.AppendText($"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}] - {log}{Environment.NewLine}"); } + + private void SaveControllerSettingsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var dialog = new SaveFileDialog(); + + dialog.Title = "Save Controller Configuration"; + dialog.Filter = "JSON Files (*.json)|*.json"; + dialog.DefaultExt = "json"; + dialog.AddExtension = true; + dialog.FileName = "lumenlab-controller-settings.json"; + + if (dialog.ShowDialog() == DialogResult.OK) + { + try + { + File.WriteAllText(dialog.FileName, "{}"); + MessageBox.Show("Successfully saved your controller settings.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Asterisk); + WriteToLogs($"Successfully saved your controller settings."); + } + catch (Exception ex) + { + MessageBox.Show("Failed to save controller settings. Try again.", "Save failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + WriteToLogs($"Failed to save controller settings. Error message: {ex}"); + } + } + } + + private void LoadControllerSettingsToolStripMenuItem_Click(object sender, EventArgs e) + { + using var dialog = new OpenFileDialog(); + + dialog.Title = "Load Controller Configuration"; + dialog.Filter = "JSON Files (*.json)|*.json"; + + if (dialog.ShowDialog() == DialogResult.OK) + { + string path = dialog.FileName; + + // Placeholder for future implementation + MessageBox.Show($"Selected config file:\n{path}"); + } + } } diff --git a/Program.cs b/Program.cs index 5040a7f..4e41826 100644 --- a/Program.cs +++ b/Program.cs @@ -5,18 +5,25 @@ using LumenLabInstaller.Services; using LumenLabInstaller.Models; -ApplicationConfiguration.Initialize(); -var host = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => +internal static class Program +{ + [STAThread] + static void Main() { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddHttpClient(); - services.AddTransient(); - }) - .Build(); + ApplicationConfiguration.Initialize(); + var host = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHttpClient(); + services.AddTransient(); + }) + .Build(); -var form = host.Services.GetRequiredService(); -Application.Run(form); \ No newline at end of file + var form = host.Services.GetRequiredService(); + Application.Run(form); + } +} \ No newline at end of file diff --git a/lumenlab-installer.csproj b/lumenlab-installer.csproj index 9b653f4..6cdec59 100644 --- a/lumenlab-installer.csproj +++ b/lumenlab-installer.csproj @@ -13,7 +13,7 @@ false true true - 1.0.0 + 99.99.99 $(Version) $(Version) From 1f0f77721e7815684d5b9f24c3260c1f26042290 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Wed, 11 Mar 2026 00:23:52 -0500 Subject: [PATCH 7/8] Consolidate redundant services --- LumenLabInstallerForm.Designer.cs | 1 - LumenLabInstallerForm.cs | 79 ++++++++++-------- Program.cs | 4 +- Services/BinaryDownloadService.cs | 45 ----------- Services/DeviceDiscoveryService.cs | 38 --------- Services/DeviceService.cs | 125 +++++++++++++++++++++++++++++ Services/FirmwareService.cs | 43 ---------- Services/GitHubReleaseService.cs | 63 --------------- Services/NetworkService.cs | 89 ++++++++++++++++++++ Services/ToolManager.cs | 63 --------------- 10 files changed, 259 insertions(+), 291 deletions(-) delete mode 100644 Services/BinaryDownloadService.cs delete mode 100644 Services/DeviceDiscoveryService.cs create mode 100644 Services/DeviceService.cs delete mode 100644 Services/FirmwareService.cs delete mode 100644 Services/GitHubReleaseService.cs create mode 100644 Services/NetworkService.cs delete mode 100644 Services/ToolManager.cs diff --git a/LumenLabInstallerForm.Designer.cs b/LumenLabInstallerForm.Designer.cs index be281aa..b03839c 100644 --- a/LumenLabInstallerForm.Designer.cs +++ b/LumenLabInstallerForm.Designer.cs @@ -519,7 +519,6 @@ private void InitializeComponent() private ToolStripMenuItem exitToolStripMenuItem; private ToolStripMenuItem helpToolStripMenuItem; private ToolStripMenuItem aboutToolStripMenuItem; - private Label LblCustomizeConfig; private CheckBox ChkCustomizeConfiguration; private Label LblTotalLeds; private Label LblMacAddress; diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index 72ed2a6..5928baf 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -17,21 +17,14 @@ namespace LumenLabInstaller; public partial class LumenLabInstallerForm : Form { private readonly InstallerContext _context; - private readonly DeviceDiscoveryService _deviceService; - private readonly FirmwareService _firmwareService; - private readonly BinaryDownloadService _downloadService; + private readonly NetworkService _networkService; private readonly BindingSource _releaseBindingSource = []; - public LumenLabInstallerForm(BinaryDownloadService downloadService, - InstallerContext context, - FirmwareService firmwareService, - DeviceDiscoveryService deviceService) + public LumenLabInstallerForm(NetworkService networkService, InstallerContext context) { - _downloadService = downloadService; + _networkService = networkService; _context = context; - _firmwareService = firmwareService; - _deviceService = deviceService; InitializeComponent(); ConfigureReleaseGrid(); } @@ -81,23 +74,32 @@ private async void LumenLabInstallerForm_Shown(object sender, EventArgs e) try { await QueryGitHub(); - await ReadConnectedDeviceVersion(); } catch (Exception ex) { - MessageBox.Show($"Error retrieving releases:{Environment.NewLine}{ex.Message}", - "GitHub API Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error); + MessageBox.Show($"Error connecting to server to download releases.{Environment.NewLine}{ex.Message}", "Network Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + try + { + var successfulRead = await ReadConnectedDeviceVersion(); + if (!successfulRead) + { + + // TODO: Just update the guide label, don't fire messagebox + //MessageBox.Show("LumenLab was not detected. Please make sure the external power is on and try again.", "LumenLab Not Detected", MessageBoxButtons.RetryCancel, MessageBoxIcon.Stop); + } + } + catch (Exception ex) + { + MessageBox.Show($"Error checking version installed on LumenLab.{Environment.NewLine}{ex.Message}", "Device Read Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private async Task QueryGitHub() { - var releaseService = new GitHubReleaseService(); - WriteToLogs("Querying GitHub to check for updates."); - var releases = await releaseService.GetReleasesAsync(); + var releases = await _networkService.GetReleasesAsync(); _context.AvailableReleases = releases.OrderByDescending(r => r.PublishedAt).ToList(); @@ -128,7 +130,7 @@ private async Task QueryGitHub() // PopulateReleaseTable(releases); } - private async Task ReadConnectedDeviceVersion() + private async Task ReadConnectedDeviceVersion() { BtnSync.Enabled = false; this.UseWaitCursor = true; @@ -137,30 +139,33 @@ private async Task ReadConnectedDeviceVersion() WriteToLogs($"Scanning USB ports to find LumenLab."); - string? port = await DeviceDiscoveryService.DetectLumenLabPortAsync(); + string? port = await DeviceService.DetectLumenLabPortAsync(); _context.PortName = port; if (port == null) { - WriteToLogs("LumenLab not detected."); + WriteToLogs("LumenLab was not detected. Please make sure the external power is on and try again."); + BtnSync.Enabled = true; + this.UseWaitCursor = false; + return false; } - else - { - WriteToLogs($"LumenLab detected on {port}"); - string? version = await FirmwareService.ReadFirmwareVersionAsync(port); - _context.FirmwareVersion = new Version(version.Substring(1)); + WriteToLogs($"LumenLab detected on {port}"); - LblInstalledVersion.Text = $"v{_context.FirmwareVersion.ToString()}"; + string? version = await DeviceService.ReadFirmwareVersionAsync(port); + _context.FirmwareVersion = new Version(version.Substring(1)); - WriteToLogs($"Currently installed version: {version}"); - //if (_context.FirmwareVersion == ) TODO: update the label to inform user if they're out of date or current. - BtnFlashFirmware.Enabled = true; - } + LblInstalledVersion.Text = $"v{_context.FirmwareVersion.ToString()}"; + WriteToLogs($"Currently installed version: {version}"); + //if (_context.FirmwareVersion == ) TODO: update the label to inform user if they're out of date or current. + + BtnFlashFirmware.Enabled = true; WriteToLogs($"Device scan complete. Ready for firmware upgrade."); + BtnSync.Enabled = true; this.UseWaitCursor = false; + return true; } private void AppendLog(string text) @@ -178,7 +183,7 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) { WriteToLogs("Starting LumenLab firmware upgrade."); BtnFlashFirmware.Enabled = false; - string? port = await DeviceDiscoveryService.DetectLumenLabPortAsync(); + string? port = await DeviceService.DetectLumenLabPortAsync(); if (port == null) { LblInstalledVersion.Text = "N/A"; @@ -237,7 +242,7 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) WriteToLogs($"Downloading LumenLab {latestRelease.TagName}"); var cts = new CancellationTokenSource(); - var result = await _downloadService.DownloadAsync( + var result = await _networkService.DownloadAsync( new Uri($"https://github.com/ericmcdaniel/lumenlab/releases/download/{latestRelease.TagName}/lumenlab-firmware.zip"), Path.Combine(downloadPath, "lumenlab-firmware.zip") ); @@ -252,7 +257,7 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) string esptoolFullCmd = $"--chip esp32 --baud 921600 --port {port} write_flash -z 0x1000 {Path.Combine(downloadPath, "bootloader.bin")} 0x8000 {Path.Combine(downloadPath, "partitions.bin")} 0x10000 {Path.Combine(downloadPath, "firmware.bin")}"; WriteToLogs($"Running esptool.exe {esptoolFullCmd}"); - var exitCode = await ToolManager.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); + var exitCode = await DeviceService.RunEsptoolAsync(esptoolFullCmd, AppendLog, cts.Token); if (exitCode == 0) { @@ -275,7 +280,11 @@ private async void BtnFlashFirmware_Click(object sender, EventArgs e) } private async void BtnSync_Click(object sender, EventArgs e) { - await ReadConnectedDeviceVersion(); + var successfulRead = await ReadConnectedDeviceVersion(); + if (!successfulRead) + { + MessageBox.Show("LumenLab was not detected. Please make sure the external power is on and try again.", "LumenLab Not Detected", MessageBoxButtons.RetryCancel, MessageBoxIcon.Stop); + } } private GitHubRelease? GetSelectedRelease() diff --git a/Program.cs b/Program.cs index 4e41826..ce609f0 100644 --- a/Program.cs +++ b/Program.cs @@ -16,9 +16,7 @@ static void Main() .ConfigureServices((context, services) => { services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddHttpClient(); + services.AddHttpClient(); services.AddTransient(); }) .Build(); diff --git a/Services/BinaryDownloadService.cs b/Services/BinaryDownloadService.cs deleted file mode 100644 index 861678b..0000000 --- a/Services/BinaryDownloadService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using LumenLabInstaller.Models; - -namespace LumenLabInstaller.Services -{ - public class BinaryDownloadService - { - private readonly HttpClient _httpClient; - - public BinaryDownloadService(HttpClient httpClient) - { - _httpClient = httpClient; - } - - public async Task DownloadAsync(Uri uri, - string destinationPath, - CancellationToken cancellationToken = default) - { - using var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - - response.EnsureSuccessStatusCode(); - - var totalBytes = response.Content.Headers.ContentLength; - - await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); - await using var fileStream = new FileStream(destinationPath, - FileMode.Create, - FileAccess.Write, - FileShare.None, - bufferSize: 8192, - useAsync: true); - - var buffer = new byte[8192]; - long totalRead = 0; - int bytesRead; - - while ((bytesRead = await contentStream.ReadAsync(buffer, cancellationToken)) > 0) - { - await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); - totalRead += bytesRead; - } - - return new DownloadResult(destinationPath, totalRead); - } - } -} \ No newline at end of file diff --git a/Services/DeviceDiscoveryService.cs b/Services/DeviceDiscoveryService.cs deleted file mode 100644 index e1c3c7a..0000000 --- a/Services/DeviceDiscoveryService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Management; - -namespace LumenLabInstaller.Services; - - -public class DeviceDiscoveryService -{ - public static Task DetectLumenLabPortAsync() - { - return Task.Run(() => - { - var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE Name LIKE '%(COM%)%'"); - - foreach (var device in searcher.Get()) - { - string name = device["Name"]?.ToString() ?? ""; - - if (name.Contains("CH340") || - name.Contains("CP2102") || - name.Contains("FT232")) - { - int start = name.IndexOf("(COM") + 1; - int end = name.IndexOf(')', start); - - if (start > 0 && end > start) - return name.Substring(start, end - start); - } - } - - return null; - }); - } -} diff --git a/Services/DeviceService.cs b/Services/DeviceService.cs new file mode 100644 index 0000000..90ba9cb --- /dev/null +++ b/Services/DeviceService.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Management; +using LumenLabInstaller.Infrastructure; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Reflection; + +namespace LumenLabInstaller.Services; + + +public class DeviceService +{ + public static Task DetectLumenLabPortAsync() + { + return Task.Run(() => + { + var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE Name LIKE '%(COM%)%'"); + + foreach (var device in searcher.Get()) + { + string name = device["Name"]?.ToString() ?? ""; + + if (name.Contains("CH340") || + name.Contains("CP2102") || + name.Contains("FT232")) + { + int start = name.IndexOf("(COM") + 1; + int end = name.IndexOf(')', start); + + if (start > 0 && end > start) + return name.Substring(start, end - start); + } + } + + return null; + }); + } + + public async static Task ReadFirmwareVersionAsync(string portName) + { + EnsureEsptoolExists(); + string versionTemp = Path.Combine(Path.GetTempPath(), "version_chunk.bin"); + + var psi = new ProcessStartInfo + { + FileName = AppPaths.EsptoolPath, + Arguments = $"--chip esp32 --port {portName} read_flash 0x10100 0x1000 \"{versionTemp}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = new Process { StartInfo = psi }; + process.Start(); + + await process.WaitForExitAsync(); + + if (!File.Exists(versionTemp)) + return null; + + byte[] firmwareBytes = await File.ReadAllBytesAsync(versionTemp); + string content = Encoding.ASCII.GetString(firmwareBytes); + + var match = Regex.Match(content, @"v?\d+\.\d+\.\d+"); + return match.Success ? match.Value : null; + } + + public static void EnsureEsptoolExists() + { + if (File.Exists(AppPaths.EsptoolPath)) + return; + + Directory.CreateDirectory(AppPaths.ToolsFolder); + + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "LumenLabInstaller.Firmware.esptool.exe"; + using var resourceStream = assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException("Embedded esptool not found."); + + using var fileStream = new FileStream(AppPaths.EsptoolPath, FileMode.Create, FileAccess.Write); + + resourceStream.CopyTo(fileStream); + } + + public static async Task RunEsptoolAsync(string arguments, Action onOutput, CancellationToken cancellationToken) + { + EnsureEsptoolExists(); + var psi = new ProcessStartInfo + { + FileName = AppPaths.EsptoolPath, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = new Process { StartInfo = psi }; + + process.OutputDataReceived += (s, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + onOutput(e.Data); + }; + + process.ErrorDataReceived += (s, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + onOutput(e.Data); + }; + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(cancellationToken); + + return process.ExitCode; + } +} diff --git a/Services/FirmwareService.cs b/Services/FirmwareService.cs deleted file mode 100644 index 014557f..0000000 --- a/Services/FirmwareService.cs +++ /dev/null @@ -1,43 +0,0 @@ -using LumenLabInstaller.Infrastructure; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace LumenLabInstaller.Services; - -public class FirmwareService -{ - public async static Task ReadFirmwareVersionAsync(string portName) - { - ToolManager.EnsureEsptoolExists(); - string versionTemp = Path.Combine(Path.GetTempPath(), "version_chunk.bin"); - - var psi = new ProcessStartInfo - { - FileName = AppPaths.EsptoolPath, - Arguments = $"--chip esp32 --port {portName} read_flash 0x10100 0x1000 \"{versionTemp}\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var process = new Process { StartInfo = psi }; - process.Start(); - - await process.WaitForExitAsync(); - - if (!File.Exists(versionTemp)) - return null; - - byte[] firmwareBytes = await File.ReadAllBytesAsync(versionTemp); - string content = Encoding.ASCII.GetString(firmwareBytes); - - var match = Regex.Match(content, @"v?\d+\.\d+\.\d+"); - return match.Success ? match.Value : null; - } -} diff --git a/Services/GitHubReleaseService.cs b/Services/GitHubReleaseService.cs deleted file mode 100644 index 545c989..0000000 --- a/Services/GitHubReleaseService.cs +++ /dev/null @@ -1,63 +0,0 @@ -using LumenLabInstaller.Configuration; -using LumenLabInstaller.Models; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace LumenLabInstaller.Services -{ - - public class GitHubReleaseService - { - private readonly HttpClient _httpClient; - - public GitHubReleaseService() - { - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LumenLabInstaller"); - _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json"); - } - - public async Task> GetReleasesAsync() - { - using var response = await _httpClient.GetAsync(AppConstants.GithubReleasesUrl); - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsStringAsync(); - - var apiReleases = JsonSerializer.Deserialize>(json); - - if (apiReleases == null) return new List(); - - var requiredAssets = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "lumenlab-firmware.zip" // can be expanded to include PS4 controllers - }; - - var filtered = apiReleases.Select(r => new GitHubRelease - { - TagName = r.TagName, - PublishedAt = r.PublishedAt, - Name = r.Name, - Assets = r.Assets?.Select(a => new GitHubAsset - { - Name = a.Name, - BrowserDownloadUrl = a.BrowserDownloadUrl - }).ToList() ?? [] - }) - .Where(release => - { - if (release.Assets.Count != 1) return false; - - var assetNames = release.Assets - .Select(a => a.Name) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - return requiredAssets.All(required => assetNames.Contains(required)); - }) - .ToList(); - - return filtered; - } - } -} diff --git a/Services/NetworkService.cs b/Services/NetworkService.cs new file mode 100644 index 0000000..fd5406f --- /dev/null +++ b/Services/NetworkService.cs @@ -0,0 +1,89 @@ +using LumenLabInstaller.Configuration; +using LumenLabInstaller.Models; +using System.Text.Json; + +namespace LumenLabInstaller.Services +{ + public class NetworkService + { + private readonly HttpClient _httpClient; + + public NetworkService(HttpClient httpClient) + { + _httpClient = httpClient; + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LumenLabInstaller"); + _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json"); + } + + public async Task DownloadAsync(Uri uri, string destinationPath, CancellationToken cancellationToken = default) + { + using var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + response.EnsureSuccessStatusCode(); + + var totalBytes = response.Content.Headers.ContentLength; + + await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + await using var fileStream = new FileStream(destinationPath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + bufferSize: 8192, + useAsync: true); + + var buffer = new byte[8192]; + long totalRead = 0; + int bytesRead; + + while ((bytesRead = await contentStream.ReadAsync(buffer, cancellationToken)) > 0) + { + await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); + totalRead += bytesRead; + } + + return new DownloadResult(destinationPath, totalRead); + } + + public async Task> GetReleasesAsync() + { + using var response = await _httpClient.GetAsync(AppConstants.GithubReleasesUrl); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + + var apiReleases = JsonSerializer.Deserialize>(json); + + if (apiReleases == null) return new List(); + + var requiredAssets = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "lumenlab-firmware.zip" // can be expanded to include PS4 controllers + }; + + var filtered = apiReleases.Select(r => new GitHubRelease + { + TagName = r.TagName, + PublishedAt = r.PublishedAt, + Name = r.Name, + Assets = r.Assets?.Select(a => new GitHubAsset + { + Name = a.Name, + BrowserDownloadUrl = a.BrowserDownloadUrl + }).ToList() ?? [] + }) + .Where(release => + { + if (release.Assets.Count != 1) return false; + + var assetNames = release.Assets + .Select(a => a.Name) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + return requiredAssets.All(required => assetNames.Contains(required)); + }) + .ToList(); + + return filtered; + } + } +} \ No newline at end of file diff --git a/Services/ToolManager.cs b/Services/ToolManager.cs deleted file mode 100644 index 554ac43..0000000 --- a/Services/ToolManager.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using LumenLabInstaller.Infrastructure; - -namespace LumenLabInstaller.Services -{ - internal static class ToolManager - { - public static void EnsureEsptoolExists() - { - if (File.Exists(AppPaths.EsptoolPath)) - return; - - Directory.CreateDirectory(AppPaths.ToolsFolder); - - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = "LumenLabInstaller.Firmware.esptool.exe"; - using var resourceStream = assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException("Embedded esptool not found."); - - using var fileStream = new FileStream(AppPaths.EsptoolPath, FileMode.Create, FileAccess.Write); - - resourceStream.CopyTo(fileStream); - } - - public static async Task RunEsptoolAsync(string arguments, Action onOutput, CancellationToken cancellationToken) - { - EnsureEsptoolExists(); - var psi = new ProcessStartInfo - { - FileName = AppPaths.EsptoolPath, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var process = new Process { StartInfo = psi }; - - process.OutputDataReceived += (s, e) => - { - if (!string.IsNullOrEmpty(e.Data)) - onOutput(e.Data); - }; - - process.ErrorDataReceived += (s, e) => - { - if (!string.IsNullOrEmpty(e.Data)) - onOutput(e.Data); - }; - - process.Start(); - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - await process.WaitForExitAsync(cancellationToken); - - return process.ExitCode; - } - } -} - From b9cf5be88428b01f3fb7f00275f23b8a7ae34d15 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Fri, 13 Mar 2026 16:55:21 -0500 Subject: [PATCH 8/8] Add screenshot of basic tool usage --- LumenLabInstallerForm.cs | 2 +- README.md | 6 +++++- assets/main-screenshot.png | Bin 0 -> 59461 bytes 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 assets/main-screenshot.png diff --git a/LumenLabInstallerForm.cs b/LumenLabInstallerForm.cs index 5928baf..454539c 100644 --- a/LumenLabInstallerForm.cs +++ b/LumenLabInstallerForm.cs @@ -283,7 +283,7 @@ private async void BtnSync_Click(object sender, EventArgs e) var successfulRead = await ReadConnectedDeviceVersion(); if (!successfulRead) { - MessageBox.Show("LumenLab was not detected. Please make sure the external power is on and try again.", "LumenLab Not Detected", MessageBoxButtons.RetryCancel, MessageBoxIcon.Stop); + MessageBox.Show("LumenLab was not detected. Please make sure the external power is on and try again.", "LumenLab Not Detected", MessageBoxButtons.RetryCancel, MessageBoxIcon.Stop); } } diff --git a/README.md b/README.md index c0e1d6f..c055226 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # LumenLab Installer -A Windows-based installation tool to streamline updating the LumenLab. +A Windows-based installation tool to help keep your LumenLab up-to-date. + +![Screenshot of the main form when updating the LumenLab](./assets/main-screenshot.png) # How To Use +(More detail will be added soon, the priority is to finish the core functionality by allowing users to flash controller configuration, store settings, and re-load them. Full documention will be drafted when that is deployed.) + [To be finalized.] \ No newline at end of file diff --git a/assets/main-screenshot.png b/assets/main-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..54ed4028e2c7a772b6bc3a35abe6c39aaaaaf260 GIT binary patch literal 59461 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

p=@18ZRi>eCUR7nCszTc2yB#bD1b5s;uD~Z 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 z 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 literal 0 HcmV?d00001