Skip to content

Answer requests for Time/Date (PGN 65254) from the TECU - #122

Open
gunicsba wants to merge 3 commits into
feature/tecu-secondaryfrom
feature/tecu-time-date-on-request
Open

gunicsba wants to merge 3 commits into
feature/tecu-secondaryfrom
feature/tecu-time-date-on-request

Conversation

@gunicsba

@gunicsba gunicsba commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #118 (secondary TECU), because whether we may answer depends on what the other Tractor ECUs provide. Split out of #87: this is its on-request half, without the 10 s broadcast. ISO 11783-7 B.1 lists Time/Date as "on request".

What changes

  • The TECU answers requests for PGN 65254 and never sends it unrequested. The answer is UTC from the PC clock plus the local offset.
  • It follows the TECU plan from Run the TECU as a secondary Tractor ECU when the tractor has one #118. The primary always answers. A secondary answers only when no lower Tractor ECU advertises or sends Time/Date (ISO 11783-9 4.4.2.1). Otherwise a global request is left alone and one addressed to us gets a NACK.
  • PGN 65033 advertises the Time/date bit under the same condition, so the primary's default payload byte 3 becomes 0xF8.
  • Time/Date seen from another Tractor ECU counts for 30 s, not the 3 s used for cyclic messages, because it may only appear when requested.
  • A PC clock outside the message's range (before 1985, e.g. never set) is not sent.

Differences from #87

Also fixes a bug in #118 (separate commit)

A secondary started from a "send everything" plan, because plan_tecu_messages() with no information reports every message as a gap. It doubled the tractor's messages for up to 2.5 s after claiming an address, contrary to the behaviour in docs/PROTOCOL.md §5.7. It now starts with an empty plan. Time/Date needs this too, so that we don't answer before knowing what the tractor provides. It can move into #118 if you prefer.

Testing

  • Builds on Windows (MSVC), 9/9 ctest pass, clang-format clean
  • New unit tests: UTC offset (daylight saving, date and year boundaries, half-hour zones) and every payload byte
  • On a bus: request PGN 65254 and check the answer as the primary
  • On a bus with a tractor TECU: check a secondary stays silent until settled, and doesn't answer when the tractor provides Time/Date

🤖 Generated with Claude Code

Summary by Sourcery

Support standards-compliant on-request Time/Date responses while coordinating primary and secondary TECU providers and preventing premature secondary transmissions.

New Features:

  • Add on-request responses for PGN 65254 Time/Date using the PC clock, including UTC offset and daylight-saving information.
  • Advertise Time/Date capability through PGN 65033 according to the TECU primary/secondary coexistence rules.

Bug Fixes:

  • Prevent secondary TECUs from transmitting messages before their coexistence plan is established.
  • Ensure secondary TECUs defer Time/Date requests when a lower Tractor ECU provides the message.

Enhancements:

  • Track Time/Date providers until they leave the bus and keep the provider state synchronized with TECU address changes.
  • Handle unsupported or unset PC clock ranges by withholding Time/Date capability and responses.

Documentation:

  • Document PGN 65254 behavior, facility advertisement, and secondary TECU coexistence rules.
  • Document the concurrency requirements for Time/Date facility state.

Tests:

  • Add coverage for Time/Date payload encoding, UTC offsets across daylight-saving, date, year, and half-hour boundaries, and provider tracking.

gunicsba and others added 2 commits November 8, 2026 19:55
setup_tecu_interfaces() said a secondary waits, but plan_tecu_messages() with
no knowledge of the other Tractor ECUs answers "send everything". A secondary
doubled the messages of the tractor for up to 2.5 s after claiming an address,
which ISO 11783-9 4.4.2.1 c forbids. Start from an empty plan instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ISO 11783-7 B.1 lists Time/Date as "on request", so the TECU answers requests
and never sends it unrequested. The answer is UTC from the PC clock plus the
local offset, which now includes daylight saving time.

It follows the TECU plan: the primary always answers, and a secondary only when
no lower Tractor ECU advertises or sends Time/Date (ISO 11783-9 4.4.2.1). The
facility bit in PGN 65033 follows the same state. Time/Date seen from another
Tractor ECU counts for 30 s rather than 3 s, because it may only appear on request.

The answer lives in TractorFacilities' PGN request handling rather than in
TimeDateInterface: that registers a global receive callback, which the stack
can still call once after the TECU is rebuilt.

Replaces the on-request half of #87, without its 10 s broadcast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gunicsba

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai

sourcery-ai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Data structures

