Add project files.

This commit is contained in:
2025-09-02 15:32:24 +01:00
parent 6a633eed7a
commit 50bb9c9781
53 changed files with 9925 additions and 0 deletions

22
.editorconfig Normal file
View File

@@ -0,0 +1,22 @@
[*.cs]
# CS8603: Possible null reference return.
dotnet_diagnostic.CS8603.severity = none
# CS8601: Possible null reference assignment.
dotnet_diagnostic.CS8601.severity = none
# CS8604: Possible null reference argument.
dotnet_diagnostic.CS8604.severity = none
# CS8602: Dereference of a possibly null reference.
dotnet_diagnostic.CS8602.severity = none
# CS8600: Converting null literal or possible null value to non-nullable type.
dotnet_diagnostic.CS8600.severity = none
# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
dotnet_diagnostic.CS8618.severity = none
# IDE1006: Naming Styles
dotnet_diagnostic.IDE1006.severity = none

2214
AiQ_GUI.Designer.cs generated Normal file

File diff suppressed because it is too large Load Diff

1846
AiQ_GUI.cs Normal file

File diff suppressed because it is too large Load Diff

528
AiQ_GUI.resx Normal file
View File

@@ -0,0 +1,528 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="ToolTipAvailable.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="ToolTipClipboard.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>159, 17</value>
</metadata>
<metadata name="TimerDDC.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>690, 19</value>
</metadata>
<metadata name="timerTypeIP.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>305, 17</value>
</metadata>
<metadata name="TimerFlash.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>422, 17</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>51</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAUAEBAAAAEAIABoBAAAVgAAABgYAAABACAAiAkAAL4EAAAgIAAAAQAgAKgQAABGDgAAMDAAAAEA
IACoJQAA7h4AAAAAAAABACAAnRUAAJZEAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAADoVwAA6FcAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGXBEkhVsRDH1WERB8VREeeFIRBXRPER5yThERa0kODmlI
DjZnRg4gY0MNH2FCDQtZPAsCWT0LHlg8CxVbPQoAhlwR44VbEUx9VhFlfFURvHhSER50TxG5ck0RcWxK
D49qSA7UZkYOo2NEDdZiQw0/Wj0LIFk9C9FXOwuhVDkKB4ZcEf2FWxFUfVYRcHxVEdF4UhEhdE8RzXJN
EYFsSg+takgOvWZGDXVjRA3sYkMNRls+C2JaPQv5VzsL4lU6CjOGXBH+hFoRd35WEYh8VRHeeFIRSHRP
EddyThF3a0kPMGpIDodmRg6xY0QN82FCDE9cPguyWj0LrFY7C81VOgqGhlwR5INZEc5/VxHie1QRyHhS
Ecl0TxHmck4RTGxJD1dqSA62ZkYOyGNEDbtgQQxJXT8LxVs+C0JVOgp4VDkKwIZcESKCWREef1cRNXxV
ERZ3UREmdVARMHNOEQVsSQ8PaUgOM2ZGDjpkRQ0VXkAMC10/Cx5cPwwEVToKC1Q5CiIAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAP//AAD//wAA//8AAP//AAD//wAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//
AAD//wAA//8AAP//AAAoAAAAGAAAADAAAAABACAAAAAAAAAJAADoVwAA6FcAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh1wRKIVbESOEWhEEflYSCH1V
ESZ7VBEffFUQAXVQEg10TxEock0RG2VFDgBsSQ8SakgORGhHDj5mRg4QY0QNIWJDDCBhQg0CWz4LAFo9
Cw1ZPQsoVzwLH1Y7CwJWOwsAh1wR6oVbEcmEWhEWflYSMX1VEd97VBG0fFUQB3VQEkx0TxHpck0RmW5L
DxZsSg+takgO+GhHDudmRQ28ZEQN32JDDLphQg0OWz4LAFo9C2xZPQvrVzsLylY7Cx5WOwsAh1wR/4Vb
Ed2EWhEYflYSNn1VEfV7VBHGfFUQCHVQElN0TxH/ck0Rpm5LD0lsSg/6akgO12hHDkFlRQ1yZEQN+2JD
DMxhQg0PXD4MC1s+C71ZPQv/VzsL/FY6Cl1WOwsAh1wR/4VbEd2EWhEYflYSNn1VEfV7VBHGfFUQCHVQ
ElN0TxH/ck0Rp25LDy1sSg/gakgO6WhHDnlmRQ15ZEQN+GJDDMxiQw4MXD8LOls+C/BZPQv0VzsL/lU6
CqpUOQoGh1wR/4VbEd+EWhEcflYSOH1VEfZ7VBHKfFQQC3VQElV0TxH/ck0RqnBMDwNsSQ9AakgOn2hH
DsRmRQ3XZEQN/2JDDMxhQg0OXT8LhFs+C/9aPQuVVzsL01U6CudUOQoyh1wR/4RaEfqCWRGqf1cRqH1V
Ef17VBH0eVIRnXZQEbd0TxH/ck0Rom5LDgNsSg9SakgOXmhHDk5mRQ2ZZEQN/2JDDL1fQQwfXT8Lzls+
C+5aPQszVjsLi1U6Cv9UOQqBh1wR54VbEd2CWRHVgFcR+31WEe17VBGgeFIR1HZREf10TxHrck0RWWZF
DAFsSg+dakgO72hHDvBmRQ32ZEQN1mJDDExeQAw/XT8L4Vw+C55cPwsFVjsLOlU6CthTOQm/h1wRJoVb
ESCCWBEaf1cRRn5WETh8VREHeFIRHnZREUp0TxE0ck4RBXFNEQBsSQ8UakgONGhHDkpmRQ1FZEQNHWtK
DABeQAwQXT8LJlw+DA9bPgsAVjoKAlU6Ch1TOQkmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAA////AP///wD///8A////AP///wD///8A////AP///wAAICEAAAAhAAAA
AQAAAAAAAAAAAAAAAAAAAAAAACCIAP///wD///8A////AP///wD///8A////AP///wD///8AKAAAACAA
AABAAAAAAQAgAAAAAAAAEAAA6FcAAOhXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AACHXRErhVsRLIRaERuEWRkAf1YSA35WEiN8VREse1QRIHpTDwF1UBMBdVASHnNOESxyTREkcU0QBG1K
DwBsSQ8WakgOUGlHDl5nRg4wZkUOB2REDSFiQwwsYUINFWJDDABbPgsAWz4LAVo9Cx1ZPQssVzwLKFY7
CwhWOwsAAAAAAIddEeqFWxHuhFoRkIRZGQB/VhIPflYSvHxVEfF7VBGselMPB3VQEwR1UBKkc04R8HJN
EcNyTRARbUoPJWxKD71qSA77aUcO/2dGDupmRQ2mZEQN2GJDDO5hQg1zYkMMAFs+CwBbPgsRWj0Lulk9
C+1XPAvjVjsLR1Y7CwBWOgoAh10R/4VbEf+EWhGdhFkZAH9WEhB+VhLNfFUR/3tUEbt6Uw8IdVATBXVQ
ErNzThH/ck0R1HJNEBVuSw+MbEoP/2pIDvxpRw6yZ0YOh2VFDctkRA3/YkMM/2FCDX1iQwwAWz4LAFs+
DEVaPQv0WT0L/1c7C/9WOwuWUzUFAVU6CgCHXRH/hVsR/4RaEZ2EWRkAf1YSEH5WEs18VRH/e1QRu3pT
Dwh1UBMFdVASs3NOEf9yTRHUcU0QF25LD6VsSg//a0kO7GpIDjRnRg4AZUUNQmREDfViQwz/YUINfWFC
DACIaAkAXD4MkFo9C/9ZPQv/VzsL/1Y6CtlVOgoeVToKAIddEf+FWxH/hFoRnYRZGAB/VhIQflYSzXxV
Ef97VBG7elMPCHVQEwV1UBKzc04R/3JNEdRyTRASbUsPXGxKD/ZqSQ7+aUcOv2dGDnxlRQ2QZEQN+GJD
DP9hQg19YEEMAF0/CxtcPgzVWj0L/1k9C+1XOwv6VjoK/FU6CltVOgoAh10R/4VbEf+EWhGdhFkZAH9W
EhB+VhLNfFUR/3tUEbt6Uw8IdVATBXVQErNzThH/ck0R1HFNEBVsSg8GbEkPZGpIDsVpRw7qZ0YO9WZF
DfhkRA3/YkMM/2FCDX1fQQwAXT8LV1w+C/paPQv7WT0Lglc7C9VWOgr/VToKqFM4CgaHXRH/hVsR/4Ra
Ec2CWRE5f1cRO35WEeB8VRH/e1QR3nlTEUd2UREtdVARzXNOEf9yTRHUcU0QFQAAAABsSg8VakgOGWlH
DidnRg44ZUUNiGREDfxiQwz/YUINekQuCwBdPwukXD4L/1o9C91aPQskVzsLlFY6Cv9UOQrjVDkKMYdd
Ef+FWxH/g1oR/4JZEe+AVxHpflYR/nxVEf96VBH7eVIR8ndREeZ1UBH9c04R/3JNEb9yTRAKbUoPF2xK
D75rSQ6/aUcOmGdGDp1lRQ3bZEQN/2JDDPVhQg1NXkAMJF0/C+JcPgv/Wz4LnlI7DQBWOwtJVjoK9lQ5
Cv5TOQmAh10R6IVbEemEWhHGglgR0IBXEf1+VhH+fFUR1XpUEXB4UhLCd1ER+3VQEf9zThHqck4RXXBN
EABtSg8YbEoPwWpJDvVpRw7+Z0YO/2ZFDfxkRA3hYkMMd2REDAVeQAxcXUAL6Fw/C9dbPgtCWj0LAFY7
CxBVOgqqVDkK61M4Cb+HXREphVsRKYRaERmBWBEbgFcRU35WEVt9VRElfVkQAHhSEhV3URFOdVARYHRP
ETRyThEEcU0QAG1KDwJsSQ8YakgONmlHDlJnRg5fZkUNUWRFDSVjQw0DX0EMAF9BDBddQAsqXD8LHVs+
DAJbPgwAVDoKAFU6ChBUOQooUzgJKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////
/////////////////////////////////////////////xACAYMQAAGDEAABgRAAIYEQAAEBEAABAAAC
AQAAAAAQAAQAEAEEAhj//////////////////////////////////////////////////////////ygA
AAAwAAAAYAAAAAEAIAAAAAAAACQAAOhXAADoVwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AACIXRIyhlwRMoVbETOEWhIrg1oQBYNaEAB+VhIAflYSEX5WETJ8VREye1QSM3pUESB4UA0AeVIRAHVQ
EgB1UBIedE8RM3NOETJyTREzcU0QFHJNEABsSg8AjWkjAGxJDx9rSQ5kakgPi2lHDoJoRw5NZ0YOD2VF
DAFkRA0gY0QMMmJDDDNhQg0jYUINAWFCDQAAAAAAWz4LAFk8CwBaPQsaWj0LM1k8CzJYPAszVzsLHl88
CwBWOwsAAAAAAAAAAACIXRLuhlwR7oVbEfCEWhLKg1oQF4NaEAB+VhIAflYSUX5WEex8VRHue1QS8npU
EZl4UA0CeVIRAHVQEgB1UBKNdE8R8nNOEe5yTRHucU0QXnJNEABmSA8AbUoPSGxJD9FrSQ79akgO/2lH
Dv9nRg73ZkYOtWVFDWVkRQ3RY0QM72JDDPJhQg2mYUINBWFCDQAAAAAAWz4LAFw/CwVaPQufWj0L8lk8
C+5YPAvxVjsLrlY7CwtWOwsAAAAAAAAAAACIXRL/hlwR/4VbEf+EWhLYg1oQGYNaEAB+VhIAflYSV35W
Efx8VRH/e1QS/3pUEaR4UA0CeVIRAHVQEgB1UBKXdE8R/3NOEf9yTRH/cU0QZXBMEABuSw8pbUoP2GxJ
D/9rSQ7/akgO/2lHDv1nRg76ZkYO/mVFDflkRQ3+Y0QM/2JDDP9hQg2yYUINBWFCDQAAAAAAWz4LAFs+
CylbPgvjWj0L/1k8C/9YPAv/VjsL7FY7CzdWOwsAAAAAAAAAAACIXRL/hlwR/4VbEf+EWhLYg1oQGYNa
EAB+VhIAflYSV35WEfx8VRH/e1QS/3pUEaR4UA0CeVIRAHVQEgB1UBKXdE8R/3NOEf9yTRH/cU0QZXBM
EABuSw90bUoP/2xJD/9rSQ7/akgO5mlHDnRnRg5KZkYOcGVFDdFkRQ3/Y0QM/2JDDP9hQg2yYUINBWFC
DQBcPgwAWz4LAFs+DG1bPgv+Wj0L/1k8C/9YPAv/VjsL/1Y6Cn9XOwsAVToKAAAAAACIXRL/hlwR/4Vb
Ef+EWhLYg1oQGYNaEAB+VhIAflYSV35WEfx8VRH/e1QS/3pUEaR4UA0CeVIRAHVQEgB1UBKXdE8R/3NO
Ef9yTRH/cU0QZXBMEABuSw+MbUoP/2xJD/9rSQ7/akgOlEhFLQBpSA8AZUUNAGVFDVllRQ38Y0QM/2JD
DP9hQg2yYUINBWFCDQBcPwwAXD8MC1s+DLlbPgv/Wj0L/1k8C/9YPAv/VjsL/1Y6CshVOgoSVToKAAAA
AACIXRL/hlwR/4VbEf+EWhLYg1oQGYNaEAB+VhIAflYSV35WEfx8VRH/e1QS/3pUEaR4UA0CeVIRAHVQ
EgB1UBKXdE8R/3NOEf9yTRH/cU0QZXBMEABuSw9kbUoP/WxJD/9rSQ7/akgOwWlHDyxnRg4HZEUMAGVF
DUxkRQ35Y0QM/2JDDP9hQg2yYUINBWFCDQBcPwsAXD8LOFw+DO1bPgv/Wj0L/1k8C/9YPAv/VjsL/1U6
CvRVOgpHVToKAAAAAACIXRL/hlwR/4VbEf+EWhLYg1oQGYNaEAB+VhIAflYSV35WEfx8VRH/e1QS/3pU
EaR4UA0CeVIRAHVQEgB1UBKXdE8R/3NOEf9yTRH/cU0QZXFMEABuSw8ZbUoPwWxJD/9rSQ7/akgO/mlH
DuJnRg64ZkYOoWVFDblkRQ39Y0QM/2JDDP9hQg2yYUINBWBBDQBaPwwAXD8LgVw+DP9bPgv/Wj0L/Vk9
C9tYPAv7VjsL/1U6Cv9VOgqTUzgKAVU6CgCIXRL/hlwR/4VbEf+EWhLYg1oQGYNaEAB+VhIAflYSV35W
Efx8VRH/e1QS/3pUEaR3UA0CeVIRAHVQEgB1UBKWdE8R/3NOEf9yTRH/cU0QZXJNEABsSg8AbUoPLWxJ
D7BrSQ7zakgO/2lHDv9nRg7/ZkYO/2VFDf9kRQ3/Y0QM/2JDDP9hQg2yYUINBV5ACwBdPwsTXT8LyVw+
DP9bPgv/Wj0L61k9C2FYPAvgVjsL/1U6Cv9VOgrWVDkKHFQ5CgCIXRL/hlwR/4VbEf+EWhLcg1oQHoNa
EAB+VhIAflYSWn5WEf18VRH/e1QS/3pUEal5Ug8EeVIRAHRPEQB1UBKadE8R/3NOEf9yTRH/cU0QZXJN
EAAAAAAAa0kOAGtJDwtrSQ4/aUgPdmlHDptnRg6yZkYOvGVFDdVkRQ3+Y0QM/2JDDP9hQg2yYUINBV0/
CwBdPwtJXT8L9Vw+DP9bPgv/Wj0LuVo9CxBYPAulVjsL/1U6Cv9VOgr6VDkKWVQ5CgCIXRL/hlwR/4Vb
Ef+EWhH6g1kRmIJYES+AWBEif1YSnn5WEf98VRH/e1QS/3pTEel5UxFteFISInZRETR1UBLNdE8R/3NO
Ef9yTRH/cU0QZHJNEABsSg8AbEoPCWxKDxtrSQ4Ga0kPAGdGDgBnRg4CZkYOCmVFDYRlRQ3/Y0QM/2JD
DP9hQg2vYUINBGNFEgFeQAuUXT8L/1w+DP9bPgv/Wj0LbVg8CwBXOwtZVjsL+lU6Cv9VOgr/VDkKpVM5
CgaIXRL/hlwR/4VbEf+EWhH/glkR/oJYEumAWBHif1cR+n5WEf98VRH/e1QS/3pTEf95UxH6eFIS4ndR
Eeh1UBL+dE8R/3NOEf9yTRH6ck0QUXJNEABsSg8AbUoPXmxJD9xrSQ60akgOhWlHDmpnRg5uZkYOnGVF
De1kRQ3/Y0QM/2JDDP9hQg2GYEEMAF5ADB1eQAvXXT8L/1w+DP9bPgvjWj0LKVk8CwBXOwscVjsL1lU6
Cv9VOgr/VDkK4VM5CS+IXRL/hlwR/4VbEf+EWhH+glkR/YJYEv+AWBH/f1cR/35WEf98VRH/e1QS8XpT
Edh5UxH+eFIS/3dREf91UBL/dE8R/3NOEf9yTRHYck0RIXJNEQBsSg8AbUoPfWxJD/9rSQ7/akgO/2lH
Dv9nRg7/ZkYO/2VFDf9kRQ3/Y0QM/2JDDM9hQg0oX0EMAF5ADFteQAz7XT8L/1w+DP9bPgumWj0LBVk9
CwBZPQ0BVjsLklU6Cv9VOgr/UzkK/FM4CXyIXRLrhlwR64VbEe2EWhHQglkRjIJYEt6AWBH+f1cR/35W
Ef98VRHufFUReHpTESZ5UhGdeFIS8HdREf91UBL/dE8R/3NOEeRyThFceoMRAHJNEQBsSg8AbUoPX2xJ
D95rSQ7yakgO/WlHDv9nRg7/ZkYO/2VFDf5lRQ3uY0QNqWJDDC9iQwwAYEIMA15ADJheQAzvXT8L7Fw+
DM1bPgs8Wz4MAF1ACwBWOwsAVjsLLlU6CsRVOgrsUzkK7lM4Cb+IXRIuhlwRLoVbES+EWhIng1kRBoFY
EiiAVxFqf1cRi35WEnx9VRE8fFURBXpTEgB4UhIIeFIRPXdREXp1UBKNdE8RdHNOETByThECck4RAG1K
DwBsSg8AbUoPBmxJDx5rSQ48aUgPXWlHDnlnRg6HZkYOhGVFDWplRQ03Y0QNCWREDQBfQQwAX0EMA19B
DCVeQAwvXT8LLlw/DBhbPQ0BWz4MAAAAAABWOgoAXjoKAFU6ChVVOgouUzkKL1M4CS4AAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA////////
AAD///////8AAP///////wAA////////AAD///////8AAP///////wAA////////AAD///////8AAP//
/////wAA////////AAD///////8AAP///////wAABg4OAB4PAAAGBgwAHAcAAAYGCAAcBwAABgYIABwH
AAAGBgg4GAMAAAYGCAgYAwAABgYIABgBAAAGBgwAEAEAAAYGDgAQAQAAAAAMYABAAAAAAAwAIEAAAAAA
DAAgQAAAAAAcAEDgAAAAEBwAwPAAAP///////wAA////////AAD///////8AAP///////wAA////////
AAD///////8AAP///////wAA////////AAD///////8AAP///////wAA////////AAD///////8AAP//
/////wAA////////AAD///////8AAP///////wAA////////AACJUE5HDQoaCgAAAA1JSERSAAABAAAA
AQAIBgAAAFxyqGYAABVkSURBVHja7d17fFTlncfxzwlXM0mGBFtFu1a0rVattfXWykUFaQkUV4QVEK+0
YFG0XdtV0UqVterLVu2CrZbUreUiFYx2g1xDNcQVcNWqrVWk2osXvCAMuXMx8+wfvwnEEEgm85zMZPi+
Xy9aCTnPPHNmzu885zy/53dARERERERERERERERERERERERERERERERERERERERERERERERERERERERE
RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE
RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE
RERERERERERERERERERERERERERERNImSHcHAPqMv6UIOD5wnA6cAu5Y4DAgCvQMHIDbCVQB7wGvA8/F
cesCxyuxxf+5LfQ+jr6+bwAnAKcDJ4M7ZncfHT0DHMDHQD2wFXgr7ngN3HMBPA9uY+x/7m5I975ORsHw
K3sFuALgYKAfjsOAw+PQL8AdAhRhn1EE53oD3ZttvgvcDqAWiDnHB8Bb4N4E3gD+AWyuKS/Zle73eSBL
WwAoGn9rLnCSg2KHGwIcGzj6ADnYwbSnkxYAPvEzB3GHiwWOvwDLwJUBG2KLb3Ptef129fH8GyLAyQ5G
OOfOCuAL2Bf+k310ELD3y8at37sC+BDcSzieAFbEyu7+R7r2e2sKiq/qkXhfh+LckcDRwOeA/gHucCwA
FODoDfSIQ7DX+3Wt7XbXyj+7XUANsAnYADwLrANerSkviYX1HvPOvozapx7q/J2b4YLCi+/8yZ4o4Hb/
b9MHFuCafY57PtDd2zT74B3gcLWB49exhTdvbu0Fi8bPLAQ3HJgIDHDQxyXaDVp5nT0/3ysA4HDNt3kL
WASUxBbftjGVnVJ0/g19gWLgIuDrQEHcuRbRst0BILGdsw7DRmA+8FCs7O53UulnR0RHTOuJowg4Atwx
Do4DvggchQWAKNBz975v8T4B4rTyftsfAFrrVjXwGs6VA08AL9es/vV2n+877+xLuwGXYO+3ZX/jwMdu
/++jLoC/4fgT8EZt5fydPvuXjNwzxuZA8Fngy8DnseAd4Fw3oFvLvrdy+DSpCgovvtN5DgAfBY5BsYU3
b2j+SoUTZuYHjlHAFcDXwPVsto21mVoAaPqPjcDPgPmxxbclNeQuOv+GPOBcYCo21O/R9G+eAkBzL4C7
HSiLld3zcTL9bK/oiKsBegOHgzsBOBn4Mo4vAP3AFbiWo8AWB0AnBYDmv7QVqAQ3D1hds/rBah/7Iu/s
SwPgR8DMffW3jQBAAI04PgTWACXg1tRWLmj00b/2yj3jgq+A+w52gvoMzb6j++r7fgJAaRgB4P3AcVZs
4c2vAxROmJkDnAn8ADgncPTa67X8BgCAHcDcwLkfb330J++1tVMLx04Hx9dxXBfY6KR3y98JIQAArgq4
F7g7VnZPbVv9TFZ0xNUFifbPAdePpi9Ls322d5fSHgCa/rIdqHTwS2Bl7eoHUx4R5J196VeAVcDBHQwA
zffdVuAB4Ke1lQu2pdq3tkQGXNDLOb4NXA/uiLb3356+7yMA1AEX54TZ6cIJM/sBP8GG5iOh6eAPXS9g
MvBg0dibjtpvH8dMj+K4DigFzqOVgz9EUeAm4LbCc6/ND6H93sBA4Aianym6ht7AN4AFOEryhn77RA9t
vgas99S/IuAGB/dGBk8sDHNHRAZc0Au4HhvZHpFic01eAZ4OLQAUTph5Jnbg34DdREqHYuCBorE3tbrT
CsdMPwZ4EAtS/dLUxx7AVThuLBx1bRgBslOHqCGIABeBeyxv6KRJeUMndXgf1T712+1Amcd9koPdV7g+
MnhiKAE2MuCCALgYCwAHeWx6OfBRGAGgOzAVgoXY2SfdhgF3FY29Kdr8h4Vjbjwbgt8BY/jk9FU6dAeu
AS4sHHVtmruSsY4GZgO35w2dVJRCO08CPm++5gDfBYaE9L6PBaYDuR7bjAHLG9aXEkYA6It9mdN1Rm3N
GODqorE35URH3xj0GXPjWOC3wEnp7lgzudjlwHHp7kgGywW+D9yXN3RSR79f/wQqPPcrClwRGTzR6+Vj
4ux/CTZD49P/YZcAoQSAgAxJMGqm6Qw7MMhhPHZj6V/S3alWHA18r3DUtV3ter0z5QATgFl5Qyd9OtmN
a5/67cfYZYDvabzBwPGe2zwEGOW5zTjwRMP60noIJwBkqk8B9xPwX4n/zlRjHHw13Z3oAsYAt+UNnRTp
wLZrselin/oCxZHTxvhscyA2z+/Te0B5018OpAAANrzO5IMf7Is0oc+oaw+0zyZZAXBp4NyUgrMvS27E
6dwHwMoQ+jSCXr36+GgoMmBcdywnpWeqbbVQCfyt6S/6kmWm4WTWPZRM1RP4DxcEpyWzUW3FXIdlHPrN
vXDuROAUT60dieXP+LQLKGtYX7p7/YUCQGbqD+7UdHeii+gH/CB/yOXJTpH9EXjJc18iwLcigy70cQ9s
KJbp59ObwNPNf6AAkJl6AgP6jPq+j7a8LY7KYMXYTbh2q62YWw0sxf/+GQYcmkoDkQHjemM3/3wfn+XY
PYDdFAAy15chSHVaaQe2PDkVDksbfZ89q/f+ACzD7qYvx6bVXsDOMFuxoWZnygMm5g+5PNl8jhXAR577
8nlgUIptHAd8zWuvAuqxu//x5j9OdwKM7NuRWLrpphTaqMHmfJO5Lq3GEmU2Aq9iB/0/gQ+AbUADNoUW
T/zJwVag9cIOxIOxdNXjgdOAU4HDO2F/nQX0B/6axDYbsNRgn1NtPYBzI4MufKzu6YeTXuQVGTAObETT
1/P++TPwXMsfKgBkrr7YjEWHA0DVstnx6IirS7Cc+s/t49casOIcf8TW5b9I4kxeveKXyZzJG7AA8Q52
bV1W8I0pPROvOwa4DP8JLc0dhgWbdgeA2oq52/POuqQMGMFey2hTMjjxXjsy1RhN9Me35TSyV72FdAWA
j7Gh4ofYWQogH/vCH4zfD6OjGhN93IydFePYTZ5PJf6EnaxzEF7OAt1fgo8nA7dgy4F7YZWV/oLdEKrE
ssI+rF7+C6/rBqpXzdmJjSJezR825XHgVmzBVRiXnt2Ak/OHXP5wzZO/SWa7J4G3sRGXL4djN/E6EgBO
wdb5exPAVgfLGp4r3evfOjsAvIXdiCgn8aUDmpZ59gI+ndgB44Bz8D8H2h7/xK5xV2PDpg+xs5tL9LEv
9gGNAb6FBYUw9AAKUm2katm9ABXREdNGA18C+mCfwxtVy3/hfQnyvtSUz3klf9iUKVjAv4RwskW/gO23
ZEYuTanBl3nsRw4wKjLowofqnn643TUpIgPHBzgXxndqd+pvS50VAN4FHsIq4Wzc+rsZ8VZ+pwa7IfNq
4bgfPw58G/gx9oXtDG8n+vgwsHHrY3e21sdaYAuwsXD0dWXABcCd2PDTtxw8rv6qWnZfDDvbp01N+Zwt
+cMmT8dSnsNYKPZpbJ+1OwDUVsxtTFwGTMDvcvXTsfsgzyexzaHY5ZpPcWBJw7rSVgNR2LMAjdid4tHA
j2ILZ2zYx8H/CbFHbq0BZgF3YZcLYffx98Do2IknzNj62J0b9nHwf7KPj9+1I/b4XfNw3ETqd9pbE5AZ
l0Je1ZSXvAfcg42qfMulY6PGMFKDi4DiyKALk9lmIPu+V9NBwSZsNNuqMANAPXYAXxZbOOO52MIZSW0c
e+TWOFCCTS+FpQ64A7g8VnrHC9wysSNtPMJ+drC06ins8sq3ji5E+5CwUoPbOYKNDBzfKam/LYUVAGqB
GcAtsYUzOlzpNfbIrR8FjiUh93FmrPSObR3u4+/vaghwj9H1C290mprykm20MiWVLs1Sg2tSbauFE7GZ
ifboT0ipv/XrSvc5ig7jHsAu7Mw/K7Zwho+EkOewG4U+11o3Aj8HZsdK7/DRxxexKTDfc7dZI3/YlBys
nHpT5do2azV2shex6ctUk3iay8VSg/9Q9/TDbV1WDsV/vsQbtEj9bSmMABADFscW3uwrG2wTFpl9BoCd
wOpY6e2++rgZmzI8IAJA/jendg9wvbHEnygQxblCoDDx9zwgAi7X/p9c5zgIOyAOwj7LjKrHUFsxtzpy
1iVLsetwnzMUTanB+8zniAwcfxCdlPrbUhgBII7f/Op6LKU1k/lIuc04BcOv7Bbg+mCzHEfiOAo4Km6L
VA7FAl4UO7B7YVNwXTm9fAVWvdrnkvHPYaOKR/bzO8dhswY+1QFP1K97dL/HYlfIBGxKOfUt06oWpV3B
8CtzsCo0x2H5GF8Fmh6B1oeuV1k4WRuwGYF/9dhmU2pwaWupwb0HjodOTP1tqSsEAAlRfvFVQQCfBQYA
w3DuNOzvPotQdgl1FXN3RM68eAmW4OU7Nfho7JmWn5BjgTWM1N9l2H2p/VIAOEBFi6cdBO7rDv4Ny7rs
TxbmHXTAk1imZH+PbR6G7ePXW/m3U7DZAp+2Asvr1z3a5i8qABxgosXTemAr56Zid55TTjfOMm9hqcE+
A0AiNXjCf9c9vXB3AlTuwPEB4aSTP4ut9WhXx+QAER0x7SgCfo49sGU0Ovj3UrdmXlP2qucbz+5U7PHy
zR2KzRL4FAeW1K97tF2ZlgoAB4DoiKuC6IhpxcCjwJV03vqKrmotrQ/XU5FIDZ7Q/GeD8F/1911sMVu7
KABkueiIq3tAzhXYQqevpLs/XcRmbErQtxFYrgS5Ayc0pf76nlnZb+pvSwoAWcwOfr4H/BRbKSftULdm
nsPqBfpODf4Se1KD+5NkHcM2BUEi9ffRdi+gUwDIUtHiaTnAFKwQSF66+9MFvZj441MuMCpv8IQAmxXw
nfr7V9pI/W1JASBb5eSMwirwhFWwJKvVrZlXgy0Q8l01+BznOBoYSRipv869n8wGCgBZKDrymmOA28m8
tQkOu0u9i/DrPPiwErsf4FN/bArW93Mf6oAl9etLkwpYygPIMtGR1/QGbqBznzK8A8s6+whbW9+0OKoK
u46uwwqANGArO7dj01/T0r2/2vA6NiNwnsc2e+H4Lh6rPSX8iQ7UzlAAyD7nAGNDfo1GrITaH7F6c3/G
Kgtvxuos7Khe9av9rt/IHzY5429K1q2ZtyNy5sVl2Eo9n1mSYaRZtyv1tyUFgCwSHXlNBBtehnXTbztQ
EcDvsJtNb1evfKCjS6q7yuVnBVY4NMyS5qnaAiyvX7s46Q0VALLLGfivKtPkTeA2oLR65f2+p8cymHsL
K2GWyQHgWaz8etK6ShSWNkRHXpMDnE84d/1fAS6qWnn/Q1UH1MEPdWvmNwJLyNyaFJb6u3Zxh4qsKgBk
j0MI5+y/BfhB1Yr716f7DabRWqxWQCZ6hyRSf1tSAMgeX8Tv022aPEYKX7D96EpLjz8inNRgH9YAf+/o
xgoAWSM4Ef9TSzuBJVUr7g+j4nGXCQB1a+Y3pQZXp7svLewEyurXLu5wToUCQBaIjrwWvD9QAoBqR9Du
hSVJ6jIBIOEl/KcGp+qvwDOpNKAAkBXi3bC15d4bJrznHXSp+oJ1a+aHlRqcinIgqdTflhQAsoLrRjhz
/3mEE1ggPQ9+TdVKLNMxE1jq79rFKQUkBYDs0NFHYrUlN8Cdecjwy8Poc1G4uyQUG7EZgUzwMh4em6cA
kBWCRsJ7LsG47fQ+0meD+cMmH4GlLHcptWvm78DKhaX7MXBNNyWrUm1IASArBB8DH4TU+LHA9OjwqV7y
1/OHTT4SuBf/lXA7SwWWGpxOW4AVHUn9bUkBIAtULb0X7DlwYQiAy4Cbo8OndriIaP6wKQX5w6aMBxZj
GYtd9cEsb2Olw9Mo6HDqb0taC5A13J/w/xDVJj2BH+L4fPSbU+928EL1yvt3trVRwTemdMceMjLEOcZh
Dx8Jo3+dprZyfmPe4IvKgIvS9F7i2Nz/dh+NKQBkj1ewoekxIbXfHRgDnOmgouCb3/0Dtgb9PaAe5+LY
1F4BVurqBHBnAKcBR5Bdo831WGrwSWl47XfwOAJRAMge72Or1sIKAE0OxuoNjMWKfWzDpqQasZFCPvbA
UN9ZiRnENaUGn5SGF6/Aai94kU1R+YBWtXRWHKv7X9uJL5uPPeb7WOB4rMb9oWT1wQ+1lQvSlRqcSP1d
5K2cmgJAdllHOAt3ZG8vYRWROtNGUkz9bUkBIItULZ1VD8wGYunuS7arrVxQSyenBgcBq5zzO92rAJB9
KrGnAGVSznq26szU4FrgiYZ1i7x+rgoAWaZq6axdwM/wPFSUVnkfku/Hy4RwyaEAkIWqls7aBPwQq+OX
iXZiiUvxVBtKp9rKBTux1OCwn3HgLfW3JQWALFW1dNazwDXYvHEmeRO4GriOzK2zl4wKwk8N3gKsqHtm
kfeGFQCy2zKsTHiHS0Z51ADMg+C8mvKSOcDzwKZ0d8qDzkgNXge8FkbDCgBZrGrZbKqWzX4CuAR7gEe6
/Bl7UOkVNeVzXkn87F0sb6FLq6tcEMcuA7yk5raiEVhS98yiUNpXADgAVC2b/b/AeOA32Jm4s2wD7gPO
qymfM7+mfM7u164pL4ljU5bZkLewnpDO0HhO/W0pjADQHb8rvQL8pyz77iP4r3HntchH1bL7/g5cCXwH
G36HOU3YgF1+jAf+vXrVnFbrCtaUl7yLjQzmY2nFqUpXavsWwqsavAaPqb8tdQcWem6zCr8pkrVYaWqf
T7rdSYq11Fppbym2IMfngeX12r1q2X3bgYejxdMqsIPzIuBL+DtwtmJ5CHOB1dWr5rR5UNeUl/wtf9jk
KdjoZDCWWtxaf9pTQ/Ad0nBjsa5ygYsMnvgI8Bn8BiEHPFT3zKLQCpB0x74EPjn8HgSbsbvZvs/YPqeg
aoHpIfQxlLN01fL7NgH3RIuvWgAMAc4FTgcOA3ol0VQjdtC/hg1TVwAvV6/6VVLXqzXlJQ2J7Z8EyD/n
O/vaj23tX9/fvXarq1zwcmTQhZe2o4/JtfvMolCnSrtqUQbxqKD4qh7Ys+9OxbnTsVHBZ4GDA1wudqno
cDQAsTi8E+A2YJcSzwMbqlc+sC3d70NERERERERERERERERERERERERERERERERERERERERERERERERE
RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE
RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERE
REREREREREREREREREREREREREREREREREREpEv5f37QL4f4ngMLAAAAAElFTkSuQmCC
</value>
</data>
</root>

