AIS Daten werden schon halbwegs okay empfangen

This commit is contained in:
Daniel Schick 2022-10-09 11:38:21 +02:00
parent ac84f46ae8
commit e98d4ae563
19 changed files with 1888 additions and 32 deletions

View File

@ -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.");
}
}

View File

@ -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;
}
/// <summary>
/// Factory method to create messages based on the message type
/// </summary>
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
/// <summary>
/// mehr dazu hier:
/// http://www.codeguru.com/cpp/cpp/algorithms/general/article.php/c5115/
/// </summary>
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
}
}

View File

@ -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<string> _inputLines;
private readonly ConcurrentQueue<AISClass> _outputAISClasses;
private Thread _thread;
private readonly Dictionary<string, List<AISQueueElement>> fragmentDict = new Dictionary<string, List<AISQueueElement>>();
private const int sleepMS = 250;
private bool _stopFlag = false;
private static readonly ILog _log = LogManager.GetLogger(typeof(AISDecoder));
#endregion
#region construction
public AISDecoder(ConcurrentQueue<string> input, ConcurrentQueue<AISClass> 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
/// <summary>
/// Thread entry
/// </summary>
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<AISQueueElement>();
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
/// <summary>
/// check to see if all fragments are available
/// </summary>
private static bool FragmentsComplete(List<AISQueueElement> 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;
}
/// <summary>
/// assembles message fragments. Care must be taken since fragments can appear
/// out of order
/// </summary>
private static string ConcatenateFragments(List<AISQueueElement> 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
}
}

View File

@ -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<IAISThread> _tasks = new List<IAISThread>();
private static readonly ConcurrentQueue<string> _inputLines = new ConcurrentQueue<string>();
private static readonly ConcurrentQueue<AISClass> _decodedClasses = new ConcurrentQueue<AISClass>();
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);
}
}
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,133 @@
using System;
using System.Collections;
using System.Text;
using System.Diagnostics;
using log4net;
namespace bsmd.AIS2Service
{
/// <summary>
/// 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 ;)
/// </summary>
// 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;
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -4,17 +4,41 @@
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="bsmd.AIS2Service.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,Log4net"/>
</configSections>
<log4net>
<root>
<level value="ALL"/>
<appender-ref ref="LogFileAppender"/>
<appender-ref ref="TraceAppender"/>
</root>
<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="E:\temp\AIS2Service.log"/>
<param name="AppendToFile" value="true"/>
<rollingStyle value="Size"/>
<maxSizeRollBackups value="10"/>
<maximumFileSize value="10MB"/>
<staticLogFileName value="true"/>
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%date [%thread] %-5level [%logger] - %message%newline"/>
</layout>
</appender>
<appender name="TraceAppender" type="log4net.Appender.TraceAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%d [%t] %-5p %c %m%n"/>
</layout>
</appender>
</log4net>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<applicationSettings>
<bsmd.AIS2Service.Properties.Settings>
<setting name="DataSourceHost" serializeAs="String">
<value>192.168.2.24</value>
<value>192.168.2.25</value>
</setting>
<setting name="DataSourcePort" serializeAs="String">
<value>0</value>
<value>32100</value>
</setting>
</bsmd.AIS2Service.Properties.Settings>
</applicationSettings>

View File

@ -22,5 +22,10 @@ namespace bsmd.AIS2Service
/// if this happens the whole show must be stopped
/// </summary>
event EventHandler FatalErrorOccurred;
/// <summary>
/// descriptive name of this thread to use in logging
/// </summary>
string Name { get; }
}
}

View File

@ -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();
/// <summary>
/// public method to decode input data strings into respective NMEA sentences
/// </summary>
/// <param name="data"></param>
/// <param name="status"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// Factory method for nmea types
/// </summary>
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
}
}

View File

@ -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
/// <summary>
/// 1-based total number of sentences for this ais message
/// </summary>
public int Total_Sentence_Nr
{
get { return this.total_sentence_nr; }
}
/// <summary>
/// 1-based fragment number of sentences
/// </summary>
public int Msg_Sentence_Nr
{
get { return this.msg_sentence_nr; }
}
/// <summary>
/// sequential message id for multi-sentence messages (can be empty)
/// </summary>
public string Seq_Message_Ident
{
get { return this.seq_message_ident; }
}
/// <summary>
/// 'A' = 161.975Mhz (87B),
/// 'B' = 162.025Mhz (88B)
/// </summary>
public string AIS_Channel_nr
{
get { return this.ais_channel_nr; }
}
/// <summary>
/// AIS message data
/// </summary>
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
}
}

View File

@ -0,0 +1,60 @@
using System;
namespace bsmd.AIS2Service
{
/// <summary>
/// NMEA PNMLS sentence
/// sentence shows signal level for preceding message
/// </summary>
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
}
}

View File

@ -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
/// </summary>
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);
}
}
}
}

View File

@ -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"]));

View File

@ -3,10 +3,10 @@
<Profiles />
<Settings>
<Setting Name="DataSourceHost" Type="System.String" Scope="Application">
<Value Profile="(Default)">192.168.2.24</Value>
<Value Profile="(Default)">192.168.2.25</Value>
</Setting>
<Setting Name="DataSourcePort" Type="System.UInt32" Scope="Application">
<Value Profile="(Default)">0</Value>
<Value Profile="(Default)">32100</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -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<string> _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<string> 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<string> 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

View File

@ -55,7 +55,18 @@
<Compile Include="AIS2_Service.Designer.cs">
<DependentUpon>AIS2_Service.cs</DependentUpon>
</Compile>
<Compile Include="AISClass.cs" />
<Compile Include="AISDecoder.cs" />
<Compile Include="AISManager.cs" />
<Compile Include="AIS_ClassB.cs" />
<Compile Include="AIS_ClassBExt.cs" />
<Compile Include="AIS_ClassBStatic.cs" />
<Compile Include="AIS_PosReport.cs" />
<Compile Include="AIS_StaticData.cs" />
<Compile Include="IAISThread.cs" />
<Compile Include="NMEA.cs" />
<Compile Include="NMEA_AIS_Sentence.cs" />
<Compile Include="NMEA_PNMLS_Sentence.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Settings.Designer.cs">
@ -73,9 +84,7 @@
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Parser\" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<EmbeddedResource Include="AIS2_Service.resx">
<DependentUpon>AIS2_Service.cs</DependentUpon>