Skip to content

Commit

Permalink
Updates and fixes to NMEA and Seatalk libraries (#2351)
Browse files Browse the repository at this point in the history
* Updates and fixes to NMEA and Seatalk libraries

Intensive real-world testing has been done on
these.

* Fix unit test

* Avoid crash in finalizer

* Handle additional exception

* Add missing documentation
  • Loading branch information
pgrawehr authored Sep 27, 2024
1 parent 3f2c387 commit e78918a
Show file tree
Hide file tree
Showing 26 changed files with 1,122 additions and 64 deletions.
63 changes: 63 additions & 0 deletions src/devices/Common/Iot/Device/Common/PositionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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.Text;

namespace Iot.Device.Common
{
/// <summary>
/// Extensions for positions
/// </summary>
public static partial class PositionExtensions
{
/// <summary>
/// Normalizes the longitude to +/- 180°
/// </summary>
public static GeographicPosition NormalizeAngleTo180(this GeographicPosition position)
{
return new GeographicPosition(position.Latitude, NormalizeAngleTo180(position.Longitude), position.EllipsoidalHeight);
}

/// <summary>
/// Normalizes the angle to +/- 180°
/// </summary>
public static double NormalizeAngleTo180(double angleDegree)
{
angleDegree %= 360;
if (angleDegree <= -180)
{
angleDegree += 360;
}
else if (angleDegree > 180)
{
angleDegree -= 360;
}

return angleDegree;
}

/// <summary>
/// Normalizes the longitude to [0..360°)
/// </summary>
public static GeographicPosition NormalizeAngleTo360(this GeographicPosition position)
{
return new GeographicPosition(position.Latitude, NormalizeAngleTo360(position.Longitude), position.EllipsoidalHeight);
}

/// <summary>
/// Normalizes an angle to [0..360°)
/// </summary>
public static double NormalizeAngleTo360(double angleDegree)
{
angleDegree %= 360;
if (angleDegree < 0)
{
angleDegree += 360;
}

return angleDegree;
}
}
}
75 changes: 75 additions & 0 deletions src/devices/Common/Iot/Device/Common/SimpleFileLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Iot.Device.Common
{
/// <summary>
/// A simple logger that creates textual log files. Created via <see cref="SimpleFileLoggerFactory"/>
/// </summary>
public sealed class SimpleFileLogger : ILogger
{
private readonly string _category;
private TextWriter _writer;

/// <summary>
/// Creates a new logger
/// </summary>
/// <param name="category">Logger category name</param>
/// <param name="writer">The text writer for logging.</param>
/// <remarks>
/// The <paramref name="writer"/> must be a thread-safe file writer!
/// </remarks>
public SimpleFileLogger(string category, TextWriter writer)
{
_category = category;
_writer = writer;
Enabled = true;
}

/// <summary>
/// Used by the factory to terminate all its loggers
/// </summary>
internal bool Enabled
{
get;
set;
}

/// <summary>
/// Does nothing and returns an empty IDisposable
/// </summary>
/// <typeparam name="TState">Current logger state</typeparam>
/// <param name="state">State argument</param>
/// <returns>An empty <see cref="IDisposable"/></returns>
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return new LogDispatcher.ScopeDisposable();
}

/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return Enabled;
}

/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (Enabled)
{
string msg = formatter(state, exception);
var time = DateTime.Now;
_writer.WriteLine($"{time.ToShortDateString()} {time.ToLongTimeString()} - {_category} - {logLevel} - {msg}");
}
}
}
}
72 changes: 72 additions & 0 deletions src/devices/Common/Iot/Device/Common/SimpleFileLoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Iot.Device.Common
{
/// <summary>
/// Provides a very simple console logger that does not require a reference to Microsoft.Extensions.Logging.dll
/// </summary>
public class SimpleFileLoggerFactory : ILoggerFactory, IDisposable
{
private TextWriter? _writer;
private List<SimpleFileLogger> _createdLoggers;

/// <summary>
/// Create a logger factory that creates loggers to logs to the specified file
/// </summary>
/// <param name="fileName">File name to log to (full path)</param>
public SimpleFileLoggerFactory(string fileName)
{
_writer = TextWriter.Synchronized(new StreamWriter(fileName, true, Encoding.UTF8));
_createdLoggers = new List<SimpleFileLogger>();
}

/// <summary>
/// The console logger is built-in here
/// </summary>
/// <param name="provider">Argument is ignored</param>
public void AddProvider(ILoggerProvider provider)
{
}

/// <inheritdoc/>
public ILogger CreateLogger(string categoryName)
{
if (_writer == null)
{
return NullLogger.Instance;
}

var newLogger = new SimpleFileLogger(categoryName, _writer);
_createdLoggers.Add(newLogger);
return newLogger;
}

/// <inheritdoc />
public void Dispose()
{
foreach (var d in _createdLoggers)
{
d.Enabled = false;
}

_createdLoggers.Clear();

if (_writer != null)
{
_writer.Close();
_writer.Dispose();
_writer = null;
}
}
}
}
7 changes: 4 additions & 3 deletions src/devices/Mcp23xxx/Mcp23xxx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,12 @@ protected override void Dispose(bool disposing)
{
_controller?.Dispose();
_controller = null;

_pinValues.Clear();
_bus?.Dispose();
_bus = null!;
}