91
AiQ_GUI_NET_Test.csproj Normal file
View File

@@ -0,0 +1,91 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<ApplicationIcon>Resources\mav_new.ico</ApplicationIcon>
<PlatformTarget>AnyCPU</PlatformTarget>
<Platforms>AnyCPU;x86</Platforms>
<BaseOutputPath>C:\ProgramData\MAV</BaseOutputPath>
<Title>AiQ GUI</Title>
<Company>MAV Systems Ltd</Company>
<Product>AiQ GUI</Product>
<Authors>MAV Systems Ltd</Authors>
<PackageId>AiQ GUI</PackageId>
<Version>3.12.0</Version>
<Description>A GUI to control and test the AiQ</Description>
<Copyright>MAV Systems Ltd 2025</Copyright>
<PackageIcon>MAV - Plain - Blue.png</PackageIcon>
<UseWPF>False</UseWPF>
<AssemblyName>AiQ_GUI</AssemblyName>
<RootNamespace>AiQ_GUI</RootNamespace>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<Optimize>True</Optimize>
</PropertyGroup>
<ItemGroup>
<Content Include="Resources\mav_new.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Emgu.CV" Version="4.12.0.5763" />
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.12.0.5763" />
<PackageReference Include="Google.Apis.Auth" Version="1.70.0" />
<PackageReference Include="Google.Apis.Gmail.v1" Version="1.70.0.3833" />
<PackageReference Include="Google.Apis.Sheets.v4" Version="1.70.0.3819" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PDFsharp-MigraDoc-gdi" Version="6.2.1" />
<PackageReference Include="Selenium.Support" Version="4.35.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.35.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="139.0.7258.15400" />
<PackageReference Include="SSH.NET" Version="2025.0.0" />
<PackageReference Include="System.Data.OleDb" Version="9.0.8" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Update="FakeCamera\Diagnostics.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FakeCamera\Versions.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
</Project>

