-
Notifications
You must be signed in to change notification settings - Fork 8
Fixes and improvements for Linux #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ec0754e
a4c8078
f5ba6f5
5757b25
23a48f7
0f9fe8a
a93e5aa
50f3ce9
949c02a
b9c5a74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| #include "score/mw/log/logging.h" | ||
| #include "score/time_slave/src/common/logging_contexts.h" | ||
|
|
||
| #include <iostream> | ||
| #include <thread> | ||
|
|
||
| namespace score | ||
|
|
@@ -28,12 +29,25 @@ namespace | |
| constexpr std::int32_t kInitSuccess = 0; | ||
| constexpr std::int32_t kInitFailure = -1; | ||
|
|
||
| void PrintUsage(const std::string& app_name) | ||
| { | ||
| std::cout << "Usage: " << app_name | ||
| << " [options]\n" | ||
| "Options:\n" | ||
| " -h, --help Print this message and exit.\n" | ||
| " -i, --interface <iface> Define the ethernet interface to be used (default is \"" | ||
| << details::GptpEngineOptions().iface_name << "\").\n"; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| TimeSlave::TimeSlave() = default; | ||
|
|
||
| std::int32_t TimeSlave::Initialize(const score::mw::lifecycle::ApplicationContext& /*context*/) | ||
| std::int32_t TimeSlave::Initialize(const score::mw::lifecycle::ApplicationContext& context) | ||
| { | ||
| if (!ParseAndApplyCmdLineArgs(context)) | ||
| return kInitFailure; | ||
|
|
||
| engine_ = std::make_unique<details::GptpEngine>(opts_); | ||
|
|
||
| if (!engine_->Initialize()) | ||
|
|
@@ -52,6 +66,39 @@ std::int32_t TimeSlave::Initialize(const score::mw::lifecycle::ApplicationContex | |
| return kInitSuccess; | ||
| } | ||
|
|
||
| bool TimeSlave::ParseAndApplyCmdLineArgs(const score::mw::lifecycle::ApplicationContext& context) | ||
| { | ||
| const auto& args = context.get_arguments(); | ||
| auto iter = args.cbegin(); | ||
|
BjoernAtBosch marked this conversation as resolved.
|
||
| const auto& app_name = *iter; | ||
|
BjoernAtBosch marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we check if args. are empty before resolving the iterator?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current impl is. It stores all C args in the vector. It has a separate private variable |
||
|
|
||
| for (++iter; iter != args.cend(); ++iter) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have already and command parses solutions in the base libs already?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems not: Neither lifecycle mgm has nor I found some in baselibs. |
||
| { | ||
| if (*iter == "-h" || *iter == "--help") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning kInitFailure for --help is problematic — in an automotive lifecycle framework this can trigger watchdog restarts or fault reports. --help is intentional, not an error. Consider exit(0) directly after printing usage, or a separate return code.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can change that, but not sure if returning a 0 (zero) is the right thing. Documentation of the Initialize function tells:
That's not an issue. That is what we want in case help was requested. But:
Could it cause issues if we return a zero here from the terminated process (signalling success)? Normally, I'd expect that it should not because the process is expected to be running ... I mean, this is a bit artificial because in a correctly configured system the time_slave shouldn't be called with |
||
| { | ||
| PrintUsage(app_name); | ||
| return false; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would believe, it shall return true here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But would it terminate then? Typical behavior if querying help of a cmd is to print usage and terminate. |
||
| } | ||
| else if (*iter == "-i" || *iter == "--interface") | ||
| { | ||
| ++iter; | ||
| if (iter == args.cend()) | ||
| { | ||
| PrintUsage(app_name); | ||
| return false; | ||
| } | ||
| opts_.iface_name = *iter; | ||
| } | ||
| else | ||
| { | ||
| std::cerr << "Unknown argument \"" << *iter << "\"." << std::endl; | ||
| PrintUsage(app_name); | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| std::int32_t TimeSlave::Run(const score::cpp::stop_token& token) | ||
| { | ||
| constexpr auto kPublishInterval = std::chrono::milliseconds{50}; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,7 +32,7 @@ namespace details | |
| namespace | ||
| { | ||
|
|
||
| constexpr int kRxTimeoutMs = 100; // poll timeout; keeps RxLoop responsive to shutdown | ||
| constexpr int kRxTimeoutMs = 500; // poll timeout; keeps RxLoop responsive to shutdown | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. kRxTimeoutMs raised from 100 → 500 ms with no explanation in the commit message. Worst-case shutdown latency for RxLoop is now 5× longer. Please document the reason (CPU load? NIC behaviour?) so the timing impact can be assessed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My intention was to reduce checking load and 0.5 sec isn't a long period. But we can switch back to the previous value if preferred.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Understood. Worth noting though that gPTP Sync messages arrive every ~125 ms, so a 500 ms poll wait could span 4 Sync cycles. Not a bug, just something to keep in mind.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why was it encreased? any justifications?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See reply to gordon9901's comment. |
||
| constexpr int kRxBufferSize = 2048; | ||
|
|
||
| } // namespace | ||
|
|
@@ -70,13 +70,17 @@ GptpEngine::~GptpEngine() noexcept | |
|
|
||
| bool GptpEngine::Initialize() | ||
| { | ||
| mw::log::LogInfo(kTimeSlaveAppContext) << "GptpEngine: Initializing on " << opts_.iface_name; | ||
| if (running_.load(std::memory_order_acquire)) | ||
| { | ||
| mw::log::LogWarn(kTimeSlaveAppContext) << "GptpEngine::Initialize: Engine is already running"; | ||
| return true; | ||
| } | ||
|
|
||
| if (!identity_->Resolve(opts_.iface_name)) | ||
| { | ||
| score::mw::log::LogError(kTimeSlaveAppContext) | ||
| << "GptpEngine: failed to resolve ClockIdentity for " << opts_.iface_name; | ||
| << "GptpEngine::Initialize: Failed to resolve ClockIdentity for " << opts_.iface_name; | ||
| return false; | ||
| } | ||
|
|
||
|
|
@@ -85,14 +89,14 @@ bool GptpEngine::Initialize() | |
| if (!socket_->Open(opts_.iface_name)) | ||
| { | ||
| score::mw::log::LogError(kTimeSlaveAppContext) | ||
| << "GptpEngine: failed to open raw socket on " << opts_.iface_name; | ||
| << "GptpEngine::Initialize: Failed to open raw socket on " << opts_.iface_name; | ||
| return false; | ||
| } | ||
|
|
||
| if (!socket_->EnableHwTimestamping()) | ||
| { | ||
| score::mw::log::LogWarn(kTimeSlaveAppContext) | ||
| << "GptpEngine: HW timestamping not available on " << opts_.iface_name << ", falling back to SW timestamps"; | ||
| score::mw::log::LogWarn(kTimeSlaveAppContext) << "GptpEngine::Initialize: HW timestamping not available on " | ||
| << opts_.iface_name << ", falling back to SW timestamps"; | ||
| } | ||
|
|
||
| running_.store(true, std::memory_order_release); | ||
|
|
@@ -108,7 +112,7 @@ bool GptpEngine::Initialize() | |
| catch (const std::system_error& e) | ||
| { | ||
| score::mw::log::LogError(kTimeSlaveAppContext) | ||
| << "GptpEngine: failed to create RxThread: " << std::string_view{e.what()}; | ||
| << "GptpEngine::Initialize: Failed to create RxThread: " << std::string_view{e.what()}; | ||
| running_.store(false, std::memory_order_release); | ||
| socket_->Close(); | ||
| return false; | ||
|
|
@@ -123,7 +127,7 @@ bool GptpEngine::Initialize() | |
| catch (const std::system_error& e) | ||
| { | ||
| score::mw::log::LogError(kTimeSlaveAppContext) | ||
| << "GptpEngine: failed to create PdelayThread: " << std::string_view{e.what()}; | ||
| << "GptpEngine::Initialize: Failed to create PdelayThread: " << std::string_view{e.what()}; | ||
| Deinitialize(); | ||
| return false; | ||
| } | ||
|
|
@@ -258,11 +262,13 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, const ::timesp | |
| switch (msg.msgtype) | ||
| { | ||
| case kPtpMsgtypePdelayReq: | ||
| mw::log::LogDebug(kGPtpMachineContext) << "PdelayReq message received, hw_ts=" << hw_ts.ns << " ns"; | ||
| if (msg.ptpHdr.domainNumber == opts_.domain_number) | ||
| SendPDelayResponseAndFollowUp(msg, hw_ts); | ||
| break; | ||
|
|
||
| case kPtpMsgtypeSync: | ||
| mw::log::LogDebug(kGPtpMachineContext) << "Sync message received, hw_ts=" << hw_ts.ns << " ns"; | ||
| if (msg.ptpHdr.domainNumber != opts_.domain_number) | ||
| break; | ||
| msg.recvHardwareTS = hw_ts; | ||
|
|
@@ -271,6 +277,7 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, const ::timesp | |
| break; | ||
|
|
||
| case kPtpMsgtypeFollowUp: | ||
| mw::log::LogDebug(kGPtpMachineContext) << "FollowUp message received, hw_ts=" << hw_ts.ns << " ns"; | ||
| if (msg.ptpHdr.domainNumber != opts_.domain_number) | ||
| break; | ||
| msg.parseMessageTs = TimestampToTmv(msg.follow_up.preciseOriginTimestamp); | ||
|
|
@@ -295,19 +302,24 @@ void GptpEngine::HandlePacket(const std::uint8_t* frame, int len, const ::timesp | |
| break; | ||
|
|
||
| case kPtpMsgtypePdelayResp: | ||
| mw::log::LogDebug(kGPtpMachineContext) << "PdelayResp message received, hw_ts=" << hw_ts.ns << " ns"; | ||
| msg.recvHardwareTS = hw_ts; | ||
| msg.parseMessageTs = TimestampToTmv(msg.pdelay_resp.requestReceiptTimestamp); | ||
| if (pdelay_) | ||
| pdelay_->OnResponse(msg); | ||
| break; | ||
|
|
||
| case kPtpMsgtypePdelayRespFollowUp: | ||
| mw::log::LogDebug(kGPtpMachineContext) | ||
| << "PdelayRespFollowUp message received, hw_ts=" << hw_ts.ns << " ns"; | ||
| msg.parseMessageTs = TimestampToTmv(msg.pdelay_resp_fup.responseOriginReceiptTimestamp); | ||
| if (pdelay_) | ||
| pdelay_->OnResponseFollowUp(msg); | ||
| break; | ||
|
|
||
| default: | ||
| mw::log::LogDebug(kGPtpMachineContext) | ||
| << "Unhandled message received: " << static_cast<int>(msg.msgtype) << ", hw_ts=" << hw_ts.ns << " ns"; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,11 @@ | |
| ********************************************************************************/ | ||
| #include "score/time_slave/src/gptp/details/raw_socket_impl.h" | ||
|
|
||
| #include "score/mw/log/logging.h" | ||
| #include "score/time_slave/src/common/logging_contexts.h" | ||
|
|
||
| #include <arpa/inet.h> | ||
| #include <linux/filter.h> | ||
| #include <linux/if_ether.h> | ||
| #include <linux/net_tstamp.h> | ||
| #include <linux/sockios.h> | ||
|
|
@@ -64,32 +68,80 @@ bool RawSocketImpl::Open(const std::string& iface) | |
| { | ||
| Close(); | ||
|
|
||
| const int fd = sys_->socket_call(AF_PACKET, SOCK_RAW, htons(ETH_P_1588)); | ||
| const int fd = sys_->socket_call(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The BPF filter only checks EtherType at offset 12 == 0x88F7, but VLAN-tagged frames have 0x8100 there — the inner 0x88F7 is at offset 16. Those frames are silently dropped before reaching frame_codec, which already handles them correctly. If VLAN-tagged PTP traffic is not expected in the target environment this is fine,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the previous configuration, I wasn't able to receive gPTP frames in the time_slave. That's the reason to change it. Any gPTP frame should not be VLAN tagged - see my reply to Valery's comment. |
||
| if (fd < 0) | ||
| { | ||
| score::mw::log::LogError(kTimeSlaveAppContext) | ||
| << "RawSocket::Open: Failed to create raw socket endpoint (errno=" << errno << ")"; | ||
| return false; | ||
| } | ||
|
|
||
| ::ifreq ifr{}; | ||
| std::strncpy(ifr.ifr_name, iface.c_str(), IFNAMSIZ - 1); | ||
| ifr.ifr_name[IFNAMSIZ - 1] = '\0'; | ||
| if (sys_->ioctl_call(fd, SIOCGIFINDEX, &ifr) < 0) | ||
| { | ||
| sys_->close_call(fd); | ||
| score::mw::log::LogError(kTimeSlaveAppContext) | ||
| << "RawSocket::Open: Failed to manipulate raw socket endpoint (errno=" << errno << ")"; | ||
| return false; | ||
| } | ||
|
|
||
| ::sockaddr_ll sa{}; | ||
| sa.sll_family = AF_PACKET; | ||
| sa.sll_protocol = htons(ETH_P_1588); | ||
| sa.sll_protocol = htons(ETH_P_ALL); | ||
| sa.sll_ifindex = ifr.ifr_ifindex; | ||
| if (sys_->bind_call(fd, reinterpret_cast<::sockaddr*>(&sa), sizeof(sa)) < 0) | ||
| { | ||
| sys_->close_call(fd); | ||
| score::mw::log::LogError(kTimeSlaveAppContext) | ||
| << "RawSocket::Open: Failed to bind raw socket endpoint to interface " << iface << " (errno=" << errno | ||
| << ")"; | ||
| return false; | ||
| } | ||
|
|
||
| // SO_BINDTODEVICE: best-effort, don't fail if it doesn't work | ||
| (void)sys_->setsockopt_call(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.c_str(), static_cast<socklen_t>(iface.size())); | ||
|
|
||
| // Enable promiscuous mode so the NIC passes all frames (including PTP multicast) to the kernel. | ||
| ::packet_mreq mr{}; | ||
| mr.mr_ifindex = ifr.ifr_ifindex; | ||
| mr.mr_type = PACKET_MR_PROMISC; | ||
| if (sys_->setsockopt_call(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)) < 0) | ||
| { | ||
| score::mw::log::LogWarn(kTimeSlaveAppContext) | ||
| << "RawSocket::Open: Failed to set promiscuous mode (errno=" << errno << ")"; | ||
| } | ||
|
|
||
| // BPF filter: pass only frames with EtherType 0x88F7 (PTP/gPTP). This is fine ss gPTP frames | ||
| // are n9ot VLAN (nor priority) tagged. The BPF filter runs in-kernel before delivery | ||
| // to userspace, so non-PTP frames are dropped with zero overhead in the application. | ||
| static const ::sock_filter kPtpBpfCode[] = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't know if that makes sense as this raw_socket impl is Linux-specific
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The socket is, but the EtherType for PTP/gPTP is the same across platforms no?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah - got it now. But no, they are not exactly the same. Furthermore, these setting are quite driver-specific, so I'd like to keep it as is for now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok - both filter instructions do the same. (Wasn't aware how the filter is exactly working - that part was fixed by AI). But still - I think this is quite OS specific, so I'd keep those separatly |
||
| BPF_STMT(BPF_LD | BPF_H | BPF_ABS, 12), // load EtherType | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we change the "12" to some not -magic number? |
||
| BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ETH_P_1588, 1, 0), // == 0x88F7 → PASS | ||
| BPF_STMT(BPF_RET | BPF_K, 0U), // FAIL: drop | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the same here and on the line below
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean the 1 and the 0 in line 122? That's just the jump width to the filter instruction to be executed next ... |
||
| BPF_STMT(BPF_RET | BPF_K, static_cast<unsigned int>(-1)), // PASS: accept | ||
| }; | ||
| ::sock_fprog prog{}; | ||
| prog.len = static_cast<unsigned short>(sizeof(kPtpBpfCode) / sizeof(kPtpBpfCode[0])); | ||
| prog.filter = const_cast<::sock_filter*>(kPtpBpfCode); | ||
| if (sys_->setsockopt_call(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)) < 0) | ||
| { | ||
| score::mw::log::LogWarn(kTimeSlaveAppContext) | ||
| << "RawSocket::Open: Failed to set BPF filter (errno=" << errno << ")"; | ||
| } | ||
|
|
||
| // Enable kernel layer software timestamps as a baseline, so we get at least some timestamp | ||
| // even if hw timestamping isn't available or fails to configure. The HW timestamp will come | ||
| // in the same control message but different field, so the caller can use it if present and | ||
| // fall back to SW timestamp if not. | ||
| const int enable = 1; | ||
| if (sys_->setsockopt_call(fd, SOL_SOCKET, SO_TIMESTAMPNS, &enable, sizeof(enable)) < 0) | ||
| { | ||
| score::mw::log::LogWarn(kTimeSlaveAppContext) | ||
| << "RawSocket::Open: Failed to enable SO_TIMESTAMPNS (errno=" << errno << ")"; | ||
| } | ||
|
|
||
| fd_.store(fd, std::memory_order_release); | ||
| iface_ = iface; | ||
| return true; | ||
|
|
@@ -168,7 +220,14 @@ int RawSocketImpl::Recv(std::uint8_t* buf, std::size_t buf_len, ::timespec& hwts | |
| continue; | ||
| const auto* ts = reinterpret_cast<const ::timespec*>(CMSG_DATA(cm)); | ||
| if (ts[2].tv_sec != 0 || ts[2].tv_nsec != 0) | ||
| { | ||
| hwts = ts[2]; | ||
| break; // Prefer raw HW timestamp if available | ||
| } | ||
| } | ||
| else if (cm->cmsg_type == SO_TIMESTAMPNS) | ||
| { | ||
| memcpy(&hwts, CMSG_DATA(cm), sizeof(hwts)); | ||
| } | ||
| } | ||
| return len; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have more options, which are needed for the slave? I would assume, yes
shall we specify then the config file and make it mandatory to be run?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention was to solve the issue first that the interface name differs between QNX and Linux. Furthermore it is quite likely that the user wants to use another interface.
Further options could be added as needed.