_pinValues.Clear();
_bus?.Dispose();
_bus = null!;
base.Dispose(disposing);
}

Expand Down
5 changes: 5 additions & 0 deletions src/devices/Nmea0183/NmeaParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ private void Parser()
FireOnParserError(x.Message, NmeaError.PortClosed);
continue;
}
catch (OperationCanceledException x)
{
FireOnParserError(x.Message, NmeaError.PortClosed);
continue;
}

if (currentLine == null)
{
Expand Down
36 changes: 29 additions & 7 deletions src/devices/Nmea0183/NmeaUdpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class NmeaUdpServer : NmeaSinkAndSource
{
private readonly int _localPort;
private readonly int _remotePort;
private readonly string _broadcastAddress;

private UdpClient? _server;
private NmeaParser? _parser;
Expand Down Expand Up @@ -57,10 +58,26 @@ public NmeaUdpServer(string name, int port)
/// <param name="localPort">The port to receive data on</param>
/// <param name="remotePort">The network port to send data to (must be different than local port when communicating to a local process)</param>
public NmeaUdpServer(string name, int localPort, int remotePort)
: this(name, localPort, remotePort, "255.255.255.255")
{
}

/// <summary>
/// Create an UDP server with the given name on the given port, using an alternate outgoing port. The outgoing and incoming
/// port may be equal only if the sender and the receiver are not on the same computer.
/// </summary>
/// <param name="name">The network source name</param>
/// <param name="localPort">The port to receive data on</param>
/// <param name="remotePort">The network port to send data to (must be different than local port when communicating to a local process)</param>
/// <param name="broadcastAddress">Broadcast address of the network interface to use. This is the IP-Address of that interfaces with all
/// bits set to 1 that are NOT set in the subnetmask. For a default subnet mask of 255.255.255.0 and a local ip of 192.168.1.45 this is therefore
/// 192.168.1.255.</param>
public NmeaUdpServer(string name, int localPort, int remotePort, string broadcastAddress)
: base(name)
{
_localPort = localPort;
_remotePort = remotePort;
_broadcastAddress = broadcastAddress;
}

/// <summary>
Expand Down Expand Up @@ -89,8 +106,11 @@ public override void StartDecode()
throw new InvalidOperationException("Server already started");
}

_server = new UdpClient(_localPort);

_server = new UdpClient();
_server.EnableBroadcast = true;
_server.Client.Bind(new IPEndPoint(IPAddress.Any, _localPort));
// byte[] bytes = Encoding.UTF8.GetBytes("Test message\r\n");
// _server.Send(bytes, bytes.Length, "192.168.1.255", _localPort);
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// This is unsupported on MacOS (https://github.com/dotnet/runtime/issues/27653), but this shouldn't
Expand All @@ -110,7 +130,7 @@ public override void StartDecode()
_server.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000);
}

_clientStream = new UdpClientStream(_server, _localPort, _remotePort, this);
_clientStream = new UdpClientStream(_server, _localPort, _remotePort, this, _broadcastAddress);
_parser = new NmeaParser($"{InterfaceName} (Port {_localPort})", _clientStream, _clientStream);
_parser.OnNewSequence += OnSentenceReceivedFromClient;
_parser.OnParserError += ParserOnParserError;
Expand Down Expand Up @@ -179,6 +199,7 @@ private sealed class UdpClientStream : Stream, IDisposable
private readonly int _remotePort;
private readonly NmeaUdpServer _parent;
private readonly Queue<byte> _data;
private readonly string _broadcastAddress;

private object _disposalLock = new object();

Expand All @@ -187,11 +208,12 @@ private sealed class UdpClientStream : Stream, IDisposable
private CancellationTokenSource _cancellationSource;
private CancellationToken _cancellationToken;

public UdpClientStream(UdpClient client, int localPort, int remotePort, NmeaUdpServer parent)
public UdpClientStream(UdpClient client, int localPort, int remotePort, NmeaUdpServer parent, string broadcastAddress)
{
_client = client;
_localPort = localPort;
_remotePort = remotePort;
_broadcastAddress = broadcastAddress;
_parent = parent;
_data = new Queue<byte>();
_knownSenders = new();
Expand Down Expand Up @@ -230,7 +252,7 @@ public override int Read(byte[] buffer, int offset, int count)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
#if NET6_O_OR_GREATER
#if NET6_0_OR_GREATER
var result = _client.ReceiveAsync(_cancellationToken).GetAwaiter().GetResult();
datagram = result.Buffer;
#else
Expand Down Expand Up @@ -362,8 +384,8 @@ public override void Write(byte[] buffer, int offset, int count)

try
{
IPEndPoint pt = new IPEndPoint(IPAddress.Broadcast, _remotePort);
_client.Send(tempBuf, count, pt);
IPEndPoint pt = new IPEndPoint(IPAddress.Parse(_broadcastAddress), _remotePort);
_client.Send(tempBuf, count, pt);
_lastUnsuccessfulSend.Stop();
}
catch (SocketException x)
Expand Down
Loading

0 comments on commit e78918a

Please sign in to comment.