File: MessageRouter.cs
Web Access
Project: ..\..\..\src\Iot.Device.Bindings\Iot.Device.Bindings.csproj (Iot.Device.Bindings)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using Iot.Device.Nmea0183.Sentences;
 
namespace Iot.Device.Nmea0183
{
    /// <summary>
    /// Message routing for NMEA messages. See the Readme for an usage example.
    /// This is used to route messages between different NMEA sources and sinks. Instances of <see cref="FilterRule"/> can be used
    /// to describe which messages need to go from where to where.
    /// </summary>
    public sealed class MessageRouter : NmeaSinkAndSource
    {
        /// <summary>
        /// The name of the local message source. This field is constant.
        /// </summary>
        public const string LocalMessageSource = "LOCAL";
 
        /// <summary>
        /// The name of the default logger. This field is constant.
        /// </summary>
        public const string LoggingSinkName = "LOGGER";
 
        /// <summary>
        /// The AIS manager as NMEA target
        /// </summary>
        public const string AisManager = "AIS";
        private readonly Dictionary<string, NmeaSinkAndSource> _sourcesAndSinks;
        private List<FilterRule> _filterRules;
        private bool _localInterfaceActive;
        private NmeaSinkAndSource _loggingSink;
 
        /// <summary>
        /// Creates a message router, optionally configuring the logging options
        /// </summary>
        /// <param name="loggingConfiguration">(Optional) Logging configuration. This can be used to log all incoming messages to a file.</param>
        public MessageRouter(LoggingConfiguration? loggingConfiguration = null)
        : base(LocalMessageSource)
        {
            _sourcesAndSinks = new Dictionary<string, NmeaSinkAndSource>();
            // Always add ourselves as message source
            _sourcesAndSinks.Add(LocalMessageSource, this);
 
            // Also always add a logging sink
            _loggingSink = new LoggingSink(LoggingSinkName, loggingConfiguration);
            _sourcesAndSinks.Add(LoggingSinkName, _loggingSink);
            if (loggingConfiguration != null)
            {
                // Immediately start logger, unless inactive
                _loggingSink.StartDecode();
            }
 
            _filterRules = new List<FilterRule>();
            _localInterfaceActive = true;
        }
 
        /// <summary>
        /// The list of routing endpoints
        /// </summary>
        public IReadOnlyDictionary<string, NmeaSinkAndSource> EndPoints
        {
            get
            {
                return _sourcesAndSinks;
            }
        }
 
        /// <summary>
        /// Adds a routing end point
        /// </summary>
        /// <param name="parser">The new end point</param>
        /// <returns>True on success, false if an interface with the same name has already been registered.</returns>
        public bool AddEndPoint(NmeaSinkAndSource parser)
        {
            if (!_sourcesAndSinks.ContainsKey(parser.InterfaceName))
            {
                _sourcesAndSinks.Add(parser.InterfaceName, parser);
                parser.OnNewSequence += OnSequenceReceived;
                // Todo: Also monitor errors, should eventually attempt to reconnect
                return true;
            }
 
            return false;
        }
 
        private void OnSequenceReceived(NmeaSinkAndSource source, NmeaSentence sentence)
        {
            // Get name of source for this message
            string name = source.InterfaceName;
 
            foreach (var filter in _filterRules)
            {
                if (filter.SentenceMatch(name, sentence))
                {
                    SendSentenceToFilterItems(source, sentence, filter);
 
                    if (!filter.ContinueAfterMatch)
                    {
                        return;
                    }
                }
            }
        }
 
        private void SendSentenceToFilterItems(NmeaSinkAndSource source, NmeaSentence sentence, FilterRule filter)
        {
            foreach (var sinkName in filter.Sinks)
            {
                if (_sourcesAndSinks.TryGetValue(sinkName, out var sink))
                {
                    // If both source and sink are the local interface, we abort here, as we would be causing a
                    // stack overflow. Note that it is legal to loop messages over Uart interfaces as the TX and RX
                    // line need not be connected to the same physical device. If the receiver is doing the same there,
                    // we'll end up in trouble anyway, though.
                    if (source.InterfaceName == LocalMessageSource && sink.InterfaceName == LocalMessageSource)
                    {
                        continue;
                    }
 
                    if (filter.ForwardingAction != null)
                    {
                        var newMsg = filter.ForwardingAction(source, sink, sentence);
                        if (newMsg != null)
                        {
                            sink.SendSentence(source, newMsg);
                        }
                    }
                    else
                    {
                        sink.SendSentence(source, sentence);
                    }
                }
            }
        }
 
        /// <summary>
        /// Adds a filter rule to the end of the rule set
        /// </summary>
        /// <param name="rule">The rule to add</param>
        /// <exception cref="ArgumentException">The filter rule cannot be added because it is invalid (i.e. an attempt to
        /// add a rule for an inexistent interface was made)</exception>
        public void AddFilterRule(FilterRule rule)
        {
            foreach (var sinkName in rule.Sinks)
            {
                if (!_sourcesAndSinks.ContainsKey(sinkName))
                {
                    throw new ArgumentException($"Rule contains sink {sinkName} which is unknown.");
                }
            }
 
            if (rule.SourceName != "*" && !_sourcesAndSinks.ContainsKey(rule.SourceName))
            {
                throw new ArgumentException($"Cannot define a rule for the unknown source {rule.SourceName}.");
            }
 
            // So we can update the rule list in an atomic operation without requiring a lock
            List<FilterRule> newRules = new List<FilterRule>(_filterRules);
            newRules.Add(rule);
            _filterRules = newRules;
        }
 
        /// <inheritdoc />
        public override void StartDecode()
        {
            _localInterfaceActive = true;
        }
 
        /// <inheritdoc />
        public override void SendSentence(NmeaSinkAndSource source, NmeaSentence sentence)
        {
            if (_localInterfaceActive)
            {
                if (source != this)
                {
                    // We're not the source, so this should use the sink here.
                    DispatchSentenceEvents(source, sentence);
                    return;
                }
 
                // Forward to routing method with ourselves as source
                OnSequenceReceived(this, sentence);
            }
        }
 
        /// <inheritdoc />
        public override void StopDecode()
        {
            _localInterfaceActive = false;
        }
    }
}