From b73258e79d1630fbd3683b82f894842b6ec053a0 Mon Sep 17 00:00:00 2001 From: Michael Betz Date: Wed, 6 Aug 2025 17:22:57 +0200 Subject: [PATCH 1/4] Interop.cs: add GetLastTimeStamp() which calls ioctl(SIOCGSTAMP) to get the timestamp of the last read message from the kernel --- src/devices/SocketCan/Interop.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/devices/SocketCan/Interop.cs b/src/devices/SocketCan/Interop.cs index 58d047b0a4..2d09f7f8ef 100644 --- a/src/devices/SocketCan/Interop.cs +++ b/src/devices/SocketCan/Interop.cs @@ -36,6 +36,9 @@ internal class Interop [DllImport("libc", EntryPoint = "ioctl", CallingConvention = CallingConvention.Cdecl)] private static extern int Ioctl3(int fd, uint request, ref InterfaceIndexQuery ifr); + [DllImport("libc", EntryPoint = "ioctl", CallingConvention = CallingConvention.Cdecl)] + private static extern int Ioctl4(int fd, uint request, ref TimeVal tv); + [DllImport("libc", EntryPoint = "bind", CallingConvention = CallingConvention.Cdecl)] private static extern int BindSocket(int fd, ref CanSocketAddress addr, uint addrlen); @@ -149,6 +152,26 @@ private static unsafe int GetInterfaceIndex(int fd, string name) return ifr.ifr_ifindex; } + public static unsafe ulong GetLastTimeStamp(SafeHandle handle) + { + // From Linux 6.12.33+deb13-amd64 x86_64 + const uint SIOCGSTAMP = 0x8906; + + InterfaceIndexQuery tv = new TimeVal(); + int ret = Ioctl4((int)handle.DangerousGetHandle(), SIOCGSTAMP, ref tv); + if (ret == -1) + { + throw new IOException("Could not get socketcan timestamp"); + } + return (tv.tv_sec * 1000000 + tv.tv_usec); // unix time in [us] + } + + internal unsafe struct TimeVal + { + public uint tv_sec; /* Seconds */ + public uint tv_usec; /* Microseconds */ + } + internal unsafe struct InterfaceIndexQuery { internal const int IFNAMSIZ = 16; From fd3f9e2925a7f31d42c6946788bb4360939fcf0d Mon Sep 17 00:00:00 2001 From: Michael Betz Date: Mon, 11 Aug 2025 12:11:06 +0200 Subject: [PATCH 2/4] CanRaw.cs: add public GetLastTimeStamp() also fix some bugs: * time_t and suseconds_t are of type `long` on debian * print timestamp in sample project Tested on hardware on a Linux 6.12.33+deb13-amd64 x86_64 machine with an Innomaker USB2CAN dongle --- src/devices/SocketCan/CanRaw.cs | 9 +++++++++ src/devices/SocketCan/Interop.cs | 9 +++++---- src/devices/SocketCan/samples/Program.cs | 6 ++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/devices/SocketCan/CanRaw.cs b/src/devices/SocketCan/CanRaw.cs index 1f3c0118ea..f2f7905fbd 100644 --- a/src/devices/SocketCan/CanRaw.cs +++ b/src/devices/SocketCan/CanRaw.cs @@ -117,6 +117,15 @@ public bool TryReadFrame(Span data, out int frameLength, out CanId id) return true; } + /// + /// Reads the timestamp of the last read frame from the kernel + /// + /// unix time of the last read frame in [us] + public ulong GetLastTimeStamp() + { + return Interop.GetLastTimeStamp(_handle); + } + /// /// Set filter on the bus to read only from specified recipient. /// diff --git a/src/devices/SocketCan/Interop.cs b/src/devices/SocketCan/Interop.cs index 2d09f7f8ef..5e7544479c 100644 --- a/src/devices/SocketCan/Interop.cs +++ b/src/devices/SocketCan/Interop.cs @@ -157,19 +157,20 @@ public static unsafe ulong GetLastTimeStamp(SafeHandle handle) // From Linux 6.12.33+deb13-amd64 x86_64 const uint SIOCGSTAMP = 0x8906; - InterfaceIndexQuery tv = new TimeVal(); + TimeVal tv = new TimeVal(); int ret = Ioctl4((int)handle.DangerousGetHandle(), SIOCGSTAMP, ref tv); if (ret == -1) { throw new IOException("Could not get socketcan timestamp"); } - return (tv.tv_sec * 1000000 + tv.tv_usec); // unix time in [us] + + return ((ulong)tv.tv_sec * 1000000 + (ulong)tv.tv_usec); // unix time in [us] } internal unsafe struct TimeVal { - public uint tv_sec; /* Seconds */ - public uint tv_usec; /* Microseconds */ + public long tv_sec; /* Seconds */ + public long tv_usec; /* Microseconds */ } internal unsafe struct InterfaceIndexQuery diff --git a/src/devices/SocketCan/samples/Program.cs b/src/devices/SocketCan/samples/Program.cs index c48c69788b..cfa0f94bff 100644 --- a/src/devices/SocketCan/samples/Program.cs +++ b/src/devices/SocketCan/samples/Program.cs @@ -79,7 +79,8 @@ void ReceiveAllExample() Span data = new Span(buffer, 0, frameLength); string type = id.ExtendedFrameFormat ? "EFF" : "SFF"; string dataAsHex = string.Join(string.Empty, data.ToArray().Select((x) => x.ToString("X2"))); - Console.WriteLine($"Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); + ulong ts = can.GetLastTimeStamp(); // Get the kernel timestamp of the frame + Console.WriteLine($"Ts: {ts} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); } else { @@ -87,7 +88,8 @@ void ReceiveAllExample() Span data = new Span(buffer, 0, frameLength); string type = id.ExtendedFrameFormat ? "EFF" : "SFF"; string dataAsHex = string.Join(string.Empty, data.ToArray().Select((x) => x.ToString("X2"))); - Console.WriteLine($"Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); + ulong ts = can.GetLastTimeStamp(); + Console.WriteLine($"Ts: {ts} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); } } } From f82e5df09aa06be5910375705a764102cba3468a Mon Sep 17 00:00:00 2001 From: Michael Betz Date: Sun, 24 Aug 2025 19:05:30 +0200 Subject: [PATCH 3/4] CanRaw: provide timestamp in overload of TryReadFrame() * remove GetTimeStamp() * adapt samples/Program.cs * add can interface name as a CLI parameter to Program.cs --- src/devices/SocketCan/CanRaw.cs | 14 ++++++++++---- src/devices/SocketCan/samples/Program.cs | 14 ++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/devices/SocketCan/CanRaw.cs b/src/devices/SocketCan/CanRaw.cs index f2f7905fbd..89f6d0f5b3 100644 --- a/src/devices/SocketCan/CanRaw.cs +++ b/src/devices/SocketCan/CanRaw.cs @@ -118,12 +118,18 @@ public bool TryReadFrame(Span data, out int frameLength, out CanId id) } /// - /// Reads the timestamp of the last read frame from the kernel + /// Reads frame from the bus with the timestamp when it was received. /// - /// unix time of the last read frame in [us] - public ulong GetLastTimeStamp() + /// Data where output data should be written to + /// Length of the data read + /// Recipient identifier + /// Unix time of the last read frame in [us] + /// + public bool TryReadFrame(Span data, out int frameLength, out CanId id, out ulong timestamp) { - return Interop.GetLastTimeStamp(_handle); + bool ret = TryReadFrame(data, out frameLength, out id); + timestamp = Interop.GetLastTimeStamp(_handle); + return ret; } /// diff --git a/src/devices/SocketCan/samples/Program.cs b/src/devices/SocketCan/samples/Program.cs index cfa0f94bff..5a61602acb 100644 --- a/src/devices/SocketCan/samples/Program.cs +++ b/src/devices/SocketCan/samples/Program.cs @@ -19,9 +19,9 @@ { "receive-on-specific-address", ReceiveOnAddressExample }, }; -if (args.Length == 0 || !samples.ContainsKey(args[0])) +if (args.Length < 2 || !samples.ContainsKey(args[0])) { - Console.WriteLine("Usage: SocketCan.Sample "); + Console.WriteLine("Usage: SocketCan.Sample "); Console.WriteLine("Available samples:"); foreach (var kv in samples) @@ -38,7 +38,7 @@ void SendExample() { Console.WriteLine($"Sending to id = 0x{id.Value:X2}"); - using CanRaw can = new CanRaw(); + using CanRaw can = new CanRaw(args[1]); byte[][] buffers = new byte[][] { new byte[8] { 1, 2, 3, 40, 50, 60, 70, 80 }, @@ -69,17 +69,16 @@ void ReceiveAllExample() { Console.WriteLine("Listening for any id"); - using CanRaw can = new(); + using CanRaw can = new(args[1]); byte[] buffer = new byte[8]; while (true) { - if (can.TryReadFrame(buffer, out int frameLength, out CanId id)) + if (can.TryReadFrame(buffer, out int frameLength, out CanId id, out ulong ts)) { Span data = new Span(buffer, 0, frameLength); string type = id.ExtendedFrameFormat ? "EFF" : "SFF"; string dataAsHex = string.Join(string.Empty, data.ToArray().Select((x) => x.ToString("X2"))); - ulong ts = can.GetLastTimeStamp(); // Get the kernel timestamp of the frame Console.WriteLine($"Ts: {ts} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); } else @@ -88,7 +87,6 @@ void ReceiveAllExample() Span data = new Span(buffer, 0, frameLength); string type = id.ExtendedFrameFormat ? "EFF" : "SFF"; string dataAsHex = string.Join(string.Empty, data.ToArray().Select((x) => x.ToString("X2"))); - ulong ts = can.GetLastTimeStamp(); Console.WriteLine($"Ts: {ts} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); } } @@ -98,7 +96,7 @@ void ReceiveOnAddressExample() { Console.WriteLine($"Listening for id = 0x{id.Value:X2}"); - using CanRaw can = new(); + using CanRaw can = new(args[1]); byte[] buffer = new byte[8]; can.Filter(id); From 5fd09b6bcb8a441ef47c7bddb4e88f679b74b6b1 Mon Sep 17 00:00:00 2001 From: Michael Betz Date: Sun, 24 Aug 2025 20:01:34 +0200 Subject: [PATCH 4/4] TryReadFrame(): output timestamp as DateTime instead of long --- src/devices/SocketCan/CanRaw.cs | 4 ++-- src/devices/SocketCan/Interop.cs | 15 ++++++++++----- src/devices/SocketCan/samples/Program.cs | 6 +++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/devices/SocketCan/CanRaw.cs b/src/devices/SocketCan/CanRaw.cs index 89f6d0f5b3..01f342f3ec 100644 --- a/src/devices/SocketCan/CanRaw.cs +++ b/src/devices/SocketCan/CanRaw.cs @@ -123,9 +123,9 @@ public bool TryReadFrame(Span data, out int frameLength, out CanId id) /// Data where output data should be written to /// Length of the data read /// Recipient identifier - /// Unix time of the last read frame in [us] + /// UTC time when this frame was received with microsecond resolution /// - public bool TryReadFrame(Span data, out int frameLength, out CanId id, out ulong timestamp) + public bool TryReadFrame(Span data, out int frameLength, out CanId id, out DateTime timestamp) { bool ret = TryReadFrame(data, out frameLength, out id); timestamp = Interop.GetLastTimeStamp(_handle); diff --git a/src/devices/SocketCan/Interop.cs b/src/devices/SocketCan/Interop.cs index 5e7544479c..295279aff5 100644 --- a/src/devices/SocketCan/Interop.cs +++ b/src/devices/SocketCan/Interop.cs @@ -37,7 +37,7 @@ internal class Interop private static extern int Ioctl3(int fd, uint request, ref InterfaceIndexQuery ifr); [DllImport("libc", EntryPoint = "ioctl", CallingConvention = CallingConvention.Cdecl)] - private static extern int Ioctl4(int fd, uint request, ref TimeVal tv); + private static extern int Ioctl4(int fd, uint request, out TimeVal tv); [DllImport("libc", EntryPoint = "bind", CallingConvention = CallingConvention.Cdecl)] private static extern int BindSocket(int fd, ref CanSocketAddress addr, uint addrlen); @@ -152,21 +152,26 @@ private static unsafe int GetInterfaceIndex(int fd, string name) return ifr.ifr_ifindex; } - public static unsafe ulong GetLastTimeStamp(SafeHandle handle) + public static unsafe DateTime GetLastTimeStamp(SafeHandle handle) { // From Linux 6.12.33+deb13-amd64 x86_64 const uint SIOCGSTAMP = 0x8906; - TimeVal tv = new TimeVal(); - int ret = Ioctl4((int)handle.DangerousGetHandle(), SIOCGSTAMP, ref tv); + TimeVal tv; + int ret = Ioctl4((int)handle.DangerousGetHandle(), SIOCGSTAMP, out tv); if (ret == -1) { throw new IOException("Could not get socketcan timestamp"); } - return ((ulong)tv.tv_sec * 1000000 + (ulong)tv.tv_usec); // unix time in [us] + DateTime dt_utc = DateTimeOffset.FromUnixTimeSeconds(tv.tv_sec) + .AddTicks(tv.tv_usec * 10) // 1 tick = 100 ns + .UtcDateTime; + + return dt_utc; // UtcDateTime } + [StructLayout(LayoutKind.Sequential)] internal unsafe struct TimeVal { public long tv_sec; /* Seconds */ diff --git a/src/devices/SocketCan/samples/Program.cs b/src/devices/SocketCan/samples/Program.cs index 5a61602acb..df90e747bb 100644 --- a/src/devices/SocketCan/samples/Program.cs +++ b/src/devices/SocketCan/samples/Program.cs @@ -74,12 +74,12 @@ void ReceiveAllExample() while (true) { - if (can.TryReadFrame(buffer, out int frameLength, out CanId id, out ulong ts)) + if (can.TryReadFrame(buffer, out int frameLength, out CanId id, out DateTime ts)) { Span data = new Span(buffer, 0, frameLength); string type = id.ExtendedFrameFormat ? "EFF" : "SFF"; string dataAsHex = string.Join(string.Empty, data.ToArray().Select((x) => x.ToString("X2"))); - Console.WriteLine($"Ts: {ts} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); + Console.WriteLine($"Ts: {ts:o} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); } else { @@ -87,7 +87,7 @@ void ReceiveAllExample() Span data = new Span(buffer, 0, frameLength); string type = id.ExtendedFrameFormat ? "EFF" : "SFF"; string dataAsHex = string.Join(string.Empty, data.ToArray().Select((x) => x.ToString("X2"))); - Console.WriteLine($"Ts: {ts} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); + Console.WriteLine($"Ts: {ts:o} Id: 0x{id.Value:X2} [{type}]: {dataAsHex}"); } } }