Skip to content

Commit

Permalink
Merge pull request #577 from WildernessLabs/bug/linux-ntp
Browse files Browse the repository at this point in the history
handling of Linux NTP and time zone shenanigans
  • Loading branch information
adrianstevens authored Aug 12, 2024
2 parents 3562a19 + 5294201 commit 40d2df5
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 103 deletions.
100 changes: 100 additions & 0 deletions Source/Meadow.Core/NtpClientBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using static Meadow.Logging.Logger;

namespace Meadow;

/// <summary>
/// base Client for Network Time Protocol
/// </summary>
public abstract class NtpClientBase : INtpClient
{
private int _ntpLock = 1;

/// <inheritdoc/>
public event TimeChangedEventHandler TimeChanged = default!;

/// <inheritdoc/>
public abstract bool Enabled { get; }
/// <inheritdoc/>
public abstract TimeSpan PollPeriod { get; set; }

/// <summary>
/// Raises the TimeChanged event with a given time
/// </summary>
/// <param name="utcTime">The new time</param>
protected void RaiseTimeChanged(DateTime utcTime) => TimeChanged?.Invoke(utcTime);

/// <inheritdoc/>
public Task<bool> Synchronize(string? ntpServer = null)
{
if (ntpServer == null)
{
if (Resolver.Device.PlatformOS.NtpServers.Length == 0)
{
ntpServer = "0.pool.ntp.org";
Resolver.Log.Info($"No configured NTP servers. Defaulting to {ntpServer}", MessageGroup.Core);
}
else
{
ntpServer = Resolver.Device.PlatformOS.NtpServers[0];
}
}

if (Interlocked.Exchange(ref _ntpLock, 0) == 1)
{
try
{
var m_ntpPacket = new byte[48];
//LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
m_ntpPacket[0] = 0x1B;

UdpClient client = new UdpClient();
client.Connect(ntpServer, 123);
client.Send(m_ntpPacket, m_ntpPacket.Length);
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
byte[] data = client.Receive(ref ep);

// receive date data is at offset 32
// Data is 64 bits - first 32 is seconds
// it is not in an endian order, so we must rearrange
byte[] endianSeconds = new byte[4];
endianSeconds[0] = data[32 + 3];
endianSeconds[1] = data[32 + 2];
endianSeconds[2] = data[32 + 1];
endianSeconds[3] = data[32 + 0];
uint seconds = BitConverter.ToUInt32(endianSeconds, 0);

// second 32 is fraction of a second
endianSeconds[0] = data[32 + 7];
endianSeconds[1] = data[32 + 6];
endianSeconds[2] = data[32 + 5];
endianSeconds[3] = data[32 + 4];

uint fraction = BitConverter.ToUInt32(endianSeconds, 0);

var s = double.Parse($"{seconds}.{fraction}");

var dt = new DateTime(1900, 1, 1).AddSeconds(s);
Resolver.Device.PlatformOS.SetClock(dt);
TimeChanged?.Invoke(dt);
return Task.FromResult(true);
}
catch (Exception ex)
{
Resolver.Log.Error($"Failed to query NTP Server: '{ex.Message}'.", MessageGroup.Core);
return Task.FromResult(false);
}
finally
{
Interlocked.Exchange(ref _ntpLock, 1);
}

}

return Task.FromResult(false);
}
}
90 changes: 4 additions & 86 deletions Source/implementations/f7/Meadow.F7/Devices/NtpClient.cs
Original file line number Diff line number Diff line change
@@ -1,113 +1,31 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using static Meadow.Logging.Logger;

namespace Meadow;