Track lower TECUs that have sent Time/Date until they leave or their address is reused, so secondary-provider decisions do not rely on a cyclic-message timeout.

  • include/tecu_coexistence.hpp L102-134 (+3 small edits)

Expose the Time/Date PGN, payload helpers, and request-handling state used to answer requests and advertise the facility only when it can be provided.

  • include/tractor_facilities.hpp L98-105, L163-169, L179-184 (+3 small edits)

API

Add the synchronized PGN 65254 request callback lifecycle and public helpers for encoding the Time/Date payload.

  • src/tractor_facilities.cpp L247-303, L434-439, L479-503 (+2 small edits)

Uses of the API

Initialize and update the TECU plan so secondary startup begins empty and Time/Date request handling follows the plan's provider decision.

  • src/app.cpp L488, L505, L640

Clear remembered Time/Date providers when TECUs leave or change identity, mark observed providers, and use that persistent state in coexistence decisions.

  • src/tecu_coexistence.cpp L180-190, L227-230, L292-299

Implementation

Advertise Time/Date only when the request handler is registered, the TECU plan permits responses, and the PC clock is representable; encode the response from UTC and local clock fields.

  • src/tractor_facilities.cpp L388-392

Configuration

Document the concurrency guarantees for the atomic Time/Date activation flag shared between plan updates and request callbacks.

  • docs/CONCURRENCY.md L74-75

Documentation

Document on-request Time/Date behavior, its PGN 65033 facility bit, and the revised secondary-provider rules.

  • docs/PROTOCOL.md L274-346 (5 small edits)

Tests

Add unit coverage for persistent Time/Date provider tracking and UTC-offset and payload encoding boundaries.

  • tools/test_tecu_coexistence.cpp L140-153, L163
  • tools/test_tractor_facilities.cpp L326-380, L391-392

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 6 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/tractor_facilities.cpp" line_range="432-434" />
<code_context>
 	  PGN_REQUIRED_TRACTOR_FACILITIES, &TractorFacilities::on_required_facilities, this);
 	std::cout << "[" << get_timestamp() << "] [TractorFacilities] Registered PGN 65032 diagnostic listener." << std::endl;

+	if (!pgnRequestProtocol->register_pgn_request_callback(PGN_TIME_DATE, &TractorFacilities::on_time_date_request, this))
+	{
+		std::cout << "[" << get_timestamp() << "] [TractorFacilities] Failed to register PGN 65254 request callback." << std::endl;
+	}
+
</code_context>
<issue_to_address>
**Advertised service gets no response**

When registration of the PGN 65254 request callback fails, `initialize()` logs the failure but still marks the object initialized and returns success; the application can then keep Time/Date active and advertise it in PGN 65033, although no callback answers requests.

Make initialization fail or disable the Time/Date facility when callback registration fails.
</issue_to_address>

### Comment 2
<location path="src/app.cpp" line_range="505" />
<code_context>
 		tractorFacilities->set_speed_messages_interface(speedMessagesInterface.get());
 		tractorFacilities->set_nmea2000_message_interface(nmea2000MessageInterface.get());
 		tractorFacilities->set_provided_facilities(facilities_for_plan(plan));
+		tractorFacilities->set_time_date_active(plan.sends(TecuMessage::TimeDate));
 		tractorFacilities->initialize();
 		tecuPlan = plan;
</code_context>
<issue_to_address>
**Advertised time service cannot respond**

When the PC clock is outside the PGN's representable range, `set_time_date_active()` follows the plan, so `build_payload()` advertises Time/Date even though `read_system_time_date()` rejects the clock and `on_time_date_request()` refuses to answer; implement clients see an unusable capability.

Advertise Time/Date only when the PC clock is representable.

Also at `src/tractor_facilities.cpp:487-489`, `src/app.cpp:640`.
</issue_to_address>

