/// <summary> /// A standard filter rule. When the filter matches /// </summary> /// <param name="sourceName">Name of the source (Nmea stream name) for which the filter applies or * for all</param> /// <param name="talkerId">TalkerId for which the rule applies or <see cref="TalkerId.Any"/></param> /// <param name="sentenceId">SentenceId for which the rule applies or <see cref="SentenceId.Any"/></param> /// <param name="sinks">Where to send the message when the filter matches</param> /// <param name="rawMessagesOnly">The filter matches raw messages only. This is the default, because otherwise known message /// types would be implicitly duplicated on forwarding</param> /// <param name="continueAfterMatch">True to continue processing after a match, false to stop processing this message</param> public FilterRule(string sourceName, TalkerId talkerId, SentenceId sentenceId, IEnumerable <string> sinks, bool rawMessagesOnly = true, bool continueAfterMatch = false) { _rawMessagesOnly = rawMessagesOnly; SourceName = sourceName; TalkerId = talkerId; SentenceId = sentenceId; Sinks = sinks; ContinueAfterMatch = continueAfterMatch; }
/// <summary> /// A standard filter rule. When the filter matches /// </summary> /// <param name="sourceName">Name of the source (Nmea stream name) for which the filter applies or * for all</param> /// <param name="talkerId">TalkerId for which the rule applies or <see cref="TalkerId.Any"/></param> /// <param name="sentenceId">SentenceId for which the rule applies or <see cref="SentenceId.Any"/></param> /// <param name="sinks">Where to send the message when the filter matches</param> /// <param name="forwardingAction">When the message is about to be sent, this method is called that can modify the /// message. First arg is the source of the message, second the designated sink.</param> /// <param name="rawMessagesOnly">The filter matches raw messages only. This is the default, because otherwise known message /// types would be implicitly duplicated on forwarding</param> /// <param name="continueAfterMatch">True to continue processing after a match, false to stop processing this message</param> public FilterRule(string sourceName, TalkerId talkerId, SentenceId sentenceId, IEnumerable <string> sinks, Func <NmeaSinkAndSource, NmeaSinkAndSource, NmeaSentence, NmeaSentence> forwardingAction, bool rawMessagesOnly = true, bool continueAfterMatch = false) { _rawMessagesOnly = rawMessagesOnly; SourceName = sourceName; TalkerId = talkerId; SentenceId = sentenceId; Sinks = sinks; ForwardingAction = forwardingAction; ContinueAfterMatch = continueAfterMatch; }
/// <summary> /// Gets the last sentence of the given type. /// Does not return sentences that are part of a group (i.e. GSV, RTE) /// </summary> /// <param name="id">Sentence Id to query</param> /// <returns>The last sentence of that type, or null.</returns> public NmeaSentence?GetLastSentence(SentenceId id) { lock (_lock) { if (_sentences.TryGetValue(id, out var sentence)) { return(sentence); } return(null); } }
/// <summary> /// Constructs a message from a typed sentence /// </summary> /// <param name="sentence">Sentence to send. It must be valid</param> public TalkerSentence(NmeaSentence sentence) { TalkerId = sentence.TalkerId; Id = sentence.SentenceId; var content = sentence.ToNmeaParameterList(); if (string.IsNullOrWhiteSpace(content) || sentence.Valid == false) { throw new InvalidOperationException("Input sentence not valid or cannot be encoded"); } _fields = content.Split(new char[] { ',' }, StringSplitOptions.None); }
/// <summary> /// Gets the last sentence of the given type. /// Does not return sentences that are part of a group (i.e. GSV, RTE) /// </summary> /// <param name="id">Sentence Id to query</param> /// <param name="maxAge">Maximum age of the sentence</param> /// <returns>The last sentence of that type, or null if none was received within the given timespan.</returns> public NmeaSentence?GetLastSentence(SentenceId id, TimeSpan maxAge) { lock (_lock) { if (_sentences.TryGetValue(id, out var sentence)) { if (sentence.Age < maxAge) { return(sentence); } } return(null); } }
/// <summary> /// Tries to get a sentence of the given type /// </summary> /// <typeparam name="T">The type of the sentence to query</typeparam> /// <param name="id">The sentence id for T</param> /// <param name="sentence">Receives the sentence, if any was found</param> /// <returns>True on success, false if no such message was received</returns> public bool TryGetLastSentence <T>(SentenceId id, #if NET5_0_OR_GREATER [NotNullWhen(true)] #endif out T sentence) where T : NmeaSentence { var s = GetLastSentence(id); if (s is T) { sentence = (T)s; return(true); } sentence = null !; return(false); }
/// <summary> /// Reads NMEA0183 query sentence from provided string /// </summary> /// <param name="sentence">NMEA0183 query sentence</param> /// <returns>QuerySentence instance</returns> /// <remarks><paramref name="sentence"/> does not include new line characters</remarks> public static QuerySentence FromSentenceString(string sentence) { // http://www.tronico.fi/OH6NT/docs/NMEA0183.pdf // page 3 // $XXYYY ... const int SentenceLength = 6; if (sentence == null) { throw new ArgumentNullException(nameof(sentence)); } if (sentence.Length != SentenceLength) { throw new ArgumentException($"Sentence length must be exactly {SentenceLength} characters", nameof(sentence)); } if (sentence[0] != '$') { throw new ArgumentException("Sentence must start with '$'", nameof(sentence)); } TalkerId requesterId = new TalkerId(sentence[1], sentence[2]); TalkerId deviceId = new TalkerId(sentence[3], sentence[4]); if (sentence[5] != 'Q') { throw new ArgumentException("Valid query sentence must have character 'Q' at index 5"); } if (sentence[6] != ',') { throw new ArgumentException("Valid query sentence must have character ',' at index 6"); } SentenceId requestedSentence = new SentenceId(sentence[7], sentence[8], sentence[9]); return(new QuerySentence(requesterId, deviceId, requestedSentence)); }
/// <summary> /// Constructs NMEA0183 talker identifier /// </summary> /// <param name="talkerId">NMEA0183 talker identifier of the device sending the sentence</param> /// <param name="sentenceId">NMEA0183 sentence identifier</param> /// <param name="fields">fields related to the sentence</param> public TalkerSentence(TalkerId talkerId, SentenceId sentenceId, IEnumerable <string> fields) { TalkerId = talkerId; Id = sentenceId; _fields = fields.ToArray(); }
/// <summary> /// Registers sentence identifier as known. Registered sentences are used by <see cref="TryGetTypedValue"/>. /// </summary> /// <param name="id">NMEA0183 sentence identifier</param> /// <param name="producer">Function which produces typed object given <see cref="TalkerSentence"/>.</param> public static void RegisterSentence(SentenceId id, Func <TalkerSentence, DateTimeOffset, NmeaSentence> producer) { s_registeredSentences[id] = producer; }
/// <summary> /// Reads NMEA0183 talker sentence from provided string /// </summary> /// <param name="sentence">NMEA0183 talker sentence</param> /// <param name="expectedTalkerId">If this is not TalkerId.Any, only messages with this talker id are parsed, /// all others are ignored. This reduces workload if a source acts as repeater, but the repeated messages are not needed.</param> /// <param name="errorCode">Returns an error code, if the parsing failed</param> /// <returns>TalkerSentence instance, or null in case of an error</returns> /// <remarks><paramref name="sentence"/> does not include new line characters</remarks> public static TalkerSentence?FromSentenceString(string sentence, TalkerId expectedTalkerId, out NmeaError errorCode) { // $XXY, ... const int sentenceHeaderMinLength = 4; // http://www.tronico.fi/OH6NT/docs/NMEA0183.pdf page 2 // defines this as 80 + 1 (for $), but we don't really care if it is something within a reasonable limit. const int MaxSentenceLength = 256; if (sentence == null) { throw new ArgumentNullException(nameof(sentence)); } if (sentence.Length < sentenceHeaderMinLength) { errorCode = NmeaError.MessageToShort; return(null); } if (sentence.Length > MaxSentenceLength) { errorCode = NmeaError.MessageToLong; return(null); } if (sentence[0] != '$' && sentence[0] != '!') { // Valid sentences start with $ or ! (for the AIS sentences) errorCode = NmeaError.NoSyncByte; return(null); } TalkerId talkerId = new TalkerId(sentence[1], sentence[2]); if (expectedTalkerId != TalkerId.Any && expectedTalkerId != talkerId) { errorCode = NmeaError.None; return(null); } int firstComma = sentence.IndexOf(',', 1); if (firstComma == -1) { errorCode = NmeaError.MessageToShort; return(null); } string sentenceIdString = sentence.Substring(3, firstComma - 3); SentenceId sentenceId = new SentenceId(sentenceIdString); string[] fields = sentence.Substring(firstComma + 1).Split(','); int lastFieldIdx = fields.Length - 1; // This returns null as the checksum if there was none, or a very big number if the checksum couldn't be parsed (int?checksum, string lastField) = GetChecksumAndLastField(fields[lastFieldIdx]); fields[lastFieldIdx] = lastField; TalkerSentence result = new TalkerSentence(talkerId, sentenceId, fields); if (checksum.HasValue) { byte realChecksum = CalculateChecksumFromSentenceString(sentence.AsSpan()); if (realChecksum != checksum.Value) { errorCode = NmeaError.InvalidChecksum; return(null); } } errorCode = NmeaError.None; return(result); }
/// <summary> /// Constructs NMEA0183 query sentence /// </summary> /// <param name="requesterId">Talker identifier of the requester</param> /// <param name="deviceId">Talker identifier of the device</param> /// <param name="requestedSentence">Requested sentence</param> public QuerySentence(TalkerId requesterId, TalkerId deviceId, SentenceId requestedSentence) { RequesterId = requesterId; DeviceId = deviceId; RequestedSentence = requestedSentence; }