31
AiQ_GUI_NET_Test.sln Normal file
View File

@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.36105.23
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AiQ_GUI_NET_Test", "AiQ_GUI_NET_Test.csproj", "{F19648CA-201B-48A6-A9EF-C000532E5C86}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Debug|x86.ActiveCfg = Debug|x86
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Debug|x86.Build.0 = Debug|x86
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Release|Any CPU.Build.0 = Release|Any CPU
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Release|x86.ActiveCfg = Release|x86
{F19648CA-201B-48A6-A9EF-C000532E5C86}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65E4A16A-2E12-4C99-839A-91EB64B53D48}
EndGlobalSection
EndGlobal

18
App.config Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="AiQ_GUI.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
</configSections>
<userSettings>
<AiQ_GUI.Properties.Settings>
<setting name="FirstRun" serializeAs="String">
<value>True</value>
</setting>
<setting name="UnitTesting" serializeAs="String">
<value>NOT_STARTED</value>
</setting>
</AiQ_GUI.Properties.Settings>
</userSettings>
</configuration>

99
Camera/CameraModules.cs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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; }
}
}

View File

@@ -0,0 +1,59 @@
{
"cameraName": "AiQ-ANPR-Camera",
"version": "1.6.4",
"revision": "bf16134",
"serialNumber": "K1005001",
"modelNumber": "AB12CD",
"MAC": "3C:6D:66:0A:BA:18",
"timeStamp": 1754296734,
"licenses": {
"saf1": true,
"saf2": true,
"saf3": false,
"saf4": false,
"audit": false,
"stream": false,
"raptorKeyID": "833051022471605126"
},
"internalTemperature": 50.5,
"cpuUsage": 80.8987815001732,
"trim": [
-57,
48
],
"zoomLock": true,
"IRmodule": {
"zoom": 0,
"focus": 40960,
"expMode": 0,
"shutter": 11,
"iris": 12,
"gain": 3,
"firmwareVer": "1.1"
},
"OVmodule": {
"zoom": 0,
"focus": 16384,
"expMode": 0,
"shutter": 12,
"iris": 12,
"gain": 0,
"firmwareVer": "1.1"
},
"ledChannelVoltages": [
21.5,
22.75,
21.75,
21.5,
21,
21.25
],
"ledChannelCurrents": [
92,
89,
89,
95,
96,
90
]
}

View File

@@ -0,0 +1,59 @@
{
"cameraName": "AiQ-ANPR-Camera",
"version": "1.2",
"revision": "77e042f",
"serialNumber": "K1005001",
"modelNumber": "AB12CD",
"MAC": "3C:6D:66:0A:BA:18",
"timeStamp": 1754296734,
"licenses": {
"saf1": true,
"saf2": true,
"saf3": false,
"saf4": false,
"audit": false,
"stream": false,
"raptorKeyID": "833051022471605126"
},
"internalTemperature": 110.2,
"cpuUsage": 1000,
"trim": [
-57,
12
],
"zoomLock": true,
"IRmodule": {
"zoom": 934500,
"focus": 4,
"expMode": 3000,
"shutter": 110,
"iris": 120,
"gain": 3000,
"firmwareVer": "99"
},
"OVmodule": {
"zoom": 9345000,
"focus": 1,
"expMode": -1,
"shutter": 1200,
"iris": 120,
"gain": 9999,
"firmwareVer": "-1"
},
"ledChannelVoltages": [
250,
2.3,
261,
21,
282,
21.25
],
"ledChannelCurrents": [
12,
100,
132,
51,
34,
61
]
}

View File

@@ -0,0 +1,59 @@
{
"cameraName": "AiQ-ANPR-Camera",
"version": "1.6.4",
"revision": "77e042f",
"serialNumber": "K1005001",
"modelNumber": "AB12CD",
"MAC": "21",
"timeStamp": 1754296734,
"licenses": {
"saf1": no,
"saf2": yes,
"saf3": 0,
"saf4": -1,
"audit": false,
"stream": true,
"raptorKeyID": "000"
},
"internalTemperature": NOPE,
"cpuUsage": "WHAT IS A CPU",
"trim": [
-57,
48
],
"zoomLock": NahNOTTODAY,
"IRmodule": {
"zoom": 934dff,
"focus": 4096d0,
"expMode": 3,
"shutter": 11,
"iris": 12,
"gain": 3,
"firmwareVer": "FIRMWARE"
},
"OVmodule": {
"zoom": 0,
"focus": FOCAL,
"expMode": 02321243,
"shutter": 1232323,
"iris": 12,
"gain": 0,
"firmwareVer": "1.1"
},
"ledChannelVoltages": [
"ddsda",
22.75,
21.75,
"####",
"!!!!!",
"你好"
],
"ledChannelCurrents": [
"你好",
####,
-1,
262000,
TEST,
265
]
}

354
FakeCamera/FakeCamera.cs Normal file
View File

@@ -0,0 +1,354 @@
using Newtonsoft.Json;
using System.Net;
using System.Text;
namespace AiQ_GUI
{
public enum CAMTYPE
{
GOOD,
BAD,
BIZARRE
}
public class FakeCamera
{
public const string JSONLoc = "C:\\Users\\BradleyBorn\\OneDrive - MAV Systems Ltd\\Desktop\\AIQ_GUI_TEST\\FakeCameraGood\\";
public static bool Snapshot = false;
private HttpListener listener;
public FakeCamera(int port = 8080)
{
listener = new HttpListener();
listener.Prefixes.Add($"http://*:{port}/");
listener.Prefixes.Add("http://localhost:8080/");
}
public async Task StartAsync(CAMTYPE camType)
{
try
{
listener.Start();
MainForm.Instance.AddToActionsList("Started server successfully");
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Failed to start server: {ex.Message}");
return;
}
while (true)
{
HttpListenerContext? context = await listener.GetContextAsync();
_ = HandleRequestAsync(context, camType);
}
}
public void Stop()
{
if (listener.IsListening)
{
listener.Stop();
listener.Close();
}
}
public static bool ValidateCredentials(string username, string password, CAMTYPE camType)
{
string filePath = camType switch
{
CAMTYPE.GOOD => $"{JSONLoc}Versions.json",
CAMTYPE.BAD => $"{JSONLoc}Versions_Bad.json",
CAMTYPE.BIZARRE => $"{JSONLoc}Versions_Bizarre.json",
_ => throw new ArgumentOutOfRangeException(nameof(camType), "Invalid camera type")
};
string versJson = File.ReadAllText(filePath);
Versions vers = JsonConvert.DeserializeObject<Versions>(versJson);
TimeSpan t = DateTime.UtcNow - new DateTime(1970, 1, 1);
int secondsSinceEpoch = (int)t.TotalSeconds;
string PASS = Lics.GeneratePassword(vers.MAC, vers.version, secondsSinceEpoch);
return username == "developer" && password == PASS;
}
private static async Task HTTPReplySetup(HttpListenerContext context, string ResponseText, int StatusCode, string ContentType)
{
try
{
context.Response.StatusCode = StatusCode;
context.Response.ContentType = ContentType;
byte[] responseBytes = Encoding.UTF8.GetBytes(ResponseText);
context.Response.ContentLength64 = responseBytes.Length;
await context.Response.OutputStream.WriteAsync(responseBytes);
}
finally
{
context.Response.OutputStream.Close();
}
}
private static async Task HTTPReplySetup(HttpListenerContext context, byte[] content, int statusCode, string contentType)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = contentType;
context.Response.ContentLength64 = content.Length;
await context.Response.OutputStream.WriteAsync(content);
await context.Response.OutputStream.FlushAsync();
context.Response.OutputStream.Close();
}
private static async Task HandleRequestAsync(HttpListenerContext context, CAMTYPE camType)
{
string path = context.Request.Url?.AbsolutePath ?? "/";
string Method = context.Request.HttpMethod;
string headers = context.Request.Headers.ToString();
string Content = context.Request.InputStream?.ToString() ?? "";
// Define endpoints that do NOT require authentication
string[] publicEndpoints = ["/api/versions", "/api/diagnostics"];
bool requiresAuth = !publicEndpoints.Contains(path);
if (requiresAuth)
{
string authHeader = context.Request.Headers["Authorization"];
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic "))
{
context.Response.AddHeader("WWW-Authenticate", "Basic realm=\"FakeCamera\"");
await HTTPReplySetup(context, "", 401, "text/plain");
return;
}
try
{
string encodedCredentials = authHeader.Substring("Basic ".Length).Trim();
string decodedCredentials = Encoding.UTF8.GetString(Convert.FromBase64String(encodedCredentials));
string[] parts = decodedCredentials.Split(':', 2);
if (parts.Length != 2)
{
throw new Exception("Invalid Basic auth format. Expected username:password.");
}
string username = parts[0];
string password = parts[1];
if (!ValidateCredentials(username, password, camType))
{
throw new Exception("Invalid credentials");
}
}
catch
{
context.Response.AddHeader("WWW-Authenticate", "Basic realm=\"FakeCamera\"");
await HTTPReplySetup(context, "", 401, "text/plain");
return;
}
}
try
{
if (Method == "GET")
{
if (path.Equals("/Infrared/led-controls")) // Works
{
string[] allowedPowers = ["LOW", "MID", "HIGH", "SAFE", "OFF"];
string power = context.Request.QueryString["power"]?.ToUpper();
if (allowedPowers.Contains(power))
await HTTPReplySetup(context, "Power levels set successfully", 200, "text/plain");
else
await HTTPReplySetup(context, "Power level not accepted", 400, "text/plain");
}
else if (path.Equals("/api/versions"))
{
string versionsJson = File.ReadAllText(camType switch
{
CAMTYPE.GOOD => $"{JSONLoc}Versions.json",
CAMTYPE.BAD => $"{JSONLoc}Versions_Bad.json",
CAMTYPE.BIZARRE => $"{JSONLoc}Versions_Bizarre.json",
_ => throw new ArgumentOutOfRangeException()
});
Versions vers = JsonConvert.DeserializeObject<Versions>(versionsJson);
vers.timeStamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
versionsJson = JsonConvert.SerializeObject(vers);
await HTTPReplySetup(context, versionsJson, 200, "application/json");
}
else if (path.Equals("/api/diagnostics"))
{
string filePath = camType switch
{
CAMTYPE.GOOD => $"{JSONLoc}Diagnostics.json",
CAMTYPE.BAD => $"{JSONLoc}Diagnostics_Bad.json",
CAMTYPE.BIZARRE => $"{JSONLoc}Diagnostics_Bizarre.json",
_ => throw new ArgumentOutOfRangeException()
};
string diagsJson = File.ReadAllText(filePath);
if (camType == CAMTYPE.GOOD)
{
Diags diags = JsonConvert.DeserializeObject<Diags>(diagsJson);
diags.timeStamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
diagsJson = JsonConvert.SerializeObject(diags);
}
await HTTPReplySetup(context, diagsJson, 200, "application/json");
}
else if (path.Equals("/api/zoomLock")) // Fixed
{
string enable = context.Request.QueryString["enable"];
if (!string.IsNullOrEmpty(enable) && enable.Equals("true", StringComparison.OrdinalIgnoreCase)) // FIXED Use http://localhost:8080/api/zoomLock?enable=true
{
await HTTPReplySetup(context, "Zoom lock enabled.", 200, "text/plain");
}
else if (!string.IsNullOrEmpty(enable) && enable.Equals("false", StringComparison.OrdinalIgnoreCase)) // FIXED Use http://localhost:8080/api/zoomLock?enable=false
{
await HTTPReplySetup(context, "Zoom lock disabled.", 200, "text/plain");
}
else
{
await HTTPReplySetup(context, "Missing or invalid 'enable' parameter.", 400, "text/plain");
}
}
else if (path.Equals("/Infrared-camera-factory-reset") || path.Equals("/Colour-camera-factory-reset")) // WORKS
{
await HTTPReplySetup(context, "Factory reset OK.", 200, "text/plain");
}
else if (path.Equals("/Infrared-camera-control") || path.Equals("/Colour-camera-control")) // Fixed
{
string viscaReply = "9041FF9051FF";
if (!RegexCache.VISCARegex().IsMatch(context.Request.QueryString["commandHex"]))
viscaReply = "9060FF";
await HTTPReplySetup(context, viscaReply, 200, "text/plain");
}
else if (path.Equals("/SightingCreator-plate-positions")) //Works
{
string trimJson = File.ReadAllText($"{JSONLoc}Trim.json");
await HTTPReplySetup(context, trimJson, 200, "application/json");
}
else if (path.Equals("/Infrared-snapshot")) // Fixed
{
string jpegPath;
if (!Snapshot)
{
jpegPath = $"{JSONLoc}IR_Open_image.jpg"; // Light Image
Snapshot = true;
}
else
{
jpegPath = $"{JSONLoc}IR_Tight_image.jpg"; // Dark Image
}
byte[] jpegBytes = File.ReadAllBytes(jpegPath);
await HTTPReplySetup(context, jpegBytes, 200, "image/jpeg");
}
else if (path.Equals("/Colour-snapshot")) // Fixed
{
byte[] jpegBytes = File.ReadAllBytes($"{JSONLoc}OV_image.jpg");
await HTTPReplySetup(context, jpegBytes, 200, "image/jpeg");
}
else if (path.Equals("/api/fetch-config"))
{
ConfigObject config = JsonConvert.DeserializeObject<ConfigObject>(Content);
if (config.Id == "GLOBAL--NetworkConfig")
{
// TODO - Return success message?
}
else
{
await HTTPReplySetup(context, "ID not valid.", 200, "text/plain");
}
}
else if (path.Equals("/api/RaptorOCR-auto-license")) // Works
{
string vaxtorJson = "{ \"protectionKeyId\":\"123456789012345678\" }";
await HTTPReplySetup(context, vaxtorJson, 200, "application/json");
}
else
{
await HTTPReplySetup(context, $"GET request for {path} not supported", 400, "text/plain");
}
}
else if (Method == "POST")
{
if (path.Equals("/api/update-config"))
{
ConfigObject fo = JsonConvert.DeserializeObject<ConfigObject>(Content);
if (fo.Id == "GLOBAL--NetworkConfig" || fo.Id == "SightingCreator" || fo.Id == "RaptorOCR")
{
await HTTPReplySetup(context, "Update successful.", 200, "text/plain");
}
else if (fo.Id == "Internal Config")
{
string SerialNumber = "";
string ModelNumber = "";
fo.Fields.ForEach(field =>
{
if (field.Property == "propSerialNumber")
SerialNumber = field.Value;
else if (field.Property == "propMavModelNumber")
ModelNumber = field.Value;
});
// Update JSON with new SerialNumber and ModelNumber beofre sending back to the client
using StreamReader r = new($"{JSONLoc}InternalConfig.json");
string InternalConfig_JSON = r.ReadToEnd();
InternalConfig_JSON = InternalConfig_JSON.Replace("SSSSSS", SerialNumber).Replace("MMMMMM", ModelNumber);
HTTPReplySetup(context, InternalConfig_JSON, 200, "application/json");
}
else
{
await HTTPReplySetup(context, "ID not valid.", 200, "text/plain");
}
}
else
{
await HTTPReplySetup(context, $"POST request for {path} not supported.", 400, "text/plain");
}
}
else
{
await HTTPReplySetup(context, "Bad Request.", 400, "text/plain");
}
}
catch (Exception ex)
{
await HTTPReplySetup(context, $"Server error: {ex.Message}", 500, "text/plain");
MainForm.Instance.AddToActionsList($"Server error handling {Method} {path}: {ex.Message}");
}
}
public class ConfigObject
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("fields")]
public List<Field> Fields { get; set; }
}
public class Field
{
[JsonProperty("property")]
public string Property { get; set; }
[JsonProperty("value")]
public string Value { get; set; }
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,40 @@
{
"id": "GLOBAL",
"configHash": "1698097425",
"propPipelineFile": {
"value": "DUAL_CAMERA_BASIC_ANPR.json",
"datatype": "java.lang.String"
},
"propPipelineLocked": {
"value": "false",
"datatype": "boolean"
},
"propUpdateDisabled": {
"value": "false",
"datatype": "boolean"
},
"propSerialNumber": {
"value": "SSSSSS",
"datatype": "java.lang.String"
},
"propMavModelNumber": {
"value": "MMMMMM",
"datatype": "java.lang.String"
},
"propHardwareIdentifiers": {
"value": "N/A",
"datatype": "java.lang.String"
},
"propStartupWaitMillis": {
"value": "5000",
"datatype": "long"
},
"propGodMode": {
"value": "true",
"datatype": "boolean"
},
"propYUY2ByteArrayPoolSize": {
"value": "0",
"datatype": "int"
}
}

BIN
FakeCamera/OV_image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

6
FakeCamera/Trim.json Normal file
View File

@@ -0,0 +1,6 @@
{
"infraredX": 1197,
"infraredY": 792,
"colourX": 1167,
"colourY": 806
}

12
FakeCamera/Versions.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": "1.6.4",
"revision": "77e042f",
"buildtime": "2025-07-24T10:35:36.445241784Z",
"appname": "FlexiAI",
"MAC": "3C:6D:66:0A:BA:18",
"timeStamp": 1754296711,
"UUID": "b7c1ab51-4543-4b1b-8f05-2697905047ff",
"proquint": "hijag-hosir",
"Serial No.": "K1005001",
"Model No.": "AB12CD"
}

View File

@@ -0,0 +1,12 @@
{
"version": "1.2",
"revision": "77e042f",
"buildtime": "2025-07-24T10:35:36.445241784Z",
"appname": "FlexiAI",
"MAC": "3C:6D:66:0A:BA:18",
"timeStamp": 1754296711,
"UUID": "b7c1ab51-4543-4b1b-8f05-2697905047ff",
"proquint": "hijag-hosir",
"Serial No.": "K1005001",
"Model No.": "AB12CD"
}

View File

@@ -0,0 +1,12 @@
{
"version": "BAAAAAAA",
"revision": "77e042f",
"buildtime": "2025-07-24T10:35:36.445241784Z",
"appname": "",
"MAC": "-1",
"timeStamp": 1754296711,
"UUID": "b7c1ab51-4543-4b1b-8f05-2697905047ff",
"proquint": "hijag-hosir",
"Serial No.": "K1005001",
"Model No.": "31"
}

114
GUIUpdate.cs Normal file
View File

