using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using Image = System.Drawing.Image; namespace AiQ_GUI { internal class SoakTest { // Main soak test loop: Randomise dropdowns, run luminance test every hour, power cycle at 7am public static async Task StartSoak(Camera CamInfo, CancellationToken token) { if (CamInfo.Serial == "N/A") CamInfo.Serial = "UNKNOWN"; // If serial is not set, set it to UNKNOWN. Cannot have N/A in file names. string SoakLogFile = $"SoakLog_{CamInfo.Serial}_{CamInfo.Model}.log"; ChromeDriver driver = null; try { driver = Selenium.OpenDriver(); // Keep retrying until connected or cancelled bool connected = false; while (!connected && !token.IsCancellationRequested) { try { // Attempt initial connection and navigation to setup tab Selenium.GoToUrl($"http://{CamInfo.IP}", driver); Selenium.ClickElementByID("tabSetup", driver); connected = true; } catch (Exception ex) { Logging.LogErrorMessage($"Initial connection failed: {ex.Message}", SoakLogFile); MainForm.Instance.AddToActionsList($"[{CamInfo.IP}] Initial connection failed: {ex.Message}"); // Wait 10 seconds before trying again await Task.Delay(TimeSpan.FromSeconds(10), token); } } ElementID elementID = null; try { // Try to retrieve all required element IDs from the UI elementID = Selenium.GetElementIds(driver); } catch (Exception ex) { Logging.LogErrorMessage($"Failed to get element IDs: {ex.Message}", SoakLogFile); return; } int lastHour = DateTime.Now.Hour; while (!token.IsCancellationRequested) { int currentHour = DateTime.Now.Hour; // At 7am, power cycle the camera if (currentHour == 7 && lastHour != 7) { Logging.LogMessage($"7am detected, restarting camera via /api/restart-hardware.", SoakLogFile); try { await FlexiAPI.APIHTTPRequest("/api/restart-hardware", CamInfo.IP); await Task.Delay(TimeSpan.FromMinutes(4), token); // Wait for restart // Retry ping until camera responds or cancelled while (!await Network.PingIP(CamInfo.IP) && !token.IsCancellationRequested) { Logging.LogErrorMessage($"Camera did not respond after restart.", SoakLogFile); await Task.Delay(TimeSpan.FromMinutes(1), token); // Retry after delay of 1 minute } if (!token.IsCancellationRequested) { // Reconnect and re-acquire element IDs after restart Selenium.GoToUrl($"http://{CamInfo.IP}", driver); Selenium.ClickElementByID("tabSetup", driver); elementID = Selenium.GetElementIds(driver); } } catch (Exception ex) { Logging.LogErrorMessage($"Error during power cycle: {ex.Message}", SoakLogFile); } } // Every hour, run ImageCheck if (currentHour != lastHour) { Logging.LogMessage($"Hour changed to {currentHour}, running ImageCheck.", SoakLogFile); try { ImageCheck(driver, SoakLogFile, CamInfo.IP, CamInfo.DevPass, elementID); } catch (Exception ex) { Logging.LogErrorMessage($"ImageCheck failed: {ex.Message}", SoakLogFile); } lastHour = currentHour; } // Every cycle, randomly change dropdowns to simulate interaction try { // If it is auto mode, set it to manual IWebElement modeElement = driver.FindElement(By.Id(elementID.modeId)); string selectedText = new OpenQA.Selenium.Support.UI.SelectElement(modeElement).SelectedOption.Text; if (selectedText == "Auto") await Selenium.Dropdown_Change(elementID.modeId, "Manual", driver, SoakLogFile, elementID.CamAct); await ChangeRandomDropdown(driver, SoakLogFile, elementID); } catch (Exception ex) { Logging.LogErrorMessage($"ChangeRandomDropdown failed: {ex.Message}", SoakLogFile); } try { await Task.Delay(TimeSpan.FromSeconds(15), token); // Small delay between each loop iteration } catch (TaskCanceledException) { break; // Graceful exit when cancellation is requested } } } finally { // Ensure driver cleanup regardless of success/failure Logging.LogMessage("Driver quiting and ending soak test.", SoakLogFile); driver?.Quit(); // Create Test report in the same directory as the final test reports. string soakLogPath = LDS.MAVPath + SoakLogFile; string SoakTestPath = PDF.CreateSoakTestReport(CamInfo, MainForm.Instance.CbBxUserName.Text, DateTime.Now, soakLogPath); if (SoakTestPath != null) { // Delete the soak test log file if the report was created successfully try { if (File.Exists(soakLogPath)) File.Delete(soakLogPath); } catch (Exception ex) { Logging.LogErrorMessage($"Failed to delete soak log file: {ex.Message}{Environment.NewLine}Please delete if you find this.", SoakLogFile); } // Find the final test report PDF for this camera string finalTestDir = PDF.TestRecordDir + CamInfo.Model + "\\"; // Directory to final test record string finalTestPattern = $"FinalTestReport_{CamInfo.Model}_{CamInfo.Serial}_*.pdf"; // Pattern to match final test report string finalTestPath = null; try { if (Directory.Exists(finalTestDir)) { string[] finalTestFiles = Directory.GetFiles(finalTestDir, finalTestPattern); finalTestPath = finalTestFiles.OrderByDescending(f => File.GetCreationTime(f)).FirstOrDefault(); // If multiple, pick the most recent } } catch (Exception ex) { MainForm.Instance.AddToActionsList($"Failed to find final test report: {ex.Message}"); } // Link PDFs if both exist if (!string.IsNullOrEmpty(finalTestPath) && File.Exists(finalTestPath) && !string.IsNullOrEmpty(SoakTestPath) && File.Exists(SoakTestPath)) { string outputPath = finalTestDir + $"Final&SoakTestReport_{CamInfo.Model}_{CamInfo.Serial}_{DateTime.Now.ToString("dd-MM-yyyy_HH-mm-ss")}.pdf"; try { if (PDF.LinkPDFs(finalTestPath, SoakTestPath, outputPath)) // Delete the separate soak and final test reports if Linking was successful { Logging.LogMessage($"Linked PDFs successfully: {outputPath}"); File.Delete(finalTestPath); File.Delete(SoakTestPath); } else MainForm.Instance.AddToActionsList($"Failed to link or delete PDFs"); } catch (Exception ex) { MainForm.Instance.AddToActionsList($"Failed to link or delete PDFs: {ex.Message}"); } } } } } // Capture's bright and dark images, then compares their luminance to verify sufficient contrast and adds results to the log public static async Task LuminescenceMean(string FullID, ChromeDriver driver, string[] SettingMinMax, string SoakLogFile, string IP, string DevPass, string CamAct) { string controlType = FullID.Split('_')[0]; // Extract control type from FullID (e.g. "Shutter_1234" → "Shutter") // Set bright setting Logging.LogMessage($"Setting {controlType} to bright value: {SettingMinMax[0]}", SoakLogFile); await Selenium.Dropdown_Change(FullID, SettingMinMax[0], driver, SoakLogFile, CamAct); await Task.Delay(500); // Take bright image Image ImageBright = await ImageProcessing.GetProcessedImage("Infrared-snapshot", IP, DevPass); if (ImageBright == null) { Logging.LogWarningMessage($"Bright image is null for {controlType} at setting {SettingMinMax[0]}", SoakLogFile); MainForm.Instance.AddToActionsList($"Bright image is null for {controlType} at setting {SettingMinMax[0]}"); return; } // Set dark setting Logging.LogMessage($"Setting {controlType} to dark value: {SettingMinMax[1]}", SoakLogFile); await Selenium.Dropdown_Change(FullID, SettingMinMax[1], driver, SoakLogFile, CamAct); await Task.Delay(500); // Take dark image Image ImageDark = await ImageProcessing.GetProcessedImage("Infrared-snapshot", IP, DevPass); if (ImageDark == null) { Logging.LogWarningMessage($"Dark image is null for {controlType} at setting {SettingMinMax[1]}", SoakLogFile); MainForm.Instance.AddToActionsList($"Dark image is null for {controlType} at setting {SettingMinMax[1]}"); return; } // Brightness test between min and max settings double Bright_Lum = ImageProcessing.GetMeanLuminance(ImageBright); double Dark_Lum = ImageProcessing.GetMeanLuminance(ImageDark); if (Bright_Lum < Dark_Lum * 1.01) { Logging.LogErrorMessage( $"Insufficient luminance contrast. Bright: {Bright_Lum:F2}, Dark: {Dark_Lum:F2} | Type: {controlType} | Bright Setting: {SettingMinMax[0]}, Dark Setting: {SettingMinMax[1]}", SoakLogFile ); } else { Logging.LogMessage( $"Sufficient luminance contrast. Bright: {Bright_Lum:F2}, Dark: {Dark_Lum:F2} | Type: {controlType} | Bright Setting: {SettingMinMax[0]}, Dark Setting: {SettingMinMax[1]}", SoakLogFile ); } } // Performs a series of camera control adjustments and luminance checks to verify image settings functionality public async static void ImageCheck(ChromeDriver driver, string SoakLogFile, string IP, string DevPass, ElementID elementID) { await Selenium.Dropdown_Change(elementID.modeId, "Manual", driver, SoakLogFile, elementID.CamAct); await Selenium.Dropdown_Change(elementID.shutterId, "1/1000", driver, SoakLogFile, elementID.CamAct); await Selenium.Dropdown_Change(elementID.gainId, "0dB", driver, SoakLogFile, elementID.CamAct); await Selenium.Dropdown_Change(elementID.irisId, "F4.0", driver, SoakLogFile, elementID.CamAct); await Selenium.Dropdown_Change(elementID.irLevelId, "Safe", driver, SoakLogFile, elementID.CamAct); await LuminescenceMean(elementID.shutterId, driver, ["1/100", "1/10000"], SoakLogFile, IP, DevPass, elementID.CamAct); // Check Shutter goes from min to max await Selenium.Dropdown_Change(elementID.shutterId, "1/1000", driver, SoakLogFile, elementID.CamAct); // Reset to default await LuminescenceMean(elementID.irisId, driver, ["F2.0", "F16"], SoakLogFile, IP, DevPass, elementID.CamAct); // Check iris goes from min to max await Selenium.Dropdown_Change(elementID.irisId, "F4.0", driver, SoakLogFile, elementID.CamAct); // Reset to default await LuminescenceMean(elementID.gainId, driver, ["20dB", "0dB"], SoakLogFile, IP, DevPass, elementID.CamAct); // Check gain goes from min to max } public async static Task ChangeRandomDropdown(ChromeDriver driver, string SoakLogFile, ElementID elementID) { (string gain, string shutter, string iris, string irLevel) = GetRandoms(); await Selenium.Dropdown_Change(elementID.shutterId, shutter, driver, SoakLogFile, elementID.CamAct); await Selenium.Dropdown_Change(elementID.gainId, gain, driver, SoakLogFile, elementID.CamAct); await Selenium.Dropdown_Change(elementID.irisId, iris, driver, SoakLogFile, elementID.CamAct); await Selenium.Dropdown_Change(elementID.irLevelId, irLevel, driver, SoakLogFile, elementID.CamAct); } private static (string gain, string shutter, string iris, string irLevel) GetRandoms() { (string[] gainOptions, string[] shutterOptions, string[] irisOptions, string[] irLevelOptions) = GetControlOptions(); Random rand = new(); string iris = "F" + irisOptions[rand.Next(irisOptions.Length)]; string irLevel = irLevelOptions[rand.Next(irLevelOptions.Length)]; string gain = gainOptions[rand.Next(gainOptions.Length)] + "dB"; string shutter = "1/" + shutterOptions[rand.Next(shutterOptions.Length)]; return (gain, shutter, iris, irLevel); } // Helper function to grab the model and serial numbers. Helpful for naming the Soak files and identfying the camera private static (string[] gain, string[] shutter, string[] iris, string[] irLevel) GetControlOptions() { return ( new[] { "0", "2", "6", "8", "10", "12", "16", "18", "20", "24" }, new[] { "10000", "2000", "1000", "500", "250", "100" }, new[] { "2.0", "2.8", "4.0", "5.6", "8.0", "11", "16" }, new[] { "Off", "Safe", "Low", "Mid", "High" } ); } public static CheckBox MakeNewCheckbox(Camera soakInfo, int YLoc) { CheckBox dynamicButton = new() { Location = new Point(12, YLoc), Height = 20, Width = 220, ForeColor = SystemColors.Control, Text = soakInfo.IP + " - " + soakInfo.Serial + " - " + soakInfo.FlexiVersion, Name = "BtnImage" + soakInfo.IP, Checked = true }; dynamicButton.CheckedChanged += (s, e) => { soakInfo.IsChecked = dynamicButton.Checked; }; return dynamicButton; } } public class ElementID { public string modeId { get; set; } public string shutterId { get; set; } public string irisId { get; set; } public string gainId { get; set; } public string irLevelId { get; set; } public string CamAct { get; set; } } }