Add project files.
22
.editorconfig
Normal 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
1846
AiQ_GUI.cs
Normal file
528
AiQ_GUI.resx
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||
}
|
||||
}
|
59
FakeCamera/Diagnostics.json
Normal 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
|
||||
]
|
||||
}
|
59
FakeCamera/Diagnostics_Bad.json
Normal 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
|
||||
]
|
||||
}
|
59
FakeCamera/Diagnostics_Bizarre.json
Normal 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
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
BIN
FakeCamera/IR_Open_image.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
FakeCamera/IR_Tight_image.jpg
Normal file
After Width: | Height: | Size: 4.3 KiB |
40
FakeCamera/InternalConfig.json
Normal 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
After Width: | Height: | Size: 18 KiB |
6
FakeCamera/Trim.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"infraredX": 1197,
|
||||
"infraredY": 792,
|
||||
"colourX": 1167,
|
||||
"colourY": 806
|
||||
}
|
12
FakeCamera/Versions.json
Normal 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"
|
||||
}
|
12
FakeCamera/Versions_Bad.json
Normal 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"
|
||||
}
|
12
FakeCamera/Versions_Bizarre.json
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 C–F
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
Properties/Settings.settings
Normal 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
After Width: | Height: | Size: 18 KiB |
BIN
Resources/MAV - Plain - White.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
Resources/homepage-banner.jpg
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
Resources/mav_new.ico
Normal file
After Width: | Height: | Size: 22 KiB |
145
Soak/Selenium.cs
Normal 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
@@ -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
@@ -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"
|
||||
//}
|
||||
]
|
||||
}
|