@@ -0,0 +1,114 @@
using System.Diagnostics;
using System.Reflection;
using File = System.IO.File;
namespace AiQ_GUI
{
internal class GUIUpdate
{
public static string GUIVerShort = "";
public static string FindGUIVersion()
{
string GUIVersion = Convert.ToString(Assembly.GetExecutingAssembly().GetName().Version);
int dotLocation = GUIVersion.IndexOf('.'); // Find the first dot location
// If there is no dot in the version string, return the version as is
if (dotLocation < 0)
{
return GUIVersion;
}
// Check if the next character after the dot is "0"
if (dotLocation + 1 < GUIVersion.Length && GUIVersion[dotLocation + 1] == '0')
{
return GUIVersion.Substring(0, dotLocation); // If it's "0", remove everything after the first dot
}
else
{
// Otherwise, trim the version to the second dot (if it exists)
int secondDotLocation = GUIVersion.IndexOf('.', dotLocation + 1);
if (secondDotLocation >= 0)
{
return GUIVersion.Substring(0, secondDotLocation);
}
else
{
// If there's no second dot, return the version up to the first dot
return GUIVersion.Substring(0, dotLocation);
}
}
}
// Checks if the version in the model info spreadsheet is newer than the one installed and goes to update path if so and opens file
// Because it has the same application name Windows treats it as an update and installs it as the same program, therefore no need for acrhive folder
// It will install as 1 program as not to fill up the PC with lots of different apps
public static void UpdateGUI()
{
if (ComapreVersions()) // Checks if the current version is older than the latest version
{
string GUIPath = $"{GoogleAPI.DrivePath}AiQ\\GUI's\\AiQ_Final_Test\\AiQ_GUI.application";
// Check if path is real
if (!File.Exists(GUIPath))
{
MainForm.Instance.AddToActionsList("Error finding new app version in Google Drive.");
return;
}
// Brings up messagebox to ask user if they want to update
DialogResult result = MessageBox.Show(
$"Do you want to update to version {UniversalData.LatestVersion}?",
"Update Available",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly
);
if (result == DialogResult.Yes)
{
ProcessStartInfo psi = new()
{
FileName = GUIPath,
UseShellExecute = true // Lets the OS decide how to open it
};
Logging.LogMessage($"Updating to {UniversalData.LatestVersion} at {GUIPath}");
Process.Start(psi);
Application.Exit();
}
else
{
Logging.LogWarningMessage($"Refused Update to {UniversalData.LatestVersion} at {GUIPath}");
}
}
}
private static bool ComapreVersions()
{
// Handles missing dots and patch numbers
string[] currentParts = GUIVerShort.Split('.');
string[] latestParts = UniversalData.LatestVersion.Split('.');
int currentMajor = currentParts.Length > 0 && int.TryParse(currentParts[0], out int cm) ? cm : 0;
int latestMajor = latestParts.Length > 0 && int.TryParse(latestParts[0], out int lm) ? lm : 0;
int currentMinor = currentParts.Length > 1 && int.TryParse(currentParts[1], out int cmi) ? cmi : 0;
int latestMinor = latestParts.Length > 1 && int.TryParse(latestParts[1], out int lmi) ? lmi : 0;
int currentPatch = currentParts.Length > 2 && int.TryParse(currentParts[2], out int cp) ? cp : 0;
int latestPatch = latestParts.Length > 2 && int.TryParse(latestParts[2], out int lp) ? lp : 0;
if (latestMajor > currentMajor)
return true; // Newer major version
else if (latestMajor == currentMajor)
{
if (latestMinor > currentMinor)
return true; // Newer minor version
else if (latestMinor == currentMinor)
{
if (latestPatch > currentPatch)
return true; // Newer patch version
}
}
return false; // There is not a newer version
}
}
}

358
GoogleAPI.cs Normal file
View File

@@ -0,0 +1,358 @@
using Google.Apis.Auth.OAuth2;
using Google.Apis.Gmail.v1;
using Google.Apis.Services;
using Google.Apis.Sheets.v4;
using Google.Apis.Sheets.v4.Data;
using Google.Apis.Util.Store;
using System.Net.Mail;
using System.Net.Mime;
using System.Reflection;
namespace AiQ_GUI
{
internal class GoogleAPI
{
public static UserCredential credential;
public static SheetsService service = new();
const string ApplicationName = "Google Sheets API .NET Quickstart";
static readonly string credPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public const string spreadsheetId_ModelInfo = "1bCcCr4OYqfjmydt6UqtmN4FQETezXmZRSStJdCCcqZM";
public const string DrivePath = @"G:\Shared drives\MAV Production GUI's\"; // Path to google shared drive
// Startup and make necessary connections to Google servers and make sure user is logged in
public static bool Setup()
{
try
{
string streamPath = $"{DrivePath}R50IQ\\client_secret.json";
string[] Scopes = [SheetsService.Scope.Spreadsheets];
FileStream stream = new(streamPath, FileMode.Open, FileAccess.Read);
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(GoogleClientSecrets.FromStream(stream).Secrets, Scopes, "user", CancellationToken.None, new FileDataStore(credPath, true)).Result;
service = new SheetsService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
return true;
}
catch
{
return false;
}
}
// Write a 1D array range to a spreadsheet
public static void WriteToSS(List<object> ToWrite, string Range, string SSID)
{
ValueRange ValueRange = new() { Values = [ToWrite] };
SpreadsheetsResource.ValuesResource.UpdateRequest Update = service.Spreadsheets.Values.Update(ValueRange, SSID, Range);
Update.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.RAW;
Update.Execute();
}
// Read a range from a spreadsheet, always returns a 2D object even if 1D is requested
public static IList<IList<object>> ReadSS(string sheet, string Location)
{
return service.Spreadsheets.Values.Get(sheet, Location).Execute().Values;
}
// Checks the WIP columns of a given serial number
public static bool CheckWIP(string SerialNumber, string spreadsheetId)
{
int Row = CheckSerialNumRow(spreadsheetId, SerialNumber); // Get row of serial number - 1 because don't want next row in this case
IList<IList<object>> valuesRow = ReadSS(spreadsheetId, $"S{Row}"); // Check if that row is WIP or not
return Convert.ToBoolean(valuesRow[0][0]);
}
// Update serial number register with relevant details about the camera that has passed
public static string UpdateSpreadSheetPreTest(string spreadsheetId, Versions Vers, string CamDesc, string ModelOnTest)
{
try
{
// Finds next row to be used by length of returned array and add 1
IList<IList<object>> values = service.Spreadsheets.Values.Get(spreadsheetId, "B1:B").Execute().Values;
int nextRow = values != null ? values.Count + 1 : 0;
if (values?.Count > 0)
{
// Serial number location = nextRow - 2 (Array starts at 0 but spreadsheet starts at 1 & next row has already added one so need to get rid)
string lastSerialNumber = Convert.ToString(values[nextRow - 2][0]);
int NewSerialNumberInt = Convert.ToInt32(lastSerialNumber.Substring(1)) + 1; // Generate new serial number knowing the last
string newSerialNumber = "K" + NewSerialNumberInt.ToString();
// Send data to spreadsheet and set WIP to TRUE
List<object> oblistAE = // Write columns A-E
[
ModelOnTest,
newSerialNumber,
CamDesc,
"Pre Test: " + DateTime.Now.ToString("dd/MM/yyyy"),
Vers.version + " - " + Vers.revision + Environment.NewLine + Vers.buildtime + Environment.NewLine + Vers.proquint,
];
WriteToSS(oblistAE, $"A{nextRow}:E{nextRow}", spreadsheetId);
// Write MAC to column H
List<object> oblistH = [Vers.MAC];
WriteToSS(oblistH, $"H{nextRow}", spreadsheetId);
List<object> oblistN = [$"GUI Version: {GUIUpdate.GUIVerShort}"]; // Write column N
WriteToSS(oblistN, $"N{nextRow}", spreadsheetId);
// Write TRUE to WIP checkbox in column S
List<object> oblistS = ["TRUE"];
WriteToSS(oblistS, $"S{nextRow}", spreadsheetId);
return newSerialNumber;
}
else
{
return "Last serial number not found";
}
}
catch (Exception ex)
{
return $"ERROR: {ex.Message}";
}
}
// Update serial number register with relevant details about the camera that has passed
public static string UpdateSpreadSheetRePreTest(string spreadsheetId, Versions Vers)
{
try
{
int CamRow = CheckSerialNumRow(spreadsheetId, Vers.Serial);
if (CamRow != 0)
{
// Send data to spreadsheet and set WIP to TRUE
List<object> oblistDE = // Write columns D-E
[
"Pre Test: " + DateTime.Now.ToString("dd/MM/yyyy"),
Vers.version + " - " + Vers.revision + Environment.NewLine + Vers.buildtime + Environment.NewLine + Vers.proquint,
];
WriteToSS(oblistDE, $"D{CamRow}:E{CamRow}", spreadsheetId);
// Write MAC to column H
List<object> oblistH = [Vers.MAC];
WriteToSS(oblistH, $"H{CamRow}", spreadsheetId);
List<object> oblistN = [$"GUI Version: {GUIUpdate.GUIVerShort}"]; // Write column N
WriteToSS(oblistN, $"N{CamRow}", spreadsheetId);
// Write TRUE to WIP checkbox in column S
List<object> oblistS = ["TRUE"];
WriteToSS(oblistS, $"S{CamRow}", spreadsheetId);
return "OK";
}
else
{
return "Serial number not found";
}
}
catch (Exception ex)
{
return $"ERROR: {ex.Message}";
}
}
// Update serial number register with relevant details about the camera that has passed
public static string UpdateSpreadSheetFinalTest(string spreadsheetId, Diags DiagsAPI, SSHData sshData, int RMANum)
{
try
{
int CamRow = CheckSerialNumRow(spreadsheetId, DiagsAPI.serialNumber);
IList<IList<object>> valuesRow = ReadSS(spreadsheetId, $"D{CamRow}");
string TestDate = Convert.ToString(valuesRow[0][0]) + Environment.NewLine + "Final Test: " + DateTime.Now.ToString("dd/MM/yyyy");
if (RMANum != 0)
TestDate = TestDate.Replace("Final", "RMA"); // So it will say RMA Test in the spreadsheet.
// Write column D
List<object> oblistD = [TestDate];
WriteToSS(oblistD, $"D{CamRow}", spreadsheetId);
List<object> oblistFG = // Write columns F-G
[
DiagsAPI.licenses.raptorKeyID,
sshData.packages
];
WriteToSS(oblistFG, $"F{CamRow}:G{CamRow}", spreadsheetId);
List<object> oblistNR = // Write columns N-S
[
$"GUI Version: {GUIUpdate.GUIVerShort}",
DiagsAPI.licenses.saf1,
DiagsAPI.licenses.audit,
DiagsAPI.licenses.stream,
sshData.tailscale,
"FALSE" // Write FALSE to WIP checkbox in column S
];
WriteToSS(oblistNR, $"N{CamRow}:S{CamRow}", spreadsheetId);
return string.Empty;
}
catch (Exception ex)
{
return "Failed to update spreadsheet data, please check manually" + ex.Message;
}
}
// Update Vaxtor spreadsheet
public static string UpdateSpreadSheetVaxtor(VaxtorLic VaxtorLicResp, string serial, string model)
{
try
{
string spreadsheetId = "1n5zhmI4Tz6JFr0stLNFOR6GsxGEKBEhrVKQ6yncM-LA";
int nextRow = CheckNextFree(spreadsheetId);
List<object> oblistCF = // Write columns C-F
[
model,
serial,
DateTime.Now.ToString("dd/MM/yyyy"),
VaxtorLicResp.protectionKeyId,
];
WriteToSS(oblistCF, $"C{nextRow}:F{nextRow}", spreadsheetId);
// Write PROD to column H
List<object> oblistH = ["PROD"];
WriteToSS(oblistH, $"H{nextRow}", spreadsheetId);
return string.Empty;
}
catch (Exception ex)
{
return "Failed to update spreadsheet data, please check manually" + ex.Message;
}
}
// Checks RMA control sheet for a model and serial that match current camera
public static int CheckRMANum(string serial, string model)
{
string spreadsheetId_RMAControl = "1tZhkYrqBQ3BcL7ZS4q3ghzCgHSJ8f5LVSj7nh6fIRC8";
try
{
// Get all info in H and I columns
IList<IList<object>> valuesRMA = service.Spreadsheets.Values.Get(spreadsheetId_RMAControl, "H2:I").Execute().Values;
for (int i = 0; i < valuesRMA.Count; i++)
{
try // In case line is blank
{
// Checks is serial and model num in RMA control spreadsheet match what is in the camera
if (valuesRMA[i][0].ToString().Contains(serial) && valuesRMA[i][1].ToString().Contains(model))
{
int OnRow = i + 2; // Offset for start of the sheet and starting at 1
valuesRMA = service.Spreadsheets.Values.Get(spreadsheetId_RMAControl, "A" + OnRow).Execute().Values;
return Convert.ToInt16(valuesRMA[0][0]); // Once it has found the serial it gusses at the RMA number
}
}
catch { }
}
}
catch { }
return 0; // If it can't be found
}
// Checks RMA control sheet for a model and serial that match current camera
public static int CheckSerialNumRow(string spreadsheetId, string serial)
{
// Get all info in B column
IList<IList<object>> valuesRMA = service.Spreadsheets.Values.Get(spreadsheetId, "B:B").Execute().Values;
for (int i = 0; i < valuesRMA.Count; i++)
{
try // In case line is blank
{
// Checks is serial and model num in RMA control spreadsheet match what is in the camera
if (valuesRMA[i][0].ToString().Contains(serial))
{
return i + 1; // Offset for sheet starting at 1
}
}
catch { }
}
return 0; // If it can't be found
}
// Checks Vaxtor sheet for next free row
public static int CheckNextFree(string spreadsheetId)
{
IList<IList<object>> valuesRMA = service.Spreadsheets.Values.Get(spreadsheetId, "C2:C").Execute().Values; // Get how many C cows
return valuesRMA.Count + 2; // Gets the amount of rows, offsets for start from 1 error and adds one to be next row
}
public static void EmailApproval(string ApprovalRow, string User)
{
FileStream GmailStream = new($"{DrivePath}R50IQ\\creds.json", FileMode.Open, FileAccess.Read);
string[] ScopesGmail = [GmailService.Scope.GmailSend];
using (GmailStream)
{
string credPathGmail = Path.Combine(credPath, ".credentials/gmail-dotnet-quickstart.json");
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(GoogleClientSecrets.FromStream(GmailStream).Secrets, ScopesGmail, "user", CancellationToken.None, new FileDataStore(credPathGmail, true)).Result;
GmailStream.Close();
}
// Build the MIME message with attachment
using MailMessage mail = new MailMessage();
mail.From = new MailAddress("me");
mail.To.Add("richard.porter@mav-systems.com");
mail.To.Add("bradley.relyea@mav-systems.com");
mail.To.Add("bradley.born@mav-systems.com");
mail.Subject = "Approval required";
mail.Body = $"Dear Rich,<br><br>Camera needs approval<br>" +
$"https://docs.google.com/spreadsheets/d/1bCcCr4OYqfjmydt6UqtmN4FQETezXmZRSStJdCCcqZM/edit#gid=1931079354&range=A{ApprovalRow}" +
$"<br><br><br>Thanks,<br><br>{User}";
mail.IsBodyHtml = true;
// Attach the log file if it exists
string logFilePath = LDS.MAVPath + Logging.LogFileName;
if (File.Exists(logFilePath))
{
Attachment logAttachment = new(logFilePath, MediaTypeNames.Text.Plain);
logAttachment.Name = Logging.LogFileName;
mail.Attachments.Add(logAttachment);
}
// Save the MIME message to a stream
using MemoryStream ms = new();
SmtpClient smtpClient = new(); // Only used to access the internal Write method
Type mailWriterType = typeof(SmtpClient).Assembly.GetType("System.Net.Mail.MailWriter");
object? mailWriter = Activator.CreateInstance(
mailWriterType,
BindingFlags.Instance | BindingFlags.NonPublic,
null,
[ms, true],
null);
typeof(MailMessage).InvokeMember(
"Send",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod,
null,
mail,
[mailWriter, true, true]);
ms.Position = 0;
byte[] rawBytes = ms.ToArray();
GmailService service = new(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
Google.Apis.Gmail.v1.Data.Message newMsg = new()
{
Raw = Convert.ToBase64String(rawBytes)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "")
};
service.Users.Messages.Send(newMsg, "me").Execute();
}
}
}

109
HWAccessories/Ez.cs Normal file
View File

@@ -0,0 +1,109 @@
using System.Text;
namespace AiQ_GUI
{
internal class Ez
{
public static string PoEPowerIP = "";
// Controls the EzOutlet state (ON/OFF)
public async static Task<bool> EzOutletControl(string state)
{
try
{
string currentState = await GetEzOutletState();
if (string.IsNullOrEmpty(currentState)) return false;
// Check if a state change is required
if ((currentState.Contains("1,") && state == "OFF") || (currentState.Contains("0,") && state == "ON"))
{
// Uses XOR so if only one is true. As if both are true something is wrong as it can't have both versions of code and if none are true then it failed
bool invertResult = await InvertEzOutletState();
bool ezOutlet2Result = EzOutlet2Control(ToTitleCase(state)).Result;
return invertResult ^ ezOutlet2Result;
}
return true; // State already matches
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error: {ex.Message}");
return false;
}
}
// Controls EzOutlet2 state by sending an HTTP request
public async static Task<bool> EzOutlet2Control(string state)
{
try
{
string upperState = state.ToUpper();
string URL = $"http://{PoEPowerIP}:100/overview?onoff={upperState}";
// Send ON/OFF command twice to handle login prompt, admin/admin creds true
await SendHttpRequest(URL, true);
// TODO Is twice needed??
await SendHttpRequest(URL, true);
await Task.Delay(1000); // Check flags every 1000ms
// Verify the state change
string response = await SendHttpRequest(URL.Substring(0, URL.IndexOf('?')), false); // Cut down to only get up to overview
return response.Contains($"Status: {state} Mode: Manual Control");
}
catch
{
return false;
}
}
// Inverts the current EzOutlet state
public async static Task<bool> InvertEzOutletState()
{
try
{
string response = await SendHttpRequest($"http://{PoEPowerIP}:100/invert.cgi", false);
return response.Contains(','); // Completed message includes comma
}
catch
{
return false;
}
}
// Helper to get the current EzOutlet state
private async static Task<string> GetEzOutletState()
{
return await SendHttpRequest($"http://{PoEPowerIP}:100/socket.cgi", false);
}
private static async Task<string> SendHttpRequest(string url, bool useCreds)
{
try
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
if (useCreds)
{
byte[] byteArray = Encoding.ASCII.GetBytes("admin:admin");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
}
using HttpResponseMessage response = await Network.Client.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error: {ex.Message}");
return $"Error: {ex.Message}";
}
}
// Converts a string to TitleCase (first letter uppercase, rest lowercase)
private static string ToTitleCase(string input)
{
return string.IsNullOrEmpty(input) || input.Length == 1 ? input : char.ToUpper(input[0]) + input.Substring(1).ToLower();
}
}
}

111
HWAccessories/PSU.cs Normal file
View File

