Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seatalk1 interface support #2265

Merged
merged 31 commits into from
Jan 28, 2024
Merged
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0d055da
Build up infrastructure for new binding
pgrawehr Dec 25, 2023
9f5d35e
More stuff for first draft
pgrawehr Dec 25, 2023
a369e7f
Doesn't compile?
pgrawehr Dec 25, 2023
eefd720
First successful parser tests
pgrawehr Dec 26, 2023
baba280
It starts to do something useful
pgrawehr Dec 26, 2023
c92423c
More messages, and high-level support
pgrawehr Dec 27, 2023
e534f9f
First messages can be sent
pgrawehr Dec 29, 2023
2629339
It works!
pgrawehr Dec 29, 2023
f579d32
The most important commands work, I can control the AP remotely!
pgrawehr Dec 30, 2023
4ecbf0c
Deadband mode selection works
pgrawehr Dec 30, 2023
68f2d9e
Lots of unit testing
pgrawehr Dec 30, 2023
1d84b44
Testing and bugfixing
pgrawehr Dec 31, 2023
65badd9
Some new messages added
pgrawehr Dec 31, 2023
15dd3e9
Apparent wind command appears to work
pgrawehr Dec 31, 2023
c5b5df5
Track message and Autopilot Track Mode work
pgrawehr Jan 1, 2024
d5e052a
Basic roundtrip for all messages fixed
pgrawehr Jan 1, 2024
bfa3c70
Add Schematic for interface
pgrawehr Jan 3, 2024
83710e2
Ignore KCad backup directory
pgrawehr Jan 3, 2024
db1237e
HTD sentence supported
pgrawehr Jan 6, 2024
4164586
Add support for $STALK message
pgrawehr Jan 6, 2024
007db2f
Raw Seatalk1 to NMEA conversion works
pgrawehr Jan 7, 2024
b95310f
More simulation stuff
pgrawehr Jan 7, 2024
96c53d0
Schema cleanup
pgrawehr Jan 7, 2024
6bb711f
Add some message translation between Seatalk and NMEA
pgrawehr Jan 7, 2024
95339ae
Various tests added and NavigationToWaypoint reversed
pgrawehr Jan 7, 2024
9bbff7f
Minor cleanup, create empty PCB
pgrawehr Jan 8, 2024
7be59bd
Schema updated, PCB drawn (unverified!)
pgrawehr Jan 13, 2024
38c4225
Documentation started
pgrawehr Jan 13, 2024
353f3d7
Finalize documentation
pgrawehr Jan 14, 2024
9b04cc9
Fix linter errors
pgrawehr Jan 14, 2024
af64d8b
Formatting fix
pgrawehr Jan 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
First messages can be sent
pgrawehr committed Dec 29, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit e534f9f1a94a6a56478958d92ca419865c72b775
46 changes: 43 additions & 3 deletions src/devices/Seatalk1/AutoPilotRemoteController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
// 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 System.Text;
@@ -8,10 +11,17 @@

