Initial commit
This commit is contained in:
239
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeTimer.cs
Normal file
239
srcs/RobotNet10/Commons/RobotNet10.Realtime/RealtimeTimer.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace RobotNet10.Realtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides high-resolution real-time timers using Linux timerfd.
|
||||
/// More accurate than System.Threading.Timer for real-time applications.
|
||||
/// </summary>
|
||||
public class RealtimeTimer : IDisposable
|
||||
{
|
||||
private int _fd = -1;
|
||||
private readonly RealtimeClock _clock;
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of RealtimeTimer.
|
||||
/// </summary>
|
||||
/// <param name="clock">Clock type to use (default: CLOCK_MONOTONIC)</param>
|
||||
/// <param name="nonBlocking">Whether the timer should be non-blocking</param>
|
||||
public RealtimeTimer(RealtimeClockType clock = RealtimeClockType.Monotonic, bool nonBlocking = false)
|
||||
{
|
||||
_clock = new RealtimeClock(clock);
|
||||
int flags = LinuxNative.TFD_CLOEXEC;
|
||||
if (nonBlocking)
|
||||
{
|
||||
flags |= LinuxNative.TFD_NONBLOCK;
|
||||
}
|
||||
|
||||
_fd = LinuxNative.timerfd_create((int)clock, flags);
|
||||
if (_fd < 0)
|
||||
{
|
||||
LinuxNative.ThrowLastError("timerfd_create");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file descriptor for the timer.
|
||||
/// </summary>
|
||||
public int FileDescriptor => _fd;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the timer to fire periodically.
|
||||
/// </summary>
|
||||
/// <param name="interval">Interval between timer expirations</param>
|
||||
public void SetPeriodic(TimeSpan interval)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
||||
}
|
||||
|
||||
var spec = new LinuxNative.itimerspec
|
||||
{
|
||||
it_interval = TimeSpanToTimespec(interval),
|
||||
it_value = TimeSpanToTimespec(interval) // First expiration
|
||||
};
|
||||
|
||||
var oldSpec = new LinuxNative.itimerspec();
|
||||
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
|
||||
if (result != 0)
|
||||
{
|
||||
LinuxNative.ThrowLastError("timerfd_settime");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the timer to fire once after a delay.
|
||||
/// </summary>
|
||||
/// <param name="delay">Delay before timer expiration</param>
|
||||
public void SetOneShot(TimeSpan delay)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
||||
}
|
||||
|
||||
var spec = new LinuxNative.itimerspec
|
||||
{
|
||||
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 }, // No repeat
|
||||
it_value = TimeSpanToTimespec(delay)
|
||||
};
|
||||
|
||||
var oldSpec = new LinuxNative.itimerspec();
|
||||
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
|
||||
if (result != 0)
|
||||
{
|
||||
LinuxNative.ThrowLastError("timerfd_settime");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the timer to fire at an absolute time.
|
||||
/// </summary>
|
||||
/// <param name="absoluteTime">Absolute time when timer should expire</param>
|
||||
public void SetAbsolute(DateTime absoluteTime)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
||||
}
|
||||
|
||||
var clockTime = _clock.GetTime();
|
||||
var targetTime = absoluteTime.ToUniversalTime();
|
||||
var delay = targetTime - DateTime.UtcNow;
|
||||
|
||||
var spec = new LinuxNative.itimerspec
|
||||
{
|
||||
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 },
|
||||
it_value = TimeSpanToTimespec(delay)
|
||||
};
|
||||
|
||||
var oldSpec = new LinuxNative.itimerspec();
|
||||
int result = LinuxNative.timerfd_settime(_fd, LinuxNative.TFD_TIMER_ABSTIME, ref spec, ref oldSpec);
|
||||
if (result != 0)
|
||||
{
|
||||
LinuxNative.ThrowLastError("timerfd_settime");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disarms the timer.
|
||||
/// </summary>
|
||||
public void Disarm()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
||||
}
|
||||
|
||||
var spec = new LinuxNative.itimerspec
|
||||
{
|
||||
it_interval = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 },
|
||||
it_value = new LinuxNative.timespec { tv_sec = 0, tv_nsec = 0 }
|
||||
};
|
||||
|
||||
var oldSpec = new LinuxNative.itimerspec();
|
||||
int result = LinuxNative.timerfd_settime(_fd, 0, ref spec, ref oldSpec);
|
||||
if (result != 0)
|
||||
{
|
||||
LinuxNative.ThrowLastError("timerfd_settime");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current timer value.
|
||||
/// </summary>
|
||||
/// <returns>Current timer interval and value</returns>
|
||||
public (TimeSpan interval, TimeSpan value) GetTime()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
||||
}
|
||||
|
||||
var spec = new LinuxNative.itimerspec();
|
||||
int result = LinuxNative.timerfd_gettime(_fd, ref spec);
|
||||
if (result != 0)
|
||||
{
|
||||
LinuxNative.ThrowLastError("timerfd_gettime");
|
||||
}
|
||||
|
||||
return (TimespecToTimeSpan(spec.it_interval), TimespecToTimeSpan(spec.it_value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the timer expiration count (number of times timer has expired since last read).
|
||||
/// In non-blocking mode, returns 0 if timer hasn't expired yet (EAGAIN).
|
||||
/// </summary>
|
||||
/// <returns>Number of expirations, or 0 if timer hasn't expired (non-blocking mode)</returns>
|
||||
public ulong ReadExpirations()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RealtimeTimer));
|
||||
}
|
||||
|
||||
IntPtr bufferPtr = Marshal.AllocHGlobal(8);
|
||||
try
|
||||
{
|
||||
long bytesRead = LinuxNative.read(_fd, bufferPtr, new IntPtr(8));
|
||||
if (bytesRead < 0)
|
||||
{
|
||||
int errno = LinuxNative.GetLastError();
|
||||
if (errno == 11) // EAGAIN - timer hasn't expired yet (non-blocking mode)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
LinuxNative.ThrowLastError("read");
|
||||
}
|
||||
if (bytesRead != 8)
|
||||
{
|
||||
throw new InvalidOperationException($"Expected to read 8 bytes, got {bytesRead}");
|
||||
}
|
||||
return (ulong)Marshal.ReadInt64(bufferPtr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(bufferPtr);
|
||||
}
|
||||
}
|
||||
|
||||
private static LinuxNative.timespec TimeSpanToTimespec(TimeSpan ts)
|
||||
{
|
||||
long totalSeconds = (long)ts.TotalSeconds;
|
||||
long nanoseconds = (long)((ts.TotalMilliseconds % 1000) * 1000000);
|
||||
return new LinuxNative.timespec
|
||||
{
|
||||
tv_sec = totalSeconds,
|
||||
tv_nsec = nanoseconds
|
||||
};
|
||||
}
|
||||
|
||||
private static TimeSpan TimespecToTimeSpan(LinuxNative.timespec ts)
|
||||
{
|
||||
return TimeSpan.FromSeconds(ts.tv_sec) + TimeSpan.FromTicks(ts.tv_nsec / 100);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed && _fd >= 0)
|
||||
{
|
||||
LinuxNative.close(_fd);
|
||||
_fd = -1;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional P/Invoke declarations needed for timerfd
|
||||
internal static partial class LinuxNative
|
||||
{
|
||||
[LibraryImport("libc", SetLastError = true)]
|
||||
public static partial long read(int fd, IntPtr buf, IntPtr count);
|
||||
|
||||
[LibraryImport("libc", SetLastError = true)]
|
||||
public static partial int close(int fd);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user