using System; using System.Runtime.InteropServices; namespace RobotNet10.Realtime; /// /// Provides high-resolution real-time timers using Linux timerfd. /// More accurate than System.Threading.Timer for real-time applications. /// public class RealtimeTimer : IDisposable { private int _fd = -1; private readonly RealtimeClock _clock; private bool _disposed = false; /// /// Initializes a new instance of RealtimeTimer. /// /// Clock type to use (default: CLOCK_MONOTONIC) /// Whether the timer should be non-blocking 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"); } } /// /// Gets the file descriptor for the timer. /// public int FileDescriptor => _fd; /// /// Sets the timer to fire periodically. /// /// Interval between timer expirations 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"); } } /// /// Sets the timer to fire once after a delay. /// /// Delay before timer expiration 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"); } } /// /// Sets the timer to fire at an absolute time. /// /// Absolute time when timer should expire 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"); } } /// /// Disarms the timer. /// 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"); } } /// /// Gets the current timer value. /// /// Current timer interval and value 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)); } /// /// 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). /// /// Number of expirations, or 0 if timer hasn't expired (non-blocking mode) 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); }