Files
AiQ_GUI/Camera/FlexiAPI.cs

561 lines
23 KiB
C#
Raw Normal View History

2025-09-02 15:32:24 +01:00
using Newtonsoft.Json;
using System.Net.Http.Headers;
namespace AiQ_GUI
{
public enum LEDPOWER
{
LOW,
MID,
HIGH,
SAFE,
OFF
}
public class FlexiAPI
{
// GET API from camera
public static async Task<string> APIHTTPRequest(string EndPoint, string IPAddress, int? Timeout = 2)
{
try
{
string URL = $"http://{IPAddress}{EndPoint}";
return await Network.SendHttpRequest(URL, HttpMethod.Get, Timeout);
}
catch (Exception ex)
{
2025-12-02 12:59:40 +00:00
return $"Error during GET request: {ex.Message}{Level.ERROR}";
2025-09-02 15:32:24 +01:00
}
}
// For 'update-config' sending a change to the AiQ
// TODO - Not many of the references check the output is positive. Need them all to check.
public static async Task<string> HTTP_Update(string ID, string IPAddress, string[,] jsonArrayData)
{
try
{
string JSONdata = BuildJsonUpdate(jsonArrayData, ID);
2025-10-21 14:01:27 +01:00
JSONdata = JSONdata.Replace("\"14\"", "14").Replace("\"30\"", "30"); // Fixes & encoding issue
2025-09-02 15:32:24 +01:00
string url = $"http://{IPAddress}/api/update-config";
return await Network.SendHttpRequest(url, HttpMethod.Post, 2, JSONdata);
2025-11-04 12:56:16 +00:00
2025-09-02 15:32:24 +01:00
}
catch (Exception ex)
{
2025-12-02 12:59:40 +00:00
return $"Error in HTTP_Update: {ex.Message}{Level.ERROR}";
2025-09-02 15:32:24 +01:00
}
}
// For 'fetch-config' getting info from AiQ
public static async Task<string> HTTP_Fetch(string ID, string IPAddress, int? Timeout = 2)
{
try
{
2025-09-16 10:42:51 +01:00
string url = $"http://{IPAddress}/api/fetch-config?id={ID}";
return await Network.SendHttpRequest(url, HttpMethod.Get, Timeout);
2025-09-02 15:32:24 +01:00
}
catch (Exception ex)
{
2025-12-02 12:59:40 +00:00
return $"Error in HTTP_Fetch: {ex.Message}{Level.ERROR}";
2025-09-02 15:32:24 +01:00
}
}
// For sending VISCA directly to the camera module
// Be aware this does bypass Flexi's watchdog so settings like zoom, focus, SIG wont keep forever
public static async Task<string> APIHTTPVISCA(string IPAddress, string VISCA, bool IR)
{
// http://localhost:8080/Infrared-camera-control?commandHex=8101044700000000FF
2025-09-02 15:32:24 +01:00
string suffix = $"-camera-control?commandHex={VISCA}";
if (IR) // Add on which camera to control
suffix = "/Infrared" + suffix;
else
suffix = "/Colour" + suffix;
return await APIHTTPRequest(suffix, IPAddress);
}
// Sets the LED level into the camera
public static async Task<string> APIHTTPLED(string IPAddress, LEDPOWER LEVEL)
{
// Always Infrared as LED's are controlled from infrared page
// Level can be word eg. SAFE or hex eg. 0x0E
string suffix = $"/Infrared/led-controls?power={LEVEL}";
return await APIHTTPRequest(suffix, IPAddress, 5);
2025-09-02 15:32:24 +01:00
}
public static async Task<string> SendBlobFileUpload(string url, string filePath, string fileName)
{
try
{
Network.Client.DefaultRequestHeaders.ExpectContinue = false;
byte[] fileBytes = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false);
MemoryStream ms = new(fileBytes);
StreamContent streamContent = new(ms);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
2025-09-16 10:42:51 +01:00
MultipartFormDataContent content = new() { { streamContent, "upload", fileName } };
2025-09-02 15:32:24 +01:00
using HttpResponseMessage response = await Network.Client.PostAsync(url, content);
string responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
2025-12-02 12:59:40 +00:00
return $"Server returned {(int)response.StatusCode}: {response.ReasonPhrase}. Details: {responseBody}{Level.ERROR}";
2025-09-02 15:32:24 +01:00
return responseBody;
}
catch (TaskCanceledException)
{
2025-12-02 12:59:40 +00:00
return $"Timeout uploading to {url}.{Level.ERROR}";
2025-09-02 15:32:24 +01:00
}
catch (HttpRequestException ex)
{
2025-12-02 12:59:40 +00:00
return $"HTTP error uploading to {url}: {ex.Message}{Level.ERROR}";
2025-09-02 15:32:24 +01:00
}
catch (Exception ex)
{
2025-12-02 12:59:40 +00:00
return $"Unexpected error uploading to {url}: {ex.Message} {(ex.InnerException?.Message ?? string.Empty)} {Level.ERROR}";
2025-09-02 15:32:24 +01:00
}
}
public static async Task<Versions> GetVersions(string IPAddress)
{
string JSON = await APIHTTPRequest("/api/versions", IPAddress); // Version API request
if (JSON == null || JSON.Contains("Error") || JSON.Contains("Timeout"))
return null;
Logging.LogMessage("Received versions JSON: " + JSON);
try
{
return JsonConvert.DeserializeObject<Versions>(JSON);
}
catch
{
MainForm.Instance.AddToActionsList("Cannot deserialise Versions JSON" + Environment.NewLine + JSON);
return null; // If it fails to parse the JSON
}
}
public static async Task<Diags> GetDiagnostics(string IPAddress)
{
string JSON = await APIHTTPRequest("/api/diagnostics", IPAddress, 20); // Version API request
if (JSON == null || JSON.Contains("Error") || JSON.Contains("Timeout"))
{
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Error talking to Flexi, are you sure this is an AiQ?{Level.WARNING}" + Environment.NewLine + JSON);
2025-09-02 15:32:24 +01:00
return null;
}
Logging.LogMessage("Received diagnostics JSON: " + JSON);
try
{
return JsonConvert.DeserializeObject<Diags>(JSON);
}
catch
{
MainForm.Instance.AddToActionsList("Cannot deserialise Diagnostics JSON" + Environment.NewLine + JSON);
return null; // If it fails to parse the JSON
}
}
public static async Task<bool> SetVaxtorMinMaxPlate(string IP)
{
try
{
// Build JSON array for Vaxtor min/max plate configuration
string[,] Vaxtor_JSON = { { "propMinCharHeight", "14" }, { "propMaxCharHeight", "40" }, { "propMinGlobalConfidence", "30" } };
string response = await HTTP_Update("RaptorOCR".Trim(), IP, Vaxtor_JSON);
// Treat "operation was canceled" as a successful apply
if (response.Contains("The operation was canceled", StringComparison.OrdinalIgnoreCase))
{
2025-12-02 12:59:40 +00:00
Logging.LogMessage($"SetVaxtorMinMaxPlate: Camera applied config but closed connection early (safe to ignore).{Level.WARNING}");
return true;
}
if (response.Contains("error", StringComparison.OrdinalIgnoreCase))
{
2025-12-02 12:59:40 +00:00
MainForm.Instance.DisplayQuestion($"SetVaxtorMinMaxPlate: failed - Please set manually{Level.WARNING}");
return false;
}
return true;
}
catch (Exception ex)
{
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Could not set Vaxtor Plate height and min confidence: {ex.Message}{Level.ERROR}");
return false;
}
}
2025-12-02 11:02:24 +00:00
public static async Task GPSFix(string IPAddress)
2025-09-02 15:32:24 +01:00
{
2025-12-02 11:02:24 +00:00
string sysstatus = await APIHTTPRequest("/sysstatus", IPAddress, 5);
SysStatus status = JsonConvert.DeserializeObject<SysStatus>(sysstatus);
if (status.gpsState == 0 || status.gpsPresent == "Not Fitted")
2025-09-02 15:32:24 +01:00
{
2025-12-02 11:02:24 +00:00
MainForm.Instance.AddToActionsList($"GPS not present in camera. State: {status.gpsState} & Status: {status.gpsPresent}");
return;
2025-09-02 15:32:24 +01:00
}
}
2025-09-16 10:42:51 +01:00
public static async Task SetTrim(string IPAddress, string LblTxt, int RetryCount = 0) // Sets trim by getting plate postion as metric
{
Trim trim;
string trimData = await APIHTTPRequest("/SightingCreator-plate-positions", IPAddress, 5); // Get plate positions
try // Deserialise the JSON
{
Logging.LogMessage("Trim Data: " + trimData);
trim = JsonConvert.DeserializeObject<Trim>(trimData);
}
catch
{
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Error reading trim JSON - {Level.ERROR}" + trimData);
2025-09-16 10:42:51 +01:00
return;
}
// Check no value is -1 (no plate found) or if the positions are identical (one plate found). If it is then try again 3 times
if (new[] { trim.infraredX, trim.infraredY, trim.colourX, trim.colourY }.Any(value => value == -1) || (trim.infraredX == trim.colourX && trim.infraredY == trim.colourY))
2025-09-16 10:42:51 +01:00
{
if (RetryCount >= 3)
{
2025-12-02 12:59:40 +00:00
await MainForm.Instance.DisplayOK($"Please align trim in webpage then click OK.{Level.WARNING}"); // Awaited till OK has been clicked
2025-09-16 10:42:51 +01:00
return;
}
await Task.Delay(5000); // Give 5 second delay for it to see a plate
await SetTrim(IPAddress, LblTxt, RetryCount + 1);
2025-09-16 10:42:51 +01:00
}
int offset = 105;
if (LblTxt == "❌") // Test tube not connected so do the 2.7m check.
offset = 98;
// Horizontal distance offset for 2.7m compared to 30m is 98 pixels. This was empirically found from testing in the car park
// Colour camera is to the right of the infrared so it gets the offset.
// Using similar triangles going from 2.7m -> 0.65m which is the length of the test tube (Also exactly 1/4 length).
// 98 * (29.35/27.3) = 105.35 pixels
int OverviewX = trim.colourX + offset;
if (OverviewX > 1920) // If adding on the offset has pushed it out of limits then remove 0.1
{
if (OverviewX < 2120 && trim.infraredX > 400) // Within enough of a limit to automatically do it
{
OverviewX -= 200;
trim.infraredX -= 200;
}
else // Ask user to centre the plate in the field of view
{
2025-12-02 12:59:40 +00:00
await MainForm.Instance.DisplayOK($"Please centralise plate in view THEN press OK {Level.WARNING}"); // Awaited till OK has been clicked
2025-09-16 10:42:51 +01:00
if (RetryCount >= 3)
{
2025-12-02 12:59:40 +00:00
await MainForm.Instance.DisplayOK($"Please align trim in webpage then click OK. {Level.WARNING}"); // Awaited till OK has been clicked
2025-09-16 10:42:51 +01:00
return;
}
await Task.Delay(5000); // Give 5 second delay for it to see a plate
await SetTrim(IPAddress, LblTxt, RetryCount + 1);
2025-09-16 10:42:51 +01:00
}
}
// Compensated trim values, therefore should be close to 0,0 with limits of ±5% of 1920 and 1080 respectivly being ±96 and ±54
int TrimX = trim.infraredX - OverviewX;
int TrimY = trim.infraredY - trim.colourY;
// Update trim values
string[,] Trim_JSON = { { "propInterCameraOffsetX", Convert.ToString(TrimX) }, { "propInterCameraOffsetY", Convert.ToString(TrimY) } };
string TrimResp = await HTTP_Update("SightingCreator", IPAddress, Trim_JSON);
if (!TrimResp.Contains($"\"propInterCameraOffsetX\": {{\"value\": \"{Convert.ToString(TrimX)}\", \"datatype\": \"int\"}}, \"propInterCameraOffsetY\": {{\"value\": \"{Convert.ToString(TrimY)}\", \"datatype\": \"int\"}},"))
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Could not set camera trim{Level.ERROR}");
2025-09-16 10:42:51 +01:00
}
2025-09-02 15:32:24 +01:00
// Processes the network config from the camera and returns a string indicating the status
public static async Task<string> ProcessNetworkConfig(string IPAddress)
{
try
{
string JSON = await HTTP_Fetch("GLOBAL--NetworkConfig", IPAddress, 10);
NetworkConfig NC = JsonConvert.DeserializeObject<NetworkConfig>(JSON);
if (Convert.ToBoolean(NC.propDHCP.Value) == false)
return "Set to DHCP";
else
return "Set to 211";
}
catch (Exception ex)
{
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Error in getting network config from camera: {ex.Message}{Level.ERROR}");
2025-09-02 15:32:24 +01:00
return null; // Return empty string if there is an error
}
}
// Knowing the format this builds the message to send to AiQ
2025-09-16 10:42:51 +01:00
public static string BuildJsonUpdate(string[,] jsonData, string id)
2025-09-02 15:32:24 +01:00
{
if (jsonData == null || jsonData.GetLength(1) != 2)
throw new ArgumentException("Input data must be a non-null 2D array with two columns.");
int rows = jsonData.GetLength(0);
List<object> fields = [];
for (int i = 0; i < rows; i++) // Puts each part of the array into correct format
{
fields.Add(new
{
property = jsonData[i, 0],
value = jsonData[i, 1]
});
}
var updateObject = new // Correct naming and format so it is JSON ready
{
id,
fields
};
return JsonConvert.SerializeObject(updateObject, Formatting.None); // Uses no white space
}
// Change network settings to 192.168.1.211 and wait 5 seconds to see if it takes effect
public static async Task<bool> ChangeNetwork211(string IPAddress)
{
// Update GLOBAL--NetworkConfig with fixed IP and turn off DHCP
string[,] TEST_JSON = { { "propDHCP", "false" }, { "propHost", "192.168.1.211" }, { "propNetmask", "255.255.255.0" }, { "propGateway", "192.168.1.1" } };
await HTTP_Update("GLOBAL--NetworkConfig", IPAddress, TEST_JSON);
await Task.Delay(5000); // Wait for 5 seconds to allow the camera to restart
IList<string> FoundCams = await Network.SearchForCams(); // Have to check via broadcast becuase Ping sometimes fails across subnets
return FoundCams.Contains("192.168.1.211");
}
// Change network settings to DHCP and restart camera for it to take effect
2025-11-04 12:56:16 +00:00
public async static Task<bool> ChangeNetworkToDHCP(string IPAddress)
2025-09-02 15:32:24 +01:00
{
2025-11-04 12:56:16 +00:00
string[,] TEST_JSON = { { "propDHCP", "true" } };
2025-11-04 14:29:38 +00:00
await HTTP_Update("GLOBAL--NetworkConfig", IPAddress, TEST_JSON); // Don't care about response because it will fail as it has changed IP.
2025-11-04 12:56:16 +00:00
await Task.Delay(5000); // Wait for 5 seconds to allow the camera to restart
2025-11-04 14:29:38 +00:00
IList<string> FoundCams = await Network.SearchForCams();
2025-11-04 12:56:16 +00:00
if (FoundCams.Contains("192.168.1.211"))
{
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Could not set camera to DHCP please check camera.{Level.ERROR}");
2025-11-04 12:56:16 +00:00
return false;
}
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Camera successfully set to DHCP.{Level.Success}");
2025-11-04 12:56:16 +00:00
return true;
2025-09-02 15:32:24 +01:00
}
2025-11-04 12:56:16 +00:00
2025-11-04 14:29:38 +00:00
public static async Task UploadWonwooSet(string ipAddress, bool isIR)
{
string fileToUpload = null;
using OpenFileDialog openFileDialog1 = new()
{
2025-11-26 14:39:07 +00:00
InitialDirectory = GoogleAPI.GoogleDrivePath,
2025-11-04 14:29:38 +00:00
Filter = "CSV files (*.csv)|*.csv",
FilterIndex = 0
};
if (openFileDialog1.ShowDialog() == DialogResult.OK)
fileToUpload = openFileDialog1.FileName;
else
{
2025-11-26 14:39:07 +00:00
MainForm.Instance.AddToActionsList("File selection cancelled.", Level.WARNING);
2025-11-04 14:29:38 +00:00
return;
}
//Filename validation
string filename = Path.GetFileName(fileToUpload).ToUpper();
2025-11-11 13:49:42 +00:00
if ((isIR && !filename.Contains("IR")) || (!isIR && !filename.Contains("OV")))
2025-11-04 14:29:38 +00:00
{
2025-11-26 14:39:07 +00:00
MainForm.Instance.AddToActionsList($"Incorrect file selected. Expected {(isIR ? "IR" : "OV")} file", Level.WARNING);
2025-11-04 14:29:38 +00:00
return;
}
string[] lines = File.ReadAllLines(fileToUpload);
for (int i = 1; i < lines.Length; i++)
{
string[] parts = lines[i].Split(',').Select(p => p.Trim()).ToArray();
if (parts.Length < 3)
{
2025-11-26 14:39:07 +00:00
MainForm.Instance.AddToActionsList($"Invalid row format at line {i + 1}", Level.WARNING);
2025-11-04 14:29:38 +00:00
continue;
}
string name = parts[0];
string command = parts[1];
string expectedResponse = parts[2];
// VISCA format check
if (!RegexCache.VISCAAPIRegex().IsMatch(command))
{
2025-11-26 14:39:07 +00:00
MainForm.Instance.AddToActionsList($"{name}: Invalid VISCA command ({command})", Level.WARNING);
2025-11-04 14:29:38 +00:00
continue; // do not send it if bad
}
string result = await APIHTTPVISCA(ipAddress, command, isIR);
if (result.Contains(expectedResponse))
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"{name}: Success ({(isIR ? "IR" : "Colour")})", Level.Success);
2025-11-04 14:29:38 +00:00
else
2025-11-26 14:39:07 +00:00
MainForm.Instance.AddToActionsList($"{name}: Unexpected response ({result})", Level.ERROR);
2025-11-04 14:29:38 +00:00
await Task.Delay(150);
}
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Upload complete ({(isIR ? "IR" : "Colour")}).", Level.Success);
2025-11-26 14:39:07 +00:00
}
public static async void UploadBlob(List<Camera> soakCameraList)
{
const string networkFolderPath = @"G:\Shared drives\MAV Production\MAV_146_AiQ_Mk2\Flexi";
string fileToUpload = null;
if (await MainForm.Instance.DisplayQuestion("Do you want the latest Flexi version from the MAV Production folder?"))
{
fileToUpload = Directory.GetFiles(networkFolderPath, "*.blob").OrderByDescending(File.GetLastWriteTime).FirstOrDefault();
if (fileToUpload == null)
{
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList("No .blob file found in the directory.", Level.WARNING);
2025-11-26 14:39:07 +00:00
return;
}
}
else
{
using OpenFileDialog openFileDialog1 = new()
{
InitialDirectory = networkFolderPath,
Filter = "Blob files (*.blob)|*.blob",
FilterIndex = 0
};
if (openFileDialog1.ShowDialog() == DialogResult.OK)
fileToUpload = openFileDialog1.FileName;
else
{
MainForm.Instance.AddToActionsList("File selection cancelled.", Level.WARNING);
return;
}
}
string fileName = Path.GetFileName(fileToUpload);
MainForm.Instance.AddToActionsList($"Selected file to upload: {fileToUpload}", Level.LOG);
foreach (Camera? cam in soakCameraList.Where(c => c.IsChecked))
{
string apiUrl = $"http://{cam.IP}/upload/software-update/2";
Network.Initialize("developer", cam.DevPass);
await Task.Delay(1000); // Gives extra time to allow for Network to initialize
MainForm.Instance.AddToActionsList($"Uploading to {cam.IP}...", Level.LOG);
string result = await SendBlobFileUpload(apiUrl, fileToUpload, fileName);
// Retry once on transient errors
if (result.Contains("Error while copying content to a stream") || result.Contains("Timeout"))
{
MainForm.Instance.AddToActionsList($"Retrying upload to {cam.IP}...", Level.WARNING);
await Task.Delay(1000);
result = await SendBlobFileUpload(apiUrl, fileToUpload, fileName);
}
2025-12-02 12:59:40 +00:00
MainForm.Instance.AddToActionsList($"Upload result for {cam.IP}: {result}");
2025-11-26 14:39:07 +00:00
await Task.Delay(500);
}
2025-11-04 14:29:38 +00:00
}
2025-09-02 15:32:24 +01:00
}
//Items recieved in Versions API
public class Versions
{
public string version { get; set; } = string.Empty;
public string revision { get; set; } = string.Empty;
public string buildtime { get; set; } = string.Empty;
public string MAC { get; set; } = string.Empty;
public int timeStamp { get; set; }
public string proquint { get; set; } = string.Empty;
[JsonProperty("Serial No.")]
public string Serial { get; set; } = string.Empty;
[JsonProperty("Model No.")]
public string Model { get; set; } = string.Empty;
}
// Items returned in the diagnostics api
public class Diags
{
[JsonProperty("version")]
public string FlexiVersion { get; set; } = string.Empty;
[JsonProperty("revision")]
public string FlexiRevision { get; set; } = string.Empty;
public string serialNumber { get; set; } = string.Empty;
public string modelNumber { get; set; } = string.Empty;
public string MAC { get; set; } = string.Empty;
public long timeStamp { get; set; }
public Licenses licenses { get; set; } = new Licenses();
[JsonProperty("internalTemperature")]
public double IntTemperature { get; set; }
[JsonProperty("cpuUsage")]
public double CPUusage { get; set; }
public List<int> trim { get; set; } = [];
public bool zoomLock { get; set; }
public Module IRmodule { get; set; } = new Module();
public Module OVmodule { get; set; } = new Module();
[JsonProperty("ledChannelVoltages")]
public List<double> LedVoltage { get; set; } = [];
[JsonProperty("ledChannelCurrents")]
public List<double> LedCurrent { get; set; } = [];
}
public class Module
{
public int zoom { get; set; }
public int expMode { get; set; }
public string firmwareVer { get; set; } = string.Empty;
}
public class Trim
{
public int infraredX { get; set; }
public int infraredY { get; set; }
public int colourX { get; set; }
public int colourY { get; set; }
}
2025-12-02 11:02:24 +00:00
public class SysStatus
{
public int gpsState { get; set; } = 0;
public string gpsPresent { get; set; } = string.Empty;
}
2025-09-02 15:32:24 +01:00
public class NetworkConfig
{
public Property propDHCP { get; set; } = new Property();
}
public class Property
{
public string Value { get; set; } = string.Empty;
}
}