@@ -0,0 +1,111 @@
using System.Diagnostics;
using System.Net.Sockets;
using System.Text;
namespace AiQ_GUI
{
public class PSU
{
public static string PSUIP = "";
public static string SendDataPsu(string dataTx, string IPAddress)
{
dataTx += "\n"; // Ensure command ends with newline
Socket psuSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
if (!psuSocket.Connected)
{
try
{
IAsyncResult result = psuSocket.BeginConnect(IPAddress, 9221, null, null);
bool success = result.AsyncWaitHandle.WaitOne(1000, true);
if (!psuSocket.Connected)
{
psuSocket.Close();
return "Error: Failed to connect to PSU";
}
}
catch
{
psuSocket.Close();
return "Error: Caught failure to connect to PSU";
}
}
try
{
psuSocket.Send(Encoding.ASCII.GetBytes(dataTx));
}
catch
{
return "Error: Failed to send bytes to PSU";
}
if (dataTx.Contains('?'))
{
byte[] data = new byte[1024];
psuSocket.ReceiveTimeout = 500;
try
{
int receivedDataLength = psuSocket.Receive(data);
psuSocket.Close();
return Encoding.ASCII.GetString(data, 0, receivedDataLength);
}
catch { }
}
else
{
psuSocket.Close();
return "CmdDone";
}
psuSocket.Close();
return "Error: Failed to Send message to PSU.";
}
public static void PSU_OFF(string IPAddress)
{
SendDataPsu("OP1 0", IPAddress);
if (!SendDataPsu("OP1?", IPAddress).Contains("Error"))
{
MainForm.Instance.btnPsuOff.BackColor = System.Drawing.Color.Green;
MainForm.Instance.btnPsuOn.BackColor = MainForm.BtnColour;
}
else
MainForm.Instance.AddToActionsList("Cannot turn PSU off");
}
public static void PSU_ON(string IPAddress)
{
SendDataPsu("OP1 1", IPAddress);
if (!SendDataPsu("OP1?", IPAddress).Contains("Error"))
{
MainForm.Instance.btnPsuOn.BackColor = Color.Green;
MainForm.Instance.btnPsuOff.BackColor = MainForm.BtnColour;
}
else
MainForm.Instance.AddToActionsList("Cannot turn PSU off");
}
public static void DisplayState(string IPAddress)
{
string DataFromPSU = SendDataPsu("OP1?", IPAddress);
Debug.WriteLine(DataFromPSU);
if (DataFromPSU.Contains('1'))
{
MainForm.Instance.btnPsuOff.BackColor = MainForm.BtnColour;
MainForm.Instance.btnPsuOn.BackColor = Color.Green;
}
else if (DataFromPSU.Contains('0'))
{
MainForm.Instance.btnPsuOff.BackColor = Color.Green;
MainForm.Instance.btnPsuOn.BackColor = MainForm.BtnColour;
}
}
}
}

77
HWAccessories/Printer.cs Normal file
View File

@@ -0,0 +1,77 @@
using System.Net.Sockets;
namespace AiQ_GUI
{
internal class Printer
{
public static string ZebraIP = "";
// Sends a .prn string to the Zebra printer
public static bool PrintToZebra(string stringToPrint)
{
try
{
using TcpClient client = new();
client.Connect(ZebraIP, 9100); // Open connection
client.SendTimeout = 5000; // 5 second timeout
using StreamWriter writer = new(client.GetStream());
writer.Write(stringToPrint); // Write ZPL string to connection
writer.Flush(); // Flush ensures data is sent
return true;
}
catch
{
return false;
}
}
public static void PrintSerialLbl(string Model, string Serial, string Processor)
{
// Model and serial label with new MAV logo and QR code
// Has to be Zebra font in the Zebra designer to be a variable in the .prn file
// Replace model, serial and description in label before printing
string AiQLbl = "^XA~TA000~JSN^LT0^MNW^MTT^PON^PMN^LH0,0^JMA^PR2,2~SD15^JUS^LRN^CI0^XZ\n" +
"^XA\n" +
"^MMT\n" +
"^PW384\n" +
"^LL0096\n" +
"^LS0\n" +
"^FO256,0^GFA,01536,01536,00016,:Z64:\n" +
"eJzN072K20AQB/ARW6gRp1aCYL2CzDU2BJtUeQ2BC7cBF+eAyCpsiBuB2gSO82skTZAwRM1hv4LMQVRGxzYSLJ6sOa2+sFPfdD+W0ez+tQvw+ougoDe09B3MLhsMb+9XaIF32U74AYPZnSVqzx/X0iu79ceBabyY+P/xMsnwfuCwsRO+T2J8aOaBobtkRjv76xvFcnz/e9U5X8+yvIJWrBPJ0O9K+rxpVB02p4UW2ap/YJLz/CBSfiK1075hKuy9A+SX2dg6e6RsC+vLCMiDXvfr8RK8OT/GnvKfnmGe3bzRopN7xSPyFUBnP4zaM7I52xr4m1HPjw06R1hlt7Uz41PPVeG53jSwd8FlTwxLEGSlykv61LVr2NK7xgnPE6YXI5XP0EA3MU0Ky7xsmW8l7pAja1yeFtK7dl36qXFs5nJ+YN2+7JdkfUMVYLKVH1HzKjmtYzJ567tOwhEbr/v2S9dsDcizPUzDkQvKx66JJvSC6Azr/IY+H1iLdPaZZ623OtM6JlFreb/XyMC0H73Llu9jLCqmxVfshNSl+RMWteX7dGnKB5brmbL1LFKmHa6ajrcaHjv9PYNBtU3KvydBa5ZydZ9e/Jf/VEbhxIBRpf7nwK+p/gEmr4ir:A94D\n" +
"^FO0,0^GFA,01280,01280,00020,:Z64:\n" +
"eJztz7ENAyEMQFGjKygZgVFuNBiNUxbhdAsQpQgFwrFDlARwkzr3JTePAhvg/8LkMhisKoGOjTRS4BAv9PIyw+Zp9vI2y7bRXAvYMFiuvR3NVt9sDTaaI8OS3ce8DWyKzMG3JbLVS1Yns0GwrUxm9t54P/59NH0T7J4mW1Cy2BnfpgZLTwuTgWh+NicZnJ392gMKlLLa:A7F4\n" +
$"^FT159,84^A0N,25,24^FH\\^FD{Serial}^FS\n" +
"^FT26,56^A0N,14,14^FH\\^FDModel:^FS\n" +
$"^FT159,32^A0N,25,28^FH\\^FDAiQ {Processor}^FS\n" +
"^FT159,56^A0N,14,14^FH\\^FDSerial Number:^FS\n" +
$"^FT26,84^A0N,25,24^FH\\^FD{Model}^FS\n" +
"^PQ1,0,1,Y^XZ";
if (!PrintToZebra(AiQLbl))
MainForm.Instance.AddToActionsList("Error printing AiQ label");
}
public static void PrintGBLbl()
{
// New Label with UKCA logo
const string MadeInGB = "^XA~TA000~JSN^LT0^MNW^MTT^PON^PMN^LH0,0^JMA^PR2,2~SD15^JUS^LRN^CI0^XZ\n" +
"^XA\n" +
"^MMT\n" +
"^PW384\n" +
"^LL0096\n" +
"^LS0\n" +
"^FO192,0^GFA,01152,01152,00012,:Z64:\n" +
"eJzt0DEKgDAMBdCUDo49Qm9ij2aP1qN4BEeHYvziL0TURRcHA4VXaEryRf76agWciVZcaQ8nuoMHOsBKxypO2ZqK+Jke0NK+XNAy0hUuOx3exkzjbZSzPdw3j0cnzY999efdDHZOO/9hL7tvyuIrHSfEyGbk5vQiT5uzzX+bwDX/9a5WFTRBPA==:FF83\n" +
"^FO320,0^GFA,00768,00768,00008,:Z64:\n" +
"eJxjYBjOgAWIHYCYA4gNGOwYBEBs9v8HFEBycnY2IDkmOwabBiDNaP//L1jTBAYxMB3AIAOmDaC0AIMNmJawLwDTMvUTwLQQswCYlmRSANMcLVA6A0obQOkKVHlOqHouRgjNw/AAIs+QALEHSuswfADTLow/QBRjC2MNiGZi//8L7Ec2xo8MUD82gJ3JANaoAHY65UD+////D0ighxYAAPB/L1Q=:B1A7\n" +
"^FO0,0^GFA,02688,02688,00028,:Z64:\n" +
"eJzt071OwzAQB/BEHjL6ETzwAowMSH4Y3gMXdejII/RRcJWBsY9QVx0YsdTFEsbH3fmjSaGMDKhXZ/oldc73T9dd61r/sLTXcACnu8ENXsBCAHRGLhRbyLaQTqKFbDZbUmxWsaVsLhtINvwpemjJ5osNv9jI5o1TEW1kC8X2bAGvKLweQ4cWi3m2z2zqwJaqGTLAKwkvs0GxqIuZRzJIZGsyBUmRGXCPRvhhahJAsr27NLFdsRWZRtPNdkBGLZGprYt6WffbsvUAezL5vMdDqe/5ynYL4HlGEm2s/Y1sdwbCw8nMODVs8IbtAHhA5Tw3bPeKZnayPAfLphVAtjd6SShzN2wrujfbcKxzt5ptSXuwObQwsz1t4y5kEOB40Qx8XDLMJ1wyzCfNPOJd1FASoUs115hPMrC0qtmWTzZeaENEcy2fbJ5WNd/yObc0Md8skEmy0PJJtvRaBN3h2ZHlXDPPDNBSNTUz9YIG1WTZr9oitcxD7W9g02zrmXEPZIpsN7PQzKaS66k5Mhx6Krmu9hTprMnApZLrn208N8X/id/AueHnK32zzfm7DGQ9m/1m1HsfRUg116fe+cz6wKZnZvERNBH6WO1a1/qD+gLCtYx7:603A\n" +
"^FO256,0^GFA,01152,01152,00012,:Z64:\n" +
"eJzt0DEKwzAMBVAJDxlzBB/F18pQcI7mo/gIGlVwqzpBrkSbDoGO+V4egXy+DXDlv8EKtMCyO1QkRjZPZJ6LOa7mDOZmfrBZyLk4rz++1+9/kZDuzcxPtQhx0suIVI7FPGtRlsJBi6LAuOPpRKFpDM1CcYzrG5Lw21mNu5tuBrj1M9zUoU/kkxbt9N46D83O21tpD3hn5+R85SMvvQiYIA==:00E3\n" +
"^PQ1,0,1,Y^XZ";
if (!PrintToZebra(MadeInGB))
MainForm.Instance.AddToActionsList("Error printing GB label");
}
}
}

88
HWAccessories/TestTube.cs Normal file
View File

@@ -0,0 +1,88 @@
using Newtonsoft.Json;
namespace AiQ_GUI
{
internal class TestTube
{
public static string TTPiPicoIP = "";
// Gets the API JSON from the Test Tube Pi Pico
public static async Task<TestTubeAPI> GetAPI()
{
string url = $"http://{TTPiPicoIP}/api";
try
{
using CancellationTokenSource cts = new CancellationTokenSource(2000);
string JSON = await Network.Client.GetStringAsync(url, cts.Token);
TestTubeAPI? result = JsonConvert.DeserializeObject<TestTubeAPI>(JSON);
if (result == null)
{
MainForm.Instance.AddToActionsList("JSON deserialization from test tube returned null");
return new TestTubeAPI();
}
return result;
}
catch (TaskCanceledException)
{
MainForm.Instance.AddToActionsList($"Timeout calling {url} after 2s.");
return new TestTubeAPI();
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error in GetAPI: {ex.Message}");
return new TestTubeAPI();
}
}
// Sets LED's to medium or safe depending on whether the switch is pressed down or not
public static async Task<bool> CheckInTestTube(string CamIP)
{
if (await InTestTube()) // Switch pressed down
{
string LEDreply = await FlexiAPI.APIHTTPLED(CamIP, LEDPOWER.MID); // Set LED's to medium (0x30)
if (!LEDreply.Contains("Power levels set successfully"))
MainForm.Instance.AddToActionsList($"LED level could not be set: {LEDreply}");
return true;
}
else
{
string LEDreply = await FlexiAPI.APIHTTPLED(CamIP, LEDPOWER.SAFE); // Set LED's to safe (0x0E)
if (!LEDreply.Contains("Power levels set successfully"))
MainForm.Instance.AddToActionsList($"LED level could not be set: {LEDreply}");
await MainForm.Instance.DisplayOK("Please put camera in test tube then click OK"); // Awaited till OK has been clicked
return await InTestTube(); // Check again after user says they have put it in the test tube
}
}
// Checks whether the TestTube has the switch presses down or not
public async static Task<bool> InTestTube()
{
TestTubeAPI TTAPI = await GetAPI();
try
{
if (TTAPI.Switch[0] == true) // Switch not pressed down
return true;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error checking Test Tube switch state: {ex.Message}");
}
return false;
}
}
public class TestTubeAPI
{
public List<bool> Switch { get; set; } = [];
}
}

229
Helper.cs Normal file
View File

