Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions score/time_daemon/src/application/svt/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER
"svt_handler",
False,
[
"//score/time_daemon/src/ptp_machine/shm:gptp_shm_machine",
"//score/time_daemon/src/ptp_machine:shm_ptp_machine",
"//score/time_daemon/src/verification_machine/svt:svt_verification_machine",
],
),
(
"svt_handler_for_utests",
True,
[
"//score/time_daemon/src/ptp_machine/shm:gptp_shm_machine",
"//score/time_daemon/src/ptp_machine:shm_ptp_machine",
"//score/time_daemon/src/verification_machine/svt:svt_verification_machine_for_utests",
],
),
Expand Down
6 changes: 6 additions & 0 deletions score/time_daemon/src/ptp_machine/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ alias(
visibility = ["//score/time_daemon:__subpackages__"],
)

alias(
name = "shm_ptp_machine",
actual = "//score/time_daemon/src/ptp_machine/shm:gptp_shm_machine",
visibility = ["//score/time_daemon:__subpackages__"],
)

cc_unit_test_suites_for_host_and_qnx(
name = "unit_test_suite",
test_suites_from_sub_packages = [
Expand Down
49 changes: 48 additions & 1 deletion score/time_slave/src/application/time_slave.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 \""

Copy link
Copy Markdown
Contributor

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?

Copy link
Copy Markdown
Contributor Author

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.

<< 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())
Expand All @@ -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();
Comment thread
BjoernAtBosch marked this conversation as resolved.
const auto& app_name = *iter;
Comment thread
BjoernAtBosch marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we check if args. are empty before resolving the iterator?
ApplicationContxt might not be == to the classical C argv*

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 m_app_path to store the app path, but no public getter :-O


for (++iter; iter != args.cend(); ++iter)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

@BjoernAtBosch BjoernAtBosch Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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:

If non-zero returned, application will exit with exit
code returned from Initialize and Run method won't be called.

That's not an issue. That is what we want in case help was requested. But:

NOTE: In case non-zero is returned, the whole group where this
application belongs to will not leave the machine state Init and
thus, all the applications of such group will never be promoted to
the machine state Running. This could lead to the whole ECU not being
able to startup at all which will result in continuous general rejects.

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 -h or --help.

{
PrintUsage(app_name);
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would believe, it shall return true here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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};
Expand Down
2 changes: 2 additions & 0 deletions score/time_slave/src/application/time_slave.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class TimeSlave final : public score::mw::lifecycle::Application
std::int32_t Run(const score::cpp::stop_token& token) override;

private:
bool ParseAndApplyCmdLineArgs(const score::mw::lifecycle::ApplicationContext& context);

details::GptpEngineOptions opts_;
std::unique_ptr<details::GptpEngine> engine_;
details::GptpIpcPublisher publisher_;
Expand Down
2 changes: 2 additions & 0 deletions score/time_slave/src/gptp/details/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ cc_library(
":os_syscalls",
":ptp_types",
":raw_socket",
"//score/time_slave/src/common:logging_contexts",
"@score_baselibs//score/mw/log:frontend",
],
)

Expand Down
26 changes: 19 additions & 7 deletions score/time_slave/src/gptp/gptp_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was it encreased? any justifications?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See reply to gordon9901's comment.

constexpr int kRxBufferSize = 2048;

} // namespace
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}
}
Expand Down
63 changes: 61 additions & 2 deletions score/time_slave/src/gptp/platform/linux/raw_socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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,

@BjoernAtBosch BjoernAtBosch Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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[] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kPtpBpfCode value (not type, qnx one is obviously qnx-specific) is identical to kPtp1588FilterInsns defined in gptp/platform/qnx/qnx_raw_shim.cpp - can we combine these somewhere common?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

@BjoernAtBosch BjoernAtBosch Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same here and on the line below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -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;
Expand Down
Loading