namespace Iot.Device.Seatalk1
{
/// <summary>
/// Remote controller for an autopilot connected via Seatalk1
/// </summary>
/// <remarks>
/// Type is not disposable, to prevent accidental disposal by clients. They don't get ownership of the instance.
/// </remarks>
public class AutoPilotRemoteController
{
private readonly SeatalkInterface _parentInterface;
private readonly object _lock = new object();
private DateTime _lastUpdateTime = new DateTime(0);

internal AutoPilotRemoteController(SeatalkInterface parentInterface)
{
@@ -26,7 +36,7 @@ internal AutoPilotRemoteController(SeatalkInterface parentInterface)
public Angle? AutopilotHeading { get; private set; }

public Angle? AutopilotDesiredHeading { get; private set; }

public Angle? RudderAngle { get; private set; }

public bool RudderAngleAvailable { get; private set; }
@@ -43,6 +53,7 @@ private void AutopilotMessageInterpretation(SeatalkMessage obj)
{
lock (_lock)
{
_lastUpdateTime = DateTime.UtcNow;
if (obj is CompassHeadingAutopilotCourse ch)
{
Status = ch.AutopilotStatus;
@@ -90,11 +101,40 @@ private void AutopilotMessageInterpretation(SeatalkMessage obj)
AutopilotKeysPressed?.Invoke(keystroke);
}

if (Status == AutopilotStatus.Standby)
if (Status == AutopilotStatus.Standby || Status == AutopilotStatus.Offline)
{
DeadbandMode = DeadbandMode.Automatic; // Resets automatically when going to standby (the message is not sent periodically)
}
}
}

internal void UpdateStatus()
{
lock (_lock)
{
if (_lastUpdateTime + TimeSpan.FromSeconds(5) < DateTime.UtcNow)
{
// The autopilot hasn't sent anything for 5 seconds. Assume it's offline
Status = AutopilotStatus.Offline;
DeadbandMode = DeadbandMode.Automatic;
RudderAngle = null;
AutopilotDesiredHeading = null;
AutopilotHeading = null;
}
}
}

/// <summary>
/// Returns a textual representation of the current AP status
/// </summary>
/// <returns></returns>
public override string ToString()
{
string hdg = AutopilotHeading.HasValue ? AutopilotHeading.Value.ToString() : "N/A";
string desiredHdg = AutopilotDesiredHeading.HasValue ? AutopilotDesiredHeading.Value.ToString() : "N/A";
string rudderAngle = RudderAngleAvailable ? RudderAngle.GetValueOrDefault().ToString() : "N/A";
string ret = $"MODE: {Status}; HDG: {hdg}; DESHDG: {desiredHdg}; RUD: {rudderAngle}; DB: {DeadbandMode}; ALRT: {Alarms}";
return ret;
}
}
}
92 changes: 92 additions & 0 deletions src/devices/Seatalk1/SeatalkInterface.cs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Iot.Device.Seatalk1.Messages;

@@ -16,6 +17,9 @@ public class SeatalkInterface : IDisposable
{
private readonly SerialPort _port;
private readonly Seatalk1Parser _parser;
private readonly Thread _watchDog;
private readonly CancellationTokenSource _cancellation;
private readonly AutoPilotRemoteController _autopilotController;

public event Action<SeatalkMessage>? MessageReceived;

@@ -47,23 +51,111 @@ public SeatalkInterface(string uart)
_parser = new Seatalk1Parser(_port.BaseStream);
_parser.NewMessageDecoded += OnNewMessage;
_parser.StartDecode();

_autopilotController = new AutoPilotRemoteController(this);

_cancellation = new CancellationTokenSource();
_watchDog = new Thread(WatchDog);
_watchDog.Start();
}

public Seatalk1Parser Parser => _parser;

/// <summary>
/// Periodic watchdog tasks
/// </summary>
private void WatchDog()
{
while (!_cancellation.IsCancellationRequested)
{
Thread.Sleep(1000);
_autopilotController.UpdateStatus();
}
}

private void OnNewMessage(SeatalkMessage obj)
{
MessageReceived?.Invoke(obj);
}

private int BitCount(int b)
{
int count = 0;
while (b != 0)
{
count++;
b &= (b - 1); // walking through all the bits which are set to one
}

return count;
}

public void SendDatagram(byte[] data)
{
// Send byte-by-byte
bool isCommandByte = true;
for (int i = 0; i < data.Length; i++)
{
byte b = data[i];
// We need to send the first byte (the command byte) with a parity of "mark", all the remaining bytes with a parity of "space",
// but since Linux doesn't seem to properly support Mark or Space (on the raspberry pi, that setting results in no parity bit to be sent)
// we cheat here and count what the parity bit should be and use even or odd to achieve the desired result.
bool isEven = BitCount(b) % 2 == 0;
Parity parityToSend;
if (isEven)
{
if (isCommandByte)
{
// command byte so far is even, we need the parity bit to be 1, so use odd parity
// (because the parity setting "odd" means that the data bits including the parity bit count to odd)
parityToSend = Parity.Odd;
}
else
{
parityToSend = Parity.Even;
}
}
else
{
if (isCommandByte)
{
parityToSend = Parity.Even;
}
else
{
parityToSend = Parity.Odd;
}
}

// parityToSend = parityToSend == Parity.Odd ? Parity.Even : Parity.Odd;
_port.Parity = parityToSend;
_port.Write(data, i, 1);
while (_port.BytesToWrite != 0)
{
Thread.Yield();
}

isCommandByte = false;
}
}

/// <summary>
/// Get an interface to the Autopilot remote controller.
/// </summary>
/// <returns>An interface to monitor and control an Autopilot connected via Seatalk1</returns>
public AutoPilotRemoteController GetAutopilotRemoteController() => _autopilotController;

protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_cancellation.Cancel();
_parser.StopDecode();
_parser.Dispose();
_port.Close();
_port.Dispose();
_watchDog.Join();
_cancellation.Dispose();
}
}

