From 061f3e121387f9f59a54a8cf17bd010e4cda5101 Mon Sep 17 00:00:00 2001 From: Daniel Schick Date: Sun, 4 Feb 2018 10:09:48 +0000 Subject: [PATCH] Initialer Stand von AIS nach BSMD portiert, bisher ohne Testing --- AIS/bsmd.AISService/AIS/AIS.cs | 266 +++++++++ AIS/bsmd.AISService/AIS/AIS_ClassB.cs | 128 +++++ AIS/bsmd.AISService/AIS/AIS_ClassBExt.cs | 130 +++++ AIS/bsmd.AISService/AIS/AIS_ClassBStatic.cs | 130 +++++ AIS/bsmd.AISService/AIS/AIS_Configuration.cs | 162 ++++++ AIS/bsmd.AISService/AIS/AIS_Decoder.cs | 194 +++++++ AIS/bsmd.AISService/AIS/AIS_PosReport.cs | 187 +++++++ AIS/bsmd.AISService/AIS/AIS_QueueManager.cs | 218 ++++++++ AIS/bsmd.AISService/AIS/AIS_StaticData.cs | 381 +++++++++++++ AIS/bsmd.AISService/AIS/AIS_Target.cs | 327 +++++++++++ .../AIS/AIS_Target_Comparer.cs | 104 ++++ AIS/bsmd.AISService/AIS/AIS_Telnet.cs | 150 +++++ AIS/bsmd.AISService/AIS/NMEA.cs | 116 ++++ AIS/bsmd.AISService/AIS/NMEA_AIS_Sentence.cs | 86 +++ .../AIS/NMEA_PNMLS_Sentence.cs | 58 ++ AIS/bsmd.AISService/AIS/SerialDataHandler.cs | 69 +++ AIS/bsmd.AISService/AIS/Serial_IO.cs | 117 ++++ AIS/bsmd.AISService/AIS/TelnetDataHandler.cs | 112 ++++ AIS/bsmd.AISService/AISService.Designer.cs | 37 ++ AIS/bsmd.AISService/AISService.cs | 93 ++++ AIS/bsmd.AISService/App.config | 18 + AIS/bsmd.AISService/DB/AISPosReport.cs | 79 +++ AIS/bsmd.AISService/DB/AISStaticData.cs | 521 ++++++++++++++++++ AIS/bsmd.AISService/DB/AISStation.cs | 258 +++++++++ AIS/bsmd.AISService/DB/AISWatchkeeper.cs | 50 ++ AIS/bsmd.AISService/DB/DBConnector.cs | 212 +++++++ AIS/bsmd.AISService/DB/Hotposition.cs | 83 +++ AIS/bsmd.AISService/Program.cs | 127 +++++ .../Properties/AssemblyInfo.cs | 36 ++ .../Properties/Settings.Designer.cs | 35 ++ .../Properties/Settings.settings | 9 + AIS/bsmd.AISService/bsmd.AISService.csproj | 126 +++++ .../bsmd.AISService.licenseheader | 16 + AIS/bsmd.AISService/bsmd.AISService.sln | 22 + AIS/bsmd.AISService/bsmdKey.snk | Bin 0 -> 596 bytes AIS/bsmd.AISService/packages.config | 4 + ENI-2/ENI2/ENI2/App.config | 8 +- Stundensheet.xlsx | Bin 36105 -> 36168 bytes .../bsmd.ExcelReadService/ExcelReader.cs | 2 +- nsw/Source/bsmd.database/INFO.cs | 5 +- 40 files changed, 4669 insertions(+), 7 deletions(-) create mode 100644 AIS/bsmd.AISService/AIS/AIS.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_ClassB.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_ClassBExt.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_ClassBStatic.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_Configuration.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_Decoder.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_PosReport.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_QueueManager.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_StaticData.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_Target.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_Target_Comparer.cs create mode 100644 AIS/bsmd.AISService/AIS/AIS_Telnet.cs create mode 100644 AIS/bsmd.AISService/AIS/NMEA.cs create mode 100644 AIS/bsmd.AISService/AIS/NMEA_AIS_Sentence.cs create mode 100644 AIS/bsmd.AISService/AIS/NMEA_PNMLS_Sentence.cs create mode 100644 AIS/bsmd.AISService/AIS/SerialDataHandler.cs create mode 100644 AIS/bsmd.AISService/AIS/Serial_IO.cs create mode 100644 AIS/bsmd.AISService/AIS/TelnetDataHandler.cs create mode 100644 AIS/bsmd.AISService/AISService.Designer.cs create mode 100644 AIS/bsmd.AISService/AISService.cs create mode 100644 AIS/bsmd.AISService/App.config create mode 100644 AIS/bsmd.AISService/DB/AISPosReport.cs create mode 100644 AIS/bsmd.AISService/DB/AISStaticData.cs create mode 100644 AIS/bsmd.AISService/DB/AISStation.cs create mode 100644 AIS/bsmd.AISService/DB/AISWatchkeeper.cs create mode 100644 AIS/bsmd.AISService/DB/DBConnector.cs create mode 100644 AIS/bsmd.AISService/DB/Hotposition.cs create mode 100644 AIS/bsmd.AISService/Program.cs create mode 100644 AIS/bsmd.AISService/Properties/AssemblyInfo.cs create mode 100644 AIS/bsmd.AISService/Properties/Settings.Designer.cs create mode 100644 AIS/bsmd.AISService/Properties/Settings.settings create mode 100644 AIS/bsmd.AISService/bsmd.AISService.csproj create mode 100644 AIS/bsmd.AISService/bsmd.AISService.licenseheader create mode 100644 AIS/bsmd.AISService/bsmd.AISService.sln create mode 100644 AIS/bsmd.AISService/bsmdKey.snk create mode 100644 AIS/bsmd.AISService/packages.config diff --git a/AIS/bsmd.AISService/AIS/AIS.cs b/AIS/bsmd.AISService/AIS/AIS.cs new file mode 100644 index 00000000..47d85a2c --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +// ship details suchbar hier: http://www.itu.int/cgi-bin/htsh/mars/ship_search.sh + +namespace bsmd.AISService.AIS +{ + public abstract class AIS + { + protected const double ERAD = 6378.135; + protected const double DE2RA = 0.01745329252; + protected const double AVG_ERAD = 6371.0; + + + #region enumerations + + public enum Status + { + OK, + UNKNOWN_TYPE, + UNSUPPORTED, + ILLEGAL_ARGUMENT, + PARSE_ERROR + } + + public enum AISType + { + AIS_NONE = 0, + POSITION_REPORT, + POSITION_REPORT_ASSIGNED, + POSITION_REPORT_SPECIAL, + BASE_STATION_REPORT, + STATIC_VOYAGE_DATA, + BINARY_ADDR_MSG, + BINARY_ACK, + BINARY_BCAST, + SAR_AIRCRAFT, + UTC_INQUIRY, + UTC_RESPONSE, + SAFETY_RELATED_MSG, + SAFETY_RELATED_ACK, + SAFETY_RELATED_BCAST, + INTERROGATION, + ASSIGN_MODE_CMD, + DGNSS_BCAST, + POSITION_REPORT_B_EQUIP, + POSITION_REPORT_B_EQUIP_EXT, + DATA_LINK_MAN, + AIDS_TO_NAV_REPORT, + CHANNEL_MNGNT, + GROUP_ASSIGNMENT, + CLASS_B_STATIC_DATA + }; + + #endregion + + #region private members + + private AISType type = AISType.AIS_NONE; + protected int userId; + protected string data; + private string station; + + #endregion + + #region Properties + + public AISType MessageType + { + get { return this.type; } + } + + public int MMSI + { + get { return this.userId; } + } + + public string Station + { + get { return this.station; } + set { this.station = value; } + } + + #endregion + + #region abstract method signatures + + protected abstract Status Decode(); + + #endregion + + #region static methods + + internal static AIS Decode(string data, ref Status status) + { + AIS result = null; + + if (data == null || data.Length == 0) + { + status = Status.ILLEGAL_ARGUMENT; + return null; + } + + BitArray bits = AIS.DecodeChar(data[0]); + int type = AIS.GetInt(bits, 0, 5); + + result = AIS.CreateMessage(type); + if (result != null) + { + result.data = data; + status = result.Decode(); + } + else + { + status = Status.UNSUPPORTED; + } + + return result; + } + + /// + /// Factory method to create messages based on the message type + /// + protected static AIS CreateMessage(int type) + { + AIS result = null; + + switch (type) + { + case 1: + result = new AIS_PosReport(); + result.type = AISType.POSITION_REPORT; + break; + case 2: + result = new AIS_PosReport(); + result.type = AISType.POSITION_REPORT_ASSIGNED; + break; + case 3: + result = new AIS_PosReport(); + result.type = AISType.POSITION_REPORT_SPECIAL; + break; + + case 5: + result = new AIS_StaticData(); + result.type = AISType.STATIC_VOYAGE_DATA; + break; + + case 18: + result = new AIS_ClassB(); + result.type = AISType.POSITION_REPORT_B_EQUIP; + break; + + case 19: + result = new AIS_ClassBExt(); + result.type = AISType.POSITION_REPORT_B_EQUIP_EXT; + break; + + case 24: + result = new AIS_ClassBStatic(); + result.type = AISType.CLASS_B_STATIC_DATA; + break; + + default: + break; + } + + return result; + } + + #region static helpers + + protected static BitArray DecodeChar(char c) + { + Byte b = Convert.ToByte(c); + return DecodeByte(b); + } + + protected static BitArray DecodeByte(byte c) + { + c += 40; + if (c > 128) c += 32; + else c += 40; + + c &= 63; + + BitArray b = new BitArray(6); + + for (int i = 0; i < 6; i++) + { + b[i] = ((c >> (5-i)) & 1) == 1; + } + + return b; + } + + protected static BitArray DecodeBinary(string data) + { + BitArray result = new BitArray(data.Length * 6, false); + + for (int i = 0; i < data.Length; i++) + { + BitArray charBits = DecodeChar(data[i]); + + for (int j = 0; j < charBits.Count; j++) + result[i * 6 + j] = charBits[j]; + + } + + return result; + } + + protected static int GetInt(BitArray bits, int lo, int hi) + { + int result = 0; + int test = 1; + + for (int i = hi; i >= lo; i--, test <<= 1) + { + if (bits[i]) result |= test; + } + return result; + } + + protected static char GetAISChar(int val) + { + if(val < 32) return Convert.ToChar(val + 64); + if(val < 64) return Convert.ToChar(val); + return ' '; + } + + #endregion + + #region public static helpers + + /// + /// mehr dazu hier: + /// http://www.codeguru.com/cpp/cpp/algorithms/general/article.php/c5115/ + /// + public static double GetDistance(double lat1, double lon1, double lat2, double lon2) + { + lat1 *= AIS.DE2RA; + lon1 *= AIS.DE2RA; + lat2 *= AIS.DE2RA; + lon2 *= AIS.DE2RA; + double d = Math.Sin(lat1) * Math.Sin(lat2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Cos(lon1 -lon2); + return (AIS.AVG_ERAD * Math.Acos(d)); + } + + + #endregion + + #endregion + + #region overrides + + public override string ToString() + { + return Enum.GetName(typeof(AIS.AISType), this.MessageType); + } + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_ClassB.cs b/AIS/bsmd.AISService/AIS/AIS_ClassB.cs new file mode 100644 index 00000000..afe47a95 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_ClassB.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections; +using System.Text; +using System.Diagnostics; + +namespace bsmd.AISService.AIS +{ + public class AIS_ClassB : AIS + { + #region private members + + private int repeatIndicator; + private int reserved; + private int sog; + private int accuracy; + private int longitude; + private int latitude; + private int cog; + private int trueHeading; + private int utcTimestampSecs; + private DateTime timestamp; + private int reservedRegional; + private int spare; + private int assignedModeFlag; + private int raimFlag; + private int commStateSelectedFlag; + private int commState; + + #endregion + + #region Properties + + public int CogVal { get { return this.cog; } } + public int SogVal { get { return this.sog; } } + public int LatitudeVal { get { return this.latitude; } } + public int LongitudeVal { get { return this.longitude; } } + + public string DBTimestamp + { + get + { + return string.Format("{0}-{1}-{2} {3}:{4}:{5}", + this.timestamp.Year, this.timestamp.Month, this.timestamp.Day, + this.timestamp.Hour, this.timestamp.Minute, this.timestamp.Second); + } + } + + + + public double Cog + { + get { return this.cog / 10.0f; } + } + + public double Sog + { + get { return this.sog / 10.0f; } + } + + public double Latitude + { + get { return this.latitude / 600000.0f; } + } + + public double Longitude + { + get { return this.longitude / 600000.0f; } + } + + public DateTime Timestamp + { + get { return this.timestamp; } + } + + public int? TrueHeading + { + get + { + if (this.trueHeading == 511) return null; + return this.trueHeading; + } + } + + #endregion + + protected override AIS.Status Decode() + { + BitArray bits = AIS.DecodeBinary(this.data); + Status result = Status.OK; + + try + { + int type = AIS.GetInt(bits, 0, 5); + if (type != 18) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + this.repeatIndicator = AIS.GetInt(bits, 6, 7); + this.userId = AIS.GetInt(bits, 8, 37); + this.reserved = AIS.GetInt(bits, 38, 45); + this.sog = AIS.GetInt(bits, 46, 55); + this.accuracy = AIS.GetInt(bits, 56, 56); + this.longitude = AIS.GetInt(bits, 57, 84); + this.latitude = AIS.GetInt(bits, 85, 111); + this.cog = AIS.GetInt(bits, 112, 123); + this.trueHeading = AIS.GetInt(bits, 124, 132); + this.utcTimestampSecs = AIS.GetInt(bits, 133,138); + this.timestamp = DateTime.Now; + this.reservedRegional = AIS.GetInt(bits, 139, 140); + this.spare = AIS.GetInt(bits, 141, 145); + this.assignedModeFlag = AIS.GetInt(bits, 146, 146); + this.raimFlag = AIS.GetInt(bits, 147, 147); + this.commStateSelectedFlag = AIS.GetInt(bits, 148, 148); + this.commState = AIS.GetInt(bits, 149, 167); + } + } + catch (Exception e) + { + Trace.WriteLine(string.Format("Error decoding AIS class B posreport: {0}", e.Message)); + result = Status.PARSE_ERROR; + } + + return result; + } + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_ClassBExt.cs b/AIS/bsmd.AISService/AIS/AIS_ClassBExt.cs new file mode 100644 index 00000000..a3956611 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_ClassBExt.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections; +using System.Text; +using System.Diagnostics; + +namespace bsmd.AISService.AIS +{ + /// + /// Diese Nachricht wird normalerweise von Class B Geräten nicht verschickt. + /// Sie wird nur als Antwort auf einen sog. "Base station poll" gesendet. + /// Wir lesen sie trotzdem ;) + /// + // Todo + public class AIS_ClassBExt : AIS + { + #region private members + + private int repeatIndicator; + private int spare1; + private int sog; + private int accuracy; + private int longitude; + private int latitude; + private int cog; + private int trueHeading; + private int utcTimestampSecond; + private DateTime timestamp; + private int spare2; + private string name; + private int shipType; + private int dimension; + private int typeofDevice; + private int raimFlag; + private int dte; + private int assignedMode; + private int spare3; + + #endregion + + #region Properties + + public string Name + { + get { return this.name; } + } + + public DateTime Timestamp + { + get { return this.timestamp; } + } + + public double Latitude + { + get { return this.latitude / 600000.0f; } + } + + public double Longitude + { + get { return this.longitude / 600000.0f; } + } + + public int TrueHeading + { + get { return this.trueHeading; } + } + + public double Cog + { + get { return (double)this.cog / 10.0f; } + } + + #endregion + + protected override AIS.Status Decode() + { + BitArray bits = AIS.DecodeBinary(this.data); + Status result = Status.OK; + + try + { + int type = AIS.GetInt(bits, 0, 5); + if (type != 19) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + this.repeatIndicator = AIS.GetInt(bits, 6, 7); + this.userId = AIS.GetInt(bits, 8, 37); + this.spare1 = AIS.GetInt(bits, 38, 45); + this.sog = AIS.GetInt(bits, 46, 55); + this.accuracy = AIS.GetInt(bits, 56, 56); + this.longitude = AIS.GetInt(bits, 57, 84); + this.latitude = AIS.GetInt(bits, 85, 111); + this.cog = AIS.GetInt(bits, 112, 123); + this.trueHeading = AIS.GetInt(bits, 124, 132); + this.utcTimestampSecond = AIS.GetInt(bits, 133, 138); + this.timestamp = DateTime.Now; + this.spare2 = AIS.GetInt(bits, 139, 142); + + StringBuilder sb_name = new StringBuilder(20); + for (int i = 0; i < 20; i++) + { + int cval = AIS.GetInt(bits, 143 + (6 * i), 148 + (6 * i)); + char ch = AIS.GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_name.Append(ch); + } + this.name = sb_name.ToString().Trim(); + + this.shipType = AIS.GetInt(bits, 263, 270); + this.dimension = AIS.GetInt(bits, 271, 300); + this.typeofDevice = AIS.GetInt(bits, 301, 304); + this.raimFlag = AIS.GetInt(bits, 305, 305); + this.dte = AIS.GetInt(bits, 306, 306); + this.assignedMode = AIS.GetInt(bits, 307, 307); + this.spare3 = AIS.GetInt(bits, 308, 311); + } + } + + catch (Exception e) + { + Trace.WriteLine(string.Format("Error decoding AIS class B Ext posreport: {0}", e.Message)); + result = Status.PARSE_ERROR; + } + + return result; + } + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_ClassBStatic.cs b/AIS/bsmd.AISService/AIS/AIS_ClassBStatic.cs new file mode 100644 index 00000000..89c51757 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_ClassBStatic.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections; +using System.Text; +using System.Diagnostics; + +namespace bsmd.AISService.AIS +{ + public class AIS_ClassBStatic : AIS + { + #region private members + + private int repeatIndicator; + private int partNumber; + private string name; + private int shipType; + private string vendorId; + private string callsign; + private int dimension; + private int spare; + + #endregion + + #region Properties + + public bool IsPartA + { + get { return this.partNumber == 0; } + } + + public bool IsPartB + { + get { return this.partNumber == 1; } + } + + public string Name + { + get { return this.name; } + } + + public string ShipType + { + get { return AIS_StaticData.GetShipType(this.shipType); } + } + + public string VendorId + { + get { return this.vendorId; } + } + + public string Callsign + { + get { return this.callsign; } + } + + public int ShipTypeVal + { + get { return this.shipType; } + } + + // Todo: Dimensions.. + + #endregion + + protected override AIS.Status Decode() + { + BitArray bits = AIS.DecodeBinary(this.data); + Status result = Status.OK; + + try + { + int type = AIS.GetInt(bits, 0, 5); + if (type != 24) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + this.repeatIndicator = AIS.GetInt(bits, 6, 7); + this.userId = AIS.GetInt(bits, 8, 37); + this.partNumber = AIS.GetInt(bits, 38, 39); + if (this.IsPartA) + { + StringBuilder sb_name = new StringBuilder(20); + for (int i = 0; i < 20; i++) + { + int cval = AIS.GetInt(bits, 40 + (6 * i), 45 + (6 * i)); + char ch = AIS.GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_name.Append(ch); + } + this.name = sb_name.ToString().Trim(); + } + else + { + this.shipType = AIS.GetInt(bits, 40, 47); + + StringBuilder sb_vendor = new StringBuilder(7); + for (int i = 0; i < 7; i++) + { + int cval = AIS.GetInt(bits, 48 + (6 * i), 53 + (6 * i)); + char ch = AIS.GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_vendor.Append(ch); + } + this.vendorId = sb_vendor.ToString().Trim(); + + StringBuilder sb_callsign = new StringBuilder(7); + for (int i = 0; i < 7; i++) + { + int cval = AIS.GetInt(bits, 90 + (6 * i), 95 + (6 * i)); + char ch = AIS.GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_callsign.Append(ch); + } + this.callsign = sb_callsign.ToString().Trim(); + this.dimension = AIS.GetInt(bits, 141, 161); + this.spare = AIS.GetInt(bits, 162, 167); + } + } + } + catch (Exception e) + { + Trace.WriteLine(string.Format("Error decoding AIS class B static data: {0}", e.Message)); + result = Status.PARSE_ERROR; + } + + return result; + } + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_Configuration.cs b/AIS/bsmd.AISService/AIS/AIS_Configuration.cs new file mode 100644 index 00000000..53fb5a4f --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_Configuration.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Reflection; +using System.Xml; +using System.Xml.Serialization; + +namespace bsmd.AISService.AIS +{ + [Serializable] + public class AIS_Configuration + { + private string filename; + private string dbConnectionString; + private int dbUpdateInterval = 500; // milliseconds + private int dbMinPosReportTimeDifference = 120; // seconds + private int stationIsOfflineTimeDifferenceSecs = 180; // seconds + private int targetStaleMins = 31; // minutes + + public List SerialPorts = new List(); + public List TelnetConnections = new List(); + + #region Properties + + public string Configuration_Path + { + get { return this.filename; } + set { this.filename = value; } + } + + public string DBConnectionString + { + get { return this.dbConnectionString; } + set { this.dbConnectionString = value; } + } + + /// + /// timer interval for database updates + /// + public int DBUpdateInterval + { + get { return this.dbUpdateInterval; } + set { this.dbUpdateInterval = value; } + } + + /// + /// minimum amount of minutes between two position reports to be + /// written to database + /// + public int DBMinPosReportTimeDifference + { + get { return this.dbMinPosReportTimeDifference; } + set { this.dbMinPosReportTimeDifference = value; } + } + + /// + /// number of seconds after which a station is marked offline since + /// sending the last pos report + /// + public int StationIsOfflineTimeDifferenceSecs + { + get { return this.stationIsOfflineTimeDifferenceSecs; } + set { this.stationIsOfflineTimeDifferenceSecs = value; } + } + + /// + /// if last update is older than this value then the target ist removed from + /// the current target queue (target went offline or out of range) + /// + public int TargetStaleMins + { + get { return this.targetStaleMins; } + set { this.targetStaleMins = value; } + } + + /// + /// Root path to where Viewer stores OSM tiles + /// + public string TilePath { get; set; } + + /// + /// full path to logfile + /// + public string LogfilePath { get; set; } + + /// + /// outputs assembly version + /// + public static string VersionInfo + { + get + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + return version.ToString(); + } + } + + #endregion + + #region Load/Save + + public static AIS_Configuration Load(string filename) + { + if (!File.Exists(filename)) return null; + + // Create an instance of the XmlSerializer specifying type and namespace. + XmlSerializer serializer = new XmlSerializer(typeof(AIS_Configuration)); + + // A FileStream is needed to read the XML document. + FileStream fs = new FileStream(filename, FileMode.Open); + XmlReader reader = new XmlTextReader(fs); + + AIS_Configuration configuration = serializer.Deserialize(reader) as AIS_Configuration; + reader.Close(); + configuration.filename = filename; + + + return configuration; + } + + public bool Save() + { + bool retval = true; + try + { + XmlSerializer serializer = new XmlSerializer(typeof(AIS_Configuration)); + Stream fs = new FileStream(this.filename, FileMode.Create); + XmlWriter writer = new XmlTextWriter(fs, new UTF8Encoding()); + serializer.Serialize(writer, this); + writer.Close(); + } + catch (Exception e) + { + System.Diagnostics.Debug.Write("Error during Serialize: " + e.ToString()); + retval = false; + } + return retval; + } + + #endregion + + #region internal classes + + public class SerialPort + { + public string station; + public string ComPort; + public int BaudRate = 9600; + public bool enabled = false; + } + + public class TelnetConnection + { + public string ipAddress; + public int port; + } + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_Decoder.cs b/AIS/bsmd.AISService/AIS/AIS_Decoder.cs new file mode 100644 index 00000000..46f8476e --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_Decoder.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Text; + +namespace bsmd.AISService.AIS +{ + + /// + /// Diese Klasse setzt fragmentierte AIS Telegramme wieder zusammen und decodiert sie + /// + public class AIS_Decoder + { + + public delegate void AISMessageHandler(AIS message); + public event AISMessageHandler AISMessageReceived; + + #region class AISQueueElement + + public class AISQueueElement + { + public int seq_nr; + public int total_nr; + public int? id; + public string data; + public string station; + } + + #endregion + + #region private members + + private Queue inputDataQueue = new Queue(); + private Thread decodingThread; + private bool runDecoder = true; + private int sleepMS = 250; + private Dictionary> fragmentDict = new Dictionary>(); + + #endregion + + #region Properties + + public int QueueSize + { + get { return this.inputDataQueue.Count; } + } + + #endregion + + #region public methods + + public void Decode(string data, int seq_nr, int total_nr, int? id, string station) + { + lock (this.inputDataQueue) + { + AISQueueElement element = new AISQueueElement(); + element.data = data; + element.seq_nr = seq_nr; + element.total_nr = total_nr; + element.id = id; + element.station = station; + + this.inputDataQueue.Enqueue(element); + } + } + + public void Start() + { + this.decodingThread = new Thread(new ThreadStart(this.Run)); + this.decodingThread.Start(); + } + + public void Stop() + { + this.runDecoder = false; + if((this.decodingThread != null) && + (this.decodingThread.ThreadState == ThreadState.Running)) + this.decodingThread.Join(); + this.inputDataQueue.Clear(); // discard unread elements + } + + #endregion + + /// + /// Thread worker method + /// + protected void Run() + { + while (this.runDecoder) + { + AISQueueElement inputData = null; + + lock (this.inputDataQueue) + { + if (this.inputDataQueue.Count > 0) + { + inputData = this.inputDataQueue.Dequeue(); + } + } + + if (inputData == null) + Thread.Sleep(this.sleepMS); + else + { + string aisRawData = null; + if (inputData.total_nr == 1) + { + aisRawData = inputData.data; + } + else + { + int id = inputData.id ?? -1; + + if (!this.fragmentDict.ContainsKey(id)) + this.fragmentDict.Add(id, new List()); + this.fragmentDict[id].Add(inputData); + + // sind alle Fragmente vorhanden? + if (AIS_Decoder.FragmentsComplete(this.fragmentDict[id])) + { + // Fragmente zusammensetzen + aisRawData = AIS_Decoder.ConcatenateFragments(this.fragmentDict[id]); + this.fragmentDict.Remove(id); + } + + } + + if (aisRawData != null) + { + AIS.Status status = AIS.Status.OK; + AIS message = AIS.Decode(aisRawData, ref status); + if (status == AIS.Status.OK) + { + message.Station = inputData.station; + this.OnAISMessageReceived(message); + } + } + + } + } + } + + #region private helpers + + /// + /// check to see if all fragments are available + /// + private static bool FragmentsComplete(List elements) + { + if (elements == null || elements.Count == 0) return false; + int num = elements[0].total_nr; + + for (int i = 1; i <= num; i++) + { + bool foundElements = false; + for (int j = 0; j < elements.Count; j++) + { + if (elements[j].seq_nr == i) + foundElements = true; + } + if (!foundElements) return false; // etwas fehlt noch + } + return true; + } + + /// + /// assembles message fragments. Care must be taken since fragments can appear + /// out of order + /// + private static string ConcatenateFragments(List elements) + { + if (elements == null || elements.Count == 0) return string.Empty; + int num = elements[0].total_nr; + StringBuilder sb = new StringBuilder(); + + for (int i = 1; i <= num; i++) + { + for (int j = 0; j < elements.Count; j++) + if (elements[j].seq_nr == i) + sb.Append(elements[j].data); + } + return sb.ToString(); + } + + #endregion + + protected void OnAISMessageReceived(AIS message) + { + if (this.AISMessageReceived != null) + this.AISMessageReceived(message); + } + + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_PosReport.cs b/AIS/bsmd.AISService/AIS/AIS_PosReport.cs new file mode 100644 index 00000000..949e5203 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_PosReport.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.Text; + +namespace bsmd.AISService.AIS +{ + public class AIS_PosReport : AIS + { + private int navstatus; + private int rot; + private int sog; + private int accur; + private int longitude; + private int latitude; + private int cog; + private int trueheading; + private DateTime timestamp; + private int utcTimeSecond; + private int reserved; + private int spare; + private int raim; + private int commstate; + + #region Properties + + public int NavStatusVal { get { return this.navstatus; } } + public int ROTVal { get { return this.rot; } } + public int SOGVal { get { return this.sog; } } + public int COGVal { get { return this.cog; } } + public int Accuracy { get { return this.accur; } } + public int LatitudeVal { get { return this.latitude; } } + public int LongitudeVal { get { return this.longitude; } } + public string DBTimestamp + { + get + { + return string.Format("{0}-{1}-{2} {3}:{4}:{5}", + this.timestamp.Year, this.timestamp.Month, this.timestamp.Day, + this.timestamp.Hour, this.timestamp.Minute, this.timestamp.Second); + } + } + + public double SOG + { + get + { + return ((double)this.sog) / 10.0f; + } + } + + public double COG + { + get + { + return ((double)this.cog) / 10.0f; + } + } + + public int ROT + { + get + { + return (int)((double)(this.rot * this.rot) / 22.401289); + } + } + + public double Latitude + { + get + { + return ((double)this.latitude) / 600000.0f; + } + } + + public double Longitude + { + get + { + return ((double)this.longitude) / 600000.0f; + } + } + + + public string NavStatus + { + get { return GetNavStatus(this.navstatus); } + } + + public DateTime Timestamp + { + get { return this.timestamp; } + } + + public int? TrueHeading + { + get + { + if (this.trueheading == 511) return null; + return this.trueheading; + } + } + + #endregion + + #region static methods + + public static string GetNavStatus(int navstatus) + { + switch (navstatus) + { + case 0: + return "under way using engine"; + case 1: + return "at anchor"; + case 2: + return "not under command"; + case 3: + return "restricted manoeuvrability"; + case 4: + return "contrained by her draught"; + case 5: + return "moored"; + case 6: + return "aground"; + case 7: + return "engaged in fishing"; + case 8: + return "under way sailing"; + case 9: + return "reserved for future amendment of Navigational Status for HSC"; + case 10: + return "reserved for future amendment of Navigational Status for WIG"; + case 11: + case 12: + case 13: + case 14: + return "reserved for future use"; + default: + return "not defined"; + } + } + + #endregion + + #region overrides + + protected override Status Decode() + { + Status result = Status.OK; + BitArray bits = AIS.DecodeBinary(this.data); + + try + { + this.userId = AIS.GetInt(bits, 8, 37); + this.navstatus = AIS.GetInt(bits, 38, 41); + this.rot = AIS.GetInt(bits, 42, 49); + this.sog = AIS.GetInt(bits, 50, 59); + this.accur = AIS.GetInt(bits, 60, 60); + this.longitude = AIS.GetInt(bits, 61, 88); + this.latitude = AIS.GetInt(bits, 89, 115); + this.cog = AIS.GetInt(bits, 116, 127); + this.trueheading = AIS.GetInt(bits, 128, 136); + this.utcTimeSecond = AIS.GetInt(bits, 137, 142); + this.reserved = AIS.GetInt(bits, 143, 146); + this.spare = AIS.GetInt(bits, 147, 147); + this.raim = AIS.GetInt(bits, 148, 148); + this.commstate = AIS.GetInt(bits, 149, 167); + } + catch (Exception e) + { + Trace.WriteLine(string.Format("Error decoding AIS pos report: {0}", e.Message)); + result = Status.PARSE_ERROR; + } + + this.timestamp = DateTime.Now; + return result; + } + + public override string ToString() + { + return string.Format("{0} - MMSI {1}", base.ToString(), this.MMSI); + } + + #endregion + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_QueueManager.cs b/AIS/bsmd.AISService/AIS/AIS_QueueManager.cs new file mode 100644 index 00000000..faecdace --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_QueueManager.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Timers; +using System.Diagnostics; + +namespace bsmd.AISService.AIS +{ + /// + /// Hier laufen die Fäden zusammen. Diese Klasse enthält alle Objekte und kann direkt von + /// Konsolen / services und Windowsprogrammen verwendet werden + /// +public class AIS_QueueManager + { + + public delegate void AISQueueChangedHandler(AIS_Target target); + public event AISQueueChangedHandler AISQueueChanged; + public event AISQueueChangedHandler DBUpdateRequired; + + private Dictionary activeTargets = new Dictionary(); + private List activeTargetList = new List(); + private List databaseTargets = new List(); + private List watchkeeperTargets = new List(); + private AIS_Configuration configuration; + private List serialHandlerList = new List(); + private List telnetHandlerList = new List(); + private List dbUpdateQueue = new List(); + private Timer dbUpdateTimer = new Timer(); + private bool isStarted = false; + + #region Construction + + public AIS_QueueManager(AIS_Configuration configuration, List serialIOs, List ais_Telnets) + { + this.configuration = configuration; + + foreach (Serial_IO serialIO in serialIOs) + { + AIS_Decoder decoder = new AIS_Decoder(); + decoder.AISMessageReceived += new AIS_Decoder.AISMessageHandler(this.decoder_AISMessageReceived); + SerialDataHandler handler = new SerialDataHandler(serialIO, decoder); + this.serialHandlerList.Add(handler); + } + + foreach (AIS_Telnet aisTelnet in ais_Telnets) + { + AIS_Decoder decoder = new AIS_Decoder(); + decoder.AISMessageReceived += new AIS_Decoder.AISMessageHandler(this.decoder_AISMessageReceived); + TelnetDataHandler tdn = new TelnetDataHandler(aisTelnet, decoder); + this.telnetHandlerList.Add(tdn); + } + + AIS_Target.dbUpdateInterval = new TimeSpan(0, 0, configuration.DBMinPosReportTimeDifference); + this.dbUpdateTimer.Interval = configuration.DBUpdateInterval; + this.dbUpdateTimer.Elapsed += new ElapsedEventHandler(dbUpdateTimer_Elapsed); + } + + #endregion + + #region Properties + + public List ActiveTargets + { + get + { + return this.activeTargetList; + } + } + + public bool IsStarted + { + get { return this.isStarted; } + } + + #endregion + + #region event handler + + void dbUpdateTimer_Elapsed(object sender, ElapsedEventArgs e) + { + while (this.dbUpdateQueue.Count > 0) + { + AIS_Target currentTarget = null; + lock (this.dbUpdateQueue) + { + // Trace.WriteLine(string.Format("Update queue size: {0}", this.dbUpdateQueue.Count)); + currentTarget = this.dbUpdateQueue[0]; + this.dbUpdateQueue.RemoveAt(0); + } + this.OnDBUpdateRequired(currentTarget); + } + + // remove stale targets + lock (this.activeTargetList) + { + + for(int i=0;i this.configuration.TargetStaleMins) + { + this.activeTargetList.RemoveAt(i); + i--; + } + } + } + + } + + void decoder_AISMessageReceived(AIS message) + { + lock (this.activeTargets) + { + // Trace.WriteLine(string.Format("Queue manager: AIS message received, queue size: {0}", activeTargets.Count)); + if (!this.activeTargets.ContainsKey(message.MMSI)) + { + AIS_Target target = new AIS_Target(message.MMSI); + this.activeTargets.Add(message.MMSI, target); + lock (this.activeTargetList) + { + this.activeTargetList.Add(target); + } + } + + this.activeTargets[message.MMSI].Update(message); + this.OnAISQueueChanged(this.activeTargets[message.MMSI]); + + if (this.activeTargets[message.MMSI].UpdateDB) + { + lock (this.dbUpdateQueue) + { + if (!this.dbUpdateQueue.Contains(this.activeTargets[message.MMSI])) + this.dbUpdateQueue.Add(this.activeTargets[message.MMSI]); + } + } + } + } + + #endregion + + #region public methods + + public bool Start(ref string message) + { + bool retval = true; + if (this.isStarted) + { + message = "Queue manager already started"; + return true; + } + + foreach (SerialDataHandler sdh in this.serialHandlerList) + { + string messagePart = ""; + retval &= sdh.Start(ref messagePart); + if (!retval) + message += messagePart + Environment.NewLine; + if(retval) sdh.AIS_Decoder.Start(); + } + + foreach (TelnetDataHandler tdh in this.telnetHandlerList) + { + string messagePart = ""; + retval &= tdh.Start(ref messagePart); + if (!retval) + message += messagePart + Environment.NewLine; + if (retval) tdh.AIS_Decoder.Start(); + } + + if (retval) + this.dbUpdateTimer.Start(); + + if (retval) this.isStarted = true; + + return retval; + } + + public void Stop() + { + if (this.isStarted) + { + foreach (SerialDataHandler sdh in this.serialHandlerList) + { + sdh.Stop(); + sdh.AIS_Decoder.Stop(); + } + foreach (TelnetDataHandler tdh in this.telnetHandlerList) + { + tdh.Stop(); + tdh.AIS_Decoder.Stop(); + } + this.dbUpdateTimer.Stop(); + this.isStarted = false; + } + } + + #endregion + + #region OnEvent methods + + protected void OnAISQueueChanged(AIS_Target target) + { + if (this.AISQueueChanged != null) this.AISQueueChanged(target); + } + + protected void OnDBUpdateRequired(AIS_Target target) + { + if (this.DBUpdateRequired != null) this.DBUpdateRequired(target); + } + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_StaticData.cs b/AIS/bsmd.AISService/AIS/AIS_StaticData.cs new file mode 100644 index 00000000..6bff5030 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_StaticData.cs @@ -0,0 +1,381 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.Text; + +namespace bsmd.AISService.AIS +{ + public class AIS_StaticData : AIS + { + #region private members + + private int ais_version; + private int imoNumber; + private string callsign; + private string name; + private int shiptype; + private int dimension; + private int a; + private int b; + private int c; + private int d; + + private int typeofdevice; + private int etamonth; + private int etaday; + private int etahour; + private int etaminute; + private DateTime? eta; + + private int maxpresetstaticdraught; + private string destination; + private int dte; + private int spare; + + #endregion + + #region Properties + + public int ShipTypeVal { get { return this.shiptype; } } + + public string Callsign + { + get { return this.callsign; } + } + + public string Name + { + get { return this.name; } + } + + public DateTime? ETA + { + get { return this.eta; } + } + + public string Destination + { + get { return this.destination; } + } + + public int IMONumber + { + get { return this.imoNumber; } + } + + public string DeviceName + { + get + { + switch (typeofdevice) + { + case 1: + return "GPS"; + case 2: + return "GLONASS"; + case 3: + return "Combined GPS/GLONASS"; + case 4: + return "Loran-C"; + case 5: + return "Chayka"; + case 6: + return "Integrated Navigation System"; + case 7: + return "surveyed"; + case 8: + return "Galileo"; + default: + return "undefined"; + } + } + } + + public int Draught + { + get { return this.maxpresetstaticdraught; } + } + + public int Breadth + { + get + { + return this.c + this.d; + } + } + + public int Length + { + get + { + return this.a + this.b; + } + } + + public string ShipType + { + get + { + return AIS_StaticData.GetShipType(this.shiptype); + } + } + + public string DBETA + { + get + { + if (this.eta.HasValue) + { + return string.Format("{0}-{1}-{2} {3}:{4}:{5}", + this.eta.Value.Year, this.eta.Value.Month, this.eta.Value.Day, + this.eta.Value.Hour, this.eta.Value.Minute, this.eta.Value.Second); + } + else + return ""; + } + } + + + #endregion + + #region abstract method implementation + + protected override Status Decode() + { + BitArray bits = AIS.DecodeBinary(this.data); + Status result = Status.OK; + + + try + { + int type = AIS.GetInt(bits, 0, 5); + if (type != 5) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + this.userId = AIS.GetInt(bits, 6, 37); + this.ais_version = AIS.GetInt(bits, 38, 39); + this.imoNumber = AIS.GetInt(bits, 40, 69); + + StringBuilder sb_callsign = new StringBuilder(7); + for (int i = 0; i < 7; i++) + { + int cval = AIS.GetInt(bits, 70 + (6 * i), 75 + (6 * i)); + char ch = AIS.GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_callsign.Append(ch); + } + this.callsign = sb_callsign.ToString().Trim(); + + StringBuilder sb_name = new StringBuilder(20); + for (int i = 0; i < 20; i++) + { + int cval = AIS.GetInt(bits, 112 + (6 * i), 117 + (6 * i)); + char ch = AIS.GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_name.Append(ch); + } + this.name = sb_name.ToString().Trim(); + + this.shiptype = AIS.GetInt(bits, 232, 239); + this.dimension = AIS.GetInt(bits, 240, 269); + this.a = AIS.GetInt(bits, 240, 248); + this.b = AIS.GetInt(bits, 249, 257); + this.c = AIS.GetInt(bits, 258, 263); + this.d = AIS.GetInt(bits, 264, 269); + this.typeofdevice = AIS.GetInt(bits, 270, 273); + this.etamonth = AIS.GetInt(bits, 274, 277); + this.etaday = AIS.GetInt(bits, 278, 282); + this.etahour = AIS.GetInt(bits, 283, 287); + this.etaminute = AIS.GetInt(bits, 288, 293); + try + { + if ((this.etahour < 24) && (this.etaday > 0) && (this.etaminute < 60) && (this.etamonth > 0)) + { + this.eta = new DateTime(DateTime.Now.Year, this.etamonth, this.etaday, this.etahour, this.etaminute, 0); + } + } + catch(Exception) { + Trace.WriteLine("ERROR creating ETA timestamp"); + } + this.maxpresetstaticdraught = AIS.GetInt(bits, 294, 301); + + StringBuilder sb_destination = new StringBuilder(20); + for (int i = 0; i < 20; i++) + { + int cval = AIS.GetInt(bits, 302 + (6 * i), 307 + (6 * i)); + char ch = AIS.GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_destination.Append(ch); + } + this.destination = sb_destination.ToString().Trim(); + + this.dte = AIS.GetInt(bits, 422, 422); + this.spare = AIS.GetInt(bits, 423, 423); + + } + } + catch (Exception e) + { + Trace.WriteLine(string.Format("Error decoding AIS static data: {0}", e.Message)); + result = Status.PARSE_ERROR; + } + return result; + } + + public override string ToString() + { + return string.Format("{0} - {1} [{2}]", base.ToString(), this.MMSI, this.Name); + } + + #endregion + + #region public static methods + + public static AIS_Target.Type GetShipTypeSimple(int shiptype) + { + switch (shiptype) + { + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + return AIS_Target.Type.TUG; + default: + int d1 = shiptype / 10; + switch (d1) + { + case 2: + return AIS_Target.Type.WIG; + case 3: + return AIS_Target.Type.OTHER; + case 4: + return AIS_Target.Type.HSC; + case 6: + return AIS_Target.Type.PASSENGER; + case 7: + return AIS_Target.Type.CARGO; + case 8: + return AIS_Target.Type.TANKER; + } + return AIS_Target.Type.OTHER; + } + } + + public static string GetShipType(int shiptype) + { + if (shiptype > 199) return "preserved for future use"; + if (shiptype > 99) return "preserved for regional use"; + int dig1, dig2; + switch (shiptype) + { + case 50: + return "Pilot vessel"; + case 51: + return "SAR vessel"; + case 52: + return "Tug"; + case 53: + return "Port tender"; + case 54: + return "Vessel with anti-pollution facility or equipment"; + case 55: + return "Law enforcment vessel"; + case 56: + return "Spare [local vessel]"; + case 57: + return "Spare [local vessel]"; + case 58: + return "Medical transport"; + case 59: + return "Ship according to Resolution No. 18 (Mob-83)"; + default: + { + string comb = ""; + dig1 = shiptype / 10; + dig2 = shiptype % 10; + switch (dig1) + { + case 1: + comb += "reserved for future use"; + break; + case 2: + comb += "WIG"; + break; + case 3: + comb += "Vessel"; + switch (dig2) + { + case 0: + comb += " Fishing"; break; + case 1: + comb += " Towing"; break; + case 2: + comb += " Towing and length of tow exceeds 200m or breadth exceeds 25m"; break; + case 3: + comb += " Engaged in dredging or underwater operations"; break; + case 4: + comb += " Engaged in diving operations"; break; + case 5: + comb += " Engaged in military operations"; break; + case 6: + comb += " Sailing"; break; + case 7: + comb += " Pleasure craft"; break; + default: + comb += " reserved for future use"; + break; + } + return comb; + case 4: + comb += "HSC"; + break; + case 6: + comb += "Passenger ship"; + break; + case 7: + comb += "Cargo ship"; + break; + case 8: + comb += "Tanker"; + break; + default: + case 9: + comb += "other"; + break; + } + switch (dig2) + { + case 0: break; + case 1: + comb += " carrying DG, HS or MP IMO hazard or pollutant category A"; break; + case 2: + comb += " carrying DG, HS or MP IMO hazard or pollutant category B"; break; + case 3: + comb += " carrying DG, HS or MP IMO hazard or pollutant category C"; break; + case 4: + comb += " carrying DG, HS or MP IMO hazard or pollutant category D"; break; + case 5: + case 6: + case 7: + case 8: + comb += " reserved for future use"; break; + case 9: + comb += " no additional information"; break; + } + return comb; + } + } + } + + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_Target.cs b/AIS/bsmd.AISService/AIS/AIS_Target.cs new file mode 100644 index 00000000..f3002406 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_Target.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace bsmd.AISService.AIS +{ + public class AIS_Target + { + + #region private members + + public static TimeSpan dbUpdateInterval = new TimeSpan(0, 2, 0); // neue Position in DB schreiben (min Interval) + private int mmsi; + private bool isClassB = false; + private bool? isWatchkeeper = null; + private DateTime? lastUpdate; + private bool updateDB = false; + private string name; + private string station; + private string lastDBName; + private string callSign; + private bool selected = false; + + private AIS staticInfo; + private AIS posReport; + private AIS lastAdditionalData; + + private AIS_Target.Type type = Type.OTHER; + private AIS_Target.NavStatus navStatus = AIS_Target.NavStatus.UNKNOWN; + + #endregion + + #region public defs + + + + public enum Type + { + PASSENGER, + CARGO, + TANKER, + HSC, + WIG, + TUG, + YACHT, + OTHER + } + + /// + /// vereinfacht + /// + public enum NavStatus + { + UNKNOWN, + UNDERWAY, + MOORED + } + + #endregion + + #region Construction + + public AIS_Target(int mmsi) + { + this.mmsi = mmsi; + } + + #endregion + + #region Properties + + public bool UpdateDB + { + get { return this.updateDB; } + set { this.updateDB = value; } + } + + public int MMSI + { + get { return this.mmsi; } + } + + public DateTime? LastUpdate + { + get { return this.lastUpdate; } + } + + public string Name + { + get { + if ((this.name == null) || (this.name.Length == 0)) + return this.LastDBName; + return this.name; + } + set { this.name = value; } + } + + public string Callsign + { + get { return this.callSign; } + set { this.callSign = value; } + } + + public string LastDBName + { + get { return this.lastDBName; } + set { this.lastDBName = value; } + } + + public string ReceivedFrom + { + get { return this.station; } + } + + public AIS LastPosReport + { + get { return this.posReport; } + } + + public AIS LastStaticData + { + get { return this.staticInfo; } + } + + public double? Latitude + { + get + { + if (this.LastPosReport == null) return null; + if (this.LastPosReport is AIS_PosReport) + return ((AIS_PosReport)this.LastPosReport).Latitude; + if (this.LastPosReport is AIS_ClassB) + return ((AIS_ClassB)this.LastPosReport).Latitude; + if (this.LastPosReport is AIS_ClassBExt) + return ((AIS_ClassBExt)this.LastPosReport).Latitude; + return null; + } + } + + public Type TargetType + { + get { return this.type; } + set { this.type = value; } + } + + public NavStatus TargetNavStatus + { + get { return this.navStatus; } + } + + public double? Longitude + { + get + { + if (this.LastPosReport == null) return null; + if (this.LastPosReport is AIS_PosReport) + return ((AIS_PosReport)this.LastPosReport).Longitude; + if (this.LastPosReport is AIS_ClassB) + return ((AIS_ClassB)this.LastPosReport).Longitude; + if (this.LastPosReport is AIS_ClassBExt) + return ((AIS_ClassBExt)this.LastPosReport).Longitude; + return null; + } + } + + public bool? IsClassB + { + get + { + return this.isClassB; + } + } + + public int? Heading + { + get + { + if (this.LastPosReport == null) return null; + if (this.LastPosReport is AIS_PosReport) + return ((AIS_PosReport)this.LastPosReport).TrueHeading; + if (this.LastPosReport is AIS_ClassB) + return ((AIS_ClassB)this.LastPosReport).TrueHeading; + if (this.LastPosReport is AIS_ClassBExt) + return ((AIS_ClassBExt)this.LastPosReport).TrueHeading; + return null; + } + } + + public int? COG + { + get + { + if (this.LastPosReport == null) return null; + if (this.LastPosReport is AIS_PosReport) + return (int)((AIS_PosReport)this.LastPosReport).COG; + if (this.LastPosReport is AIS_ClassB) + return (int)((AIS_ClassB)this.LastPosReport).Cog; + if (this.LastPosReport is AIS_ClassBExt) + return (int) ((AIS_ClassBExt)this.LastPosReport).Cog; + return null; + } + } + + public bool? IsWatchkeeperShip + { + get { return this.isWatchkeeper; } + set { this.isWatchkeeper = value; } + } + + public bool Selected + { + get { return this.selected; } + set { this.selected = value; } + } + + public string Station + { + get { return this.station; } + set { this.station = value; } + } + + #endregion + + #region public methods + + public static AIS_Target.NavStatus GetCurrentNavstatus(int status) + { + AIS_Target.NavStatus result = NavStatus.UNKNOWN; + switch (status) + { + case 0: + case 8: + result = NavStatus.UNDERWAY; + break; + default: + result = NavStatus.MOORED; + break; + } + return result; + } + + public void Update(AIS message) + { + this.station = message.Station; + + switch (message.MessageType) + { + case AIS.AISType.POSITION_REPORT: + case AIS.AISType.POSITION_REPORT_ASSIGNED: + case AIS.AISType.POSITION_REPORT_SPECIAL: + if ((this.lastUpdate.HasValue && + (((AIS_PosReport)message).Timestamp - this.lastUpdate.Value) > AIS_Target.dbUpdateInterval) + || (!this.lastUpdate.HasValue)) + { + this.updateDB = true; + this.lastUpdate = ((AIS_PosReport)message).Timestamp; + } + this.posReport = message; + this.navStatus = AIS_Target.GetCurrentNavstatus(((AIS_PosReport)message).NavStatusVal); + // System.Diagnostics.Trace.WriteLine(string.Format("pos report at {0}", this.lastUpdate)); + break; + case AIS.AISType.POSITION_REPORT_B_EQUIP: + if ((this.lastUpdate.HasValue && + (((AIS_ClassB)message).Timestamp - this.lastUpdate.Value) > AIS_Target.dbUpdateInterval) + || (!this.lastUpdate.HasValue)) + { + this.updateDB = true; + this.lastUpdate = ((AIS_ClassB)message).Timestamp; + this.isClassB = true; + this.type = Type.YACHT; + this.navStatus = NavStatus.UNDERWAY; + } + this.posReport = message; + break; + case AIS.AISType.POSITION_REPORT_B_EQUIP_EXT: + if ((this.lastUpdate.HasValue && + (((AIS_ClassBExt)message).Timestamp - this.lastUpdate.Value) > AIS_Target.dbUpdateInterval) + || (!this.lastUpdate.HasValue)) + { + this.updateDB = true; + this.lastUpdate = ((AIS_ClassBExt)message).Timestamp; + this.isClassB = true; + this.type = Type.YACHT; + this.navStatus = NavStatus.UNDERWAY; + } + this.posReport = message; + break; + case AIS.AISType.STATIC_VOYAGE_DATA: + this.staticInfo = message; + this.name = ((AIS_StaticData)message).Name; + this.callSign = ((AIS_StaticData)message).Callsign; + this.type = AIS_StaticData.GetShipTypeSimple(((AIS_StaticData)message).ShipTypeVal); + + break; + case AIS.AISType.CLASS_B_STATIC_DATA: + if (((AIS_ClassBStatic)message).IsPartA) + { + this.name = ((AIS_ClassBStatic)message).Name; + } + else + { + this.callSign = ((AIS_ClassBStatic)message).Callsign; + } + this.staticInfo = message; + this.type = Type.YACHT; + this.isClassB = true; + break; + default: + this.lastAdditionalData = message; + break; + } + + } + + #endregion + + #region overrides + + public override string ToString() + { + return string.Format("{0} [{1}]", this.Name, this.MMSI); + } + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_Target_Comparer.cs b/AIS/bsmd.AISService/AIS/AIS_Target_Comparer.cs new file mode 100644 index 00000000..300b38da --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_Target_Comparer.cs @@ -0,0 +1,104 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Text; + +namespace bsmd.AISService.AIS +{ + public class AIS_Target_Comparer : IComparer + { + private SortPropertyEnum sortProperty = SortPropertyEnum.NAME; + private ListSortDirection sortDirection = ListSortDirection.Ascending; + + public enum SortPropertyEnum + { + NONE, + MMSI, + NAME, + CALLSIGN, + LASTUPDATE, + STATION + } + + #region Properties + + public ListSortDirection SortDirection + { + get { return this.sortDirection; } + set { this.sortDirection = value; } + } + + public SortPropertyEnum SortProperty + { + get { return this.sortProperty; } + set { this.sortProperty = value; } + } + + #endregion + + + #region IComparer Members + + public int Compare(AIS_Target x, AIS_Target y) + { + switch (this.sortProperty) + { + case SortPropertyEnum.NONE: + return 0; + case SortPropertyEnum.NAME: + { + string xName = x.LastDBName; + if (xName == null) xName = ""; + string yName = y.LastDBName; + if (yName == null) yName = ""; + if (this.sortDirection == ListSortDirection.Ascending) + return xName.CompareTo(yName); + else + return yName.CompareTo(xName); + } + case SortPropertyEnum.CALLSIGN: + { + string xCallsign = x.Callsign; + if (xCallsign == null) xCallsign = ""; + string yCallsign = y.Callsign; + if (yCallsign == null) yCallsign = ""; + if (this.sortDirection == ListSortDirection.Ascending) + return xCallsign.CompareTo(yCallsign); + else + return yCallsign.CompareTo(xCallsign); + } + case SortPropertyEnum.LASTUPDATE: + { + DateTime xTime = x.LastUpdate ?? DateTime.MinValue; + DateTime yTime = y.LastUpdate ?? DateTime.MinValue; + if (this.sortDirection == ListSortDirection.Ascending) + return xTime.CompareTo(yTime); + else + return yTime.CompareTo(xTime); + } + case SortPropertyEnum.MMSI: + { + if (this.sortDirection == ListSortDirection.Ascending) + return x.MMSI.CompareTo(y.MMSI); + else + return y.MMSI.CompareTo(x.MMSI); + } + case SortPropertyEnum.STATION: + { + if (this.sortDirection == ListSortDirection.Ascending) + return x.ReceivedFrom.CompareTo(y.ReceivedFrom); + else + return y.ReceivedFrom.CompareTo(x.ReceivedFrom); + } + + default: + return 0; + } + + + + } + + #endregion + } +} diff --git a/AIS/bsmd.AISService/AIS/AIS_Telnet.cs b/AIS/bsmd.AISService/AIS/AIS_Telnet.cs new file mode 100644 index 00000000..5a8ae60b --- /dev/null +++ b/AIS/bsmd.AISService/AIS/AIS_Telnet.cs @@ -0,0 +1,150 @@ +// +// Class: AIS_Telnet +// Current CLR: 4.0.30319.296 +// System: Microsoft Visual Studio 10.0 +// Author: dani +// Created: 3/16/2013 12:58:03 PM +// +// Copyright (c) 2013 Informatikbüro Daniel Schick. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Net.Sockets; + +namespace bsmd.AISService.AIS +{ + public class AIS_Telnet + { + + #region private fields + + private const int BFSIZE = 1024; + + private NetworkStream tcpStream; + private TcpClient tcpSocket; + private string currentString = ""; + private string hostname; + private int port; + private DateTime? lastRead; + + #endregion + + public AIS_Telnet(string theHostname, int thePort) + { + this.port = thePort; + this.hostname = theHostname; + this.Connect(); + } + + #region Properties + + public bool IsConnected + { + get { return tcpSocket.Connected; } + } + + public string Hostname { get { return this.hostname; } } + + public int Port { get { return this.port; } } + + public string StationName { get; set; } + + #endregion + + #region public methods + + private string ReadCurrentUptoNewline() + { + int newlineIndex = currentString.IndexOf('\n'); + string result = this.currentString.Substring(0, newlineIndex); + if (currentString.Length > (newlineIndex + 1)) + currentString = currentString.Substring(newlineIndex + 1); + else + currentString = ""; + return result; + } + + public string ReadLine() + { + + string result = ""; + if (currentString.IndexOf('\n') >= 0) + return ReadCurrentUptoNewline(); + + + byte[] inputBuffer = new byte[1024]; + + if ((tcpSocket == null) || (!tcpSocket.Connected) || !this.tcpStream.CanRead) + this.Connect(); + if ((tcpSocket == null) || !tcpSocket.Connected) + { + System.Threading.Thread.Sleep(30000); // wait 5 mins if connect is unsuccessful + return result; + } + if (this.tcpStream.DataAvailable) + { + try + { + int bytesRead = this.tcpStream.Read(inputBuffer, 0, 1024); + if (bytesRead > 0) + { + this.lastRead = DateTime.Now; + this.currentString += Encoding.ASCII.GetString(inputBuffer, 0, bytesRead); + if (currentString.IndexOf('\n') >= 0) + return ReadCurrentUptoNewline(); + if (this.currentString.Length > 1024) this.currentString = ""; // truncate to avoid overflow for wrong client data flow + } + } + catch (Exception ex) + { + System.Diagnostics.Trace.WriteLine(string.Format("exception reading from tcp stream: {0}", ex.Message)); + result = ""; + } + } + else + { + // wenn die Verbindung wegkracht ist immer noch connected true, aber DataAvailable false + // es gibt anscheinend keinen richtig guten Workaround. Hard case: Nach einer Stunde Inaktivität schließt der Client hier die + // Verbindung und versucht reconnects. Das bekommt der LS100PortProxy aber nicht immer mit.. Folge sind dann die "stehengebliebenen" + // Verbindungen + if (lastRead == null) lastRead = DateTime.Now; + if ((DateTime.Now - lastRead.Value).TotalSeconds > 600) + { + this.tcpSocket.Close(); + this.tcpSocket = null; + System.Diagnostics.Trace.WriteLine("closing inactive TcpClient"); + this.lastRead = DateTime.Now; // reset timer + } + } + + return result; + } + + public void Close() + { + if (this.tcpStream != null) this.tcpStream.Close(); + this.tcpSocket.Close(); + this.tcpStream.Dispose(); + } + + #endregion + + public void Connect() + { + try + { + if ((this.tcpSocket != null) && (this.tcpSocket.Connected)) return; + this.tcpSocket = new TcpClient(this.hostname, this.port); + this.tcpStream = tcpSocket.GetStream(); + System.Diagnostics.Trace.WriteLine(string.Format("TCP stream connected ({0}:{1})", this.hostname, this.port)); + } + catch (Exception ex) + { + System.Diagnostics.Trace.WriteLine( + string.Format("AIS_Telnet: cannot connect to ({0}:{1}) : {2}", this.hostname, this.port, ex.Message)); + } + } + } +} diff --git a/AIS/bsmd.AISService/AIS/NMEA.cs b/AIS/bsmd.AISService/AIS/NMEA.cs new file mode 100644 index 00000000..71b74f2d --- /dev/null +++ b/AIS/bsmd.AISService/AIS/NMEA.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace bsmd.AISService.AIS +{ + internal abstract class NMEA + { + protected string type = ""; + protected string data; + protected string[] elements = null; + + public enum Status + { + OK, + UNKNOWN_TYPE, + CHECKSUM, + ILLEGAL_ARGUMENT + } + + protected abstract void Decode(); + + public static NMEA Decode(string data, ref Status status) + { + try + { + if (data == null) + { + status = Status.ILLEGAL_ARGUMENT; + return null; + } + + if (data[0] != '$' && data[0] != '!') + { + status = Status.ILLEGAL_ARGUMENT; + return null; // no NMEA sentence + } + + string[] elements = data.Trim().Substring(1).Split(','); + + NMEA sentence = NMEA.CreateNMEAElement(elements[0]); + if (sentence == null) + { + status = Status.UNKNOWN_TYPE; + return null; + } + + sentence.elements = elements; + sentence.data = data.Trim(); ; + + if (!sentence.IsChecksumOK) + { + status = Status.CHECKSUM; + return null; + } + + sentence.Decode(); + + return sentence; + } + catch (Exception ex) + { + System.Diagnostics.Trace.WriteLine(string.Format("Error decoding sentence: {0}, {1}", ex.Message, ex.StackTrace)); + return null; + } + } + + /// + /// Factory method for nmea types + /// + protected static NMEA CreateNMEAElement(string type) + { + NMEA result = null; + + switch (type.ToUpper()) + { + case "AIVDM": + result = new NMEA_AIS_Sentence(); + break; + + case "PNMLS": + result = new NMEA_PNMLS_Sentence(); + break; + + default: + break; + } + + if (result != null) + result.type = type.ToUpper(); + + return result; + } + + protected bool IsChecksumOK + { + get + { + return this.data.Substring(this.data.IndexOf('*') + 1) == this.CalculateChecksum(); + + } + } + + private string CalculateChecksum() + { + int checksum = Convert.ToByte(this.data[1]); + for (int i = 2; i < this.data.IndexOf('*'); i++) + { + checksum ^= Convert.ToByte(this.data[i]); + } + return checksum.ToString("X2"); + } + + + } +} diff --git a/AIS/bsmd.AISService/AIS/NMEA_AIS_Sentence.cs b/AIS/bsmd.AISService/AIS/NMEA_AIS_Sentence.cs new file mode 100644 index 00000000..9b916451 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/NMEA_AIS_Sentence.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace bsmd.AISService.AIS +{ + internal class NMEA_AIS_Sentence : NMEA + { + private int total_sentence_nr; + private int msg_sentence_nr; + private int? seq_message_ident; + private string ais_channel_nr; + private string ais_message; + private int fillbits; + + #region Properties + + /// + /// 1-based total number of sentences for this ais message + /// + public int Total_Sentence_Nr + { + get { return this.total_sentence_nr; } + } + + /// + /// 1-based fragment number of sentences + /// + public int Msg_Sentence_Nr + { + get { return this.msg_sentence_nr; } + } + + /// + /// sequential message id for multi-sentence messages (can be empty) + /// + public int? Seq_Message_Ident + { + get { return this.seq_message_ident; } + } + + /// + /// 'A' = 161.975Mhz (87B), + /// 'B' = 162.025Mhz (88B) + /// + public string AIS_Channel_nr + { + get { return this.ais_channel_nr; } + } + + /// + /// AIS message data + /// + public string AIS_Message + { + get { return this.ais_message; } + } + + public int FillBits + { + get { return this.fillbits; } + } + + #endregion + + protected override void Decode() + { + this.total_sentence_nr = Convert.ToInt32(this.elements[1]); + this.msg_sentence_nr = Convert.ToInt32(this.elements[2]); + if (this.elements[3].Length > 0) + this.seq_message_ident = Convert.ToInt32(this.elements[3]); + this.ais_channel_nr = this.elements[4]; + this.ais_message = this.elements[5]; + try + { + string fillbits_string = this.elements[6].Substring(0, this.elements[6].IndexOf('*')); + if(!Int32.TryParse(fillbits_string, out this.fillbits)) + System.Diagnostics.Trace.WriteLine("AIS_Sentence.Decode(): fillbits are no integer"); + } + catch (ArgumentOutOfRangeException) + { + System.Diagnostics.Trace.WriteLine("AIS_Sentence.Decode(): split() problem, trouble decoding fillbits"); + } + } + } +} diff --git a/AIS/bsmd.AISService/AIS/NMEA_PNMLS_Sentence.cs b/AIS/bsmd.AISService/AIS/NMEA_PNMLS_Sentence.cs new file mode 100644 index 00000000..65a61dea --- /dev/null +++ b/AIS/bsmd.AISService/AIS/NMEA_PNMLS_Sentence.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace bsmd.AISService.AIS +{ + /// + /// NMEA PNMLS sentence + /// sentence shows signal level for preceding message + /// + class NMEA_PNMLS_Sentence : NMEA + { + private int signal_level; + private int detection_threshold; + private int interval; + + #region Properties + + public int Signal_Level + { + get { return this.signal_level; } + } + + public int Detection_Threshold + { + get { return this.detection_threshold; } + } + + public int Interval + { + get { return this.interval; } + } + + #endregion + + #region decode func + + protected override void Decode() + { + try + { + this.signal_level = Convert.ToInt32(this.elements[1]); + this.detection_threshold = Convert.ToInt32(this.elements[2]); + + string interval_string = this.elements[3].Substring(0, this.elements[3].IndexOf('*')); + this.interval = Convert.ToInt32(interval_string); + } + catch (FormatException) + { + Trace.WriteLine("NMEA [PNMLS] input format error"); + } + } + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/AIS/SerialDataHandler.cs b/AIS/bsmd.AISService/AIS/SerialDataHandler.cs new file mode 100644 index 00000000..9e0395d8 --- /dev/null +++ b/AIS/bsmd.AISService/AIS/SerialDataHandler.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; + +namespace bsmd.AISService.AIS +{ + public class SerialDataHandler + { + private Serial_IO serial_IO; + private AIS_Decoder decoder; + + public SerialDataHandler(Serial_IO io, AIS_Decoder decoder) + { + this.serial_IO = io; + this.decoder = decoder; + this.serial_IO.LineRead += new Serial_IO.LineReadHandler(serial_IO_LineRead); + } + + public Serial_IO Serial_IO + { + get { return this.serial_IO; } + } + + public AIS_Decoder AIS_Decoder + { + get { return this.decoder; } + } + + public bool Start(ref string message) + { + return this.serial_IO.Open(ref message); + } + + public void Stop() + { + this.serial_IO.Close(); + } + + protected void serial_IO_LineRead(string data) + { + NMEA.Status nmea_Status = NMEA.Status.OK; + if (data == null || data.Length == 0) return; + + NMEA decodedSentence = NMEA.Decode(data, ref nmea_Status); + if (decodedSentence != null) + { + if (decodedSentence is NMEA_AIS_Sentence) + { + NMEA_AIS_Sentence aisSentence = decodedSentence as NMEA_AIS_Sentence; + this.decoder.Decode(aisSentence.AIS_Message, aisSentence.Msg_Sentence_Nr, + aisSentence.Total_Sentence_Nr, aisSentence.Seq_Message_Ident, this.Serial_IO.StationName); + } + } + else + { + Trace.WriteLine("Serial data handler: NMEA decoder returned null sentence"); + } + } + + public override string ToString() + { + return string.Format("Serial AIS Receiver {0} on {1}", + this.serial_IO.StationName, + this.serial_IO.ComPort); + } + + } +} diff --git a/AIS/bsmd.AISService/AIS/Serial_IO.cs b/AIS/bsmd.AISService/AIS/Serial_IO.cs new file mode 100644 index 00000000..05817b3c --- /dev/null +++ b/AIS/bsmd.AISService/AIS/Serial_IO.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO.Ports; +using System.Threading; + +namespace bsmd.AISService.AIS +{ + public class Serial_IO + { + #region private fields + private string stationName; + private SerialPort port; + private bool runReader = true; + private Thread readerThread = null; + #endregion + + // event fired if input line is available + public delegate void LineReadHandler(string data); + public event LineReadHandler LineRead; + + + public Serial_IO() + { + this.port = new SerialPort(); + } + + public bool Open(ref string message) + { + bool retval = true; + try + { + this.port.Open(); + } + catch (Exception ex) + { + message = ex.Message; + retval = false; + } + if (retval) + { + this.readerThread = new Thread(new ThreadStart(this.Read)); + this.runReader = true; + this.readerThread.Start(); + } + return retval; + } + + public void Close() + { + this.runReader = false; + if(readerThread != null) + if(readerThread.ThreadState == ThreadState.Running) + this.readerThread.Join(); + if (this.port.IsOpen) + { + this.port.BaseStream.Flush(); + this.port.Close(); + } + } + + public string[] GetComPorts() + { + return SerialPort.GetPortNames(); + } + + #region Properties + + public int BaudRate + { + get { return this.port.BaudRate; } + set { this.port.BaudRate = value; } + } + + public string ComPort + { + get { return this.port.PortName; } + set { this.port.PortName = value; } + } + + public string StationName + { + get { return this.stationName; } + set { this.stationName = value; } + } + + #endregion + + #region protected methods + + protected void Read() + { + while (runReader) + { + try + { + string line = this.port.ReadLine(); + this.OnInputLineRead(line); + //System.Diagnostics.Trace.WriteLine(line); + } + catch (Exception) { } + } + } + + protected void OnInputLineRead(string line) + { + if (this.LineRead != null) + this.LineRead(line); + } + + #endregion + + } + + + +} diff --git a/AIS/bsmd.AISService/AIS/TelnetDataHandler.cs b/AIS/bsmd.AISService/AIS/TelnetDataHandler.cs new file mode 100644 index 00000000..686c737d --- /dev/null +++ b/AIS/bsmd.AISService/AIS/TelnetDataHandler.cs @@ -0,0 +1,112 @@ +// +// Class: TelnetDataHandler +// Current CLR: 4.0.30319.296 +// System: Microsoft Visual Studio 10.0 +// Author: dani +// Created: 3/16/2013 2:12:35 PM +// +// Copyright (c) 2013 Informatikbüro Daniel Schick. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Diagnostics; + +namespace bsmd.AISService.AIS +{ + public class TelnetDataHandler + { + AIS_Telnet aisTelnet; + AIS_Decoder decoder; + Thread readerThread; + bool requestStop; + + public TelnetDataHandler(AIS_Telnet telnetConnection, AIS_Decoder aisDecoder) + { + this.aisTelnet = telnetConnection; + this.decoder = aisDecoder; + } + + public AIS_Decoder AIS_Decoder + { + get { return this.decoder; } + } + + public override string ToString() + { + return string.Format("Telnet AIS Receiver {0}:{1}", + this.aisTelnet.Hostname, + this.aisTelnet.Port); + } + + public bool Start(ref string message) + { + if (readerThread != null) return true; // already running + try + { + this.readerThread = new Thread(new ThreadStart(this.ReaderThread)); + readerThread.Start(); + this.requestStop = false; + message = "reader thread started"; + return true; + } + catch (Exception ex) + { + message = ex.Message; + return false; + } + } + + public void Stop() + { + if (readerThread.IsAlive) + { + this.requestStop = true; + readerThread.Join(); + } + this.readerThread = null; + } + + private void ReaderThread() + { + NMEA.Status nmea_Status = NMEA.Status.OK; + System.Diagnostics.Trace.WriteLine("starting telnet reader thread"); + while (!requestStop) + { + try + { + + string data = this.aisTelnet.ReadLine(); + // Trace.WriteLine(data); + if (data != null && data.Length > 0) + { + NMEA decodedSentence = NMEA.Decode(data, ref nmea_Status); + if (decodedSentence != null) + { + if (decodedSentence is NMEA_AIS_Sentence) + { + NMEA_AIS_Sentence aisSentence = decodedSentence as NMEA_AIS_Sentence; + this.decoder.Decode(aisSentence.AIS_Message, aisSentence.Msg_Sentence_Nr, + aisSentence.Total_Sentence_Nr, aisSentence.Seq_Message_Ident, this.aisTelnet.StationName); + } + } + else + { + Trace.WriteLine("Serial data handler: NMEA decoder returned null/empty sentence"); + } + } + } + catch (Exception ex) + { + var st = new StackTrace(ex, true); + var frame = st.GetFrame(0); + var line = frame.GetFileLineNumber(); + Trace.WriteLine(string.Format("Exception in telnet reader thread: {0}, top frame ln {1}", ex.Message, line)); + Trace.WriteLine(ex.StackTrace); + } + Thread.Sleep(100); + } + aisTelnet.Close(); + } + } +} diff --git a/AIS/bsmd.AISService/AISService.Designer.cs b/AIS/bsmd.AISService/AISService.Designer.cs new file mode 100644 index 00000000..6285cc2e --- /dev/null +++ b/AIS/bsmd.AISService/AISService.Designer.cs @@ -0,0 +1,37 @@ +namespace bsmd.AISService +{ + partial class AISService + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + this.ServiceName = "Service1"; + } + + #endregion + } +} diff --git a/AIS/bsmd.AISService/AISService.cs b/AIS/bsmd.AISService/AISService.cs new file mode 100644 index 00000000..8f8444b9 --- /dev/null +++ b/AIS/bsmd.AISService/AISService.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2008-2018 schick Informatik +// Description: +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Linq; +using System.ServiceProcess; +using System.Text; +using System.Threading.Tasks; + +using bsmd.AISService.AIS; +using bsmd.AISService.DB; + +using log4net; +using System.IO; + +namespace bsmd.AISService +{ + public partial class AISService : ServiceBase + { + private const string config_filename = "ais_config.xml"; + private ILog _log = LogManager.GetLogger(typeof(AISService)); + private AIS_QueueManager qManager; + + public AISService() + { + Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory); + InitializeComponent(); + } + + protected override void OnStart(string[] args) + { + string errorMessage = ""; + + this.EventLog.Source = this.ServiceName; + this.EventLog.Log = "Application"; + this.Init(args); + if (qManager.Start(ref errorMessage)) + { + this.EventLog.WriteEntry("BSMD AIS Service started.", EventLogEntryType.Information); + System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly(); + FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location); + string version = fvi.FileVersion; + _log.InfoFormat("Starting AIS Service. v.{0} -------------- ", version); + } else + { + _log.ErrorFormat("AIS Service start failed: {0}", errorMessage); + } + + } + + protected override void OnStop() + { + this.qManager.Stop(); + } + + protected void Init(string[] args) + { + AIS_Configuration configuration = AIS_Configuration.Load(config_filename); + + if (configuration == null) + { + Console.WriteLine(string.Format("cannot read configuration {0}", config_filename)); + return; + } + + DBConnector dbConnector = new DBConnector(); + dbConnector.ConnectionString = configuration.DBConnectionString; + if (!dbConnector.Open()) + { + Console.WriteLine("Error connecting to database"); + return; + } + + List stationList = AISStation.LoadStations(dbConnector); + + this.qManager = new AIS_QueueManager(configuration, AISStation.CreateSerial_IOs(stationList), AISStation.CreateAIS_Telnets(stationList)); + qManager.DBUpdateRequired += new AIS_QueueManager.AISQueueChangedHandler(dbConnector.Update); + qManager.AISQueueChanged += new AIS_QueueManager.AISQueueChangedHandler(aisDecoder_AISMessageReceived); + } + + protected void aisDecoder_AISMessageReceived(AIS_Target target) + { + Console.WriteLine(string.Format("{0}: {1} Pos:{2} {3} at {4}", target.Station, target.Name, target.Latitude, target.Longitude, target.LastUpdate)); + } + + + } +} diff --git a/AIS/bsmd.AISService/App.config b/AIS/bsmd.AISService/App.config new file mode 100644 index 00000000..0ab7508f --- /dev/null +++ b/AIS/bsmd.AISService/App.config @@ -0,0 +1,18 @@ + + + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/AIS/bsmd.AISService/DB/AISPosReport.cs b/AIS/bsmd.AISService/DB/AISPosReport.cs new file mode 100644 index 00000000..ab6e6bc7 --- /dev/null +++ b/AIS/bsmd.AISService/DB/AISPosReport.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Text; + +using bsmd.AISService.AIS; + +namespace bsmd.AISService.DB +{ + + internal class AISPosReport + { + /// + /// Saves a (class A or B) position report + /// + /// target to save + /// id of insert operation (to update hotposition table) + public static int? Save(AIS_Target target, DBConnector con, AISStation aisStation) + { + if (target.LastPosReport == null) return null; + + if (target.LastPosReport is AIS_PosReport) + { + // Trace.WriteLine("saving class A pos report"); + AIS_PosReport pr = target.LastPosReport as AIS_PosReport; + + if (aisStation != null) + { + aisStation.UpdateWithPositionReport(pr.MMSI, pr.Latitude, pr.Longitude, pr.Timestamp); + aisStation.LastPosTimestamp = pr.Timestamp; + aisStation.OnAir = true; + } + + string query = string.Format("INSERT INTO aisposreport (mmsi, navstatus, rot, cog, sog, accur, longitude, latitude, heading, timestamp, stationid) VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, '{9}', {10})", + pr.MMSI, pr.NavStatusVal, pr.ROTVal, pr.COGVal, pr.SOGVal, pr.Accuracy, + pr.LongitudeVal, pr.LatitudeVal, pr.TrueHeading ?? 511, pr.DBTimestamp, + (aisStation != null) ? aisStation.Id : 0); + + con.ExecuteNonQuery(query); + + object result = con.ExecuteScalar("SELECT LAST_INSERT_ID()"); + if (result == null) return null; + int pid = Convert.ToInt32(result); + return pid; + } + + if (target.LastPosReport is AIS_ClassB) + { + // Trace.WriteLine("saving class B pos report"); + AIS_ClassB pr = target.LastPosReport as AIS_ClassB; + aisStation.UpdateWithPositionReport(pr.MMSI, pr.Latitude, pr.Longitude, pr.Timestamp); + + string query = string.Format("INSERT INTO aisposreport (mmsi, navstatus, rot, cog, sog, accur, longitude, latitude, heading, timestamp, stationid) VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, '{9}', {10})", + pr.MMSI, 0, 0, pr.CogVal, pr.SogVal, 0, pr.LongitudeVal, pr.LatitudeVal, + pr.TrueHeading ?? 511, pr.DBTimestamp, (aisStation != null) ? aisStation.Id : 0); + + con.ExecuteNonQuery(query); + + object result = con.ExecuteScalar("SELECT LAST_INSERT_ID()"); + if (result == null) return null; + int pid = Convert.ToInt32(result); + return pid; + } + + if (target.LastPosReport is AIS_ClassBExt) + { + Trace.WriteLine("AIS class B ext not supported (yet)"); + // TODO: Import ClassB Extended report! + + } + + Trace.WriteLine(string.Format("save pos report: we should not be here.. class type: {0}", target)); + + return null; + } + + + } +} diff --git a/AIS/bsmd.AISService/DB/AISStaticData.cs b/AIS/bsmd.AISService/DB/AISStaticData.cs new file mode 100644 index 00000000..7bcc3cce --- /dev/null +++ b/AIS/bsmd.AISService/DB/AISStaticData.cs @@ -0,0 +1,521 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Data; +using System.ComponentModel; +using System.Diagnostics; + +using bsmd.AISService.AIS; + +namespace bsmd.AISService.DB +{ + + public class AISStaticData + { + + public enum DisplayStringType + { + NAME, + MMSI, + CALLSIGN + } + + private const string LoadDBShipsQuery = "SELECT aisposreport.MMSI, aisposreport.timestamp, aisposreport.latitude, aisposreport.longitude, aisposreport.stationid, aisposreport.cog, aisposreport.heading, aisposreport.navstatus, aisstaticdata.callsign, aisstaticdata.name, aisstaticdata.shiptype, aisstaticdata.classb, aisstaticdata.shipdescription FROM aisstaticdata JOIN hotposition ON aisstaticdata.mmsi = hotposition.mmsi JOIN aisposreport ON aisposreport.id = hotposition.pid ORDER BY aisstaticdata.name"; + + #region Fields + + private int mmsi; + private string name; + private string callsign; + private DateTime? lastposition; + private double? lastLatitude; + private double? lastLongitude; + private int stationid; + private int navstatus; + private int shiptype; + private string shipdescription; + private int cog; + private int heading; + private bool isClassB; + private bool isWatchkeeper; + private bool isTimedOut = false; + private bool isSelected = false; + private static DisplayStringType displayStringType = DisplayStringType.NAME; + + #endregion + + #region Properties + + public int MMSI + { + get { return this.mmsi; } + } + + public string Name + { + get { return this.name; } + } + + public string Callsign + { + get { return this.callsign; } + } + + public DateTime? LastPositionReport + { + get { return this.lastposition; } + } + + public double? LastLatitude + { + get { return this.lastLatitude; } + } + + public double? LastLongitude + { + get { return this.lastLongitude; } + } + + public bool IsClassB + { + get { return this.isClassB; } + } + + public int ShipType + { + get { return this.shiptype; } + } + + public string Description + { + get { return this.shipdescription; } + } + + public bool IsWatchkeeperShip + { + get { return this.isWatchkeeper; } + set { this.isWatchkeeper = value; } + } + + public int StationId + { + get { return this.stationid; } + } + + public int Heading + { + get { return this.heading; } + } + + public int COG + { + get { return this.cog; } + } + + public int NavStatus + { + get { return this.navstatus; } + } + + public bool Selected + { + get { return this.isSelected; } + set { this.isSelected = value; } + } + + public static DisplayStringType DisplayStringTyp + { + get { return displayStringType; } + set { displayStringType = value; } + } + + public bool IsTimedOut + { + get { return this.isTimedOut; } + set { this.isTimedOut = value; } + } + #endregion + + #region public methods + + public string ToHtmlString() + { + StringBuilder sb = new StringBuilder(); + + sb.Append(string.Format("MMSI: {0}
", this.MMSI)); + sb.Append(string.Format("Name: {0}
", this.Name)); + sb.Append(string.Format("Latitude: {0}°
", this.LastLatitude.HasValue ? this.LastLatitude.Value.ToString("N3") : "?")); + sb.Append(string.Format("Longitude: {0}°
", this.LastLongitude.HasValue ? this.LastLongitude.Value.ToString("N3") : "?")); + sb.Append(string.Format("Last report: {0}
", this.LastPositionReport.HasValue ? this.LastPositionReport.Value.ToString() : "?")); + sb.Append(string.Format("Type: {0} [{1}]
", this.Description, this.ShipType)); + sb.Append(string.Format("Navstatus: {0}
", this.NavStatus)); + + return sb.ToString(); + } + + #endregion + + #region overrides + + public override string ToString() + { + switch (displayStringType) + { + case DisplayStringType.NAME: + string result = "?"; + if (this.name != null && this.name.Length > 0) + result = this.name; + return result; + case DisplayStringType.MMSI: + return this.mmsi.ToString(); + case DisplayStringType.CALLSIGN: + if (this.Callsign == null || this.Callsign.Length == 0) + return "?"; + return this.Callsign; + default: + return string.Format("{0} - {1}", this.name, this.mmsi); + } + } + + #endregion + + #region static methods + + #region save a position report + + /// + /// Saves a (class A or B) position report + /// + /// target to save + /// id of insert operation (to update hotposition table) + public static int? Save(AIS_Target target, DBConnector con, AISStation aisStation) + { + if(target.LastStaticData == null) return null; + + int mmsi = -1; + int id = -1; + + if(target.LastStaticData is AIS_StaticData) { + mmsi = ((AIS_StaticData)target.LastStaticData).MMSI; + } + if(target.LastStaticData is AIS_ClassBStatic) { + mmsi = ((AIS_ClassBStatic)target.LastStaticData).MMSI; + } + + string query = string.Format("SELECT id FROM aisstaticdata WHERE mmsi={0}", mmsi); + object result = con.ExecuteScalar(query); + + if (result != null) // update + { + id = Convert.ToInt32(result); + } + + #region Class A + + if (target.LastStaticData is AIS_StaticData) + { + AIS_StaticData staticData = target.LastStaticData as AIS_StaticData; + + if (id >= 0) + { + if (staticData.ETA.HasValue) + { + query = string.Format("UPDATE aisstaticdata SET imonumber={0}, callsign='{1}', name='{2}', shiptype={3}, typeofdevice='{4}', shipdescription='{5}', eta='{6}', destination='{7}', breadth={8}, length={9}, draught='{10}', stationid={11}, classb=0 WHERE id={12}", + staticData.IMONumber, + staticData.Callsign.Replace("'","''"), + staticData.Name.Replace("'", "''"), + staticData.ShipTypeVal, + staticData.DeviceName, + staticData.ShipType, + staticData.DBETA, + staticData.Destination.Replace("'", "''"), + staticData.Breadth, + staticData.Length, + staticData.Draught, + aisStation.Id, + id); + } + else + { + query = string.Format("UPDATE aisstaticdata SET imonumber={0}, callsign='{1}', name='{2}', shiptype={3}, typeofdevice='{4}', shipdescription='{5}', destination='{6}', breadth={7}, length={8}, draught='{9}', stationid={10}, classb=0 WHERE id={11}", + staticData.IMONumber, + staticData.Callsign.Replace("'", "''"), + staticData.Name.Replace("'", "''"), + staticData.ShipTypeVal, + staticData.DeviceName, + staticData.ShipType, + staticData.Destination.Replace("'", "''"), + staticData.Breadth, + staticData.Length, + staticData.Draught, + aisStation.Id, + id); + + } + con.ExecuteNonQuery(query); + } + else + { + if (staticData.ETA.HasValue) + { + + query = string.Format("INSERT INTO aisstaticdata SET imonumber={0}, callsign='{1}', name='{2}', shiptype={3}, typeofdevice='{4}', shipdescription='{5}', eta='{6}', destination='{7}', breadth={8}, length={9}, draught='{10}', stationid={11}, mmsi={12}, classb=0", + staticData.IMONumber, + staticData.Callsign.Replace("'", "''"), + staticData.Name.Replace("'", "''"), + staticData.ShipTypeVal, + staticData.DeviceName, + staticData.ShipType, + staticData.DBETA, + staticData.Destination.Replace("'", "''"), + staticData.Breadth, + staticData.Length, + staticData.Draught, + aisStation.Id, + staticData.MMSI); + } + else + { + query = string.Format("INSERT INTO aisstaticdata SET imonumber={0}, callsign='{1}', name='{2}', shiptype={3}, typeofdevice='{4}', shipdescription='{5}', destination='{6}', breadth={7}, length={8}, draught='{9}', stationid={10}, mmsi={11}, classb=0", + staticData.IMONumber, + staticData.Callsign.Replace("'", "''"), + staticData.Name.Replace("'", "''"), + staticData.ShipTypeVal, + staticData.DeviceName, + staticData.ShipType, + staticData.Destination.Replace("'", "''"), + staticData.Breadth, + staticData.Length, + staticData.Draught, + aisStation.Id, + staticData.MMSI); + } + + con.ExecuteNonQuery(query); + + id = Convert.ToInt32(con.ExecuteScalar("SELECT LAST_INSERT_ID()")); + + } + + } + + #endregion + + #region Class B + + if (target.LastStaticData is AIS_ClassBStatic) + { + AIS_ClassBStatic staticData = target.LastStaticData as AIS_ClassBStatic; + + if (id >= 0) // Update + { + query = string.Format("UPDATE aisstaticdata SET stationid={0}, shiptype={1}, classb=1", aisStation.Id, staticData.ShipTypeVal); + if(staticData.Callsign != null) query += string.Format(", callsign='{0}'", staticData.Callsign); + if(staticData.Name != null) query += string.Format(", name='{0}'", staticData.Name); + if(staticData.VendorId != null) query += string.Format(", typeofdevice='{0}'", staticData.VendorId); + if(staticData.ShipType != null) query += string.Format(", shipdescription='{0}'", staticData.ShipType); + query += string.Format(" WHERE id={0}", id); + + con.ExecuteNonQuery(query); + } + else // Insert + { + query = string.Format("INSERT INTO aisstaticdata SET callsign='{0}', name='{1}', shiptype={2}, typeofdevice='{3}', shipdescription='{4}', stationid={5}, mmsi={6}, classb=1", + staticData.Callsign, + staticData.Name, + staticData.ShipTypeVal, + staticData.VendorId, + staticData.ShipType, + aisStation.Id, + staticData.MMSI + ); + + con.ExecuteNonQuery(query); + id = Convert.ToInt32(con.ExecuteScalar("SELECT LAST_INSERT_ID()")); + } + } + + #endregion + + return id; + } + + #endregion + + /// + /// Loads shipname for display (until static data has been received) + /// + public static string LoadName(int mmsi, DBConnector con) + { + string query = string.Format("SELECT name FROM aisstaticdata where mmsi={0}", mmsi); + string result = con.ExecuteScalar(query) as string; + if (result == null) result = ""; + return result; + } + + /// + /// Loads callsign for display (until static data has been received) + /// + public static string LoadCallsign(int mmsi, DBConnector con) + { + string query = string.Format("SELECT callsign FROM aisstaticdata where mmsi={0}", mmsi); + string result = con.ExecuteScalar(query) as string; + if (result == null) result = ""; + return result; + } + + /// + /// preload target with data from database until static data has been received + /// + /// target to load + public static void PreloadTarget(AIS_Target target, DBConnector con) + { + if (target.MMSI == 0) return; + string query = string.Format("SELECT name, callsign, shiptype FROM aisstaticdata where mmsi={0}", target.MMSI); + IDataReader reader = con.ExecuteQuery(query); + if (reader.Read()) + { + if (!reader.IsDBNull(0)) target.LastDBName = reader.GetString(0); + if (!reader.IsDBNull(1)) target.Callsign = reader.GetString(1); + if (!reader.IsDBNull(2)) + target.TargetType = AIS_StaticData.GetShipTypeSimple(reader.GetInt32(2)); + } + reader.Close(); + } + + /// + /// Load all ships that have a position and static data from database + /// + public static List LoadDBShips(DBConnector con) + { + List result = new List(); + IDataReader reader = con.ExecuteQuery(AISStaticData.LoadDBShipsQuery); + while (reader.Read()) + { + AISStaticData ship = new AISStaticData(); + ship.mmsi = reader.GetInt32(0); + ship.lastposition = reader.GetDateTime(1); + ship.lastLatitude = (double) Math.Round(reader.GetInt32(2) / 600000.0, 4); + ship.lastLongitude = (double) Math.Round(reader.GetInt32(3) / 600000.0, 4); + ship.stationid = reader.GetInt32(4); + ship.cog = reader.GetInt32(5); + ship.heading = reader.GetInt32(6); + ship.navstatus = reader.GetInt32(7); + if(!reader.IsDBNull(8)) + ship.callsign = reader.GetString(8); + if(!reader.IsDBNull(9)) + ship.name = reader.GetString(9); + ship.shiptype = reader.GetInt32(10); + if (reader.IsDBNull(11)) ship.isClassB = false; + else ship.isClassB = reader.GetBoolean(11); + ship.shipdescription = reader.GetString(12); + result.Add(ship); + } + reader.Close(); + Trace.WriteLine(string.Format("AISStaticData: {0} ships loaded from DB", result.Count)); + return result; + } + + #endregion + + } + + #region Comparer Class for grid + + public class AISStaticData_Comparer : IComparer + { + private SortPropertyEnum sortProperty = SortPropertyEnum.NAME; + private ListSortDirection sortDirection = ListSortDirection.Ascending; + + public enum SortPropertyEnum + { + NONE, + MMSI, + NAME, + CALLSIGN, + LASTUPDATE, + DESCRIPTION + } + + #region Properties + + public ListSortDirection SortDirection + { + get { return this.sortDirection; } + set { this.sortDirection = value; } + } + + public SortPropertyEnum SortProperty + { + get { return this.sortProperty; } + set { this.sortProperty = value; } + } + + #endregion + + public int Compare(AISStaticData x, AISStaticData y) + { + switch (this.sortProperty) + { + case SortPropertyEnum.NONE: + return 0; + case SortPropertyEnum.NAME: + { + string xName = x.Name; + if (xName == null) xName = ""; + string yName = y.Name; + if (yName == null) yName = ""; + if (this.sortDirection == ListSortDirection.Ascending) + return xName.CompareTo(yName); + else + return yName.CompareTo(xName); + } + case SortPropertyEnum.LASTUPDATE: + { + DateTime xTime = x.LastPositionReport ?? DateTime.MinValue; + DateTime yTime = y.LastPositionReport ?? DateTime.MinValue; + if (this.sortDirection == ListSortDirection.Ascending) + return xTime.CompareTo(yTime); + else + return yTime.CompareTo(xTime); + } + case SortPropertyEnum.MMSI: + { + if (this.sortDirection == ListSortDirection.Ascending) + return x.MMSI.CompareTo(y.MMSI); + else + return y.MMSI.CompareTo(x.MMSI); + } + case SortPropertyEnum.CALLSIGN: + { + string xCallsign = x.Callsign; + if (xCallsign == null) xCallsign = ""; + string yCallsign = y.Callsign; + if (yCallsign == null) yCallsign = ""; + if (this.sortDirection == ListSortDirection.Ascending) + return xCallsign.CompareTo(yCallsign); + else + return yCallsign.CompareTo(xCallsign); + } + case SortPropertyEnum.DESCRIPTION: + { + string xDescription = x.Description; + if (xDescription == null) xDescription = ""; + string yDescription = y.Description; + if (yDescription == null) yDescription = ""; + if (this.sortDirection == ListSortDirection.Ascending) + return xDescription.CompareTo(yDescription); + else + return yDescription.CompareTo(xDescription); + } + default: + return 0; + } + + + + } + + #endregion + } +} diff --git a/AIS/bsmd.AISService/DB/AISStation.cs b/AIS/bsmd.AISService/DB/AISStation.cs new file mode 100644 index 00000000..b3d159ea --- /dev/null +++ b/AIS/bsmd.AISService/DB/AISStation.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Data; +using System.Globalization; + +using bsmd.AISService.AIS; + +namespace bsmd.AISService.DB +{ + + public class AISStation + { + #region private members + + private int station_Id; + private string name; + private bool active; + private string comport; + private int baudrate; + private int telnetPort; + private string telnetHost; + private bool onAir = false; + private double rangeMax = 0; + private double rangeAverage; + private double coverage; + private double latitude; + private double longitude; + private string address; + private DateTime? lastPosTimestamp; + private bool isDirty = false; + private Dictionary targets = new Dictionary(); + + #endregion + + #region Properties + + public string Name { get { return this.name; } set { this.name = value; } } + public int Id { get { return this.station_Id; } } + public bool Active { get { return this.active; } set { this.active = value; } } + public string COMPort { get { return this.comport; } set { this.comport = value; } } + public int Baudrate { get { return this.baudrate; } set { this.baudrate = value; } } + public string TelnetHost { get { return this.telnetHost; } set { this.telnetHost = value; } } + public int TelnetPort { get { return this.telnetPort; } set { this.telnetPort = value; } } + public bool OnAir { get { return this.onAir; } set { this.onAir = value; } } + public double RangeMax { get { return this.rangeMax; } } + public double RangeAverage { get { return this.rangeAverage; } } + public double Coverage { get { return this.coverage; } } + public string CoverageText { get { return string.Format("{0} qkm", this.coverage.ToString("N2")); } } + public double Latitude + { + get { return this.latitude; } + set + { + this.latitude = value; + this.isDirty = true; + this.rangeMax = 0; + } + } + public double Longitude + { + get { return this.longitude; } + set + { + this.longitude = value; + this.isDirty = true; + this.rangeMax = 0; + } + } + public string Address { get { return this.address; } } + public bool IsDirty { get { return this.isDirty; } } + + public DateTime? LastPosTimestamp + { + get { return this.lastPosTimestamp; } + set + { + this.lastPosTimestamp = value; + } + } + + public int NumTargets + { + get { return this.targets.Count; } + } + + public Dictionary Targets { get { return this.targets; } } + + public bool MustDelete { get; set; } + + #endregion + + #region public methods + + public bool Save(DBConnector con) + { + string query = string.Format("UPDATE aisstation SET lat={0}, lon={1}, telnetHost='{2}', telnetPort={3}, comPort='{4}', name='{5}', baudrate={6} WHERE id={7}", + (int) (this.latitude * 600000), + (int) (this.longitude * 600000), + this.telnetHost, + this.telnetPort, + this.comport, + this.name, + this.baudrate, + this.station_Id); + + if (con.ExecuteNonQuery(query) == 1) + { + this.isDirty = false; + return true; + } + return false; + } + + public void UpdateWithPositionReport(int mmsi, double lat, double lon, DateTime timestamp) + { + + double distance = bsmd.AISService.AIS.AIS.GetDistance(this.Latitude, this.Longitude, lat, lon); + if (distance > this.rangeMax) + { + this.rangeMax = distance; + this.coverage = Math.PI * this.rangeMax * this.rangeMax; + } + + lock (this.Targets) + { + if (!this.targets.ContainsKey(mmsi)) + this.targets.Add(mmsi, distance); + else this.targets[mmsi] = distance; + } + + // durchschnittl. Reichweite + double sumRange = 0; + foreach (int key in this.targets.Keys) + sumRange += this.targets[mmsi]; + this.rangeAverage = sumRange / (double)this.targets.Count; + + if (!this.lastPosTimestamp.HasValue) this.lastPosTimestamp = timestamp; + else + { + if (this.lastPosTimestamp.Value < timestamp) + this.lastPosTimestamp = timestamp; + } + + } + + /// + /// clear targets and reset coverage + /// + public void ResetStation() + { + this.targets.Clear(); + this.coverage = 0; + this.rangeAverage = 0; + this.rangeMax = 0; + } + + /// + /// deletes this station + /// + public void Delete(DBConnector con) + { + string query = string.Format("DELETE FROM aisstation WHERE id={0}", this.Id); + con.ExecuteNonQuery(query); + } + + #endregion + + #region static methods + + public static List LoadStations(DBConnector con) + { + List result = new List(); + string query = "SELECT id, name, active, lat, lon, address, telnetHost, telnetPort, comPort, baudrate FROM aisstation"; + IDataReader reader = con.ExecuteQuery(query); + if (reader == null) return result; + + while (reader.Read()) + { + AISStation station = new AISStation(); + station.station_Id = reader.GetInt32(0); + station.name = reader.GetString(1); + station.active = reader.GetBoolean(2); + station.latitude = (double) reader.GetInt32(3) / 600000; + station.longitude = (double) reader.GetInt32(4) / 600000; + if(!reader.IsDBNull(5)) + station.address = reader.GetString(5); + if (!reader.IsDBNull(6)) + station.telnetHost = reader.GetString(6); + if (!reader.IsDBNull(7)) + station.telnetPort = reader.GetInt32(7); + if (!reader.IsDBNull(8)) + station.comport = reader.GetString(8); + if (!reader.IsDBNull(9)) + station.baudrate = reader.GetInt32(9); + result.Add(station); + } + reader.Close(); + return result; + } + + public static AISStation CreateStation(string name, DBConnector con) + { + AISStation newStation = new AISStation(); + newStation.name = name; + newStation.active = true; + string query = string.Format("INSERT INTO aisstation SET name='{0}',active=1", + name); + con.ExecuteNonQuery(query); + newStation.station_Id = Convert.ToInt32(con.ExecuteScalar("SELECT LAST_INSERT_ID()")); + + return newStation; + } + + public static List CreateSerial_IOs(List stationList) + { + List result = new List(); + foreach (AISStation station in stationList) + { + if ((station.COMPort != null) && (station.COMPort.Length > 0)) + { + Serial_IO serialIO = new Serial_IO(); + serialIO.BaudRate = (station.Baudrate == 0) ? 9600 : station.Baudrate; + serialIO.ComPort = station.COMPort; + serialIO.StationName = station.Name; + result.Add(serialIO); + } + } + return result; + } + + public static List CreateAIS_Telnets(List stationList) + { + List result = new List(); + foreach (AISStation station in stationList) + { + if ((station.TelnetHost != null) && (station.TelnetHost.Length > 0)) + { + try + { + AIS_Telnet telnet = new AIS_Telnet(station.TelnetHost, station.TelnetPort); + telnet.StationName = station.Name; + result.Add(telnet); + } + catch (Exception ex) + { + System.Diagnostics.Trace.WriteLine(string.Format("AIS_Telnet: cannot connect to host {0} port {1}: {2}", + station.TelnetHost ?? "", station.TelnetPort, ex.Message)); + } + } + } + return result; + } + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/DB/AISWatchkeeper.cs b/AIS/bsmd.AISService/DB/AISWatchkeeper.cs new file mode 100644 index 00000000..0732ce9d --- /dev/null +++ b/AIS/bsmd.AISService/DB/AISWatchkeeper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Data; + +using bsmd.AISService.AIS; + +namespace bsmd.AISService.DB +{ + public class AISWatchkeeper + { + private static string GetAllWatchkeeperShipsQuery = "SELECT mmsi, name, aktiv, watching, tracking FROM wk_ship"; + private int mmsi; + private string name; + private bool aktiv; + private bool watching; + private bool tracking; + + public int MMSI + { + get { return this.mmsi; } + } + + public bool Aktiv + { + get { return this.aktiv; } + } + + public static List GetWatchkeeperShips(DBConnector con) + { + List result = new List(); + IDataReader reader = con.ExecuteQuery(AISWatchkeeper.GetAllWatchkeeperShipsQuery); + if (reader == null) return result; + while (reader.Read()) + { + AISWatchkeeper wkShip = new AISWatchkeeper(); + wkShip.mmsi = reader.GetInt32(0); + wkShip.name = reader.GetString(1); + wkShip.aktiv = reader.GetBoolean(2); + wkShip.watching = reader.GetBoolean(3); + wkShip.tracking = reader.GetBoolean(4); + result.Add(wkShip); + } + reader.Close(); + return result; + } + + + } +} diff --git a/AIS/bsmd.AISService/DB/DBConnector.cs b/AIS/bsmd.AISService/DB/DBConnector.cs new file mode 100644 index 00000000..d0faf89e --- /dev/null +++ b/AIS/bsmd.AISService/DB/DBConnector.cs @@ -0,0 +1,212 @@ +// Copyright (c) 2008-2018 schick Informatik +// Description: Database connector +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Data; +using System.Data.SqlClient; + +using bsmd.AISService.AIS; + +namespace bsmd.AISService.DB +{ + + public class DBConnector + { + private string connectionString; + private SqlConnection dbCon = null; + private List watchkeeperShips = null; + private List dbShips = null; + private Dictionary updateStations = null; + + public DBConnector() { } + + #region Properties + + public string ConnectionString + { + get { return this.connectionString; } + set { this.connectionString = value; } + } + + public List DBShips + { + get + { + if (this.dbShips == null) + { + lock (this.dbCon) + { + this.dbShips = AISStaticData.LoadDBShips(this); + } + } + return this.dbShips; + } + set { this.dbShips = value; } + } + + public List WatchkeeperShips + { + get + { + if (this.watchkeeperShips == null) this.watchkeeperShips = AISWatchkeeper.GetWatchkeeperShips(this); + return this.watchkeeperShips; + } + } + + #endregion + + #region public methods + + public SqlDataReader ExecuteQuery(string query) + { + if (!this.CheckConnection()) return null; + + SqlCommand cmd = new SqlCommand(query, this.dbCon); + return cmd.ExecuteReader(); + } + + public int ExecuteNonQuery(string query) + { + if (!this.CheckConnection()) return 0; + SqlCommand cmd = new SqlCommand(query, this.dbCon); + return cmd.ExecuteNonQuery(); + } + + public object ExecuteScalar(string query) + { + if (!this.CheckConnection()) return 0; + SqlCommand cmd = new SqlCommand(query, this.dbCon); + return cmd.ExecuteScalar(); + } + + public bool Open() + { + if (this.dbCon != null && this.dbCon.State == System.Data.ConnectionState.Open) return true; + try + { + this.dbCon = new SqlConnection(this.connectionString); + this.dbCon.Open(); + if (this.dbCon.State == System.Data.ConnectionState.Open) + return true; + } + catch (SqlException anException) + { + Trace.WriteLine(string.Format("cannot open SQL DB connection: {0}", anException.Message)); + } + return false; + } + + public void Close() + { + try + { + if (this.dbCon != null && this.dbCon.State == System.Data.ConnectionState.Open) + this.dbCon.Close(); + } + catch (Exception) { } // egal + } + + public void Update(AIS_Target target) + { + if (this.dbCon.State != System.Data.ConnectionState.Open) // reopen + { + this.dbCon.Close(); + this.dbCon.Open(); + } + + if (this.updateStations == null) + { + this.updateStations = new Dictionary(); + Trace.WriteLine("loading stations.."); + List stations = AISStation.LoadStations(this); + Trace.WriteLine(string.Format("{0} stations loaded", stations.Count)); + foreach (AISStation station in stations) + if (!updateStations.ContainsKey(station.Name)) + updateStations.Add(station.Name, station); + } + + if (target.LastPosReport != null) + { + Hotposition hotposition = Hotposition.LoadForMMSI(target.MMSI, this); + int? pid = AISPosReport.Save(target, this, updateStations.ContainsKey(target.Station) ? updateStations[target.Station] : null); + if (pid.HasValue) + { + hotposition.PosReportId = pid.Value; + hotposition.Save(this); + } + } + else + { + Trace.WriteLine(string.Format("last pos report is null for target {0}", target.MMSI)); + } + if (target.LastStaticData != null) + { + AISStaticData.Save(target, this, updateStations.ContainsKey(target.Station) ? updateStations[target.Station] : null); + } + if ((target.Name == null || target.LastDBName == null) && (target.MMSI > 0)) + { + // preload values from DB + AISStaticData.PreloadTarget(target, this); + } + + target.UpdateDB = false; // reset update flag + // Watchkeeper check + + if (this.watchkeeperShips == null) + this.watchkeeperShips = AISWatchkeeper.GetWatchkeeperShips(this); + + + if (!target.IsWatchkeeperShip.HasValue && this.watchkeeperShips != null) + { + for (int i = 0; i < this.watchkeeperShips.Count; i++) + { + if (this.watchkeeperShips[i].MMSI == target.MMSI) // found it + target.IsWatchkeeperShip = true; + } + + if (!target.IsWatchkeeperShip.HasValue) // didn't find it + target.IsWatchkeeperShip = false; + } + + + } + + public void ResetWatchkeeperList() + { + this.watchkeeperShips = null; + } + + public void SaveStation(AISStation station) + { + station.Save(this); + } + + #endregion + + #region private methods + + private bool CheckConnection() + { + // if connection has been closed, re-open the connection + if (this.dbCon.State != System.Data.ConnectionState.Open) + { + try + { + this.dbCon.Close(); + this.Open(); + } + catch (SqlException ex) + { + System.Diagnostics.Trace.WriteLine(ex.ToString()); + } + } + return this.dbCon.State == System.Data.ConnectionState.Open; + } + + #endregion + + } +} diff --git a/AIS/bsmd.AISService/DB/Hotposition.cs b/AIS/bsmd.AISService/DB/Hotposition.cs new file mode 100644 index 00000000..25d85d7c --- /dev/null +++ b/AIS/bsmd.AISService/DB/Hotposition.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Data; +using System.Data.SqlClient; +using bsmd.AISService.AIS; + +namespace bsmd.AISService.DB +{ + internal class Hotposition + { + private int id; + private int mmsi; + private int pid; + + public int MMSI + { + get { return this.mmsi; } + } + + public int PosReportId + { + get { return this.pid; } + set { this.pid = value; } + } + + public void Save(DBConnector con) + { + string query = string.Format("UPDATE hotposition SET mmsi={0}, pid={1} WHERE id={2}", + this.mmsi, this.pid, this.id); + con.ExecuteNonQuery(query); + } + + public static Hotposition LoadForMMSI(int mmsi, DBConnector con) + { + List results = new List(); + string query = string.Format("SELECT id, pid FROM hotposition WHERE mmsi={0}", mmsi); + SqlDataReader reader = con.ExecuteQuery(query); + if (reader != null) + { + while (reader.Read()) + { + Hotposition hp = new Hotposition(); + hp.id = reader.GetInt32(0); + hp.mmsi = mmsi; + if (!reader.IsDBNull(1)) + hp.pid = reader.GetInt32(1); + results.Add(hp); + } + reader.Close(); + } + + if (results.Count == 0) + { + // neuen Eintrag erzeugen + Hotposition hp = new Hotposition(); + string insertQuery = string.Format("INSERT INTO hotposition SET mmsi={0}", mmsi); + con.ExecuteNonQuery(insertQuery); + + object ob = con.ExecuteScalar("SELECT LAST_INSERT_ID()"); + hp.id = Convert.ToInt32(ob); + hp.mmsi = mmsi; + return hp; + } + else if (results.Count == 1) + { + return results[0]; + } + else + { + // überschüssige HP's löschen (jeweils nur eins pro MMSI) + for (int i = 1; i < results.Count; i++) + { + string delQuery = string.Format("DELETE FROM hotposition WHERE id={0}", results[i].id); + con.ExecuteNonQuery(delQuery); + } + + return results[0]; + } + } + + } +} diff --git a/AIS/bsmd.AISService/Program.cs b/AIS/bsmd.AISService/Program.cs new file mode 100644 index 00000000..28f96537 --- /dev/null +++ b/AIS/bsmd.AISService/Program.cs @@ -0,0 +1,127 @@ +// Copyright (c) 2008-2018 schick Informatik +// Description: +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration.Install; +using System.Linq; +using System.Reflection; +using System.ServiceProcess; +using System.Text; +using System.Threading.Tasks; + +namespace bsmd.AISService +{ + static class Program + { + /// + /// The main entry point for the application. + /// + public static int Main(string[] args) + { + if (Environment.UserInteractive) + { + if (args.Length > 0) + { + string arg = args[0].ToLowerInvariant().Substring(0, 2); + switch (arg) + { + case "/i": // install + return InstallService(); + + case "/u": // uninstall + return UninstallService(); + + default: // unknown option + Console.WriteLine("Argument not recognized: {0}", args[0]); + Console.WriteLine(string.Empty); + DisplayUsage(); + return 1; + } + } + else + { + DisplayUsage(); + } + } + else + { + ServiceBase[] ServicesToRun; + ServicesToRun = new ServiceBase[] + { + new AISService() + }; + ServiceBase.Run(ServicesToRun); + } + return 0; + } + + private static void DisplayUsage() + { + //.. + } + + private static int InstallService() + { + var service = new AISService(); + + try + { + // perform specific install steps for our queue service. + //service.InstallService(); + + // install the service with the Windows Service Control Manager (SCM) + ManagedInstallerClass.InstallHelper(new string[] { Assembly.GetExecutingAssembly().Location }); + } + catch (Exception ex) + { + if (ex.InnerException != null && ex.InnerException.GetType() == typeof(Win32Exception)) + { + Win32Exception wex = (Win32Exception)ex.InnerException; + Console.WriteLine("Error(0x{0:X}): Service already installed!", wex.ErrorCode); + return wex.ErrorCode; + } + else + { + Console.WriteLine(ex.ToString()); + return -1; + } + } + + return 0; + } + + private static int UninstallService() + { + var service = new AISService(); + + try + { + // perform specific uninstall steps for our queue service + //service.UninstallService(); + + // uninstall the service from the Windows Service Control Manager (SCM) + ManagedInstallerClass.InstallHelper(new string[] { "/u", Assembly.GetExecutingAssembly().Location }); + } + catch (Exception ex) + { + if (ex.InnerException.GetType() == typeof(Win32Exception)) + { + Win32Exception wex = (Win32Exception)ex.InnerException; + Console.WriteLine("Error(0x{0:X}): Service not installed!", wex.ErrorCode); + return wex.ErrorCode; + } + else + { + Console.WriteLine(ex.ToString()); + return -1; + } + } + + return 0; + } + + } +} diff --git a/AIS/bsmd.AISService/Properties/AssemblyInfo.cs b/AIS/bsmd.AISService/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..27dbd0ff --- /dev/null +++ b/AIS/bsmd.AISService/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("bsmd.AISService")] +[assembly: AssemblyDescription("Windows Service zum Einlesen von AIS Daten aus einem TCP/IP Datenstrom")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("schick Informatik")] +[assembly: AssemblyProduct("bsmd.AISService")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a26a6de3-8505-4ec2-9eb5-12e5ebe83b11")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AIS/bsmd.AISService/Properties/Settings.Designer.cs b/AIS/bsmd.AISService/Properties/Settings.Designer.cs new file mode 100644 index 00000000..d3648523 --- /dev/null +++ b/AIS/bsmd.AISService/Properties/Settings.Designer.cs @@ -0,0 +1,35 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +namespace bsmd.AISService.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.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.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string ConnectionString { + get { + return ((string)(this["ConnectionString"])); + } + } + } +} diff --git a/AIS/bsmd.AISService/Properties/Settings.settings b/AIS/bsmd.AISService/Properties/Settings.settings new file mode 100644 index 00000000..ef60b87c --- /dev/null +++ b/AIS/bsmd.AISService/Properties/Settings.settings @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/AIS/bsmd.AISService/bsmd.AISService.csproj b/AIS/bsmd.AISService/bsmd.AISService.csproj new file mode 100644 index 00000000..158fffcc --- /dev/null +++ b/AIS/bsmd.AISService/bsmd.AISService.csproj @@ -0,0 +1,126 @@ + + + + + Debug + AnyCPU + {A26A6DE3-8505-4EC2-9EB5-12E5EBE83B11} + WinExe + Properties + bsmd.AISService + bsmd.AISService + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + bsmdKey.snk + + + + packages\log4net.2.0.8\lib\net45-full\log4net.dll + True + + + + + + + + + + + + + + + + Component + + + AISService.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + Component + + + ProjectInstaller.cs + + + + True + True + Settings.settings + + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + ProjectInstaller.cs + + + + + \ No newline at end of file diff --git a/AIS/bsmd.AISService/bsmd.AISService.licenseheader b/AIS/bsmd.AISService/bsmd.AISService.licenseheader new file mode 100644 index 00000000..fa18f4f0 --- /dev/null +++ b/AIS/bsmd.AISService/bsmd.AISService.licenseheader @@ -0,0 +1,16 @@ +extensions: designer.cs generated.cs +extensions: .cs .cpp .h +// Copyright (c) 2008-2018 schick Informatik +// Description: +// + +extensions: .aspx .ascx +<%-- +Copyright (c) 2008-2018 schick Informatik +--%> +extensions: .vb +'Sample license text. +extensions: .xml .config .xsd + \ No newline at end of file diff --git a/AIS/bsmd.AISService/bsmd.AISService.sln b/AIS/bsmd.AISService/bsmd.AISService.sln new file mode 100644 index 00000000..4b965867 --- /dev/null +++ b/AIS/bsmd.AISService/bsmd.AISService.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25123.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "bsmd.AISService", "bsmd.AISService.csproj", "{A26A6DE3-8505-4EC2-9EB5-12E5EBE83B11}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A26A6DE3-8505-4EC2-9EB5-12E5EBE83B11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A26A6DE3-8505-4EC2-9EB5-12E5EBE83B11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A26A6DE3-8505-4EC2-9EB5-12E5EBE83B11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A26A6DE3-8505-4EC2-9EB5-12E5EBE83B11}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/AIS/bsmd.AISService/bsmdKey.snk b/AIS/bsmd.AISService/bsmdKey.snk new file mode 100644 index 0000000000000000000000000000000000000000..fd20ba2478f6be8546c5c627d54f8c29dcf08704 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096^w~D#ZJcsCVr_mlR{gfkLlChM=o!wmj z_0L-E)hBSQO!7Jt^2!N~i>W4$uhp*Wb1j;`hk&bOKL`)w$Qww}9rO9B|3rVUTee(z zIbyJ1k%+Dr)CC3XYY7;!ifxRX01#k3)IF|f3^VZxq=j5&dw%SX_yDJI*lt2ig@DkeH8aMo%Vm}kdG%dt{jWR=@9d+Y6TkU^kCvqUCPuVRU{CCCh5vzI z(+TIR2?nDe6V}{wEYF1WuvC7DelFTx&DUMHyZ9Q3`EYW3T%-ZYXFCiMt1ug_7 z&-}+RvNuyUl+rWY$USV#^QjLKSa4pDHRXi)n!}3uxq+s_VS+qK!Kg&Fj@Cx#Q z@(8jx!>p(j|L!2KP)SZbUS4{YCezIKr8_ON1mM{?k-}?}i_!VssrhAPyh2#o|BVJ` zip6OpHWtCLqU{y5Ekp28#avr3vC9OH@OuV9s)KrXiU+`$=_9RozK2`;K~v8Ns}`aj z{APJB!8Ip^N6V{+4kXpSao{ldM6{51F_()VrTA7E53;cM6wnOCdH~F$m&^M*y$fh; zYm=X883N!s9!ihtBHCBq%#oCZlR$sJrfbofYP$5j`61XfOx!+m(0kAJ0Zbe#aul?i imspOFTZRS^gusS+bXmWEj+uzsUo$gRK=#>Kqs_M{J|x)y literal 0 HcmV?d00001 diff --git a/AIS/bsmd.AISService/packages.config b/AIS/bsmd.AISService/packages.config new file mode 100644 index 00000000..2b3696ab --- /dev/null +++ b/AIS/bsmd.AISService/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ENI-2/ENI2/ENI2/App.config b/ENI-2/ENI2/ENI2/App.config index 097725ac..2233c98e 100644 --- a/ENI-2/ENI2/ENI2/App.config +++ b/ENI-2/ENI2/ENI2/App.config @@ -26,12 +26,12 @@ 1000 - http://192.168.2.4/LockingService/LockingService.svc - + + http://heupferd/bsmd.LockingService/LockingService.svc - Data Source=192.168.2.12;Initial Catalog=nsw;Uid=dfuser;Pwd=dfpasswd;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False - + + Data Source=(localdb)\Projects;Initial Catalog=nsw;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False diff --git a/Stundensheet.xlsx b/Stundensheet.xlsx index ae7317bb9bdee06409251d745d837418fe200add..fc3530a89fa4d62da48343574d80b1bf5af7be7c 100644 GIT binary patch delta 25432 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 Dm4bwJ delta 25358 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 diff --git a/nsw/Source/bsmd.ExcelReadService/ExcelReader.cs b/nsw/Source/bsmd.ExcelReadService/ExcelReader.cs index 959f44e1..b33d3df0 100644 --- a/nsw/Source/bsmd.ExcelReadService/ExcelReader.cs +++ b/nsw/Source/bsmd.ExcelReadService/ExcelReader.cs @@ -310,8 +310,8 @@ namespace bsmd.ExcelReadService if (val.Length == 2) { - this.Conf.ConfirmText(lookup, val, ReadState.OK); val = val.ToUpper(); + this.Conf.ConfirmText(lookup, val, ReadState.OK); } } return val; diff --git a/nsw/Source/bsmd.database/INFO.cs b/nsw/Source/bsmd.database/INFO.cs index 694e2de7..57140b88 100644 --- a/nsw/Source/bsmd.database/INFO.cs +++ b/nsw/Source/bsmd.database/INFO.cs @@ -196,7 +196,7 @@ namespace bsmd.database public override void Validate(List errors, List violations) { - if ((PortArea.Length >= 2) && (PortArea.Length <= 4)) + if ((PortArea != null) && (PortArea.Length >= 2) && (PortArea.Length <= 4)) { if ((RuleEngine.PortAreaChecker != null) && (this.MessageCore != null)) if (!RuleEngine.PortAreaChecker(this.MessageCore.PoC, this.PortArea)) @@ -204,7 +204,8 @@ namespace bsmd.database } else { - errors.Add(RuleEngine.CreateError(ValidationCode.PORTAREA, "PortArea", this.PortArea, "INFO", "", this.Tablename)); + if(this.MessageCore.PoC != "DEHAM") + errors.Add(RuleEngine.CreateError(ValidationCode.PORTAREA, "PortArea", this.PortArea ?? "", "INFO", "", this.Tablename)); } }