Add project files.
This commit is contained in:
99
Camera/CameraModules.cs
Normal file
99
Camera/CameraModules.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
|
||||
namespace AiQ_GUI
|
||||
{
|
||||
internal class CameraModules
|
||||
{
|
||||
// Chack camera modules are in default state according to what the diagnostics API.
|
||||
public static void CheckCamModule(Module CamMod, Label Lbl)
|
||||
{
|
||||
if (CamMod == null || Lbl == null)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Camera module or label was null in CheckCamModule.");
|
||||
return;
|
||||
}
|
||||
|
||||
string errMssg = "";
|
||||
|
||||
if (CamMod.zoom != 0) // Check camera module is at full wide
|
||||
errMssg += $"Zoom not at 0 - {CamMod.zoom} ";
|
||||
|
||||
if (CamMod.firmwareVer != UniversalData.WonwooFirmware) // Check camera module firmware version is up to date.
|
||||
errMssg += $"Firmware: {CamMod.firmwareVer} should be {UniversalData.WonwooFirmware} ";
|
||||
|
||||
if (CamMod.expMode != 0) // Auto 0=0x00
|
||||
errMssg += $"Exp mode not set: {CamMod.expMode} ";
|
||||
|
||||
try
|
||||
{
|
||||
Lbl.Invoke(() =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(errMssg))
|
||||
{
|
||||
Lbl.Text += "OK";
|
||||
Lbl.ForeColor = Color.LightGreen;
|
||||
}
|
||||
else
|
||||
{
|
||||
Lbl.Text += errMssg;
|
||||
Lbl.ForeColor = Color.Red;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Exception in CheckCamModule: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sets shutter, iris and gain to the values given in the dropdowns on the GUI.
|
||||
public static async Task SetSIG(ComboBox Shutter, ComboBox Iris, ComboBox Gain, string IPAddress) // Set SIG according to the comboboxes on the images tab
|
||||
{
|
||||
if (Shutter.SelectedIndex == -1 || Iris.SelectedIndex == -1 || Gain.SelectedIndex == -1)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Shutter, Iris and Gain need selecting in images tab.");
|
||||
return;
|
||||
}
|
||||
|
||||
string ShutterVISCA = BuildVISCACommand("A", Shutter.SelectedIndex + 7); // Offset for not starting at the beggining of the VISCA table
|
||||
string IrisVISCA = BuildVISCACommand("B", Iris.SelectedIndex + 4); // Offset for not starting at the beggining of the VISCA table
|
||||
string GainVISCA = BuildVISCACommand("C", Gain.SelectedIndex);
|
||||
|
||||
if (ShutterVISCA.Contains("ERROR") || IrisVISCA.Contains("ERROR") || GainVISCA.Contains("ERROR"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Problem with selected SIG values");
|
||||
return;
|
||||
}
|
||||
|
||||
string ShutterReply = await FlexiAPI.APIHTTPVISCA(IPAddress, ShutterVISCA, true); // Set Shutter
|
||||
string IrisReply = await FlexiAPI.APIHTTPVISCA(IPAddress, IrisVISCA, true); // Set Iris
|
||||
string GainReply = await FlexiAPI.APIHTTPVISCA(IPAddress, GainVISCA, true); // Set Gain
|
||||
string OneshotReply = await FlexiAPI.APIHTTPVISCA(IPAddress, "8101041801FF", true); // Oneshot auto focus
|
||||
|
||||
if (!ShutterReply.Contains("41") || !IrisReply.Contains("41") || !GainReply.Contains("41") || !OneshotReply.Contains("41"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Could not set Shutter, Iris, Gain correctly" + Environment.NewLine + "Shutter: " + ShutterReply + Environment.NewLine + "Iris: " + IrisReply + Environment.NewLine + "Gain: " + GainReply + Environment.NewLine + "Oneshot: " + OneshotReply);
|
||||
}
|
||||
}
|
||||
|
||||
// Sets back to the latest factory defaults CSV that is in Flexi.
|
||||
public static async Task FactoryResetModules(string IPAddress)
|
||||
{
|
||||
// Set both camera modules back to MAV defaults. Found in WonwooDefaultSettingsIR.csv & WonwooDefaultSettingsOV.csv
|
||||
Task<string> IRReply = FlexiAPI.APIHTTPRequest("/Infrared-camera-factory-reset", IPAddress, 10);
|
||||
Task<string> OVReply = FlexiAPI.APIHTTPRequest("/Colour-camera-factory-reset", IPAddress, 10);
|
||||
await Task.WhenAll(IRReply, OVReply);
|
||||
|
||||
if (IRReply.Result != "Factory reset OK." || OVReply.Result != "Factory reset OK.")
|
||||
MainForm.Instance.AddToActionsList($"Could not reset camera modules to factory default.{Environment.NewLine}{IRReply}{Environment.NewLine}{OVReply}");
|
||||
}
|
||||
|
||||
public static string BuildVISCACommand(string command, int hexValue)
|
||||
{
|
||||
// Take the augmented Selected index into a two nibble hex value.
|
||||
string hex = $"{hexValue:X2}";
|
||||
// Build the final VISCA command string using the input characters split as p and q
|
||||
return $"8101044{command}00000{hex[0]}0{hex[1]}FF";
|
||||
}
|
||||
}
|
||||
}
|
335
Camera/FlexiAPI.cs
Normal file
335
Camera/FlexiAPI.cs
Normal file
@@ -0,0 +1,335 @@
|
||||
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)
|
||||
{
|
||||
return $"Error during GET request: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
string url = $"http://{IPAddress}/api/update-config";
|
||||
return await Network.SendHttpRequest(url, HttpMethod.Post, 2, JSONdata);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error in HTTP_Update: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// For 'fetch-config' getting info from AiQ
|
||||
public static async Task<string> HTTP_Fetch(string ID, string IPAddress, int? Timeout = 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
string JSONdata = "{ \"id\":\"" + ID + "\" }";
|
||||
string url = $"http://{IPAddress}/api/fetch-config";
|
||||
return await Network.SendHttpRequest(url, HttpMethod.Get, Timeout, JSONdata);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error in HTTP_Fetch: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public static async Task<string> SendBlobFileUpload(string url, string filePath, string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Network.Client.DefaultRequestHeaders.ExpectContinue = false;
|
||||
MultipartFormDataContent content;
|
||||
|
||||
byte[] fileBytes = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false);
|
||||
MemoryStream ms = new(fileBytes);
|
||||
StreamContent streamContent = new(ms);
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
|
||||
content = new MultipartFormDataContent { { streamContent, "upload", fileName } };
|
||||
|
||||
using HttpResponseMessage response = await Network.Client.PostAsync(url, content);
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return $"Server returned {(int)response.StatusCode}: {response.ReasonPhrase}. Details: {responseBody}";
|
||||
|
||||
return responseBody;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return $"Timeout uploading to {url}.";
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return $"HTTP error uploading to {url}: {ex.Message}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Unexpected error uploading to {url}: {ex.Message} {(ex.InnerException?.Message ?? "")}";
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Error talking to Flexi, are you sure this is an AiQ?" + Environment.NewLine + JSON);
|
||||
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> SetZoomLockOn(string IP)
|
||||
{
|
||||
// Set Zoomlock on and if it fails ask user to set it manually
|
||||
if (!(await APIHTTPRequest("/api/zoomLock?enable=true", IP)).Contains("Zoom lock enabled.")
|
||||
&& !await MainForm.Instance.DisplayQuestion("Could not set zoomlock on" + Environment.NewLine + "Set Zoomlock to on then click YES. Click NO to restart."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> ZoomModules(string VISCAInput, string IPAddress)
|
||||
{
|
||||
// Populate the VISCA command with the four zoom characters
|
||||
string VISCA = $"810104470{VISCAInput[0]}0{VISCAInput[1]}0{VISCAInput[2]}0{VISCAInput[3]}FF";
|
||||
|
||||
Task<string> TS1 = APIHTTPVISCA(IPAddress, VISCA, true);
|
||||
Task<string> TS2 = APIHTTPVISCA(IPAddress, VISCA, false);
|
||||
await Task.WhenAll(TS1, TS2);
|
||||
|
||||
const string ExpReply = "9041FF9051FF";
|
||||
if (TS1.Result == ExpReply && TS1.Result == ExpReply)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error in getting network config from camera: {ex.Message}");
|
||||
return null; // Return empty string if there is an error
|
||||
}
|
||||
}
|
||||
|
||||
// Knowing the format this builds the message to send to AiQ
|
||||
private static string BuildJsonUpdate(string[,] jsonData, string id)
|
||||
{
|
||||
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
|
||||
public async static Task ChangeNetworkToDHCP(string IPAddress)
|
||||
{
|
||||
string[,] TEST_JSON = { { "propDHCP", "true" } }; // Update GLOBAL--NetworkConfig with fixed IP and turn off DHCP
|
||||
await HTTP_Update("GLOBAL--NetworkConfig", IPAddress, TEST_JSON);
|
||||
// TODO - Check if this worked, if not return false
|
||||
}
|
||||
}
|
||||
|
||||
//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 appname { get; set; } = string.Empty;
|
||||
public string MAC { get; set; } = string.Empty;
|
||||
public int timeStamp { get; set; }
|
||||
public string UUID { get; set; } = string.Empty;
|
||||
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; }
|
||||
}
|
||||
|
||||
public class NetworkConfig
|
||||
{
|
||||
public Property propDHCP { get; set; } = new Property();
|
||||
}
|
||||
|
||||
public class Property
|
||||
{
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
173
Camera/ImageProcessing.cs
Normal file
173
Camera/ImageProcessing.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.CvEnum;
|
||||
using Emgu.CV.Structure;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Net;
|
||||
using Image = System.Drawing.Image;
|
||||
|
||||
namespace AiQ_GUI
|
||||
{
|
||||
internal class ImageProcessing
|
||||
{
|
||||
// API to get snapshot then downsize and downscale image to save size.
|
||||
public static async Task<Image?> GetProcessedImage(string suffix, string IPAddress, string DevPass, string? savePath = null, PictureBox? PcBx = null, bool SaveDisplay = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
string requestUrl = $"http://{IPAddress}/{suffix}";
|
||||
|
||||
HttpClientHandler handler = new HttpClientHandler
|
||||
{
|
||||
Credentials = new NetworkCredential("developer", DevPass),
|
||||
PreAuthenticate = true
|
||||
};
|
||||
|
||||
using HttpClient httpClient = new(handler);
|
||||
HttpResponseMessage response = await httpClient.GetAsync(requestUrl);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"No success from {requestUrl} replied {response.StatusCode}");
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
if (imageBytes.Length == 0) // Check if the imageBytes is empty
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"No image data received from {requestUrl}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load image into Emgu CV Mat
|
||||
Mat mat = new();
|
||||
CvInvoke.Imdecode(imageBytes, ImreadModes.AnyColor, mat);
|
||||
if (mat.IsEmpty)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Failed to decode image with Emgu CV.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Downscale image to 25% resolution of 1080p
|
||||
Mat downscaledMat = new();
|
||||
CvInvoke.Resize(mat, downscaledMat, new Size(480, 270));
|
||||
|
||||
// Compress to JPEG at 75% quality
|
||||
byte[] jpegBytes = downscaledMat.ToImage<Bgr, byte>().ToJpegData(75);
|
||||
|
||||
// Convert back to System.Drawing.Image
|
||||
using MemoryStream ms = new(jpegBytes);
|
||||
Image IMG = Image.FromStream(ms);
|
||||
|
||||
// Display image in picture box
|
||||
if (SaveDisplay && PcBx != null)
|
||||
PcBx.Image = (Image)IMG.Clone();
|
||||
|
||||
// Save image to disk
|
||||
if (SaveDisplay && !string.IsNullOrEmpty(savePath))
|
||||
IMG.Save(savePath, ImageFormat.Jpeg);
|
||||
|
||||
return IMG;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"HTTP error: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error processing image: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Checks the images taken at different iris settings and compares their brightness.
|
||||
// Also gets the colour snapshot
|
||||
public static async Task ImageCheck(PictureBox PicBxOV, PictureBox PicBxF2, PictureBox PicBxF16, Label LblF2, Label LblF16, Camera CamOnTest)
|
||||
{
|
||||
// Take OV snapshot
|
||||
Task<Image?> Colour_Response = GetProcessedImage("Colour-snapshot", CamOnTest.IP, CamOnTest.DevPass, LDS.MAVPath + LDS.OVsavePath, PicBxOV, true);
|
||||
|
||||
// Change to wide iris F2.0
|
||||
await FlexiAPI.APIHTTPVISCA(CamOnTest.IP, "8101044B00000100FF", true);
|
||||
|
||||
await Task.Delay(200); // Wait for iris to settle before taking IR image
|
||||
|
||||
// Take IR bright light image
|
||||
Image? F2_Response = await GetProcessedImage("Infrared-snapshot", CamOnTest.IP, CamOnTest.DevPass, LDS.MAVPath + LDS.IROpensavePath, PicBxF2, true);
|
||||
if (F2_Response == null)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("IR F2.0 image response is blank.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Change to tight iris F16.0
|
||||
await FlexiAPI.APIHTTPVISCA(CamOnTest.IP, "8101044B00000004FF", true);
|
||||
|
||||
await Task.Delay(200); // Wait for iris to settle before taking IR image
|
||||
|
||||
// Take IR low light image
|
||||
Image? F16_Response = await GetProcessedImage("Infrared-snapshot", CamOnTest.IP, CamOnTest.DevPass, LDS.MAVPath + LDS.IRTightsavePath, PicBxF16, true);
|
||||
if (F16_Response == null)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("IR F16.0 image response is blank.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (await Colour_Response == null)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Colour image response is blank.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error awaiting Colour snapshot: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Brightness test between min and max iris
|
||||
try
|
||||
{
|
||||
double luminanceF2 = GetMeanLuminance(F2_Response);
|
||||
double luminanceF16 = GetMeanLuminance(F16_Response);
|
||||
|
||||
LblF2.Text += luminanceF2 + "%";
|
||||
LblF16.Text += luminanceF16 + "%";
|
||||
|
||||
if (luminanceF2 < luminanceF16 * 1.01)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Insufficient luminance contrast between min and max iris");
|
||||
LblF2.ForeColor = LblF16.ForeColor = Color.Red;
|
||||
}
|
||||
else
|
||||
LblF2.ForeColor = LblF16.ForeColor = Color.LightGreen;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error calculating luminance: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static double GetMeanLuminance(Image Img)
|
||||
{
|
||||
using Bitmap bmp = new(Img); // Convert from Image to Bitmap
|
||||
|
||||
using MemoryStream ms = new(); // Convert Bitmap to byte array
|
||||
bmp.Save(ms, ImageFormat.Jpeg);
|
||||
byte[] bmpBytes = ms.ToArray();
|
||||
|
||||
Mat mat = new();
|
||||
CvInvoke.Imdecode(bmpBytes, ImreadModes.AnyColor, mat); // Convert to mat
|
||||
|
||||
Mat grayMat = new();
|
||||
CvInvoke.CvtColor(mat, grayMat, ColorConversion.Bgr2Gray); // Convert to grayscale
|
||||
|
||||
MCvScalar mean = CvInvoke.Mean(grayMat); // Calculate mean luminance
|
||||
// Translate to a percentage from a mean value between 0-255 8 bit grayscale value
|
||||
return Math.Round((mean.V0 / 255) * 100, 4); // V0 contains the mean value for grayscale
|
||||
}
|
||||
}
|
||||
}
|
52
Camera/LED.cs
Normal file
52
Camera/LED.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace AiQ_GUI
|
||||
{
|
||||
internal class LED
|
||||
{
|
||||
// Checks the LED voltages and currents against expected values, displaying results in the provided label
|
||||
public static void CheckLEDs(List<double> VorI, Label lblVorI, string VormA, double ExpVorI)
|
||||
{
|
||||
try
|
||||
{
|
||||
VorI.Sort(); // Sort the list from lowest to highest to prepare for finding the median
|
||||
double medianVorI = (VorI[2] + VorI[3]) / 2.0; // Will always be even (6) number of channels therefore average the two middle elements
|
||||
lblVorI.Text += $"Median: {medianVorI}{VormA} "; // Display median value
|
||||
|
||||
// Define the 20% threshold ranges
|
||||
double LowerThreshold = ExpVorI * 0.8;
|
||||
double UpperThreshold = ExpVorI * 1.2;
|
||||
|
||||
// Check median is within 20% of the expected value
|
||||
if (medianVorI < LowerThreshold || medianVorI > UpperThreshold)
|
||||
{
|
||||
lblVorI.Text += $" Median away from excepted {ExpVorI}{VormA}";
|
||||
lblVorI.ForeColor = Color.Red;
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> outOfRangeVoltageChannels = []; // List to store out-of-range channels
|
||||
|
||||
// Check each voltage/current channel is within 20% of expected
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (VorI[i] < LowerThreshold || VorI[i] > UpperThreshold)
|
||||
outOfRangeVoltageChannels.Add($"Ch{i + 1}");
|
||||
}
|
||||
|
||||
// If there are no single channels outside the threshold then green, else red
|
||||
if (outOfRangeVoltageChannels.Count == 0)
|
||||
{
|
||||
lblVorI.ForeColor = Color.LightGreen;
|
||||
}
|
||||
else if (outOfRangeVoltageChannels.Count != 0)
|
||||
{
|
||||
lblVorI.Text += "error on " + string.Join(", ", outOfRangeVoltageChannels); // Join all problem channels together to present on form
|
||||
lblVorI.ForeColor = Color.Red;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error checking LEDs: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
110
Camera/Licences.cs
Normal file
110
Camera/Licences.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace AiQ_GUI
|
||||
{
|
||||
public class Lics
|
||||
{
|
||||
// Challenge code salts
|
||||
const string SAFsalt = "F7W?wbD#'[+:v44]tA<:_iK4hQ}+$R{U";
|
||||
const string Streamsalt = "*;5WPsR5i/$8s1I(M)K5=z3fms{_8x4U";
|
||||
const string Auditsalt = "4t5e[E06:dXWf:C09Z[h)}V*n>}t0POP";
|
||||
const string PasswordSalt = "eP@4^4T2@e@^h12oqf!590";
|
||||
|
||||
// Generates the license response based on the challenge and type of license
|
||||
public static string GenerateLicCode(string challenge, string Type)
|
||||
{
|
||||
string salt; // Different salts for differnet licenses
|
||||
|
||||
if (Type == "Store & Forward")
|
||||
salt = SAFsalt;
|
||||
else if (Type == "Streaming")
|
||||
salt = Streamsalt;
|
||||
else if (Type == "Audit")
|
||||
salt = Auditsalt;
|
||||
else
|
||||
return "Unrecognised challenge type: " + Type;
|
||||
|
||||
if (string.IsNullOrEmpty(challenge) || challenge.Length != 6) // Check challenge format
|
||||
return "Invalid challenge format. Challenge must be 6 characters.";
|
||||
|
||||
if (string.IsNullOrEmpty(salt) || salt.Length != 32) // Check salt format
|
||||
return "Invalid salt format. Salt must be 32 characters.";
|
||||
|
||||
// Hash computation using SHA256 algorithm
|
||||
byte[] inputBytes = Encoding.UTF8.GetBytes(challenge + " " + salt); // SHA hash format challenge and salt with space between
|
||||
byte[] hashBytes = SHA256.HashData(inputBytes);
|
||||
|
||||
StringBuilder sb = new();
|
||||
foreach (byte b in hashBytes)
|
||||
{
|
||||
sb.Append(b.ToString("x2"));
|
||||
}
|
||||
string digest = sb.ToString();
|
||||
|
||||
BigInteger BigInt = BigInteger.Parse("0" + digest, NumberStyles.AllowHexSpecifier); // Leading zero is sign for big int.
|
||||
return BigInt.ToString().Substring(0, 6);
|
||||
}
|
||||
|
||||
public static string GeneratePassword(string mac, string version, int time)
|
||||
{
|
||||
try
|
||||
{
|
||||
string timeBlock = (time / 86400).ToString(); // 1-day validity
|
||||
string secret = string.Join(" ", mac, version, timeBlock, PasswordSalt);
|
||||
|
||||
byte[] digest = MD5.HashData(Encoding.UTF8.GetBytes(secret));
|
||||
return Convert.ToBase64String(digest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return "Error: Could not generate password " + ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DisplayDevPassword(Versions Vers, Camera CamOnTest)
|
||||
{
|
||||
CamOnTest.DevPass = FetchDevPassword(Vers);
|
||||
|
||||
if (CamOnTest.DevPass.Contains("Could not"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList(CamOnTest.DevPass); // Did not parse, so error.
|
||||
return;
|
||||
}
|
||||
|
||||
Network.Initialize("developer", CamOnTest.DevPass); // Reinitialise HTTP client with developer password
|
||||
}
|
||||
|
||||
public static string FetchDevPassword(Versions Vers)
|
||||
{
|
||||
try
|
||||
{
|
||||
return GeneratePassword(Vers.MAC, Vers.version, Vers.timeStamp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Exception in FetchDevPassword: " + ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Licenses
|
||||
{
|
||||
public bool saf1 { get; set; }
|
||||
public bool saf2 { get; set; }
|
||||
public bool saf3 { get; set; }
|
||||
public bool saf4 { get; set; }
|
||||
public bool audit { get; set; }
|
||||
public bool stream { get; set; }
|
||||
public string raptorKeyID { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class VaxtorLic
|
||||
{
|
||||
public string protectionKeyId { get; set; } = string.Empty;
|
||||
public string error { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
91
Camera/Router.cs
Normal file
91
Camera/Router.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Renci.SshNet;
|
||||
|
||||
namespace AiQ_GUI
|
||||
{
|
||||
internal class Router
|
||||
{
|
||||
const string RouterUsername = "router";
|
||||
const string RouterPassword = "MAV999";
|
||||
|
||||
public static RouterInfo GetRouterInfo()
|
||||
{
|
||||
RouterInfo Router = new();
|
||||
|
||||
try
|
||||
{
|
||||
using SshClient client = new("192.168.1.1", RouterUsername, RouterPassword);
|
||||
client.Connect();
|
||||
|
||||
Router.Strength = Convert.ToInt16(client.RunCommand("uci -P /var/state/ get mobile.dev_info1.strength").Result);
|
||||
|
||||
Router.SimStatus = client.RunCommand("uci -P /var/state/ get mobile.dev_info1.simstatus").Result;
|
||||
|
||||
Router.Port3Status = client.RunCommand("swconfig dev switch0 port 3 get link").Result;
|
||||
|
||||
Router.Port4Status = client.RunCommand("swconfig dev switch0 port 4 get link").Result;
|
||||
|
||||
SshCommand? pingCmd = client.RunCommand("ping -c 2 -W 2 8.8.8.8"); // Run ping and check exit status
|
||||
Router.GoodPing = pingCmd.ExitStatus == 0;
|
||||
|
||||
client.Disconnect();
|
||||
return Router;
|
||||
}
|
||||
catch
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Is the router on the network? Has the MAV Config file been applied?");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool CheckRouter(RouterInfo Router)
|
||||
{
|
||||
if (Router == null)
|
||||
return false;
|
||||
|
||||
bool PassTest = true;
|
||||
double Strength = Math.Round((Router.Strength / 31.0) * 100.0, 2); // Strength is out of 31, so we convert it to a percentage
|
||||
|
||||
if (Strength < 25.0)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Router signal strength is {Strength} which is below 25%. Please check the router connection.");
|
||||
PassTest = false;
|
||||
}
|
||||
|
||||
if (!Router.SimStatus.Contains("SIM Ready"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"SIM card is not ready. {Router.SimStatus} Please check the SIM card status.");
|
||||
PassTest = false;
|
||||
}
|
||||
|
||||
if (!Router.Port3Status.Contains("port:3 link:up speed:100baseT full-duplex"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Port 3 is not connected properly. {Router.Port3Status} Please check the connection.");
|
||||
PassTest = false;
|
||||
}
|
||||
|
||||
if (!Router.Port4Status.Contains("port:4 link:up speed:100baseT full-duplex"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Port 4 is not connected properly. {Router.Port4Status} Please check the connection.");
|
||||
PassTest = false;
|
||||
}
|
||||
|
||||
if (!Router.GoodPing)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList("Router could not ping 8.8.8.8. Please check the online connection.");
|
||||
PassTest = false;
|
||||
}
|
||||
|
||||
return PassTest;
|
||||
}
|
||||
}
|
||||
|
||||
class RouterInfo
|
||||
{
|
||||
public int Strength { get; set; } = 0;
|
||||
public string SimStatus { get; set; } = string.Empty;
|
||||
public string Port3Status { get; set; } = string.Empty;
|
||||
public string Port4Status { get; set; } = string.Empty;
|
||||
public bool GoodPing { get; set; } = false;
|
||||
}
|
||||
}
|
378
Camera/SSH.cs
Normal file
378
Camera/SSH.cs
Normal file
@@ -0,0 +1,378 @@
|
||||
using Renci.SshNet;
|
||||
|
||||
namespace AiQ_GUI
|
||||
{
|
||||
internal class SSH
|
||||
{
|
||||
public const string SSHUsername = "mav";
|
||||
public const string SSHPassword = "mavPA$$";
|
||||
|
||||
// Connects to camera over SSH and collects the Vaxtor packages, filesystem name, filesystem size, and tailscale status.
|
||||
public static SSHData CollectSSHData(string IPAddress)
|
||||
{
|
||||
SSHData Data = new();
|
||||
|
||||
try
|
||||
{
|
||||
using SshClient client = new SshClient(IPAddress, SSHUsername, SSHPassword);
|
||||
client.Connect();
|
||||
|
||||
try
|
||||
{
|
||||
Data.packages = GetVaxtorPackages(client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Failed to get Vaxtor packages: {ex.Message}");
|
||||
Data.packages = "Error";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
(Data.FilesystemName, Data.FilesystemSize) = GetRootFilesystemInfo(client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
|
||||
Data.FilesystemName = "Unknown";
|
||||
Data.FilesystemSize = "Unknown";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Data.tailscale = IsTailscaleInstalled(client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Failed to check Tailscale: {ex.Message}");
|
||||
Data.tailscale = false;
|
||||
}
|
||||
|
||||
client.Disconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network.");
|
||||
}
|
||||
|
||||
string LogMssg = string.Join(" | ", typeof(SSHData).GetProperties().Select(p => $"{p.Name}: {p.GetValue(Data)}"));
|
||||
Logging.LogMessage(LogMssg); // Log all of Data
|
||||
|
||||
return Data;
|
||||
}
|
||||
|
||||
// Gets a list of packages with Vaxtor in the name
|
||||
public static string GetVaxtorPackages(SshClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
SshCommand cmd = client.RunCommand("dpkg -l | grep vaxtor");
|
||||
if (!string.IsNullOrWhiteSpace(cmd.Error))
|
||||
throw new Exception(cmd.Error);
|
||||
|
||||
string result = cmd.Result;
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
return "No Vaxtor packages found.";
|
||||
|
||||
string[] lines = result.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
List<string> packages = [];
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length >= 3)
|
||||
packages.Add($"{parts[1]} - {parts[2]}"); // Package name - Version
|
||||
}
|
||||
|
||||
return packages.Count > 0 ? string.Join("\n", packages) : "No Vaxtor packages found.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error getting Vaxtor packages: {ex.Message}");
|
||||
return "Error";
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if Tailscale is installed on the camera
|
||||
public static bool IsTailscaleInstalled(SshClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
SshCommand cmd = client.RunCommand("dpkg -l | grep '^ii' | grep tailscale");
|
||||
if (!string.IsNullOrWhiteSpace(cmd.Error))
|
||||
throw new Exception(cmd.Error);
|
||||
|
||||
string command = cmd.Result;
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
return false;
|
||||
|
||||
string[] lines = command.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length >= 3 && parts[1].Equals("tailscale", StringComparison.OrdinalIgnoreCase))
|
||||
return true; // Return true if any line contains "tailscale" as the package name
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error checking Tailscale: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Connects to camera over SSH and collects the Vaxtor packages, filesystem name, filesystem size, and tailscale status.
|
||||
public static (string filesystem, string size) CollectFSData(string IPAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SshClient client = new SshClient(IPAddress, SSHUsername, SSHPassword);
|
||||
client.Connect();
|
||||
|
||||
try
|
||||
{
|
||||
return GetRootFilesystemInfo(client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Failed to get filesystem info: {ex.Message}");
|
||||
}
|
||||
|
||||
client.Disconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"SSH connection failed: {ex.Message}. Check password or network.");
|
||||
}
|
||||
|
||||
return (string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
// Gets the filesystem size and partition name
|
||||
public static (string filesystem, string size) GetRootFilesystemInfo(SshClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
SshCommand cmd = client.RunCommand("df -h");
|
||||
if (!string.IsNullOrWhiteSpace(cmd.Error))
|
||||
throw new Exception(cmd.Error);
|
||||
|
||||
string result = cmd.Result;
|
||||
if (string.IsNullOrWhiteSpace(result))
|
||||
return ("Unknown", "Unknown");
|
||||
|
||||
IEnumerable<string> lines = result.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1);
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
string[] parts = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Long enough & Filesystem name isn't tmpfs varient or none & Mountpoint is root
|
||||
if (parts.Length >= 6 && !parts[0].Contains("tmpfs") && parts[0] != "none" && parts[5] == "/")
|
||||
return (parts[0], parts[1]); // Return name & size of partition
|
||||
}
|
||||
|
||||
return ("Unknown", "Unknown");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error getting filesystem info: {ex.Message}");
|
||||
return ("Unknown", "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Sorts the SSH client for the filesystem call
|
||||
public static (string filesystem, string size) GetRootFilesystemInfo(string IPAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SshClient client = new(IPAddress, SSHUsername, SSHPassword);
|
||||
client.Connect();
|
||||
(string FilesystemName, string FilesystemSize) = GetRootFilesystemInfo(client);
|
||||
client.Disconnect();
|
||||
return (FilesystemName, FilesystemSize);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error getting root filesystem info: {ex.Message}");
|
||||
return ("Unknown", "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Checks the filesystem size and expands it if necessary, displays on the label how big the SD card is.
|
||||
public static async Task<SSHData> CheckFSSize(string IPAddress, Label LblFSSize, SSHData sshData)
|
||||
{
|
||||
const double MinGoodSize = 100.0; // 100GB
|
||||
const double MaxGoodSize = 150.0; // 150GB
|
||||
|
||||
double currentSize = NormaliseFSSize(sshData.FilesystemSize);
|
||||
LblFSSize.Text = $"Filesystem Size = {currentSize}GB";
|
||||
|
||||
if (currentSize >= MinGoodSize && currentSize <= MaxGoodSize)
|
||||
{
|
||||
LblFSSize.ForeColor = Color.LightGreen;
|
||||
return sshData;
|
||||
}
|
||||
|
||||
if (currentSize < MinGoodSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await ExpandFS(sshData.FilesystemName, IPAddress))
|
||||
{
|
||||
(sshData.FilesystemName, sshData.FilesystemSize) = GetRootFilesystemInfo(IPAddress);
|
||||
|
||||
double newSize = NormaliseFSSize(sshData.FilesystemSize);
|
||||
LblFSSize.Text = $"Filesystem Size = {newSize}GB";
|
||||
|
||||
if (newSize >= MinGoodSize && newSize <= MaxGoodSize)
|
||||
{
|
||||
LblFSSize.ForeColor = Color.LightGreen;
|
||||
return sshData;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error expanding filesystem: {ex.Message}");
|
||||
}
|
||||
|
||||
LblFSSize.ForeColor = Color.Red;
|
||||
MainForm.Instance.AddToActionsList("Size is too small, failed to expand FS, please try manually.");
|
||||
return sshData;
|
||||
}
|
||||
|
||||
LblFSSize.ForeColor = Color.Red;
|
||||
LblFSSize.Text += " Size is too big.";
|
||||
return sshData;
|
||||
}
|
||||
|
||||
// Makes sure the units given are accounted for when calcualting the size of the SD card.
|
||||
public static double NormaliseFSSize(string rootSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootSize)) return 0;
|
||||
|
||||
// Extract value & unit
|
||||
System.Text.RegularExpressions.Match match = RegexCache.FileSizeRegex().Match(rootSize.Trim());
|
||||
if (!match.Success)
|
||||
return 0;
|
||||
|
||||
if (!double.TryParse(match.Groups["value"].Value, out double value))
|
||||
return 0;
|
||||
|
||||
string unit = match.Groups["unit"].Value.ToUpperInvariant();
|
||||
|
||||
switch (unit) // Normalize to gigabytes
|
||||
{
|
||||
case "T": value *= 1024; break;
|
||||
case "G": break;
|
||||
case "M": value /= 1024; break;
|
||||
case "K": value /= 1024 * 1024; break;
|
||||
case "": value /= 1024 * 1024 * 1024; break; // assume bytes
|
||||
default: value = 0; break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error normalizing FS size: {ex.Message}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Expands the filesystem to max
|
||||
public async static Task<bool> ExpandFS(string device, string IPAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SshClient ssh = new SshClient(IPAddress, SSHUsername, SSHPassword);
|
||||
ssh.Connect();
|
||||
|
||||
SshCommand checkDevice = ssh.RunCommand($"[ -b {device} ] && echo OK || echo NOT_FOUND");
|
||||
if (!string.IsNullOrWhiteSpace(checkDevice.Error))
|
||||
throw new Exception(checkDevice.Error);
|
||||
|
||||
if (checkDevice.Result.Trim() != "OK") // Device not found
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Block device {device} not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
SshCommand umountCmd = ssh.RunCommand($"sudo umount {device}");
|
||||
if (!string.IsNullOrWhiteSpace(umountCmd.Error) && !umountCmd.Error.Contains("not mounted"))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Unmount error: {umountCmd.Error}");
|
||||
return false;
|
||||
}
|
||||
|
||||
await Task.Delay(1000); // Wait for mount to settle
|
||||
|
||||
SshCommand fsckCmd = ssh.RunCommand($"sudo e2fsck -f -y -v -C 0 {device}");
|
||||
if (!string.IsNullOrWhiteSpace(fsckCmd.Error))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"e2fsck error: {fsckCmd.Error}");
|
||||
return false;
|
||||
}
|
||||
|
||||
SshCommand resizeFs = ssh.RunCommand($"sudo resize2fs {device}");
|
||||
ssh.Disconnect();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resizeFs.Error))
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"resize2fs error: {resizeFs.Error}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resizeFs.ExitStatus == 0)
|
||||
return true;
|
||||
|
||||
MainForm.Instance.AddToActionsList($"resize2fs failed with exit code {resizeFs.ExitStatus}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Error expanding filesystem: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Sync(string IPAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SshClient ssh = new SshClient(IPAddress, SSHUsername, SSHPassword);
|
||||
ssh.Connect();
|
||||
|
||||
SshCommand checkDevice = ssh.RunCommand("sync");
|
||||
if (!string.IsNullOrWhiteSpace(checkDevice.Error))
|
||||
throw new Exception(checkDevice.Error);
|
||||
|
||||
if (checkDevice.Result.Trim().Length > 1) // Device not found
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Cannot sync files to disk. Replied: {checkDevice.Result}. DO NOT TURN OFF, GET SUPERVISOR");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MainForm.Instance.AddToActionsList($"Cannot sync becuase: {ex.Message}. DO NOT TURN OFF, GET SUPERVISOR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SSHData
|
||||
{
|
||||
public string packages { get; set; } = string.Empty;
|
||||
public string FilesystemName { get; set; } = string.Empty;
|
||||
public string FilesystemSize { get; set; } = string.Empty;
|
||||
public bool tailscale { get; set; }
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user