/// <summary>
/// Client for Network Time Protocol
/// </summary>
public class NtpClient : INtpClient
public class NtpClient : NtpClientBase
{
/// <summary>
/// Event raised when the device clock is adjusted by NTP
/// </summary>
public event TimeChangedEventHandler TimeChanged = default!;

/// <summary>
/// Returns <c>true</c> if the NTP Client is enabled
/// </summary>
public bool Enabled => F7PlatformOS.GetBoolean(IPlatformOS.ConfigurationValues.GetTimeAtStartup);
public override bool Enabled => F7PlatformOS.GetBoolean(IPlatformOS.ConfigurationValues.GetTimeAtStartup);

internal NtpClient()
{ }

/// <summary>
/// Time period that the NTP client attempts to query the NTP time server(s)
/// </summary>
public TimeSpan PollPeriod
public override TimeSpan PollPeriod
{
get => TimeSpan.Zero; // currently only happens at startup
set => throw new PlatformNotSupportedException("Changing NTP Poll Frequency not currently supported");
}

internal void RaiseTimeChanged()
{
TimeChanged?.Invoke(DateTime.UtcNow);
}

private int _ntpLock = 1;

/// <inheritdoc/>
public Task<bool> Synchronize(string? ntpServer = null)
{
if (ntpServer == null)
{
if (Resolver.Device.PlatformOS.NtpServers.Length == 0)
{
ntpServer = "0.pool.ntp.org";
Resolver.Log.Info($"No configured NTP servers. Defaulting to {ntpServer}", MessageGroup.Core);
}
else
{
ntpServer = Resolver.Device.PlatformOS.NtpServers[0];
}
}

if (Interlocked.Exchange(ref _ntpLock, 0) == 1)
{
try
{
var m_ntpPacket = new byte[48];
//LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)
m_ntpPacket[0] = 0x1B;

UdpClient client = new UdpClient();
client.Connect(ntpServer, 123);
client.Send(m_ntpPacket, m_ntpPacket.Length);
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
byte[] data = client.Receive(ref ep);

// receive date data is at offset 32
// Data is 64 bits - first 32 is seconds
// it is not in an endian order, so we must rearrange
byte[] endianSeconds = new byte[4];
endianSeconds[0] = data[32 + 3];
endianSeconds[1] = data[32 + 2];
endianSeconds[2] = data[32 + 1];
endianSeconds[3] = data[32 + 0];
uint seconds = BitConverter.ToUInt32(endianSeconds, 0);

// second 32 is fraction of a second
endianSeconds[0] = data[32 + 7];
endianSeconds[1] = data[32 + 6];
endianSeconds[2] = data[32 + 5];
endianSeconds[3] = data[32 + 4];

uint fraction = BitConverter.ToUInt32(endianSeconds, 0);

var s = double.Parse($"{seconds}.{fraction}");

var dt = new DateTime(1900, 1, 1).AddSeconds(s);
Resolver.Device.PlatformOS.SetClock(dt);
TimeChanged?.Invoke(dt);
return Task.FromResult(true);
}
catch (Exception ex)
{
Resolver.Log.Error($"Failed to query NTP Server: '{ex.Message}'.", MessageGroup.Core);
return Task.FromResult(false);
}
finally
{
Interlocked.Exchange(ref _ntpLock, 1);
}

}

return Task.FromResult(false);
RaiseTimeChanged(DateTime.UtcNow);
}
}
28 changes: 28 additions & 0 deletions Source/implementations/linux/Meadow.Linux/Interop/Interop.Time.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ namespace Meadow
[SuppressUnmanagedCodeSecurity]
internal static partial class Interop
{

[DllImport(LIBC, SetLastError = true)]
public static extern void tzset();

[DllImport(LIBC, SetLastError = true)]
public static extern ref Rtc_time localtime(ref long time_t);

[DllImport(LIBC, SetLastError = true)]
public static extern IntPtr localtime_r(ref long time_t, ref Tm tm);

[DllImport(LIBC, SetLastError = true)]
public static extern IntPtr gmtime_r(ref long time_t, ref Tm tm);

[DllImport(LIBC, SetLastError = true)]
public static extern int clock_settime(Clock clock, ref Timespec timespec);

Expand All @@ -26,6 +39,21 @@ public enum Clock
//#define CLOCK_BOOTTIME 6
}

public struct Tm
{
public int tm_sec;
public int tm_min;
public int tm_hour;
public int tm_mday;
public int tm_mon;
public int tm_year;
public int tm_wday;
public int tm_yday;
public int tm_isdst;
// public long tm_gmtoff; /* Seconds east of UTC */
// public string tm_zone; /* Timezone abbreviation */
}

public struct Rtc_time
{
/*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
using System;
using System.Threading.Tasks;

namespace Meadow;

/// <summary>
/// Represents an NTP (Network Time Protocol) client for Linux.
/// </summary>
public class LinuxNtpClient : INtpClient
public class LinuxNtpClient : NtpClientBase
{
/// <inheritdoc/>
public bool Enabled => false;

/// <inheritdoc/>
public TimeSpan PollPeriod { get; set; }
internal LinuxNtpClient()
{
}

/// <inheritdoc/>
public event TimeChangedEventHandler? TimeChanged;
public override bool Enabled => false;

/// <inheritdoc/>
public Task<bool> Synchronize(string? ntpServer = null)
{
TimeChanged?.Invoke(DateTime.UtcNow);

throw new NotImplementedException();
}
public override TimeSpan PollPeriod { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ public void Initialize(DeviceCapabilities capabilities, string[]? args)
}
}

/// <inheritdoc/>
public string[] NtpServers => new string[] { "pool.ntp.org" };

/// <summary>
/// Gets the name of all available serial ports on the platform
/// </summary>
Expand Down Expand Up @@ -151,6 +154,17 @@ public virtual Temperature GetCpuTemperature()
/// <param name="dateTime"></param>
public void SetClock(DateTime dateTime)
{
if (dateTime.Kind != DateTimeKind.Utc)
{
TimeZoneInfo localZone = TimeZoneInfo.Local;
dateTime = dateTime.AddMinutes(localZone.BaseUtcOffset.TotalMinutes);

if (localZone.IsDaylightSavingTime(dateTime))
{
dateTime = dateTime.AddHours(1);
}
}

var ts = Interop.Timespec.From(dateTime);
var result = Interop.clock_settime(Interop.Clock.REALTIME, ref ts);

Expand Down Expand Up @@ -300,9 +314,6 @@ public DigitalStorage GetPrimaryDiskSpaceInUse()
/// <inheritdoc/>
public AllocationInfo GetMemoryAllocationInfo() => throw new NotImplementedException();

/// <inheritdoc/>
public string[] NtpServers => throw new NotImplementedException();

/// <inheritdoc/>
public bool RebootOnUnhandledException => false;

Expand Down

0 comments on commit 40d2df5

Please sign in to comment.