### Comment 3
<location path="src/tractor_facilities.cpp" line_range="487" />
<code_context>
+	std::array<std::uint8_t, 8> payload{};
+	// Returning false leaves it to the Tractor ECU that does provide it: the stack ignores a global
+	// request and NACKs one addressed to us
+	if ((PGN_TIME_DATE != parameterGroupNumber) || (nullptr == self) || !self->timeDateActive || !read_system_time_date(payload))
+	{
+		return false;
</code_context>
<issue_to_address>
**Global requests get duplicate replies**

When a lower-instance TECU responds to the same global request without having previously advertised or sent Time/Date, `on_time_date_request()` sends immediately based on the secondary's plan, before `OtherTecuMonitor` observes the lower-instance TECU's reply, so the implement receives both responses.

Have the secondary avoid answering a global request when a lower-instance TECU can answer it.
</issue_to_address>

### Comment 4
<location path="include/tecu_coexistence.hpp" line_range="114" />
<code_context>
 	/// How long a message seen from another TECU counts as "sent by it"
 	static constexpr std::uint32_t SENT_BY_OTHERS_WINDOW_MS = 3000;
+	/// The same for time/date, which may only appear when something requests it
+	static constexpr std::uint32_t TIME_DATE_SENT_BY_OTHERS_WINDOW_MS = 30000;

 	/// @brief Register the PGN 65033 listener. Call once.
</code_context>
<issue_to_address>
**Departed provider suppresses replies**

When a lower-instance TECU sends Time/Date and then leaves before its 30-second window expires, `refresh()` removes the departed TECU's facility record but leaves its timestamp in `lastSentByOthersMs`, so `sent_by_others()` continues treating Time/Date as provided and the secondary withholds replies for the remainder of the window.

Clear the departed TECU's Time/Date timestamp when `refresh()` removes it.

Also at `src/tecu_coexistence.cpp:278`.
</issue_to_address>

### Comment 5
<location path="src/tecu_coexistence.cpp" line_range="278" />
<code_context>
 		const std::uint32_t last = lastSentByOthersMs[i];
-		sent[i] = (0 != last) && !isobus::SystemTiming::time_expired_ms(last, SENT_BY_OTHERS_WINDOW_MS);
+		const bool isTimeDate = (static_cast<std::size_t>(TecuMessage::TimeDate) == i);
+		sent[i] = (0 != last) && !isobus::SystemTiming::time_expired_ms(last, isTimeDate ? TIME_DATE_SENT_BY_OTHERS_WINDOW_MS : SENT_BY_OTHERS_WINDOW_MS);
 	}
 	return sent;
</code_context>
<issue_to_address>
**Spoofed time frames disable secondary replies**

When an attacker on the CAN bus claims a lower-instance Tractor ECU identity and sends PGN 65254, `OtherTecuMonitor` treats the frame as another TECU's time/date message, and the 30-second window suppresses `TimeDate` in the secondary's plan; `on_time_date_request` then refuses to answer, so implements lose the secondary's time/date service.

Do not let unauthenticated time/date frames alone disable the secondary's response; validate the provider against a trusted source policy before applying the suppression window.

Also at `src/tractor_facilities.cpp:487`.
</issue_to_address>

### Comment 6
<location path="src/tecu_coexistence.cpp" line_range="278" />
<code_context>
 		const std::uint32_t last = lastSentByOthersMs[i];
-		sent[i] = (0 != last) && !isobus::SystemTiming::time_expired_ms(last, SENT_BY_OTHERS_WINDOW_MS);
+		const bool isTimeDate = (static_cast<std::size_t>(TecuMessage::TimeDate) == i);
+		sent[i] = (0 != last) && !isobus::SystemTiming::time_expired_ms(last, isTimeDate ? TIME_DATE_SENT_BY_OTHERS_WINDOW_MS : SENT_BY_OTHERS_WINDOW_MS);
 	}
 	return sent;
</code_context>
<issue_to_address>
**Recent time/date transmission gets missed**

When a TECU sends PGN 65254 while the secondary evaluates its plan, `on_frame()` writes `lastSentByOthersMs` on the CAN callback thread while `sent_by_others()` reads it on the TECU update thread without synchronization. A concurrent read and write is a data race, so the secondary can make its plan without reliably seeing a recent time/date transmission and answer a request the other TECU should handle.

Synchronize accesses to `lastSentByOthersMs`, or make its elements atomic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/tractor_facilities.cpp Resolved
Comment thread src/app.cpp Resolved
Comment thread src/tractor_facilities.cpp Resolved
Comment thread include/tecu_coexistence.hpp Resolved
Comment thread src/tecu_coexistence.cpp Resolved
Comment thread src/tecu_coexistence.cpp Resolved
Time/Date is only sent on request, so the 30 s window let a secondary
answer alongside a lower TECU whenever requests came more than 30 s
apart, and kept deferring for up to 30 s after that TECU left. A lower
TECU now counts as providing Time/Date from the first time it sends it
until it leaves the bus or another TECU takes its address
(ISO 11783-9 4.4.2.1).

The Time/Date facility bit in PGN 65033 is now only set while the TECU
can answer: the PGN 65254 request callback registered and the PC clock
is within the range the message can carry (1985-2235).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant