From e98d4ae56314418d536e9f08e3f05121c06a6d5f Mon Sep 17 00:00:00 2001 From: Daniel Schick Date: Sun, 9 Oct 2022 11:38:21 +0200 Subject: [PATCH] AIS Daten werden schon halbwegs okay empfangen --- AIS/bsmd.AIS2Service/AIS2_Service.cs | 3 +- AIS/bsmd.AIS2Service/AISClass.cs | 263 +++++++++++++ AIS/bsmd.AIS2Service/AISDecoder.cs | 204 ++++++++++ AIS/bsmd.AIS2Service/AISManager.cs | 41 ++ AIS/bsmd.AIS2Service/AIS_ClassB.cs | 155 ++++++++ AIS/bsmd.AIS2Service/AIS_ClassBExt.cs | 133 +++++++ AIS/bsmd.AIS2Service/AIS_ClassBStatic.cs | 140 +++++++ AIS/bsmd.AIS2Service/AIS_PosReport.cs | 198 ++++++++++ AIS/bsmd.AIS2Service/AIS_StaticData.cs | 368 ++++++++++++++++++ AIS/bsmd.AIS2Service/App.config | 28 +- AIS/bsmd.AIS2Service/IAISThread.cs | 5 + AIS/bsmd.AIS2Service/NMEA.cs | 129 ++++++ AIS/bsmd.AIS2Service/NMEA_AIS_Sentence.cs | 93 +++++ AIS/bsmd.AIS2Service/NMEA_PNMLS_Sentence.cs | 60 +++ AIS/bsmd.AIS2Service/Program.cs | 28 +- .../Properties/Settings.Designer.cs | 4 +- .../Properties/Settings.settings | 4 +- AIS/bsmd.AIS2Service/SerialTCPReader.cs | 49 ++- AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj | 15 +- 19 files changed, 1888 insertions(+), 32 deletions(-) create mode 100644 AIS/bsmd.AIS2Service/AISClass.cs create mode 100644 AIS/bsmd.AIS2Service/AISDecoder.cs create mode 100644 AIS/bsmd.AIS2Service/AISManager.cs create mode 100644 AIS/bsmd.AIS2Service/AIS_ClassB.cs create mode 100644 AIS/bsmd.AIS2Service/AIS_ClassBExt.cs create mode 100644 AIS/bsmd.AIS2Service/AIS_ClassBStatic.cs create mode 100644 AIS/bsmd.AIS2Service/AIS_PosReport.cs create mode 100644 AIS/bsmd.AIS2Service/AIS_StaticData.cs create mode 100644 AIS/bsmd.AIS2Service/NMEA.cs create mode 100644 AIS/bsmd.AIS2Service/NMEA_AIS_Sentence.cs create mode 100644 AIS/bsmd.AIS2Service/NMEA_PNMLS_Sentence.cs diff --git a/AIS/bsmd.AIS2Service/AIS2_Service.cs b/AIS/bsmd.AIS2Service/AIS2_Service.cs index 2b9c767d..6fcc3842 100644 --- a/AIS/bsmd.AIS2Service/AIS2_Service.cs +++ b/AIS/bsmd.AIS2Service/AIS2_Service.cs @@ -29,11 +29,12 @@ namespace bsmd.AIS2Service FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location); string version = fvi.FileVersion; _log.InfoFormat("Starting AIS2 Service. v.{0} -------------- ", version); - + AISManager.Start(); } protected override void OnStop() { + AISManager.Stop(); _log.Info("AIS2 Service stopped."); } } diff --git a/AIS/bsmd.AIS2Service/AISClass.cs b/AIS/bsmd.AIS2Service/AISClass.cs new file mode 100644 index 00000000..76185a1a --- /dev/null +++ b/AIS/bsmd.AIS2Service/AISClass.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace bsmd.AIS2Service +{ + internal abstract class AISClass + { + + #region constant defs + + protected const double ERAD = 6378.135; + protected const double DE2RA = 0.01745329252; + protected const double AVG_ERAD = 6371.0; + + #endregion + + #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 fields + + private AISType _type = AISType.AIS_NONE; + protected int _mmsi; + protected string _data; + + #endregion + + #region Properties + + public AISType MessageType + { + get { return this._type; } + } + + public int MMSI + { + get { return _mmsi; } + } + + #endregion + + #region abstract method signatures + + protected abstract Status Decode(); + + #endregion + + #region static methods + + internal static AISClass Decode(string data, ref Status status) + { + AISClass result = null; + + if (data == null || data.Length == 0) + { + status = Status.ILLEGAL_ARGUMENT; + return null; + } + + BitArray bits = AISClass.DecodeChar(data[0]); + int type = AISClass.GetInt(bits, 0, 5); + + result = AISClass.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 AISClass CreateMessage(int type) + { + AISClass 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 *= DE2RA; + lon1 *= DE2RA; + lat2 *= DE2RA; + lon2 *= DE2RA; + double d = Math.Sin(lat1) * Math.Sin(lat2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Cos(lon1 - lon2); + return (AVG_ERAD * Math.Acos(d)); + } + + + #endregion + + #endregion + + #region overrides + + public override string ToString() + { + return Enum.GetName(typeof(AISClass.AISType), this.MessageType); + } + + #endregion + } +} diff --git a/AIS/bsmd.AIS2Service/AISDecoder.cs b/AIS/bsmd.AIS2Service/AISDecoder.cs new file mode 100644 index 00000000..d7b84809 --- /dev/null +++ b/AIS/bsmd.AIS2Service/AISDecoder.cs @@ -0,0 +1,204 @@ +using log4net; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace bsmd.AIS2Service +{ + internal class AISDecoder : IAISThread + { + + #region fields + + private readonly ConcurrentQueue _inputLines; + private readonly ConcurrentQueue _outputAISClasses; + private Thread _thread; + private readonly Dictionary> fragmentDict = new Dictionary>(); + private const int sleepMS = 250; + private bool _stopFlag = false; + + private static readonly ILog _log = LogManager.GetLogger(typeof(AISDecoder)); + #endregion + + #region construction + + public AISDecoder(ConcurrentQueue input, ConcurrentQueue output) + { + _inputLines = input; + _outputAISClasses = output; + } + + #endregion + + #region class AISQueueElement + + public class AISQueueElement + { + public int seq_nr; + public int total_nr; + public string id; + public string data; + } + + #endregion + + #region private methods + + /// + /// Thread entry + /// + private void ReadData() + { + NMEA.Status status = NMEA.Status.OK; + try + { + while (!_stopFlag) + { + if (_inputLines.TryDequeue(out string line)) + { + NMEA decodedSentence = NMEA.Decode(line, ref status); + if(decodedSentence != null) + { + if(decodedSentence is NMEA_AIS_Sentence aisSentence) + { + if(aisSentence.Total_Sentence_Nr == 1) + { + DecodeData(aisSentence.AIS_Message); + } + else + { + if(aisSentence.Seq_Message_Ident.Length == 0) + { + _log.WarnFormat("message sequence ident is empty, but we have multipart message. Ignoring message"); + } + else + { + if(!fragmentDict.ContainsKey(aisSentence.Seq_Message_Ident)) + fragmentDict[aisSentence.Seq_Message_Ident] = new List(); + fragmentDict[aisSentence.Seq_Message_Ident].Add(new AISQueueElement { data = aisSentence.AIS_Message, id = aisSentence.Seq_Message_Ident, seq_nr = aisSentence.Msg_Sentence_Nr, total_nr = aisSentence.Total_Sentence_Nr }); + if((fragmentDict[aisSentence.Seq_Message_Ident].Count > 1) && FragmentsComplete(fragmentDict[aisSentence.Seq_Message_Ident])) + { + string concatData = ConcatenateFragments(fragmentDict[aisSentence.Seq_Message_Ident]); + fragmentDict.Remove(aisSentence.Seq_Message_Ident); + DecodeData(concatData); + } + } + } + } + else if(decodedSentence is NMEA_PNMLS_Sentence pnmlsSentence) + { + _log.Warn("cannot decode PNMLS sentence at this point"); + } + } + else + { + _log.WarnFormat("NMEA decode failed with {0}", status); + } + } + else + { + Thread.Sleep(sleepMS); + } + } + } + + catch (Exception ex) + { + _log.ErrorFormat("Something bad has happened: {0}", ex.Message); + this.FatalErrorOccurred?.Invoke(this, new EventArgs()); + } + + } + + private void DecodeData(string data) + { + AISClass.Status aisStatus = AISClass.Status.OK; + AISClass decodedClass = AISClass.Decode(data, ref aisStatus); + if(aisStatus == AISClass.Status.OK) + { + _outputAISClasses.Enqueue(decodedClass); + _log.InfoFormat("Enqueuing AIS message for MMSI {0}", decodedClass.MMSI); + } + else + { + _log.WarnFormat("failed to decode AIS data: {0}", aisStatus); + } + } + + #endregion + + #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 + + #region IAISThread implementation + + public event EventHandler FatalErrorOccurred; + + public void Start() + { + if (_thread != null) return; // may not run twice + ThreadStart runReader = new ThreadStart(this.ReadData); + _thread = new Thread(runReader); + _thread.Start(); + } + + public void Stop() + { + if (_thread == null) return; + _stopFlag = true; + _thread.Join(); + _thread = null; + } + + public string Name + { + get { return "AIS decoder"; } + } + + #endregion + + } +} diff --git a/AIS/bsmd.AIS2Service/AISManager.cs b/AIS/bsmd.AIS2Service/AISManager.cs new file mode 100644 index 00000000..0748fbd8 --- /dev/null +++ b/AIS/bsmd.AIS2Service/AISManager.cs @@ -0,0 +1,41 @@ +using log4net; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace bsmd.AIS2Service +{ + internal static class AISManager + { + private static readonly List _tasks = new List(); + private static readonly ConcurrentQueue _inputLines = new ConcurrentQueue(); + private static readonly ConcurrentQueue _decodedClasses = new ConcurrentQueue(); + private static readonly ILog _log = LogManager.GetLogger(typeof(AISManager)); + + public static void Start() + { + _tasks.Add(new SerialTCPReader(Properties.Settings.Default.DataSourceHost, Properties.Settings.Default.DataSourcePort, _inputLines)); + _tasks.Add(new AISDecoder(_inputLines, _decodedClasses)); + + foreach (var task in _tasks) + { + task.Start(); + _log.InfoFormat("{0} started", task.Name); + } + } + + public static void Stop() + { + foreach (var task in _tasks) + { + task.Stop(); + _log.InfoFormat("{0} stopped", task.Name); + } + + } + + } +} diff --git a/AIS/bsmd.AIS2Service/AIS_ClassB.cs b/AIS/bsmd.AIS2Service/AIS_ClassB.cs new file mode 100644 index 00000000..6c3926d9 --- /dev/null +++ b/AIS/bsmd.AIS2Service/AIS_ClassB.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections; +using System.Text; +using System.Diagnostics; +using log4net; + +namespace bsmd.AIS2Service +{ + internal class AIS_ClassB : AISClass + { + #region private members + + private Guid? id; + 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; + + private static ILog _log = LogManager.GetLogger(typeof(AIS_ClassB)); + + #endregion + + #region Properties + + public Guid Id { get { if (!this.id.HasValue) this.id = Guid.NewGuid(); return this.id.Value; } } + + 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 this.timestamp.ToString("yyyy-MM-ddTHH:mm:ss.000Z"); + } + } + + 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; + } + } + + public int Reserved + { + get { return reserved; } + } + + public int Spare + { + get { return this.spare; } + } + + public int Raim + { + get { return this.raimFlag; } + } + + public int CommState + { + get { return this.commState; } + } + + #endregion + + #region abstract method implementation + + protected override Status Decode() + { + BitArray bits = DecodeBinary(this._data); + Status result = Status.OK; + + try + { + int type = GetInt(bits, 0, 5); + if (type != 18) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + this.repeatIndicator = GetInt(bits, 6, 7); + this._mmsi = GetInt(bits, 8, 37); + this.reserved = GetInt(bits, 38, 45); + this.sog = GetInt(bits, 46, 55); + this.accuracy = GetInt(bits, 56, 56); + this.longitude = GetInt(bits, 57, 84); + this.latitude = GetInt(bits, 85, 111); + this.cog = GetInt(bits, 112, 123); + this.trueHeading = GetInt(bits, 124, 132); + this.utcTimestampSecs = GetInt(bits, 133,138); + this.timestamp = DateTime.Now; + this.reservedRegional = GetInt(bits, 139, 140); + this.spare = GetInt(bits, 141, 145); + this.assignedModeFlag = GetInt(bits, 146, 146); + this.raimFlag = GetInt(bits, 147, 147); + this.commStateSelectedFlag = GetInt(bits, 148, 148); + this.commState = GetInt(bits, 149, 167); + } + } + catch (Exception e) + { + _log.WarnFormat("Error decoding AIS class B posreport: {0}", e.Message); + result = Status.PARSE_ERROR; + } + + return result; + } + + #endregion + + } +} diff --git a/AIS/bsmd.AIS2Service/AIS_ClassBExt.cs b/AIS/bsmd.AIS2Service/AIS_ClassBExt.cs new file mode 100644 index 00000000..a4d4b453 --- /dev/null +++ b/AIS/bsmd.AIS2Service/AIS_ClassBExt.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections; +using System.Text; +using System.Diagnostics; +using log4net; + +namespace bsmd.AIS2Service +{ + /// + /// 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 + internal class AIS_ClassBExt : AISClass + { + #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; + + private static ILog _log = LogManager.GetLogger(typeof(AIS_ClassBExt)); + + #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 Status Decode() + { + BitArray bits = DecodeBinary(_data); + Status result = Status.OK; + + try + { + int type = GetInt(bits, 0, 5); + if (type != 19) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + this.repeatIndicator = GetInt(bits, 6, 7); + _mmsi = GetInt(bits, 8, 37); + this.spare1 = GetInt(bits, 38, 45); + this.sog = GetInt(bits, 46, 55); + this.accuracy = GetInt(bits, 56, 56); + this.longitude = GetInt(bits, 57, 84); + this.latitude = GetInt(bits, 85, 111); + this.cog = GetInt(bits, 112, 123); + this.trueHeading = GetInt(bits, 124, 132); + this.utcTimestampSecond = GetInt(bits, 133, 138); + this.timestamp = DateTime.Now; + this.spare2 = GetInt(bits, 139, 142); + + StringBuilder sb_name = new StringBuilder(20); + for (int i = 0; i < 20; i++) + { + int cval = GetInt(bits, 143 + (6 * i), 148 + (6 * i)); + char ch = GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_name.Append(ch); + } + this.name = sb_name.ToString().Trim(); + + this.shipType = GetInt(bits, 263, 270); + this.dimension = GetInt(bits, 271, 300); + this.typeofDevice = GetInt(bits, 301, 304); + this.raimFlag = GetInt(bits, 305, 305); + this.dte = GetInt(bits, 306, 306); + this.assignedMode = GetInt(bits, 307, 307); + this.spare3 = GetInt(bits, 308, 311); + } + } + + catch (Exception e) + { + _log.WarnFormat("Error decoding AIS class B Ext posreport: {0}", e.Message); + result = Status.PARSE_ERROR; + } + + return result; + } + } +} diff --git a/AIS/bsmd.AIS2Service/AIS_ClassBStatic.cs b/AIS/bsmd.AIS2Service/AIS_ClassBStatic.cs new file mode 100644 index 00000000..01be35f8 --- /dev/null +++ b/AIS/bsmd.AIS2Service/AIS_ClassBStatic.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections; +using System.Text; +using log4net; + +namespace bsmd.AIS2Service +{ + internal class AIS_ClassBStatic : AISClass + { + #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; + private static ILog _log = LogManager.GetLogger(typeof(AIS_ClassBStatic)); + + #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; } + } + + public int Dimension { get { return this.dimension; } } + + public int Spare { get { return this.spare; } } + + // Todo: Dimensions.. + + #endregion + + #region abstract method implementation + + protected override Status Decode() + { + BitArray bits = DecodeBinary(_data); + Status result = Status.OK; + + try + { + int type = GetInt(bits, 0, 5); + if (type != 24) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + this.repeatIndicator = GetInt(bits, 6, 7); + _mmsi = GetInt(bits, 8, 37); + this.partNumber = GetInt(bits, 38, 39); + if (this.IsPartA) + { + StringBuilder sb_name = new StringBuilder(20); + for (int i = 0; i < 20; i++) + { + int cval = GetInt(bits, 40 + (6 * i), 45 + (6 * i)); + char ch = GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_name.Append(ch); + } + this.name = sb_name.ToString().Trim(); + } + else + { + this.shipType = GetInt(bits, 40, 47); + + StringBuilder sb_vendor = new StringBuilder(7); + for (int i = 0; i < 7; i++) + { + int cval = GetInt(bits, 48 + (6 * i), 53 + (6 * i)); + char ch = 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 = GetInt(bits, 90 + (6 * i), 95 + (6 * i)); + char ch = GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_callsign.Append(ch); + } + this.callsign = sb_callsign.ToString().Trim(); + this.dimension = GetInt(bits, 141, 161); + this.spare = GetInt(bits, 162, 167); + } + } + } + catch (Exception e) + { + _log.WarnFormat("Error decoding AIS class B static data: {0}", e.Message); + result = Status.PARSE_ERROR; + } + + return result; + } + + #endregion + + } +} diff --git a/AIS/bsmd.AIS2Service/AIS_PosReport.cs b/AIS/bsmd.AIS2Service/AIS_PosReport.cs new file mode 100644 index 00000000..475d4f6e --- /dev/null +++ b/AIS/bsmd.AIS2Service/AIS_PosReport.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections; +using System.Diagnostics; +using log4net; + +namespace bsmd.AIS2Service +{ + internal class AIS_PosReport : AISClass + { + private Guid? id; + 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; + + private static readonly ILog _log = LogManager.GetLogger(typeof(AIS_PosReport)); + + #region Properties + + public Guid Id { get { if (!this.id.HasValue) this.id = Guid.NewGuid(); return this.id.Value; } } + + 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 this.timestamp.ToString("yyyy-MM-ddTHH:mm:ss.000Z"); + } + } + + 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; + } + } + + public int Reserved { get { return this.reserved; } } + + public int Spare { get { return this.spare; } } + + public int Raim { get { return this.raim; } } + + public int CommState { get { return this.commstate; } } + + #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 = DecodeBinary(_data); + + try + { + _mmsi = GetInt(bits, 8, 37); + this.navstatus = GetInt(bits, 38, 41); + this.rot = GetInt(bits, 42, 49); + this.sog = GetInt(bits, 50, 59); + this.accur = GetInt(bits, 60, 60); + this.longitude = GetInt(bits, 61, 88); + this.latitude = GetInt(bits, 89, 115); + this.cog = GetInt(bits, 116, 127); + this.trueheading = GetInt(bits, 128, 136); + this.utcTimeSecond = GetInt(bits, 137, 142); + this.reserved = GetInt(bits, 143, 146); + this.spare = GetInt(bits, 147, 147); + this.raim = GetInt(bits, 148, 148); + this.commstate = GetInt(bits, 149, 167); + } + catch (Exception e) + { + _log.WarnFormat("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.AIS2Service/AIS_StaticData.cs b/AIS/bsmd.AIS2Service/AIS_StaticData.cs new file mode 100644 index 00000000..764d4317 --- /dev/null +++ b/AIS/bsmd.AIS2Service/AIS_StaticData.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.Text; + +using log4net; + +namespace bsmd.AIS2Service +{ + internal class AIS_StaticData : AISClass + { + #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; + + private static readonly ILog _log = LogManager.GetLogger(typeof(AIS_StaticData)); + + #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 int DBShipType { get { return this.shiptype; } } + + public int DBDimension { get { return this.dimension; } } + + public int DBTypeOfDevice { get { return this.typeofdevice; } } + + public int DTE { get { return this.dte; } } + + public int Spare { get { return this.spare; } } + + public string DBETA + { + get + { + if (this.eta.HasValue) + { + return this.eta.Value.ToString("yyyy-MM-ddTHH:mm:ss.000Z"); + } + else + { + return ""; + } + } + } + + #endregion + + #region abstract method implementation + + protected override Status Decode() + { + BitArray bits = DecodeBinary(_data); + Status result = Status.OK; + + if (bits.Count < 424) + { + _log.WarnFormat("AISStaticData truncated: {0}/424", bits.Count); + result = Status.PARSE_ERROR; + } + else + { + try + { + int type = GetInt(bits, 0, 5); + if (type != 5) + { + result = Status.ILLEGAL_ARGUMENT; + } + else + { + _mmsi = GetInt(bits, 6, 37); + this.ais_version = GetInt(bits, 38, 39); + this.imoNumber = GetInt(bits, 40, 69); + + StringBuilder sb_callsign = new StringBuilder(7); + for (int i = 0; i < 7; i++) + { + int cval = GetInt(bits, 70 + (6 * i), 75 + (6 * i)); + char ch = 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 = GetInt(bits, 112 + (6 * i), 117 + (6 * i)); + char ch = GetAISChar(cval); + if (ch == '@') ch = ' '; + sb_name.Append(ch); + } + this.name = sb_name.ToString().Trim(); + + this.shiptype = GetInt(bits, 232, 239); + this.dimension = GetInt(bits, 240, 269); + this.a = GetInt(bits, 240, 248); + this.b = GetInt(bits, 249, 257); + this.c = GetInt(bits, 258, 263); + this.d = GetInt(bits, 264, 269); + this.typeofdevice = GetInt(bits, 270, 273); + this.etamonth = GetInt(bits, 274, 277); + this.etaday = GetInt(bits, 278, 282); + this.etahour = GetInt(bits, 283, 287); + this.etaminute = 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 e) + { + _log.WarnFormat("ETA timestamp creation failed: {0}", e.Message); + } + this.maxpresetstaticdraught = GetInt(bits, 294, 301); + + StringBuilder sb_destination = new StringBuilder(20); + for (int i = 0; i < 20; i++) + { + int cval = GetInt(bits, 302 + (6 * i), 307 + (6 * i)); + char ch = GetAISChar(cval); + if (ch == '@') break; //ch = ' '; // alles nach einem @ nicht mehr beachten + sb_destination.Append(ch); + } + this.destination = sb_destination.ToString().Trim(); + + this.dte = GetInt(bits, 422, 422); + this.spare = GetInt(bits, 423, 423); + + } + } + catch (Exception e) + { + _log.WarnFormat("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 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.AIS2Service/App.config b/AIS/bsmd.AIS2Service/App.config index cf891f51..5aa8e79d 100644 --- a/AIS/bsmd.AIS2Service/App.config +++ b/AIS/bsmd.AIS2Service/App.config @@ -4,17 +4,41 @@
+
+ + + + + + + + + + + + + + + + + + + + + + + - 192.168.2.24 + 192.168.2.25 - 0 + 32100 diff --git a/AIS/bsmd.AIS2Service/IAISThread.cs b/AIS/bsmd.AIS2Service/IAISThread.cs index 1778cae1..2335c127 100644 --- a/AIS/bsmd.AIS2Service/IAISThread.cs +++ b/AIS/bsmd.AIS2Service/IAISThread.cs @@ -22,5 +22,10 @@ namespace bsmd.AIS2Service /// if this happens the whole show must be stopped /// event EventHandler FatalErrorOccurred; + + /// + /// descriptive name of this thread to use in logging + /// + string Name { get; } } } diff --git a/AIS/bsmd.AIS2Service/NMEA.cs b/AIS/bsmd.AIS2Service/NMEA.cs new file mode 100644 index 00000000..9d9f3bde --- /dev/null +++ b/AIS/bsmd.AIS2Service/NMEA.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Text; +using log4net; + +namespace bsmd.AIS2Service +{ + internal abstract class NMEA + { + protected string type = ""; + protected string _data; + protected string[] elements = null; + + protected static ILog _log = LogManager.GetLogger(typeof(NMEA)); + + public enum Status + { + OK, + UNKNOWN_TYPE, + CHECKSUM, + ILLEGAL_ARGUMENT + } + + protected abstract void Decode(); + + /// + /// public method to decode input data strings into respective NMEA sentences + /// + /// + /// + /// + 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) + { + _log.ErrorFormat("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": + case "AIVDO": + result = new NMEA_AIS_Sentence(); + break; + + case "PNMLS": + result = new NMEA_PNMLS_Sentence(); + break; + + default: + _log.WarnFormat("ignoring unsupported NMEA type {0}", type); + break; + } + + if (result != null) + result.type = type.ToUpper(); + + return result; + } + + #region NMEA checksum + + protected bool IsChecksumOK + { + get + { + return _data.Substring(_data.IndexOf('*') + 1) == this.CalculateChecksum(); + } + } + + private string CalculateChecksum() + { + int checksum = Convert.ToByte(_data[1]); + for (int i = 2; i < _data.IndexOf('*'); i++) + { + checksum ^= Convert.ToByte(_data[i]); + } + return checksum.ToString("X2"); + } + + #endregion + + } +} diff --git a/AIS/bsmd.AIS2Service/NMEA_AIS_Sentence.cs b/AIS/bsmd.AIS2Service/NMEA_AIS_Sentence.cs new file mode 100644 index 00000000..5e95993f --- /dev/null +++ b/AIS/bsmd.AIS2Service/NMEA_AIS_Sentence.cs @@ -0,0 +1,93 @@ +using System; + +namespace bsmd.AIS2Service +{ + internal class NMEA_AIS_Sentence : NMEA + { + + #region fields + + private int total_sentence_nr; + private int msg_sentence_nr; + private string seq_message_ident; + private string ais_channel_nr; + private string ais_message; + private int fillbits; + + #endregion + + #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 string 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 + + #region abstract method implementation + + protected override void Decode() + { + this.total_sentence_nr = Convert.ToInt32(this.elements[1]); + this.msg_sentence_nr = Convert.ToInt32(this.elements[2]); + this.seq_message_ident = this.elements[3]; // can be an empty string + 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)) + _log.Warn("AIS_Sentence.Decode(): fillbits are no integer"); + } + catch (ArgumentOutOfRangeException) + { + _log.Warn("AIS_Sentence.Decode(): split() problem, trouble decoding fillbits"); + } + } + + #endregion + + } +} diff --git a/AIS/bsmd.AIS2Service/NMEA_PNMLS_Sentence.cs b/AIS/bsmd.AIS2Service/NMEA_PNMLS_Sentence.cs new file mode 100644 index 00000000..c852bac0 --- /dev/null +++ b/AIS/bsmd.AIS2Service/NMEA_PNMLS_Sentence.cs @@ -0,0 +1,60 @@ +using System; + +namespace bsmd.AIS2Service +{ + /// + /// NMEA PNMLS sentence + /// sentence shows signal level for preceding message + /// + class NMEA_PNMLS_Sentence : NMEA + { + + #region fields + + private int signal_level; + private int detection_threshold; + private int interval; + + #endregion + + #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 abstract method implementation + + 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) + { + _log.Warn("NMEA [PNMLS] input format error"); + } + } + + #endregion + + } +} diff --git a/AIS/bsmd.AIS2Service/Program.cs b/AIS/bsmd.AIS2Service/Program.cs index 97c417fc..a518a447 100644 --- a/AIS/bsmd.AIS2Service/Program.cs +++ b/AIS/bsmd.AIS2Service/Program.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.ServiceProcess; -using System.Text; -using System.Threading.Tasks; +using System.Threading; namespace bsmd.AIS2Service { @@ -14,12 +11,25 @@ namespace bsmd.AIS2Service /// static void Main() { - ServiceBase[] ServicesToRun; - ServicesToRun = new ServiceBase[] + log4net.Config.XmlConfigurator.Configure(); + + if (Debugger.IsAttached) { + AISManager.Start(); + // TODO wait some + Thread.Sleep(60000); + // Test finish.. + AISManager.Stop(); + } + else + { + ServiceBase[] ServicesToRun; + ServicesToRun = new ServiceBase[] + { new AIS2_Service() - }; - ServiceBase.Run(ServicesToRun); + }; + ServiceBase.Run(ServicesToRun); + } } } } diff --git a/AIS/bsmd.AIS2Service/Properties/Settings.Designer.cs b/AIS/bsmd.AIS2Service/Properties/Settings.Designer.cs index 0ccdbc3e..9e308f68 100644 --- a/AIS/bsmd.AIS2Service/Properties/Settings.Designer.cs +++ b/AIS/bsmd.AIS2Service/Properties/Settings.Designer.cs @@ -25,7 +25,7 @@ namespace bsmd.AIS2Service.Properties { [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("192.168.2.24")] + [global::System.Configuration.DefaultSettingValueAttribute("192.168.2.25")] public string DataSourceHost { get { return ((string)(this["DataSourceHost"])); @@ -34,7 +34,7 @@ namespace bsmd.AIS2Service.Properties { [global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("0")] + [global::System.Configuration.DefaultSettingValueAttribute("32100")] public uint DataSourcePort { get { return ((uint)(this["DataSourcePort"])); diff --git a/AIS/bsmd.AIS2Service/Properties/Settings.settings b/AIS/bsmd.AIS2Service/Properties/Settings.settings index e76be480..7da83a50 100644 --- a/AIS/bsmd.AIS2Service/Properties/Settings.settings +++ b/AIS/bsmd.AIS2Service/Properties/Settings.settings @@ -3,10 +3,10 @@ - 192.168.2.24 + 192.168.2.25 - 0 + 32100 \ No newline at end of file diff --git a/AIS/bsmd.AIS2Service/SerialTCPReader.cs b/AIS/bsmd.AIS2Service/SerialTCPReader.cs index 04df3ea2..3a6e05da 100644 --- a/AIS/bsmd.AIS2Service/SerialTCPReader.cs +++ b/AIS/bsmd.AIS2Service/SerialTCPReader.cs @@ -1,6 +1,8 @@ using log4net; using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Sockets; using System.Text; @@ -13,48 +15,62 @@ namespace bsmd.AIS2Service { private readonly string _host; private readonly uint _port; + private readonly ConcurrentQueue _inputQueue; + private bool _stopFlag = false; - private NetworkStream tcpStream; private TcpClient tcpSocket; private Thread _thread; private static readonly ILog _log = LogManager.GetLogger(typeof(SerialTCPReader)); - public SerialTCPReader(string host, uint port) + public SerialTCPReader(string host, uint port, ConcurrentQueue inputQueue) { - _host = host; _port = port; + _host = host; _port = port; _inputQueue = inputQueue; } private void ReadData() { try { - while (true) + while (!_stopFlag) { if (this.tcpSocket == null || !this.tcpSocket.Connected) this.Connect(); - + foreach(string line in ReadLines(this.tcpSocket.GetStream(), Encoding.ASCII)) + { + _inputQueue.Enqueue(line); + if (_stopFlag) return; + } } } catch(Exception ex) { _log.ErrorFormat("Something bad has happened: {0}", ex.Message); - if(this.FatalErrorOccurred != null) - this.FatalErrorOccurred(this, new EventArgs()); + this.FatalErrorOccurred?.Invoke(this, new EventArgs()); } } private void Connect() { - this.tcpSocket = new TcpClient(_host, (int)_port); - this.tcpStream = tcpSocket.GetStream(); + this.tcpSocket = new TcpClient(_host, (int)_port); _log.InfoFormat("TCP stream connected ({0}:{1})", _host, _port); } - + private static IEnumerable ReadLines(Stream source, Encoding encoding) + { + using(StreamReader reader = new StreamReader(source, encoding)) + { + string line; + + while((line = reader.ReadLine()) != null) + { + yield return line; + } + } + } #region IAISThread implementation @@ -65,13 +81,20 @@ namespace bsmd.AIS2Service if (_thread != null) return; // may not run twice ThreadStart runReader = new ThreadStart(this.ReadData); _thread = new Thread(runReader); - - + _thread.Start(); } public void Stop() { - + if(_thread == null) return; + _stopFlag = true; + _thread.Join(); + _thread = null; + } + + public string Name + { + get { return "Serial stream reader"; } } #endregion diff --git a/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj b/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj index 66f045b6..462f2444 100644 --- a/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj +++ b/AIS/bsmd.AIS2Service/bsmd.AIS2Service.csproj @@ -55,7 +55,18 @@ AIS2_Service.cs + + + + + + + + + + + @@ -73,9 +84,7 @@ Settings.Designer.cs - - - + AIS2_Service.cs