@@ -0,0 +1,229 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace AiQ_GUI
{
internal class Helper
{
// ***** Allows moving GUI by grab and dragging *****
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool ReleaseCapture();
public static void RestartApp()
{
Application.Restart();
Process.GetCurrentProcess().Kill(); // To make sure no execution on other threads occurs eg. making a test record.
}
public static int[] Shuffle() // Generates random order for visual test
{
Random random = new();
int[] result = new int[4];
for (int i = 0; i < 4; i++)
{
int j = random.Next(0, i + 1);
if (i != j)
result[i] = result[j];
result[j] = i;
}
return result;
}
// Generates a string that is a combination fo labels thats are red and Only RHS of the =. As well as the text in the Actions rich text box.
public static string GetOverriddenActions(Panel pnlLbls, RichTextBox rhTxBxActions)
{
List<string?> redValues = pnlLbls.Controls
.OfType<Label>() // Only labels
.Where(lbl => lbl.BackColor == Color.Red) // Only red labels
.Where(lbl => lbl.Visible == true) // Only visible labels
.Select(lbl =>
{ // Only the RHS of labels
string[] parts = lbl.Text.Split('=');
return parts.Length > 1 ? parts[1].Trim() : null;
})
.Where(value => value != null) // Make sure RHS is not null
.ToList();
string? ActionsText = rhTxBxActions.Text?.Trim();
bool hasRedValues = redValues.Count != 0;
bool hasActionText = !string.IsNullOrWhiteSpace(ActionsText);
if (!hasRedValues && !hasActionText)
return ""; // Nothing to report
string result = "\n\nOverridden actions = ";
if (hasActionText) // If Actions has text then append it.
result += ActionsText;
if (hasRedValues) // If there are red values then append it.
{
if (hasActionText)
result += "\n";
result += string.Join(", ", redValues);
}
return result;
}
// Have to wait asynchronously to make UI responsive while waiting for answer
public static async Task VisualCheck(Button Btn) // To make people read the prompts change the order that they appear on random shuffle
{
foreach (int ShuffleOrder in Shuffle())
{
switch (ShuffleOrder)
{
case 0:
if (!await MainForm.Instance.DisplayQuestion("Is the sleeve aligned correctly?"))
await MainForm.Instance.TestFailed(Btn, "Visual Test Fail - Sleeve not aligned");
break;
case 1:
if (!await MainForm.Instance.DisplayQuestion("Are all the screws fitted in the front?"))
await MainForm.Instance.TestFailed(Btn, "Visual Test Fail - Not all front screws fitted");
break;
case 2:
if (!await MainForm.Instance.DisplayQuestion("Are all the screws fitted in the rear?"))
await MainForm.Instance.TestFailed(Btn, "Visual Test Fail - Not all rear screws fitted");
break;
case 3:
if (await MainForm.Instance.DisplayQuestion("Shake unit, does it rattle?"))
await MainForm.Instance.TestFailed(Btn, "Visual Test Fail - Unit rattles");
break;
default:
MainForm.Instance.AddToActionsList("Numbering problem in visual test");
break;
}
}
}
public static async Task<Camera> NewCamera(string IPAddress)
{
Versions Vers = await FlexiAPI.GetVersions(IPAddress);
try
{
if (Vers.Model == "???")
return null;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error fetching versions for {IPAddress}: {ex.Message}");
return null;
}
Camera soakInfo = new()
{
IP = IPAddress,
Model = Vers.Model,
Serial = Vers.Serial,
DevPass = Lics.FetchDevPassword(Vers),
RMANum = GoogleAPI.CheckRMANum(Vers.Serial, Vers.Model),
FlexiVersion = Vers.version,
IsChecked = true
};
return soakInfo;
}
public static void DCPowerCheck(Label Lbl)
{
if (CameraAccessInfo.PowerType.Contains('V')) // AiQ Powered from DC
{
try
{
Lbl.Visible = true; // Show the DC label
string PSUVoltage = PSU.SendDataPsu("V1O?", PSU.PSUIP);
string PSUCurrent = PSU.SendDataPsu("I1O?", PSU.PSUIP);
PSUVoltage = PSUVoltage.Remove(PSUVoltage.IndexOf('V'));
PSUCurrent = PSUCurrent.Remove(PSUCurrent.IndexOf('A'));
Lbl.Text += $" {PSUVoltage}V, {PSUCurrent}A"; // Show the PSU voltage and current on the label
double PSU_V = Convert.ToDouble(PSUVoltage);
double PSU_I = Convert.ToDouble(PSUCurrent);
double Exp_PSU_V = Convert.ToDouble(CameraAccessInfo.PowerType.Remove(CameraAccessInfo.PowerType.IndexOf('V')));
double Exp_PSU_I = Math.Round(UniversalData.PowerConsumption / Exp_PSU_V, 2); // 32W device if AiQ Mk2 on medium LED power
if (PSU_V > Exp_PSU_V + 1 || PSU_V < Exp_PSU_V - 1)
{
MainForm.Instance.AddToActionsList($"PSU Voltage out of range: {PSUVoltage}V. Expected {Exp_PSU_V}V ± 1V.");
}
if (PSU_I < Exp_PSU_I * 0.8 || PSU_I > Exp_PSU_I * 1.2)
{
MainForm.Instance.AddToActionsList($"PSU Current out of range: {PSUCurrent}A. Expected {Exp_PSU_I}A ± 20%.");
}
}
catch
{
MainForm.Instance.AddToActionsList("PSU did not reply as expected.");
}
}
}
}
// TODO - use Processor & ModuleManufacturer
public class Camera
{
public string Model { get; set; } = string.Empty; // Gets last model from LDS on boot
public string Serial { get; set; } = string.Empty;
public string DevPass { get; set; } = string.Empty; // Gets from API
public string IP { get; set; } = string.Empty;
public int RMANum { get; set; } = 0;
public string FlexiVersion { get; set; } = string.Empty; // Flexi version
public string Processor { get; set; } = string.Empty; // Nano, Xavier, Orin or Pi?
public string ModuleManufacturer { get; set; } = string.Empty; // KT&C or Wonwoo
public bool IsChecked { get; set; } // Is it checked for soak test?
}
// Static class for global flags
internal static class Flags
{
public static bool Yes { get; set; }
public static bool No { get; set; }
public static bool Done { get; set; }
public static bool Offline { get; set; }
public static bool Start { get; set; } = true;
}
// Store all precompiled regexes
public static partial class RegexCache
{
[GeneratedRegex(@"^((localhost)|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(:?(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[0-9]{1,4}))?$", RegexOptions.Compiled)]
internal static partial Regex RegexIPPattern();
[GeneratedRegex(@"^K\d{7}$", RegexOptions.Compiled)]
internal static partial Regex SerialRegex();
[GeneratedRegex(@"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", RegexOptions.Compiled)]
internal static partial Regex MACRegex();
[GeneratedRegex(@"^(00:04:4B|3C:6D:66|48:B0:2D):([0-9A-Fa-f]{2}:){2}[0-9A-Fa-f]{2}$", RegexOptions.Compiled)]
internal static partial Regex MACRegexNVIDIA();
[GeneratedRegex(@"^\d{1,3}\.\d{1,3}\.\d{1,3}$", RegexOptions.Compiled)]
internal static partial Regex FlexiVerRegex();
[GeneratedRegex(@"^[A-Z]{2}\d{2}[A-Z]{2}$", RegexOptions.Compiled)]
internal static partial Regex ModelRegex();
[GeneratedRegex(@"^(?:[FSA])?\d{6}$", RegexOptions.Compiled)]
internal static partial Regex LicCodeRegex();
[GeneratedRegex(@"^(?<value>[\d\.]+)(?<unit>[KMGTP]?)(?<suffix>B?)$", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
internal static partial Regex FileSizeRegex();
[GeneratedRegex(@"^81(( [0-9A-Fa-f]{2})+)? FF$", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
internal static partial Regex VISCARegex();
[GeneratedRegex(@"^\d{15,19}$", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
internal static partial Regex VaxtorRegex();
}
}

69
LDS.cs Normal file
View File

@@ -0,0 +1,69 @@
using Newtonsoft.Json;
namespace AiQ_GUI
{
internal class LDS
{
// Path strings
public const string MAVPath = @"C:\ProgramData\MAV\AiQ_GUI\"; // Path to local storage
public const string OVsavePath = "OV_image.jpg"; // Path to save the downloaded image
public const string IROpensavePath = "IR_Open_image.jpg"; // Path to save the downloaded image
public const string IRTightsavePath = "IR_Tight_image.jpg"; // Path to save the downloaded image
public const string LDSFileName = "LDS.json"; // Local Data Store file name
const string DefaultJSON = "{\r\n \"User\": \"\",\r\n \"LastModel\": \"\",\r\n \"PSUIP\": \"\",\r\n \"EzIP\": \"\",\r\n \"ZebraIP\": \"\",\r\n \"TestTubeIP\": \"\",\r\n \"Shutter\": 0,\r\n \"Iris\": 0,\r\n \"Gain\": 0\r\n}";
public static LocalDataStore GetLDS()
{
try
{
if (!Directory.Exists(MAVPath)) // Check the AiQ folder exists in ProgramData and if it doesn't then create it
{
Directory.CreateDirectory(MAVPath);
}
if (!File.Exists(MAVPath + LDSFileName)) // Check the LDS file exists and if it doesn't then create it
{
// Save a blank version of the JSON
File.WriteAllText(MAVPath + LDSFileName, DefaultJSON);
}
StreamReader file = new(MAVPath + LDSFileName);
string Content = file.ReadToEnd();
file.Close();
return JsonConvert.DeserializeObject<LocalDataStore>(Content);
}
catch // If file can't deserialise
{
return null; // Return null to indicate failure
}
}
// Save new version to disk
public static void SetLDS(LocalDataStore localDataStore)
{
try
{
string fileJSON = JsonConvert.SerializeObject(localDataStore, Formatting.Indented);
File.WriteAllText(MAVPath + LDSFileName, fileJSON);
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList($"Error saving Local Data Store: {ex.Message}");
}
}
}
public class LocalDataStore
{
public string User { get; set; } = string.Empty;
public string LastModel { get; set; } = string.Empty;
public string PSUIP { get; set; } = string.Empty;
public string EzIP { get; set; } = string.Empty;
public string ZebraIP { get; set; } = string.Empty;
public string TestTubeIP { get; set; } = string.Empty;
public int Shutter { get; set; }
public int Iris { get; set; }
public int Gain { get; set; }
}
}

50
Logging.cs Normal file
View File

@@ -0,0 +1,50 @@
using System.Diagnostics;
namespace AiQ_GUI
{
internal class Logging
{
public const string LogFileName = "AiQ_GUI_Log.log"; // Log file name
public const int maxFileSizeMB = 6 * 1024 * 1024; // 6mb max storage
public const int keepLines = 60000;
// Logs error message to the log file
public static async Task LogErrorMessage(string message, string? FileName = LogFileName)
{
await LogMessage("[ERROR] " + message, FileName);
}
// Logs warning message to the log file
public static async Task LogWarningMessage(string message, string? FileName = LogFileName)
{
await LogMessage("[WARNING] " + message, FileName);
}
// Method to log messages, defaults to main log file
public static async Task LogMessage(string message, string? FileName = LogFileName)
{
try
{
string logFilePath = LDS.MAVPath + FileName;
FileInfo fi = new(logFilePath);
if (fi.Exists && fi.Length > maxFileSizeMB) // Check file size is under 2mb
{
List<string> allLines = (await File.ReadAllLinesAsync(logFilePath)).TakeLast(keepLines).ToList();
await File.WriteAllLinesAsync(logFilePath, allLines);
}
// If the message ends with a newline character, remove it
string trimmedMessage = message.EndsWith("\r\n") ? message[..^2] : (message.EndsWith('\n') ? message[..^1] : message);
// Append the new message to log
using StreamWriter writer = new(logFilePath, append: true);
await writer.WriteLineAsync($"{DateTime.Now}: {trimmedMessage}");
}
catch (Exception ex)
{
Debug.WriteLine($"Error logging message: {ex.Message}");
}
}
}
}

125
Microsoft/Access.cs Normal file
View File

@@ -0,0 +1,125 @@
using System.Data.OleDb;
namespace AiQ_GUI
{
class Access
{
const string connString = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=G:\Shared drives\MAV Production GUI's\AiQ\GUI's\AiQ_Final_Test.accdb;Persist Security Info=False;OLE DB Services=-1;";
// Reads camera model numbers and descriptions from the database, sorts them alphabetically by model number (except "AB12CD", which appears last), and formats each entry as "ModelNumber - Description".
public static string[] ReadCamTypes()
{
List<Tuple<string, string>> modelTuples = new List<Tuple<string, string>>(30); // Preallocate list with estimated capacity to reduce internal resizing
using OleDbConnection conn = new(connString);
try
{
conn.Open();
}
catch
{
MessageBox.Show("Could not access Access in google drive. Is it running?");
return null;
}
const string query = "SELECT ModelNumber, Description FROM AiQ WHERE MarkNumber > 1";
using OleDbCommand cmd = new(query, conn);
using OleDbDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
// Extract each model number and description, using empty string if null
string modelNumber = reader["ModelNumber"] as string ?? string.Empty;
string description = reader["Description"] as string ?? string.Empty;
modelTuples.Add(Tuple.Create(modelNumber.Trim(), description.Trim()));
}
// Sort: push "AB12CD" to the bottom, then sort remaining items alphabetically
IOrderedEnumerable<Tuple<string, string>> sorted = modelTuples.OrderBy(t => t.Item1.Equals("AB12CD", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
.ThenBy(t => t.Item1, StringComparer.OrdinalIgnoreCase);
// Format the sorted tuples as "ModelNumber - Description" strings and return as array
return sorted.Select(t => $"{t.Item1} - {t.Item2}").ToArray();
}
// Read the universal data table from the database and populate the UniversalData class with the values.
public static void ReadUniData()
{
using OleDbConnection conn = new(connString);
try
{
conn.Open();
}
catch
{
MessageBox.Show("Could not access Access in google drive. Is it running?");
return;
}
const string query = "SELECT FlexiVersion, FlexiRevision, WonwooFirmware, AiQGUIVersion, PowerConsumption, LicencingServerURL FROM UniversalData"; // Grab the universal data
using OleDbCommand cmd = new(query, conn);
using OleDbDataReader reader = cmd.ExecuteReader();
reader.Read();
UniversalData.ExpFlexiVer = Convert.ToString(reader["FlexiVersion"]);
UniversalData.ExpFlexiRev = Convert.ToString(reader["FlexiRevision"]);
UniversalData.WonwooFirmware = Convert.ToString(reader["WonwooFirmware"]);
UniversalData.LatestVersion = Convert.ToString(reader["AiQGUIVersion"]);
UniversalData.PowerConsumption = Convert.ToInt16(reader["PowerConsumption"]);
UniversalData.LicencingServerURL = Convert.ToString(reader["LicencingServerURL"]);
}
// Knowing the model number on test, this function reads the database and populates the Camera class with the values.
public static void ReadModelRow(string ModelOnTest)
{
using OleDbConnection conn = new(connString);
try
{
conn.Open();
}
catch
{
MessageBox.Show("Could not access Access in google drive. Is it running?");
return;
}
string query = $"SELECT * FROM AiQ WHERE ModelNumber = '{ModelOnTest}';"; // Grab all the info for specified model
using OleDbCommand cmd = new(query, conn);
using OleDbDataReader reader = cmd.ExecuteReader();
reader.Read();
// Populate the CameraAccessInfo class with the values from the database
CameraAccessInfo.Processor = Convert.ToString(reader["Processor"]);
CameraAccessInfo.VaxtorLic = Convert.ToBoolean(reader["Vaxtor"]);
CameraAccessInfo.HardwareExtras = Convert.ToString(reader["HardwareExtras"]);
CameraAccessInfo.PowerType = Convert.ToString(reader["PowerType"]);
CameraAccessInfo.LED_V = Convert.ToDouble(reader["LEDVoltage"]);
CameraAccessInfo.LED_I = Convert.ToInt32(reader["LEDCurrent"]);
CameraAccessInfo.SpreadsheetID = Convert.ToString(reader["SSID"]);
}
}
// Expected universal data for the GUI, read from the database
public class UniversalData
{
public static string ExpFlexiVer { get; set; } = string.Empty;
public static string ExpFlexiRev { get; set; } = string.Empty;
public static string WonwooFirmware { get; set; } = string.Empty;
public static string LatestVersion { get; set; } = string.Empty;
public static int PowerConsumption { get; set; } = 0;
public static string LicencingServerURL { get; set; } = string.Empty;
}
// One object to contain all the camera info from the model info access database
public class CameraAccessInfo
{
public static string Processor { get; set; } = string.Empty;
public static bool VaxtorLic { get; set; } = false;
public static string HardwareExtras { get; set; } = string.Empty;
public static string PowerType { get; set; } = string.Empty;
public static double LED_V { get; set; } = 0;
public static int LED_I { get; set; } = 0;
public static string SpreadsheetID { get; set; } = string.Empty;
}
}

436
Microsoft/Excel.cs Normal file
View File

@@ -0,0 +1,436 @@
using ClosedXML.Excel;
using System.Diagnostics;
namespace AiQ_GUI
{
internal class Excel
{
public static void WriteTo(string FilePath)
{
if (File.Exists(FilePath))
{
XLWorkbook workbook = new(FilePath); // Open existing file
IXLWorksheet worksheet = workbook.Worksheets.First();
worksheet.Cell("A1").Value = "Hello World!";
worksheet.Cell("A2").FormulaA1 = "=MID(A1, 7, 5)";
workbook.SaveAs(FilePath);
}
else
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
}
}
public static string ReadFrom(string FilePath)
{
if (File.Exists(FilePath))
{
XLWorkbook workbook = new(FilePath); // Open existing file
IXLWorksheet worksheet = workbook.Worksheets.First();
return worksheet.Cell("A1").GetString();
}
else
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
}
return null;
}
public static int GetNextBlankRow(string FilePath)
{
if (!File.Exists(FilePath))
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
return -1;
}
using XLWorkbook workbook = new XLWorkbook(FilePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
// Start from row 1 and check downwards
int row = 1;
while (!worksheet.Cell(row, 1).IsEmpty())
{
row++;
}
return row;
}
public static string UpdateSpreadSheetPreTest(string FilePath, Versions Vers, string CamDesc, string ModelOnTest)
{
try
{
if (!File.Exists(FilePath))
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
return "Spreadsheet not found";
}
using XLWorkbook workbook = new XLWorkbook(FilePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
// Start from row 1 and check downwards to find next empty row in column B
int row = 1;
while (!worksheet.Cell(row, 2).IsEmpty()) // Column B = Serial numbers
{
row++;
}
// Safety check to avoid invalid index
if (row <= 1)
{
return "Last serial number not found";
}
// Generate new serial number from previous
string lastSerialNumber = worksheet.Cell(row - 1, 2).GetString(); // Column B
if (string.IsNullOrWhiteSpace(lastSerialNumber) || !lastSerialNumber.StartsWith("K"))
{
return "Invalid last serial number format";
}
int NewSerialNumberInt = Convert.ToInt32(lastSerialNumber.Substring(1)) + 1;
string newSerialNumber = "K" + NewSerialNumberInt;
// Write values to the corresponding columns
worksheet.Cell(row, 1).Value = ModelOnTest; // Column A
worksheet.Cell(row, 2).Value = newSerialNumber; // Column B
worksheet.Cell(row, 3).Value = CamDesc; // Column C
worksheet.Cell(row, 4).Value = "Pre Test: " + DateTime.Now.ToString("dd/MM/yyyy"); // Column D
worksheet.Cell(row, 5).Value = Vers.version + " - " + Vers.revision + Environment.NewLine +
Vers.buildtime + Environment.NewLine + Vers.proquint; // Column E
worksheet.Cell(row, 8).Value = Vers.MAC; // Column H
worksheet.Cell(row, 14).Value = $"GUI Version: {GUIUpdate.GUIVerShort}"; // Column N
worksheet.Cell(row, 19).Value = "TRUE"; // Column S
workbook.SaveAs(FilePath);
return newSerialNumber;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList("Error updating spreadsheet: " + ex.Message);
return $"ERROR: {ex.Message}";
}
}
public static string UpdateSpreadSheetRePreTest(string filePath, Versions Vers)
{
try
{
if (!File.Exists(filePath))
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
return "Spreadsheet not found";
}
using XLWorkbook workbook = new XLWorkbook(filePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
// Find the row with the matching serial number in column B
int row = 1;
while (!worksheet.Cell(row, 2).IsEmpty())
{
string cellSerial = worksheet.Cell(row, 2).GetString();
if (cellSerial.Contains(Vers.Serial))
{
// Update columns D-E
worksheet.Cell(row, 4).Value = "Pre Test: " + DateTime.Now.ToString("dd/MM/yyyy");
worksheet.Cell(row, 5).Value = Vers.version + " - " + Vers.revision + Environment.NewLine +
Vers.buildtime + Environment.NewLine + Vers.proquint;
// Update MAC to column H
worksheet.Cell(row, 8).Value = Vers.MAC;
// Update GUI Version to column N
worksheet.Cell(row, 14).Value = $"GUI Version: {GUIUpdate.GUIVerShort}";
// Write TRUE to WIP checkbox in column S
worksheet.Cell(row, 19).Value = "TRUE";
workbook.SaveAs(filePath);
return "OK";
}
row++;
}
return "Serial number not found";
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList("Error updating spreadsheet: " + ex.Message);
return $"ERROR: {ex.Message}";
}
}
public static string UpdateSpreadSheetFinalTest(string FilePath, Diags DiagsAPI, SSHData sshData, int RMANum)
{
try
{
if (!File.Exists(FilePath))
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
return "Spreadsheet not found";
}
using XLWorkbook workbook = new XLWorkbook(FilePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
// Find the row with the matching serial number in column B
int row = 1;
while (!worksheet.Cell(row, 2).IsEmpty())
{
string serial = worksheet.Cell(row, 2).GetString();
if (serial == DiagsAPI.serialNumber)
{
// Update column D with test date
string existingDate = worksheet.Cell(row, 4).GetString(); // Column D
string newDate = (RMANum != 0 ? "RMA Test: " : "Final Test: ") + DateTime.Now.ToString("dd/MM/yyyy");
worksheet.Cell(row, 4).Value = existingDate + Environment.NewLine + newDate;
// Update columns F-G
worksheet.Cell(row, 6).Value = DiagsAPI.licenses.raptorKeyID; // Column F
worksheet.Cell(row, 7).Value = sshData.packages; // Column G
// Update columns N-S
worksheet.Cell(row, 14).Value = $"GUI Version: {GUIUpdate.GUIVerShort}"; // Column N
worksheet.Cell(row, 15).Value = DiagsAPI.licenses.saf1; // Column O
worksheet.Cell(row, 16).Value = DiagsAPI.licenses.audit; // Column P
worksheet.Cell(row, 17).Value = DiagsAPI.licenses.stream; // Column Q
worksheet.Cell(row, 18).Value = sshData.tailscale; // Column R
worksheet.Cell(row, 19).Value = "FALSE"; // Column S (WIP checkbox)
workbook.SaveAs(FilePath);
return "OK";
}
row++;
}
return "Serial number not found";
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList("Error updating spreadsheet: " + ex.Message);
return "Failed to update spreadsheet data, please check manually: " + ex.Message;
}
}
public static string UpdateSpreadSheetVaxtor(string FilePath, VaxtorLic VaxtorLicResp, string serial, string model)
{
try
{
if (!File.Exists(FilePath))
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
return "Spreadsheet not found";
}
using XLWorkbook workbook = new XLWorkbook(FilePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
// Find next free row by checking column C
int row = 2;
while (!worksheet.Cell(row, 3).IsEmpty()) // Column C
{
row++;
}
// Write model, serial, date, and protectionKeyId to columns CF
worksheet.Cell(row, 3).Value = model; // Column C
worksheet.Cell(row, 4).Value = serial; // Column D
worksheet.Cell(row, 5).Value = DateTime.Now.ToString("dd/MM/yyyy"); // Column E
worksheet.Cell(row, 6).Value = VaxtorLicResp.protectionKeyId; // Column F
// Write "PROD" to column H
worksheet.Cell(row, 8).Value = "PROD"; // Column H
workbook.SaveAs(FilePath);
return string.Empty;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList("Error updating Vaxtor spreadsheet: " + ex.Message);
return "Failed to update spreadsheet data, please check manually: " + ex.Message;
}
}
public static int CheckRMANum(string filePath, string serial, string model)
{
try
{
if (!File.Exists(filePath))
{
MainForm.Instance.AddToActionsList("Could not find RMA Control spreadsheet :(");
return 0;
}
using XLWorkbook workbook = new XLWorkbook(filePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
int row = 2; // Start from row 2
while (!worksheet.Cell(row, 8).IsEmpty() || !worksheet.Cell(row, 9).IsEmpty()) // Columns H (8) and I (9)
{
try
{
string sheetSerial = worksheet.Cell(row, 8).GetString();
string sheetModel = worksheet.Cell(row, 9).GetString();
if (sheetSerial.Contains(serial) && sheetModel.Contains(model))
{
string rmaStr = worksheet.Cell(row, 1).GetString(); // Column A
if (int.TryParse(rmaStr, out int rmaNumber))
{
return rmaNumber;
}
}
}
catch { /* Safe to ignore bad row */ }
row++;
}
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList("Error reading RMA Control spreadsheet: " + ex.Message);
}
return 0; // Default if not found
}
public static int CheckSerialNumRow(string filePath, string serial)
{
try
{
if (!File.Exists(filePath))
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
return 0;
}
using XLWorkbook workbook = new XLWorkbook(filePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
int row = 1;
while (!worksheet.Cell(row, 2).IsEmpty()) // Column B = 2
{
try
{
string cellSerial = worksheet.Cell(row, 2).GetString();
if (cellSerial.Contains(serial))
{
return row;
}
}
catch { /* Ignore malformed rows */ }
row++;
}
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList("Error checking serial number row: " + ex.Message);
}
return 0;
}
public static int CheckNextFree(string filePath)
{
try
{
if (!File.Exists(filePath))
{
MainForm.Instance.AddToActionsList("Could not find spreadsheet :(");
return -1;
}
using XLWorkbook workbook = new XLWorkbook(filePath);
IXLWorksheet worksheet = workbook.Worksheets.First();
int row = 2; // Start from C2
while (!worksheet.Cell(row, 3).IsEmpty()) // Column C = 3
{
row++;
}
return row;
}
catch (Exception ex)
{
MainForm.Instance.AddToActionsList("Error checking next free row: " + ex.Message);
return -1;
}
}
//***** TESTING *****\\
public static void TestAllExcelFunctions()
{
string filePath = @"C:\Users\BradleyRelyea\OneDrive - MAV Systems Ltd\MAV R&D - General\ModelsInfo.xlsx";
// 1. Ensure spreadsheet exists and has valid starting data
if (!File.Exists(filePath))
{
XLWorkbook workbook = new XLWorkbook();
IXLWorksheet ws = workbook.Worksheets.Add("Sheet1");
// Pre-fill first row with dummy data for serial
ws.Cell("A1").Value = "InitialModel";
ws.Cell("B1").Value = "K1000"; // Starting serial
ws.Cell("C2").Value = "Existing"; // Simulate used row in Vaxtor (for CheckNextFree)
workbook.SaveAs(filePath);
}
// 2. Run Pre-Test update
Versions fakeVersion = new Versions
{
version = "1.6.4",
revision = "bf16134",
buildtime = "2025-07-21 10:00",
proquint = "bexog-ludeg-zokud-huqer",
MAC = "00:1A:2B:3C:4D:5E"
};
string preTestResult = Excel.UpdateSpreadSheetPreTest(filePath, fakeVersion, "Fake Cam", "TestModel");
Debug.WriteLine("Pre-Test Result: " + preTestResult);
// 3. Run Final Test update
Diags fakeDiags = new Diags
{
serialNumber = preTestResult, // Newly generated serial
licenses = new Licenses
{
raptorKeyID = "9999999999",
saf1 = true,
audit = true,
stream = true
}
};
SSHData fakeSSH = new SSHData
{
packages = "\r\nlibvaxtorocr10 - 8.4.20-1\r\nvaxtorocrdatacpu3 - 8.4.20-1",
tailscale = true
};
string finalTestResult = Excel.UpdateSpreadSheetFinalTest(filePath, fakeDiags, fakeSSH, 0);
Debug.WriteLine("Final-Test Result: " + finalTestResult);
// // 4. Run Vaxtor update
// var fakeVaxtorLic = new VaxtorLic
// {
// protectionKeyId = "VKID-555-ALPHA"
// };
// string vaxtorResult = Excel.UpdateSpreadSheetVaxtor(filePath, fakeVaxtorLic, "VX-999", "VXModel");
// Debug.WriteLine("Vaxtor Result: " + vaxtorResult);
// // 5. Check Serial Row
// int serialRow = Excel.CheckSerialNumRow(filePath, preTestResult);
// Debug.WriteLine("Serial row found at: " + serialRow);
// // 6. Check Next Free Vaxtor Row
// int nextFree = Excel.CheckNextFree(filePath);
// Debug.WriteLine("Next free row in Vaxtor sheet (Column C): " + nextFree);
//}
}
}
}

29
Microsoft/Teams.cs Normal file
View File

@@ -0,0 +1,29 @@
using Newtonsoft.Json;
using System.Text;
namespace AiQ_GUI
{
internal class Teams
{
const string webhookUrl = "https://default71bd136a1c65418fb59e927135629c.ac.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/b27c5192e83f4f48b20c1b115985b0b3/triggers/manual/paths/invoke/?api-version=1&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=1-eCbYXms6xInRKHwz3tgAcdQ9x7CSjl3Yzw2V_1MlA";
public static async Task SendMssg(string ApprovalRow, string User)
{
using HttpClient client = new HttpClient();
string link = $"https://docs.google.com/spreadsheets/d/1bCcCr4OYqfjmydt6UqtmN4FQETezXmZRSStJdCCcqZM/edit#gid=1931079354&range=A{ApprovalRow}"; // Has to be parsed like this as teams doesnt hyperlink otherwise
var payload = new
{
text = $"🔔 Camera approval required!\n\n" +
$"{link}\n\n" +
$"Thanks,\n{User}"
};
string json = JsonConvert.SerializeObject(payload);
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync(webhookUrl, content);
}
}
}

137
Microsoft/Windows.cs Normal file
View File

@@ -0,0 +1,137 @@
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Principal;
namespace AiQ_GUI
{
internal class Windows
{
private static readonly string[] targetProcesses = ["IP_Tool", "Rapier", "IPConfig", "BackdoorGUI"];
// Closes other MAV and Rudstone tools.
public static async Task CloseProcesses()
{
IEnumerable<Task> tasks = Process.GetProcesses()
.Where(p => targetProcesses.Any(tp => p.ProcessName.Contains(tp)))
.Select(clsProcess =>
{
using (clsProcess)
{
try
{
clsProcess.CloseMainWindow();
}
catch { }
}
return Task.CompletedTask;
});
await Task.WhenAll(tasks).ConfigureAwait(false); // Run all tasks concurrently
}
public static void StartAsAdmin(string ExeLoc)
{
Logging.LogMessage($"Starting exe from {ExeLoc}");
ProcessStartInfo processInfo = new ProcessStartInfo(ExeLoc)
{
UseShellExecute = true,
Verb = "runas"
};
try
{
Process.Start(processInfo);
Properties.Settings.Default.FirstRun = false;
Properties.Settings.Default.Save();
Application.Exit(); // Exit now that we have admin rights version
}
catch (Exception ex)
{
Logging.LogErrorMessage("Failed to restart with admin rights. " + ex.Message);
MessageBox.Show("Sorry, but I don't seem to be able to start this program with administrator rights!");
}
}
public static void UpdateFirewall()
{
WindowsPrincipal wp = new(WindowsIdentity.GetCurrent());
bool runAsAdmin = wp.IsInRole(WindowsBuiltInRole.Administrator);
string ExeLoc = Assembly.GetEntryAssembly().Location.Replace("dll", "exe"); // Sometimes trys to open the dll instead of exe
if (Properties.Settings.Default.FirstRun && !runAsAdmin) // On first run, put into admin mode to allow defender.
{
StartAsAdmin(ExeLoc);
}
else if (runAsAdmin)
{
try
{
// Use dynamic for COM interop
Type ruleType = Type.GetTypeFromProgID("HNetCfg.FWRule");
Type policyType = Type.GetTypeFromProgID("HNetCfg.FwPolicy2");
dynamic firewallRule = Activator.CreateInstance(ruleType);
dynamic firewallPolicy = Activator.CreateInstance(policyType);
firewallRule.ApplicationName = ExeLoc;
firewallRule.Action = 1; // NET_FW_ACTION_ALLOW
firewallRule.Description = "Programmatically added rule to allow the GUI to work";
firewallRule.Enabled = true;
firewallRule.InterfaceTypes = "All";
firewallRule.Name = "AiQ_GUI";
firewallRule.Protocol = 17; // UDP
firewallPolicy.Rules.Add(firewallRule);
Properties.Settings.Default.FirstRun = false;
Properties.Settings.Default.Save();
}
catch (Exception ex)
{
Logging.LogErrorMessage("Failed to install firewall. " + ex.Message);
MessageBox.Show("Sorry, but I couldn't install the firewall rule!");
}
}
}
}
[ComImport, Guid("AF230D27-BABA-4E42-ACED-F524F22CFCE2")]
public interface INetFwRule
{
string Name { get; set; }
string Description { get; set; }
string ApplicationName { get; set; }
string ServiceName { get; set; }
int Protocol { get; set; }
string LocalPorts { get; set; }
string RemotePorts { get; set; }
string LocalAddresses { get; set; }
string RemoteAddresses { get; set; }
string IcmpTypesAndCodes { get; set; }
int Direction { get; set; }
object Interfaces { get; set; }
string InterfaceTypes { get; set; }
bool Enabled { get; set; }
string Grouping { get; set; }
int Profiles { get; set; }
bool EdgeTraversal { get; set; }
int Action { get; set; }
}
[ComImport, Guid("98325047-C671-4174-8D81-DEFCD3F03186")]
public interface INetFwPolicy2
{
int CurrentProfileTypes { get; }
void get_FirewallEnabled(int profileType, out bool enabled);
void put_FirewallEnabled(int profileType, bool enabled);
void get_ExcludedInterfaces(int profileType, out object interfaces);
void put_ExcludedInterfaces(int profileType, object interfaces);
int BlockAllInboundTraffic { get; set; }
int NotificationsDisabled { get; set; }
int UnicastResponsesToMulticastBroadcastDisabled { get; set; }
object Rules { get; }
object ServiceRestriction { get; }
// ...other members omitted for brevity
}
}

182
Network.cs Normal file
View File

@@ -0,0 +1,182 @@
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
namespace AiQ_GUI
{
public class Network
{
public static HttpClient? SingleHTTPClient;
public static HttpClient Client => SingleHTTPClient ?? throw new InvalidOperationException("Client not initialized.");
public static void Initialize(string username, string password)
{
HttpClientHandler handler = new HttpClientHandler
{
MaxConnectionsPerServer = 25,
Credentials = new NetworkCredential(username, password)
};
SingleHTTPClient?.Dispose(); // Dispose old client if needed
SingleHTTPClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(20)
};
}
// Handles get and post to the camera API
public static async Task<string> SendHttpRequest(string url, HttpMethod method, int? Timeout, string? jsonData = null, string[]? Headers = null)
{
try
{
HttpRequestMessage request = new HttpRequestMessage(method, url);
if (jsonData != null) // Fills in the body of the request if jsonData is provided
request.Content = new StringContent(jsonData, Encoding.UTF8, "application/json");
if (Headers != null) // Fills in the headers of the request if Headers are provided
{
foreach (string Header in Headers)
{
string[] parts = Header.Split(':');
if (parts.Length == 2)
request.Headers.Add(parts[0].Trim(), parts[1].Trim());
else
throw new ArgumentException($"Invalid header format: {Header}");
}
}
int timeoutMs = (Timeout ?? 10) * 1000; // Convert from seconds to ms
using CancellationTokenSource cts = new CancellationTokenSource(timeoutMs);
using HttpResponseMessage response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cts.Token); // Use token here too if you're chaining timeouts
}
catch (TaskCanceledException ex)
{
return $"HTTP error calling {url}: {ex.Message}";
}
catch (HttpRequestException ex)
{
return $"HTTP error calling {url}: {ex.Message}";
}
catch (Exception ex)
{
return $"Unexpected error calling {url}: {ex.Message}";
}
}
public async static Task<IList<string>> SearchForCams()
{
const int sendPort = 6666;
const int receivePort = 6667;
IList<string> FoundCams = [];
byte[] discoveryPacket = [0x50, 0x4f, 0x4c, 0x4c, 0xaf, 0xb0, 0xb3, 0xb3, 0xb6, 0x01, 0xa8, 0xc0, 0x0b, 0x1a, 0x00, 0x00];
async Task SendAndListen(IPAddress localIp)
{
using (UdpClient sender = new(new IPEndPoint(localIp, sendPort)))
{
sender.EnableBroadcast = true;
sender.Connect(new IPEndPoint(IPAddress.Broadcast, sendPort));
sender.Send(discoveryPacket, discoveryPacket.Length);
}
using UdpClient receiver = new(receivePort); // Listen for replies on fixed port
receiver.Client.ReceiveTimeout = 750;
DateTime timeout = DateTime.Now.AddMilliseconds(750);
try
{
while (DateTime.Now < timeout)
{
if (receiver.Available > 0)
{
UdpReceiveResult result = await receiver.ReceiveAsync();
byte[] recvBuffer = result.Buffer;
if (recvBuffer.Length >= 52) // Safety check
{
byte[] ipBytes = recvBuffer.Skip(recvBuffer.Length - 52).Take(4).Reverse().ToArray();
string ipToAdd = string.Join(".", ipBytes);
if (!FoundCams.Contains(ipToAdd))
FoundCams.Add(ipToAdd);
}
}
await Task.Delay(50); // brief wait to allow data in
}
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
{
// No data received in time — normal case
}
}
// Get first IPv4 interface (non-loopback)
foreach (IPAddress ip in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
{
try
{
await SendAndListen(ip);
}
catch { }
}
return FoundCams;
}
// Ping to make sure devices are connected to the network, be aware it isn't consistant across subnets.
public async static Task<bool> PingIP(string ipAddress)
{
if (RegexCache.RegexIPPattern().IsMatch(ipAddress))
{
try
{
Ping myPing = new();
PingReply reply = await myPing.SendPingAsync(ipAddress, 1000); // Timeout is 1s
return reply.Status == IPStatus.Success;
}
catch (PingException ex) { MainForm.Instance.AddToActionsList($"PingException: Unable to ping {ipAddress}. Reason: {ex.Message}"); }
catch (SocketException ex) { MainForm.Instance.AddToActionsList($"SocketException: Network error while pinging {ipAddress}. Reason: {ex.Message}"); }
catch (Exception ex) { MainForm.Instance.AddToActionsList($"Unexpected error while pinging {ipAddress}: {ex.Message}"); }
}
return false;
}
// Start a thread that pings each IP in the hardware accessories menu
public static async Task PingAndUpdateUI(string IP, Label label, string ItemName, ToolTip TT, Button[]? buttonsToEnable = null)
{
bool isAvailable = await PingIP(IP);
label.Invoke(() =>
{
if (isAvailable)
{
label.Text = "✔";
label.ForeColor = Color.ForestGreen;
TT.SetToolTip(label, ItemName + " Available");
}
else
{
label.Text = "❌";
label.ForeColor = Color.Red;
TT.SetToolTip(label, "No ping from " + ItemName);
}
if (buttonsToEnable != null)
{
foreach (Button button in buttonsToEnable)
button.Enabled = isAvailable;
}
});
}
}
}

401
PDF.cs Normal file
View File

@@ -0,0 +1,401 @@
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;
using PdfSharp.Pdf;
using PdfSharp.Pdf.IO;
using Image = MigraDoc.DocumentObjectModel.Shapes.Image;
namespace AiQ_GUI
{
internal class PDF
{
public const string TestRecordDir = "G:\\Shared drives\\MAV Production Test Records\\AiQ\\";
public const string ImageDir = $"{GoogleAPI.DrivePath}AiQ\\GUI's\\";
public static void CreateFinalTestReport(Camera CamOnTest, string UserName, string fulltestvalues, DateTime PCTime)
{
// Create a new PDF document
Document document = new();
Section section = document.AddSection();
// Create a table with two columns
Table latable = section.AddTable();
latable.Borders.Visible = false; // No visible borders
latable.AddColumn(Unit.FromCentimeter(13)); // Left column for MAV logo
latable.AddColumn(Unit.FromCentimeter(3)); // Right column for AiQ logo
// Add a row for the logos
Row larow = latable.AddRow();
Cell cellMAV = larow.Cells[0];
Cell cellAiQ = larow.Cells[1];
// Add MAV logo to the left cell
Image imageMAV = cellMAV.AddImage($"{ImageDir}MAV-Logo.png");
imageMAV.LockAspectRatio = true;
imageMAV.Height = Unit.FromCentimeter(1); // Set height to 1 cm
// Add AiQ logo to the right cell
Image imageAiQ = cellAiQ.AddImage($"{ImageDir}AiQ-Logo.png");
imageAiQ.LockAspectRatio = true;
imageAiQ.Height = Unit.FromCentimeter(1.25); // Set height to 1 cm
// Add spacing below the table
Paragraph tableParagraph = section.AddParagraph();
tableParagraph.Format.SpaceAfter = "0.5cm";
// Add Title
Paragraph title = section.AddParagraph("Certificate of Conformity");
title.Format.Font.Size = 20;
title.Format.Font.Bold = true;
title.Format.Alignment = ParagraphAlignment.Center;
title.Format.SpaceAfter = "0.15cm";
// Add SubTitle
string Subtitle = $"AiQ {CamOnTest.Model} {CamOnTest.Serial}" + (CamOnTest.RMANum != 0 ? $" (RMA {CamOnTest.RMANum})" : "");
Paragraph subtitle = section.AddParagraph(Subtitle);
subtitle.Format.Font.Size = 14;
subtitle.Format.Alignment = ParagraphAlignment.Center;
subtitle.Format.SpaceAfter = "0.5cm";
// Add Date
Paragraph date = section.AddParagraph($"Date: {PCTime}{Environment.NewLine}Engineer: {UserName}{Environment.NewLine}Tested in accordance with MAV.146.060.010.01");
date.Format.Font.Size = 10;
date.Format.SpaceAfter = "0.5cm";
// Add Test Values header
Paragraph valuesHeader = section.AddParagraph("Test Values:");
valuesHeader.Format.Font.Size = 14;
valuesHeader.Format.Font.Bold = true;
valuesHeader.Format.SpaceAfter = "0.25cm";
// Add Test Values
Paragraph FTV = section.AddParagraph(fulltestvalues);
FTV.Format.Font.Size = 10;
FTV.Format.SpaceAfter = "0.5cm";
// Add Images
Paragraph imagesHeader = section.AddParagraph("Images:");
imagesHeader.Format.Font.Size = 14;
imagesHeader.Format.Font.Bold = true;
imagesHeader.Format.SpaceAfter = "0.25cm";
// Create a table with two columns
Table table = section.AddTable();
table.Borders.Width = 0; // No border
table.AddColumn(Unit.FromCentimeter(6)); // Column 1 width
table.AddColumn(Unit.FromCentimeter(6)); // Column 2 width
table.AddColumn(Unit.FromCentimeter(6)); // Column 3 width
table.Rows.Alignment = RowAlignment.Center;
Row row = table.AddRow();
// Add IR F16 to the first column
Cell cell1 = row.Cells[0];
cell1.Format.Alignment = ParagraphAlignment.Center; // Center horizontally
cell1.AddParagraph("Infrared - F16.0");
Image imageIR = cell1.AddImage(LDS.MAVPath + LDS.IRTightsavePath);
imageIR.LockAspectRatio = true; // Maintain aspect ratio
imageIR.Width = Unit.FromCentimeter(6); // Scale as needed
// Add IR F2 to the second column
Cell cell2 = row.Cells[1];
cell2.Format.Alignment = ParagraphAlignment.Center; // Center horizontally
cell2.AddParagraph("Infrared - F2.0");
Image imageIR17 = cell2.AddImage(LDS.MAVPath + LDS.IROpensavePath);
imageIR17.LockAspectRatio = true; // Maintain aspect ratio
imageIR17.Width = Unit.FromCentimeter(6); // Scale as needed
// Add Image OV to the third column
Cell cell3 = row.Cells[2];
cell3.Format.Alignment = ParagraphAlignment.Center; // Center horizontally
cell3.AddParagraph("Overview");
Image imageOV = cell3.AddImage(LDS.MAVPath + LDS.OVsavePath);
imageOV.LockAspectRatio = true; // Maintain aspect ratio
imageOV.Width = Unit.FromCentimeter(6); // Scale as needed
section.AddParagraph().Format.SpaceAfter = "0.5cm";
// Add signiture
Paragraph Signiture = section.AddParagraph("This unit is approved for shipment by:");
Signiture.Format.Font.Size = 14;
Signiture.Format.Alignment = ParagraphAlignment.Right;
Paragraph paragraph = section.AddParagraph();
Image image = paragraph.AddImage($"{ImageDir}RP-Sig.jpg");
image.Height = Unit.FromCentimeter(2);
image.LockAspectRatio = true;
paragraph.Format.Alignment = ParagraphAlignment.Right;
Paragraph PostSig = section.AddParagraph($"Richard Porter{Environment.NewLine}Head of Engineering{Environment.NewLine}MAV Systems Ltd");
PostSig.Format.Font.Size = 14;
PostSig.Format.Alignment = ParagraphAlignment.Right;
try
{
// Render PDF
PdfDocumentRenderer renderer = new()
{
Document = document
};
renderer.RenderDocument();
renderer.PdfDocument.Options.Layout = PdfWriterLayout.Compact;
// Adds RMA number to file namme if there is one
string saveLoc = $"{TestRecordDir}{CamOnTest.Model}\\FinalTestReport_{CamOnTest.Model}_{CamOnTest.Serial}_{PCTime:dd-MM-yyyy_HH-mm-ss}" +
(CamOnTest.RMANum != 0 ? $" RMA{CamOnTest.RMANum}" : "") + ".pdf";
if (!Directory.Exists($"{TestRecordDir}{CamOnTest.Model}\\")) // Does the model directory exist?
{
Directory.CreateDirectory($"{TestRecordDir}{CamOnTest.Model}\\"); // if not then create the directory now
}
renderer.PdfDocument.Save(saveLoc);
Logging.LogMessage("Final Test PDF saved to " + saveLoc);
}
catch (Exception ex)
{
// Show a message box to inform the user that PDF creation failed, displaying the exception message.
MainForm.Instance.AddToActionsList($"Failed to create PDF:\n{ex.Message}");
}
}
public static string CreateSoakTestReport(Camera CamSoak, string userName, DateTime pcTime, string logFilePath)
{
if (!File.Exists(logFilePath))
{
MainForm.Instance.AddToActionsList("Soak log file not found. Cannot create Soak Test Report.");
return null;
}
List<string> logLines = File.Exists(logFilePath) ? File.ReadAllLines(logFilePath).ToList() : new List<string>();
List<string> errorLines = logLines.Where(l => l.Contains("[ERROR]")).ToList();
List<string> warningLines = logLines.Where(l => l.Contains("[WARNING]")).ToList();
// Create PDF document
Document document = new();
Section section = document.AddSection();
// Header table with logos
Table logoTable = section.AddTable();
logoTable.Borders.Visible = false;
logoTable.AddColumn(Unit.FromCentimeter(13)); // Left column
logoTable.AddColumn(Unit.FromCentimeter(3)); // Right column
Row logoRow = logoTable.AddRow();
Cell cellMAV = logoRow.Cells[0];
Cell cellAiQ = logoRow.Cells[1];
Image mavLogo = cellMAV.AddImage($"{ImageDir}MAV-Logo.png");
mavLogo.LockAspectRatio = true;
mavLogo.Height = Unit.FromCentimeter(1);
Image aiqLogo = cellAiQ.AddImage($"{ImageDir}AiQ-Logo.png");
aiqLogo.LockAspectRatio = true;
aiqLogo.Height = Unit.FromCentimeter(1.25);
section.AddParagraph().Format.SpaceAfter = "0.5cm";
// Title
Paragraph title = section.AddParagraph("Soak Test Certificate");
title.Format.Font.Size = 20;
title.Format.Font.Bold = true;
title.Format.Alignment = ParagraphAlignment.Center;
title.Format.SpaceAfter = "0.15cm";
// Subtitle with RMA number if available
string subtitleText = $"AiQ {CamSoak.Model} {CamSoak.Serial}" + (CamSoak.RMANum != 0 ? $" (RMA {CamSoak.RMANum})" : "");
Paragraph subtitle = section.AddParagraph(subtitleText);
subtitle.Format.Font.Size = 14;
subtitle.Format.Alignment = ParagraphAlignment.Center;
subtitle.Format.SpaceAfter = "0.5cm";
// Date and engineer info
Paragraph datePara = section.AddParagraph($"Date: {pcTime:dd/MM/yyyy HH:mm:ss}{Environment.NewLine}Engineer: {userName}{Environment.NewLine}Tested in accordance with MAV.146.060.020.01");
datePara.Format.Font.Size = 10;
datePara.Format.SpaceAfter = "0.5cm";
// === Add warning/error summary with counts and color ===
if (warningLines.Count > 0 || errorLines.Count > 0)
{
Paragraph logSummary = section.AddParagraph("Log Summary:");
logSummary.Format.Font.Size = 12;
logSummary.Format.Font.Bold = true;
logSummary.Format.SpaceAfter = "0.2cm";
Paragraph totalCounts = section.AddParagraph();
totalCounts.Format.Font.Size = 10;
totalCounts.AddText("Total Errors: ");
FormattedText errorCountText = totalCounts.AddFormattedText(errorLines.Count.ToString());
errorCountText.Font.Color = Colors.Red;
totalCounts.AddText("\nTotal Warnings: ");
FormattedText warningCountText = totalCounts.AddFormattedText(warningLines.Count.ToString());
warningCountText.Font.Color = Colors.Orange;
totalCounts.Format.SpaceAfter = "0.2cm";
Dictionary<string, int> errorCounts = [];
Dictionary<string, int> warningCounts = [];
foreach (string line in errorLines)
{
string message = ExtractLogMessageContent(line);
if (errorCounts.TryGetValue(message, out int value))
errorCounts[message] = ++value;
else
errorCounts[message] = 1;
}
foreach (string line in warningLines)
{
string message = ExtractLogMessageContent(line);
if (warningCounts.TryGetValue(message, out int value))
warningCounts[message] = ++value;
else
warningCounts[message] = 1;
}
foreach (KeyValuePair<string, int> ErrorCounter in errorCounts) // Itterates through the dictionary and Adds Errors and how many times that warning has appeared
{
Paragraph errorPara = section.AddParagraph();
errorPara.Format.Font.Size = 9;
errorPara.AddFormattedText("[", TextFormat.NotBold);
FormattedText redText = errorPara.AddFormattedText("ERROR", TextFormat.NotBold);
redText.Font.Color = Colors.Red;
errorPara.AddFormattedText($"] {ErrorCounter.Key} (x{ErrorCounter.Value})");
}
foreach (KeyValuePair<string, int> WarningCounter in warningCounts) // Itterates through the dictionary and Adds warning and how many times that warning has appeared
{
Paragraph warnPara = section.AddParagraph();
warnPara.Format.Font.Size = 9;
warnPara.AddFormattedText("[", TextFormat.NotBold);
FormattedText orangeText = warnPara.AddFormattedText("WARNING", TextFormat.NotBold);
orangeText.Font.Color = Colors.Orange;
warnPara.AddFormattedText($"] {WarningCounter.Key} (x{WarningCounter.Value})");
}
section.AddParagraph().Format.SpaceAfter = "0.5cm";
}
// Signature
if (errorLines.Count == 0)
{
Paragraph approval = section.AddParagraph("This unit is approved for shipment by:");
approval.Format.Font.Size = 14;
approval.Format.Alignment = ParagraphAlignment.Right;
Paragraph sigPara = section.AddParagraph();
Image sigImage = sigPara.AddImage($"{ImageDir}RP-Sig.jpg");
sigImage.Height = Unit.FromCentimeter(2);
sigImage.LockAspectRatio = true;
sigPara.Format.Alignment = ParagraphAlignment.Right;
Paragraph signoff = section.AddParagraph($"Richard Porter{Environment.NewLine}Head of Engineering{Environment.NewLine}MAV Systems Ltd");
signoff.Format.Font.Size = 14;
signoff.Format.Alignment = ParagraphAlignment.Right;
}
// New page with full log
Section logSection = document.AddSection();
Paragraph fullLogTitle = logSection.AddParagraph("Full Log Output:");
fullLogTitle.Format.Font.Size = 12;
fullLogTitle.Format.Font.Bold = true;
fullLogTitle.Format.SpaceAfter = "0.2cm";
foreach (string line in logLines)
{
Paragraph logLine = logSection.AddParagraph();
logLine.Format.Font.Size = 8;
if (line.Contains("[ERROR]"))
{
int index = line.IndexOf("[ERROR]"); // You get rid of error only to add it back a few lines later? // this is because its difficukt to change the colour of text your importing its easier to remove and then make red
logLine.AddText(line[..index]);
logLine.AddFormattedText("ERROR", TextFormat.NotBold).Font.Color = Colors.Red;
logLine.AddText(line[(index + "[ERROR]".Length)..]);
}
else if (line.Contains("[WARNING]"))
{
int index = line.IndexOf("[WARNING]"); // You get rid of warning only to add it back a few lines later? // Same here
logLine.AddText(line[..index]);
logLine.AddFormattedText("WARNING", TextFormat.NotBold).Font.Color = Colors.Orange;
logLine.AddText(line[(index + "[WARNING]".Length)..]);
}
else
{
logLine.AddText(line);
}
}
try
{
// Render PDF
PdfDocumentRenderer renderer = new()
{
Document = document
};
renderer.RenderDocument();
renderer.PdfDocument.Options.Layout = PdfWriterLayout.Compact;
// Construct save path
string fullPath = TestRecordDir + CamSoak.Model + $"\\SoakTestReport_{CamSoak.Model}_{CamSoak.Serial}_{pcTime:dd-MM-yyyy_HH-mm-ss}" + (CamSoak.RMANum != 0 ? $" RMA{CamSoak.RMANum}" : "") + ".pdf";
renderer.PdfDocument.Save(fullPath);
Logging.LogMessage("Soak Test PDF saved to " + fullPath);
return fullPath;
}
catch (Exception ex)
{
// Show a message box to inform the user that PDF creation failed, displaying the exception message.
MainForm.Instance.AddToActionsList($"Failed to create PDF:\n{ex.Message}");
return null;
}
}
private static string ExtractLogMessageContent(string line)
{
int tagEnd = line.IndexOf("] ");
if (tagEnd != -1)
{
return line[(tagEnd + 2)..].Trim();
}
return line.Trim();
}
public static bool LinkPDFs(string pdf1Path, string pdf2Path, string outputPath)
{
bool hasError = false;
if (!File.Exists(pdf1Path))
{
MainForm.Instance.AddToActionsList($"{pdf1Path} does not exist.");
hasError = true;
}
if (!File.Exists(pdf2Path))
{
MainForm.Instance.AddToActionsList($"{pdf2Path} does not exist.");
hasError = true;
}
if (hasError)
return false;
using PdfDocument outputDocument = new(); // Create a new PDF document
AppendPdf(outputDocument, pdf1Path);
AppendPdf(outputDocument, pdf2Path);
outputDocument.Save(outputPath); // Save the output document
return true;
}
public static void AppendPdf(PdfDocument target, string sourcePath)
{
using PdfDocument inputDocument = PdfReader.Open(sourcePath, PdfDocumentOpenMode.Import);
for (int idx = 0; idx < inputDocument.PageCount; idx++) // Iterate through all pages and add them to the output
{
PdfPage page = inputDocument.Pages[idx];
target.AddPage(page);
}
}
}
}

17
Program.cs Normal file
View File

@@ -0,0 +1,17 @@
namespace AiQ_GUI
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
//ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
}

103
Properties/Resources.Designer.cs generated Normal file
View File

@@ -0,0 +1,103 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace AiQ_GUI.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AiQ_GUI.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap AiQ___Light {
get {
object obj = ResourceManager.GetObject("AiQ - Light", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap homepage_banner {
get {
object obj = ResourceManager.GetObject("homepage-banner", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap MAV___Plain___White {
get {
object obj = ResourceManager.GetObject("MAV - Plain - White", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon mav_new {
get {
object obj = ResourceManager.GetObject("mav_new", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
}
}

133
Properties/Resources.resx Normal file
View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="MAV - Plain - White" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\MAV - Plain - White.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="homepage-banner" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\homepage-banner.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="AiQ - Light" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\AiQ - Light.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="mav_new" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\mav_new.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

50
Properties/Settings.Designer.cs generated Normal file
View File

@@ -0,0 +1,50 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace AiQ_GUI.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool FirstRun {
get {
return ((bool)(this["FirstRun"]));
}
set {
this["FirstRun"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("NOT_STARTED")]
public string UnitTesting {
get {
return ((string)(this["UnitTesting"]));
}
set {
this["UnitTesting"] = value;
}
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="AiQ_GUI.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="FirstRun" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="UnitTesting" Type="System.String" Scope="User">
<Value Profile="(Default)">NOT_STARTED</Value>
</Setting>
</Settings>
</SettingsFile>

BIN
Resources/AiQ - Light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
Resources/mav_new.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

145
Soak/Selenium.cs Normal file
View File

@@ -0,0 +1,145 @@
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
namespace AiQ_GUI
{
internal class Selenium
{
// Directs to url using chrome drive and waits for the page to fully load
public static void GoToUrl(string url, ChromeDriver driver)
{
try
{
driver.Navigate().GoToUrl(url);
// Wait until the document is fully loaded
WebDriverWait wait = new(driver, TimeSpan.FromSeconds(10));
wait.Until(d => ((IJavaScriptExecutor)d).ExecuteScript("return document.readyState").Equals("complete"));
}
catch (WebDriverTimeoutException)
{
MainForm.Instance.AddToActionsList("Could not load web page " + url + "1. Check camera has no password set. " + Environment.NewLine + "2. If unable to fix speak to supervisor.");
}
}
// Retrieves lates element ID for Shutter,iris, gain and IR
public static ElementID GetElementIds(ChromeDriver driver)
{
ElementID elementID = new()
{
modeId = GetLatestElementIdContaining("Mode_", driver),
shutterId = GetLatestElementIdContaining("FixShutter_", driver),
irisId = GetLatestElementIdContaining("FixIris_", driver),
gainId = GetLatestElementIdContaining("FixGain_", driver),
irLevelId = GetLatestElementIdContaining("CameraControls_", driver),
CamAct = GetLatestElementIdContaining("CameraActivity_", driver)
};
return elementID;
}
// Clicks the element with the specified ID using Chromedriver
public static void ClickElementByID(string elementID, ChromeDriver driver)
{
ClickElementByID(elementID, true, driver);
}
// Attempts to click the web element with the specified ID; refreshes the page and retries once if the initial attempt fails
public static void ClickElementByID(string elementID, bool tryagain, ChromeDriver driver)
{
try
{
WebDriverWait wait = new(driver, TimeSpan.FromSeconds(10));
IWebElement element = wait.Until(driver => driver.FindElement(By.Id(elementID)));
element.Click();
}
catch
{
if (tryagain)
{
driver.Navigate().Refresh();
WebDriverWait wait = new(driver, TimeSpan.FromSeconds(10));
wait.Until(d => ((IJavaScriptExecutor)d).ExecuteScript("return document.readyState").Equals("complete"));
ClickElementByID(elementID, false, driver);
}
MainForm.Instance.AddToActionsList("Could not click " + elementID);
}
}
// Initialises and opens a ChromeDriver with specific options
public static ChromeDriver OpenDriver()
{
ChromeDriverService chromeDriverService = ChromeDriverService.CreateDefaultService();
chromeDriverService.HideCommandPromptWindow = true;
ChromeOptions options = new();
options.AddArguments("--app=data:,", "--window-size=960,1040", "--window-position=0,0");
options.AddExcludedArgument("enable-automation");
options.AddAdditionalChromeOption("useAutomationExtension", false);
ChromeDriver driver = new(chromeDriverService, options);
driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(5);
return driver;
}
// Changes the value of a dropdown element by ID, logs the action, and verifies the result using flashline feedback
public static async Task Dropdown_Change(string fullId, string value, ChromeDriver driver, string SoakLogFile, string CamAct)
{
WebDriverWait wait = new(driver, TimeSpan.FromSeconds(10));
IWebElement element = wait.Until(driver => driver.FindElement(By.Id(fullId)));
await Logging.LogMessage($"Changing dropdown {fullId} to value: {value}", SoakLogFile);
await Task.Delay(fullId.Contains("Mode_") ? 4000 : 200);
if (value == element.GetAttribute("value"))
return; // No change needed setting is already correct
SelectElement select = new(element);
select.SelectByValue(value);
await Task.Delay(500); // Wait for the change to take effect
if (!await Checkflashline(driver, CamAct))
{
Logging.LogWarningMessage("Bad flashline after changing: " + fullId, SoakLogFile);
MainForm.Instance.AddToActionsList("Bad flashline after changing: " + fullId);
}
}
// Monitors the flashline element's color to determine success (green) or failure (red) of a camera
public static async Task<bool> Checkflashline(ChromeDriver driver, string CamAct)
{
IWebElement flashline = driver.FindElement(By.Id(CamAct));
string flashlinecolor = flashline.GetCssValue("color");
Task FlashWait = Task.Delay(8000);
while (!FlashWait.IsCompleted)
{
if (flashlinecolor.Contains("0, 255, 127"))
return true;
else if (flashlinecolor.Contains("255, 69, 0"))
return false;
await Task.Delay(500);
flashlinecolor = flashline.GetCssValue("color");
}
return false;
}
// Retrieves the ID of the last <select> element whose ID starts with the specified partial ID
public static string GetLatestElementIdContaining(string partialId, ChromeDriver driver)
{
System.Collections.ObjectModel.ReadOnlyCollection<IWebElement> elements = driver.FindElements(By.XPath($"//*[@id][starts-with(@id, '{partialId}')]"));
List<string?> matchingIds = elements
.Select(el => el.GetAttribute("id"))
.Where(id => !string.IsNullOrEmpty(id))
.ToList();
return matchingIds.LastOrDefault();
}
}
}

332
Soak/SoakTest.cs Normal file
View File

@@ -0,0 +1,332 @@
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; }
}
}

17
testEnvironments.json Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "1",
"environments": [
// See https://aka.ms/remotetesting for more details
// about how to configure remote environments.
//{
// "name": "WSL Ubuntu",
// "type": "wsl",
// "wslDistribution": "Ubuntu"
//},
//{
// "name": "Docker dotnet/sdk",
// "type": "docker",
// "dockerImage": "mcr.microsoft.com/dotnet/sdk"
//}
]
}