82 changes: 42 additions & 40 deletions src/devices/Seatalk1/samples/Seatalk1.Samples.cs
Original file line number Diff line number Diff line change
@@ -16,9 +16,7 @@ namespace Seatalk1Sample
{
internal class Program
{
private CompassHeadingAndRudderPosition? _headingAndRudderPosition;
private CompassHeadingAutopilotCourse? _autopilotCourse;
private DeadbandMode _deadbandMode = DeadbandMode.None;
private SeatalkInterface? _seatalk;

internal static int Main(string[] args)
{
@@ -39,61 +37,65 @@ public void Run(string[] args)
{
LogDispatcher.LoggerFactory = new SimpleConsoleLoggerFactory(LogLevel.Trace);

SerialPort port1 = new SerialPort(args[0]);
port1.BaudRate = 4800;
port1.Parity = Parity.Even;
port1.StopBits = StopBits.One;
port1.DataBits = 8;
port1.Open();
_seatalk = new SeatalkInterface(args[0]);

Seatalk1Parser parser = new Seatalk1Parser(port1.BaseStream);
parser.StartDecode();
_seatalk.MessageReceived += ParserOnNewMessageDecoded;

parser.NewMessageDecoded += ParserOnNewMessageDecoded;

while (!Console.KeyAvailable)
while (true)
{
if (Console.KeyAvailable)
{
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Escape)
{
break;
}
else if (key.Key == ConsoleKey.A)
{
// For testing only
byte[] keyPlus1 = new byte[]
{
0x86, 0x11, 0x07, 0xf8
};

_seatalk.SendDatagram(keyPlus1);
}
else if (key.Key == ConsoleKey.B)
{
// For testing only
byte[] keyPlus1 = new byte[]
{
0x86, 0x11, 0x05, 0xfa
// 0xFF, 0xFF,
};

_seatalk.SendDatagram(keyPlus1);
}
}

Thread.Sleep(500);
WriteCurrentState();
}

parser.StopDecode();
Console.ReadKey(true);
_seatalk.Dispose();
_seatalk = null;

Console.WriteLine("Program is terminating");
parser.Dispose();
port1.Close();
}

private void WriteCurrentState()
{
if (_headingAndRudderPosition == null || _autopilotCourse == null)
var ctrl = _seatalk?.GetAutopilotRemoteController();
if (ctrl != null)
{
return;
Console.Write("\r");
Console.Write(ctrl.ToString());
}

Console.Write("\r");
Console.Write($"MAG: {_headingAndRudderPosition.CompassHeading} TRK: {_autopilotCourse.AutoPilotCourse} " +
$"STAT: {_autopilotCourse.AutopilotStatus} RUDDER: {_autopilotCourse.RudderPosition} ALRT: {_autopilotCourse.Alarms} DB: {_deadbandMode} ");
}

private void ParserOnNewMessageDecoded(SeatalkMessage obj)
{
if (obj is CompassHeadingAutopilotCourse ch)
{
_autopilotCourse = ch;
WriteCurrentState();
}
else if (obj is CompassHeadingAndRudderPosition rb)
{
_headingAndRudderPosition = rb;
WriteCurrentState();
}
else if (obj is DeadbandSetting dbs)
{
_deadbandMode = dbs.Mode;
WriteCurrentState();
}
else if (obj is Keystroke keystroke)
if (obj is Keystroke keystroke)
{
Console.WriteLine();
Console.WriteLine($"Pressed key(s): {keystroke}");