未验证 提交 cb24a190 编写于 作者: P Patrick Grawehr 提交者: GitHub

Add class that supports OS-independent setting of the system clock (#1619)

Co-authored-by: NJose Perez Rodriguez <joperezr@microsoft.com>
上级 1c880697
// 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.Runtime.InteropServices;
internal partial class Interop
{
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetSystemTime([In] ref SystemTime lpSystemTime);
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetSystemTime([Out] out SystemTime lpSystemTime);
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public struct SystemTime
{
public ushort Year;
public ushort Month;
public ushort DayOfWeek;
public ushort Day;
public ushort Hour;
public ushort Minute;
public ushort Second;
public ushort Milliseconds;
}
}
......@@ -2,6 +2,9 @@
The RTC devices supported by the project include DS1307, DS3231, PCF8563.
A class for setting the system clock is also provided, so that for instance the Raspberry Pi's operating system time can be synchronized
to a hardware RTC clock.
## Documentation
- DS1307 [datasheet](https://cdn.datasheetspdf.com/pdf-down/D/S/1/DS1307-Maxim.pdf)
......@@ -12,7 +15,7 @@ The RTC devices supported by the project include DS1307, DS3231, PCF8563.
![Circuit DS1307](Circuit_DS1307_bb.png)
## Usage
## Usage with Hardware clocks
### Hardware Required
......@@ -46,3 +49,12 @@ using (Ds1307 rtc = new Ds1307(device))
### Result
![Sample result](RunningResult.jpg)
## Setting the operating system clock
The class `SystemClock` contains static methods to set the operating system clock. Since this operation requires elevated permissions,
some special requirements apply unless the application is run as root (on Linux or MacOs)/administrator (on Windows).
On linux or MacOs, the user calling the `SetSystemTimeUtc` must either be root or the `date` command must have the setUid bit set. To do this, one must execute this command once: `sudo chmod +s /bin/date`. This allows everyone to set the clock.
On Windows, a system policy exists to allow anybody of a named user group to set the system clock. Normally, this right is limited to users belonging to the "Administrators" group. To configure it, open 'gpedit.msc' and go to Computer Configuration > Windows Settings > Security Settings > Local Policies > User Rights Assignments and add the user or his group to the setting 'Change System Time'.
......@@ -17,5 +17,7 @@
<Compile Include="Devices\Pcf8563\Pcf8563.cs" />
<Compile Include="Devices\Pcf8563\Pcf8563Register.cs" />
<Compile Include="RtcBase.cs" />
<Compile Include="..\Interop\Windows\Kernel32.cs" Link="Kernel32.cs" />
<Compile Include="SystemClock.cs" />
</ItemGroup>
</Project>
\ No newline at end of file

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
# Visual Studio Version 16
VisualStudioVersion = 16.0.31424.327
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B141143A-3C49-47EC-8ADA-F17F6C6A827F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rtc.Samples", "samples\Rtc.Samples.csproj", "{82FA815D-0081-43BA-8864-C7E971A6A93C}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rtc.Samples", "samples\Rtc.Samples.csproj", "{82FA815D-0081-43BA-8864-C7E971A6A93C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rtc", "Rtc.csproj", "{31E8FF38-E4AF-4F1E-9806-487548021F0D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rtc", "Rtc.csproj", "{31E8FF38-E4AF-4F1E-9806-487548021F0D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D05C3402-0D61-458E-8925-0D6EAB494E29}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rtc.Tests", "tests\Rtc.Tests.csproj", "{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
......@@ -18,9 +22,6 @@ Global
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{82FA815D-0081-43BA-8864-C7E971A6A93C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82FA815D-0081-43BA-8864-C7E971A6A93C}.Debug|Any CPU.Build.0 = Debug|Any CPU
......@@ -46,8 +47,27 @@ Global
{31E8FF38-E4AF-4F1E-9806-487548021F0D}.Release|x64.Build.0 = Release|Any CPU
{31E8FF38-E4AF-4F1E-9806-487548021F0D}.Release|x86.ActiveCfg = Release|Any CPU
{31E8FF38-E4AF-4F1E-9806-487548021F0D}.Release|x86.Build.0 = Release|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Debug|x64.ActiveCfg = Debug|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Debug|x64.Build.0 = Debug|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Debug|x86.ActiveCfg = Debug|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Debug|x86.Build.0 = Debug|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Release|Any CPU.Build.0 = Release|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Release|x64.ActiveCfg = Release|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Release|x64.Build.0 = Release|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Release|x86.ActiveCfg = Release|Any CPU
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{82FA815D-0081-43BA-8864-C7E971A6A93C} = {B141143A-3C49-47EC-8ADA-F17F6C6A827F}
{9BFDB9B9-D35B-4960-86B9-96C02FBB7A24} = {D05C3402-0D61-458E-8925-0D6EAB494E29}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3926A7E9-5885-4836-8360-231F95CA76F2}
EndGlobalSection
EndGlobal
......@@ -10,15 +10,57 @@ namespace Iot.Device.Rtc
/// </summary>
public abstract class RtcBase : IDisposable
{
private TimeZoneInfo _timeZone;
/// <summary>
/// The Device's <see cref="System.DateTime"/>
/// The Device's raw <see cref="System.DateTime"/>.
/// The caller must be aware of the device's time zone.
/// The behavior of the <see cref="System.DateTime.Kind"/> property is implementation-dependent (typically it is ignored)
/// </summary>
public virtual DateTime DateTime
public virtual DateTime RtcDateTime
{
get => ReadTime();
set => SetTime(value);
}
/// <summary>
/// Set or retrieves the current date/time. This property returns a <see cref="DateTimeOffset"/> and
/// is therefore correct regardless of the current time zone (when <see cref="LocalTimeZone"/> is set correctly).
/// </summary>
public DateTimeOffset DateTime
{
get
{
var now = RtcDateTime;
return new DateTimeOffset(now.Ticks, LocalTimeZone.GetUtcOffset(now));
}
set
{
var clockNow = new DateTime((value.UtcDateTime + LocalTimeZone.GetUtcOffset(value)).Ticks, DateTimeKind.Local);
RtcDateTime = clockNow;
}
}
/// <summary>
/// Gets or sets the time zone this instance will operate in.
/// Defaults to the local time zone from the system.
/// Changing this property will not change the time on the real time clock,
/// but instead affect the return value of <see cref="DateTime"/>
/// </summary>
public virtual TimeZoneInfo LocalTimeZone
{
get => _timeZone;
set => _timeZone = value;
}
/// <summary>
/// Creates an instance of this base class
/// </summary>
protected RtcBase()
{
_timeZone = TimeZoneInfo.Local;
}
/// <summary>
/// Set the device time
/// </summary>
......
// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace Iot.Device.Rtc
{
/// <summary>
/// Contains methods to access and update the system real time clock ("Bios clock")
/// </summary>
public class SystemClock : RtcBase
{
private static readonly DateTime UnixEpochStart = new DateTime(1970, 1, 1);
/// <summary>
/// We always use UTC when reading/writing the system clock, makes things easier.
/// Technically, the BIOS RTC is configured in local time by default on Windows, but in UTC on Linux (causing
/// weird effects when dual-booting). Both systems allow changing this setting, though.
/// </summary>
public override TimeZoneInfo LocalTimeZone
{
get
{
return TimeZoneInfo.Utc;
}
set
{
if (!value.Equals(TimeZoneInfo.Utc))
{
throw new NotSupportedException(
"The time zone configuration for the system clock cannot be changed");
}
}
}
/// <summary>
/// Set the system time to the given date/time.
/// The time must be given in utc.
/// The method requires elevated permissions. On Windows, the calling user must either be administrator or the right
/// "Change the system clock" must have been granted to the "Users" group (in Security policy management).
/// On Unix and MacOs, the current user must be root or the "date" command must have the setUid bit set.
/// </summary>
/// <remarks>
/// This method is primarily intended for setting the system clock from an external clock source, such as a DS1307 or a GNSS source when no
/// internet connection is available. If an internet connection is available, most operating systems will by default automatically sync the
/// time to a network server, which might interfere with this operation. So when using this method, the clock synchronization should be disabled,
/// or it should only be done if the time difference is large.
/// </remarks>
/// <param name="dt">Date/time to set the system clock to. This must be in UTC</param>
/// <exception cref="PlatformNotSupportedException">This method is not supported on this platform</exception>
/// <exception cref="IOException">There was an error executing a system command</exception>
/// <exception cref="UnauthorizedAccessException">The user does not have permissions to set the system clock</exception>
public static void SetSystemTimeUtc(DateTime dt)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
SetSystemTimeUtcWindows(dt);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
SetDateTimeUtcUnix(dt);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
SetDateTimeUtcMacOs(dt);
}
else
{
throw new PlatformNotSupportedException($"No implementation available for {Environment.OSVersion}");
}
}
/// <summary>
/// Gets the current system time directly using OS calls.
/// Normally, this should return the same as <see cref="DateTime.UtcNow"/>
/// </summary>
/// <returns>The current system time</returns>
public static DateTime GetSystemTimeUtc()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return GetSystemTimeUtcWindows();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return GetDateTimeUtcUnix();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return GetDateTimeUtcMacOs();
}
else
{
throw new PlatformNotSupportedException($"No implementation available for {Environment.OSVersion}");
}
}
private static void SetSystemTimeUtcWindows(DateTime dt)
{
Interop.SystemTime st = DateTimeToSystemTime(dt);
try
{
if (!Interop.SetSystemTime(ref st))
{
throw new IOException("SetSystemTime returned an unspecified error");
}
}
catch (System.Security.SecurityException x)
{
// Let's be exhaustive here, because without google, it's next to impossible to find this setting.
throw new UnauthorizedAccessException("Permission denied for setting the clock. Either run this program with elevated permissions or make sure " +
"the current user has permission to change the clock. Open 'gpedit.msc' and go to Computer Configuration > Windows Settings > " +
"Security Settings > Local Policies > User Rights Assignments and add the user or his group to the setting " +
"'Change System Time'.", x);
}
}
private static DateTime GetSystemTimeUtcWindows()
{
Interop.SystemTime st;
DateTime dt;
try
{
if (!Interop.GetSystemTime(out st))
{
throw new IOException("GetSystemTime returned an unspecified error");
}
dt = SystemTimeToDateTime(ref st);
}
catch (System.Security.SecurityException x)
{
throw new UnauthorizedAccessException("Permission denied for getting the clock", x);
}
return dt;
}
private static Interop.SystemTime DateTimeToSystemTime(DateTime dt)
{
Interop.SystemTime st;
st.Year = (ushort)dt.Year;
st.Day = (ushort)dt.Day;
st.Month = (ushort)dt.Month;
st.Hour = (ushort)dt.Hour;
st.Minute = (ushort)dt.Minute;
st.Second = (ushort)dt.Second;
st.Milliseconds = (ushort)dt.Millisecond;
st.DayOfWeek = (ushort)dt.DayOfWeek;
return st;
}
private static DateTime SystemTimeToDateTime(ref Interop.SystemTime st)
{
return new DateTime(st.Year, st.Month, st.Day, st.Hour, st.Minute, st.Second, st.Milliseconds);
}
private static DateTime GetDateTimeUtcUnix()
{
string date = RunDateCommandUnix("-u +%s.%N", out _); // Floating point seconds since epoch
if (Double.TryParse(date, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
{
DateTime dt = UnixEpochStart + TimeSpan.FromSeconds(result);
return dt;
}
throw new IOException($"The return value of the date command could not be parsed: {date} (seconds since 01/01/1970)");
}
private static DateTime GetDateTimeUtcMacOs()
{
string date = RunDateCommandUnix("-u +%s", out _); // Floating point seconds since epoch
if (Double.TryParse(date, NumberStyles.Any, CultureInfo.InvariantCulture, out var result))
{
DateTime dt = UnixEpochStart + TimeSpan.FromSeconds(result);
return dt;
}
throw new IOException($"The return value of the date command could not be parsed: {date} (seconds since 01/01/1970)");
}
private static void SetDateTimeUtcUnix(DateTime dt)
{
string formattedTime = dt.ToString("yyyy-MM-dd HH:mm:ss.fffff", CultureInfo.InvariantCulture);
int exitCode;
// Try to run the date command as user first (maybe it has the set-user-id bit set) otherwise, try root.
string output = RunDateCommandUnix($"-u -s \"{formattedTime}\"", out exitCode);
if (exitCode != 0)
{
throw new UnauthorizedAccessException($"Error running date command. Error {exitCode}: {output}");
}
}
private static void SetDateTimeUtcMacOs(DateTime dt)
{
// The format is "[[[mm]dd]HH]MM[[cc]yy][.ss]" from https://www.unix.com/man-page/osx/1/date/ - pretty weird to do this without delimiters
string formattedTime = dt.ToString("MMddHHmmyyyy.ss", CultureInfo.InvariantCulture);
int exitCode;
// Try to run the date command as user. If user doesn't have permissions, then command will fail and we throw UnauthorizedAccessException
string output = RunDateCommandUnix($"{formattedTime}", out exitCode);
if (exitCode != 0)
{
throw new UnauthorizedAccessException($"Error running date command. Error {exitCode}: {output}. Either run this program as root or ensure " +
$"/bin/date has the setuid bit set (execute 'chmod +s /bin/date' once as root)");
}
}
private static string RunDateCommandUnix(string commandLine, out int exitCode)
{
var si = new ProcessStartInfo()
{
FileName = "date",
Arguments = commandLine,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
using var process = new Process();
string outputData;
process.StartInfo = si;
process.Start();
outputData = process.StandardOutput.ReadToEnd();
var errorData = process.StandardError.ReadToEnd();
process.WaitForExit();
exitCode = process.ExitCode;
if (exitCode != 0)
{
outputData += errorData;
}
return outputData;
}
/// <inheritdoc />
protected override void SetTime(DateTime time)
{
SetSystemTimeUtc(time);
}
/// <inheritdoc />
protected override DateTime ReadTime()
{
var dt = GetSystemTimeUtc();
return dt;
}
}
}
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.Device.I2c;
using System.Threading;
using Iot.Device.Rtc;
static void TestSystemTime()
{
DateTime dt = SystemClock.GetSystemTimeUtc();
Console.WriteLine($"The system time is now {dt}");
DateTime newTime = new DateTime(2019, 4, 3, 20, 10, 10);
Console.WriteLine($"Do you want to set the time to {newTime}?");
if (Console.ReadLine()!.StartsWith("y"))
{
SystemClock.SetSystemTimeUtc(newTime);
}
}
TestSystemTime();
// This project contains DS1307, DS3231, PCF8563
I2cConnectionSettings settings = new(1, Ds3231.DefaultI2cAddress);
I2cDevice device = I2cDevice.Create(settings);
......@@ -15,7 +33,7 @@ rtc.DateTime = DateTime.Now;
while (true)
{
// read time
DateTime dt = rtc.DateTime;
DateTimeOffset dt = rtc.DateTime;
Console.WriteLine($"Time: {dt.ToString("yyyy/MM/dd HH:mm:ss")}");
Console.WriteLine($"Temperature: {rtc.Temperature.DegreesCelsius} ℃");
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net48</TargetFrameworks>
<LangVersion>9</LangVersion>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Rtc.csproj" />
</ItemGroup>
</Project>
// 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;
using Iot.Device.Rtc;
using Xunit;
namespace Iot.Device.Rtc.Tests
{
public class SystemClockTests
{
[Fact]
public void GetSystemTimeReturnsCorrectTime()
{
DateTime shouldBe = DateTime.UtcNow;
DateTime actual = SystemClock.GetSystemTimeUtc();
Assert.True((shouldBe - actual).Duration() < TimeSpan.FromSeconds(2));
}
[Fact]
public void TimeZoneConversionWorks()
{
DummyRtc rtc = new DummyRtc();
Assert.True(rtc.LocalTimeZone.Equals(TimeZoneInfo.Local));
DateTime initialTimeOfClock = new DateTime(2018, 1, 1, 12, 9, 1);
rtc.RtcDateTime = initialTimeOfClock;
Assert.Equal(initialTimeOfClock, rtc.TimeOfClock);
var now = DateTime.Now;
var utcNow = DateTime.UtcNow;
// Round the offset to minutes (otherwise the delta is not exact, causing an exception in the DateTimeOffset ctor)
TimeSpan offset = TimeSpan.FromMinutes(Math.Round((now - utcNow).TotalMinutes));
utcNow = new DateTime((now - offset).Ticks, DateTimeKind.Utc); // To make sure the delta matches afterwards
DateTimeOffset newLocalTime = new DateTimeOffset(now, offset);
rtc.DateTime = newLocalTime;
Assert.Equal(now, rtc.TimeOfClock);
Assert.Equal(utcNow, rtc.DateTime);
}
private sealed class DummyRtc : RtcBase
{
public DateTime TimeOfClock
{
get;
set;
}
protected override DateTime ReadTime()
{
return TimeOfClock;
}
protected override void SetTime(DateTime time)
{
// Convert to local time
TimeOfClock = new DateTime(time.Ticks, DateTimeKind.Local);
}
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册