Skip to content

Commit 37aaa49

Browse files
authored
Create timesync JNI for testing client (#1433)
1 parent 937bafa commit 37aaa49

File tree

69 files changed

+2256
-372
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2256
-372
lines changed

.github/workflows/build.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ jobs:
115115
- uses: actions/setup-python@v5
116116
with:
117117
python-version: '3.11'
118+
- name: Install graphviz
119+
run: |
120+
sudo apt-get update
121+
sudo apt-get -y install graphviz
118122
- name: Install dependencies
119123
working-directory: docs
120124
run: |
@@ -283,6 +287,9 @@ jobs:
283287
java-version: 17
284288
distribution: temurin
285289
architecture: ${{ matrix.architecture }}
290+
- name: Install Arm64 Toolchain
291+
run: ./gradlew installArm64Toolchain
292+
if: ${{ (matrix.artifact-name) == 'LinuxArm64' }}
286293
- run: |
287294
rm -rf photon-server/src/main/resources/web/*
288295
mkdir -p photon-server/src/main/resources/web/docs
@@ -301,7 +308,7 @@ jobs:
301308
path: photon-server/src/main/resources/web/docs
302309
- run: |
303310
chmod +x gradlew
304-
./gradlew photon-server:shadowJar -PArchOverride=${{ matrix.arch-override }}
311+
./gradlew photon-targeting:jar photon-server:shadowJar -PArchOverride=${{ matrix.arch-override }}
305312
if: ${{ (matrix.arch-override != 'none') }}
306313
- run: |
307314
chmod +x gradlew
@@ -311,6 +318,10 @@ jobs:
311318
with:
312319
name: jar-${{ matrix.artifact-name }}
313320
path: photon-server/build/libs
321+
- uses: actions/upload-artifact@v4
322+
with:
323+
name: photon-targeting_jar-${{ matrix.artifact-name }}
324+
path: photon-targeting/build/libs
314325

315326
run-smoketest-native:
316327
needs: [build-package]

.github/workflows/photonvision-docs.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ jobs:
2626
- name: Install and upgrade pip
2727
run: python -m pip install --upgrade pip
2828

29+
- name: Install graphviz
30+
run: |
31+
sudo apt-get update
32+
sudo apt-get -y install graphviz
33+
2934
- name: Install Python dependencies
3035
working-directory: docs
3136
run: |

.readthedocs.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ build:
99
os: ubuntu-22.04
1010
tools:
1111
python: "3.11"
12+
apt_packages:
13+
- graphviz
1214
jobs:
1315
post_checkout:
1416
# Cancel building pull requests when there aren't changed in the docs directory or YAML file.

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"sphinx_design",
3838
"myst_parser",
3939
"sphinx.ext.mathjax",
40+
"sphinx.ext.graphviz",
4041
]
4142

4243
# Configure OpenGraph support

docs/source/docs/contributing/design-descriptions/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
```{toctree}
44
:maxdepth: 1
55
image-rotation
6+
time-sync
67
```
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Time Synchronization Protocol Specification, Version 1.0
2+
3+
Protocol Revision 1.0, 08/25/2024
4+
5+
## Background
6+
7+
In a distributed compute environment like robots, time synchronization between computers is increasingly important. Currently, [NetworkTables Version 4.1](https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc) provides support for time synchronization of clients with the NetworkTables server using binary PING/PONG messages sent over WebSockets. This approach, while fundamentally the same as is described in this memo, has demonstrated some opportunities for improvement:
8+
9+
- PING/PONG messages are processed in the same queue as other NetworkTables messages. Depending on the underlying implementation and processor speed, this can incur message processing delays and increase client-calculated Round-Trip Time (RTT), and cause messages to arrive at the server timestamped in the future.
10+
- Messages use WebSockets over TCP for their transport layer. We don't need the robustness guarantees of TCP as our connection is stateless.
11+
12+
For these reasons, a time synchronization solution separate from NetworkTables communication was desired. Architecture decisions made to address these issues are:
13+
14+
- Use the User Datagram Protocol (UDP) transport layer, as we don't need the robustness guarantees afforded by TCP. As a Client, if a PING isn't replied to, we'll just try again at the start of the next PING window. As a bonus, we are free to use UDP port 5810 as NetworkTables only uses TCP Port 5810/5811 as of Version 4.1.
15+
- Use a separate thread from the current NetworkTables libUV runner.
16+
17+
18+
## Prior Art
19+
20+
The [NetworkTables 4.1 timestamp synchronization](https://github.com/wpilibsuite/allwpilib/blob/main/ntcore/doc/networktables4.adoc#timestamps) approach, an implementation of [Cristian's Algorithm](https://en.wikipedia.org/wiki/Cristian%27s_algorithm). We also implement Cristian’s Algorithm.
21+
22+
The [Precision Time Protocol](https://en.wikipedia.org/wiki/Precision_Time_Protocol#Synchronization) at it's core does something similar with Sync/Delay_Req/Delay_Resp. We do not have (guaranteed) access to hardware timestamping, but we utilize this PING/PONG pattern to estimate total round-trip time.
23+
24+
25+
## Roles
26+
27+
```{graphviz}
28+
digraph CristianAlgorithm {
29+
ratio=0.5;
30+
bgcolor="transparent";
31+
32+
node [
33+
fontcolor = "#e6e6e6",
34+
style = filled,
35+
color = "#e6e6e6",
36+
fillcolor = "#333333"
37+
fontsize=10;
38+
]
39+
40+
edge [
41+
color = "#e6e6e6",
42+
fontcolor = "#e6e6e6"
43+
fontsize=10;
44+
]
45+
46+
rankdir=LR;
47+
node [shape=box, style=filled, color=lightblue];
48+
49+
user_send [label="User Sends T1"];
50+
server_receive [label="Server Receives T1"];
51+
server_send [label="Server Sends T2"];
52+
user_receive [label="User Receives T2"];
53+
user_compute [label="User Computes Time"];
54+
55+
user_send -> server_receive [label="T1 (Request)"];
56+
server_receive -> server_send [label="T1 received by server"];
57+
server_send -> user_receive [label="T2 sent by server"];
58+
user_receive -> user_compute [label="T2 received by user"];
59+
user_compute -> user_send [label="Computed Time: T3 = T2 + (deltaT2 - deltaT1)/2"];
60+
}
61+
```
62+
63+
Time Synchronization Protocol (TSP) participants can assume either a server role or a client role. The server role is responsible for listening for incoming time synchronization requests from clients and replying appropriately. The client role is responsible for sending "Ping" messages to the server and listening for "Pong" replies to estimate the offset between the server and client time bases.
64+
65+
All time values shall use units of microseconds. The epoch of the time base this is measured against is unspecified.
66+
67+
Clients shall periodically (e.g. every few seconds) send, in a manner that minimizes transmission delays, a **TSP Ping Message** that contains the client's current local time.
68+
69+
When the server receives a **TSP Ping Message** from any client, it shall respond to the client, in a manner that minimizes transmission delays, with a **TSP Pong message** encoding a timestamp of its (the server's) current local time (in microseconds), and the client-provided data value.
70+
71+
When the client receives a **TSP Pong Message** from the server, it shall verify that the `Client Local Time` corresponds to the currently in-flight TSP Ping message; if not, it shall drop this packet. The round trip time (RTT) shall be computed from the delta between the message's data value and the current local time. If the RTT is less than that from previous measurements, the client shall use the timestamp in the message plus ½ the RTT as the server time equivalent to the current local time, and use this equivalence to compute server time base timestamps from local time for future messages.
72+
73+
## Transport
74+
75+
Communication between server and clients shall occur over the User Datagram Protocol (UDP) Port 5810.
76+
77+
## Message Format
78+
79+
The message format forgoes CRCs (as these are provided by the Ethernet physical layer) or packet delimination (as our packetsa are assumed be under the network MTU). **TSP Ping** and **TSP Pong** messages shall be encoded in a manor compatible with a WPILib packed struct with respect to byte alignment and endienness.
80+
81+
### TSP Ping
82+
83+
| Offset | Format | Data | Notes |
84+
| ------ | ------ | ---- | ----- |
85+
| 0 | uint8 | Protocol version | This field shall always set to 1 (0b1) for TSP Version 1. |
86+
| 1 | uint8 | Message ID | This field shall always be set to 1 (0b1). |
87+
| 2 | uint64 | Client Local Time | The client's local time value, at the time this Ping message was sent. |
88+
89+
### TSP Pong
90+
91+
| Offset | Format | Data | Notes |
92+
| ------ | ------ | ---- | ----- |
93+
| 0 | uint8 | Protocol version | This field shall always set to 1 (0b1) for TSP Version 1.
94+
| 1 | uint8 | Message ID | This field shall always be set to 2 (0b2).
95+
| 2 | uint64 | Client Local Time | The client's local time value from the Ping message that this Pong is generated in response to.
96+
| 10 | uint64 | Server Local Time | The current time at the server, at the time this Pong message was sent.
97+
98+
99+
## Optional Protocol Extensions
100+
101+
Clients may publish statistics to NetworkTables. If they do, they shall publish to a key that is globally unique per participant in the Time Synronization network. If a client implements this, it shall provide the following publishers:
102+
103+
| Key | Type | Notes |
104+
| ------ | ------ | ---- | ----- |
105+
| offset_us | Integer | The time offset that, when added to the client's local clock, provides server time |
106+
| ping_tx_count | Integer | The total number of TSP Ping packets transmitted |
107+
| ping_rx_count | Integer | The total number of TSP Ping packets received |
108+
| pong_rx_time_us | Integer | The time, in client local time, that the last pong was received |
109+
| rtt2_us | Integer | The time in us from last complete (ping transmission to pong reception) |
110+
111+
PhotonVision has chosen to publish to the sub-table `/photonvision/.timesync/{DEVICE_HOSTNAME}`. Future implementations of this protocol may decide to implement this as a structured data type.

photon-core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def nativeTasks = wpilibTools.createExtractionTasks {
1717

1818
nativeTasks.addToSourceSetResources(sourceSets.main)
1919

20+
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpilibc")
2021
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
2122
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
2223
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpiutil")

photon-core/src/main/java/org/photonvision/common/configuration/SqlConfigProvider.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ public void load() {
265265
JacksonUtils.deserialize(
266266
getOneConfigFile(conn, GlobalKeys.HARDWARE_CONFIG), HardwareConfig.class);
267267
} catch (IOException e) {
268-
logger.error("Could not deserialize hardware config! Loading defaults");
268+
logger.error("Could not deserialize hardware config! Loading defaults", e);
269269
hardwareConfig = new HardwareConfig();
270270
}
271271

@@ -274,7 +274,7 @@ public void load() {
274274
JacksonUtils.deserialize(
275275
getOneConfigFile(conn, GlobalKeys.HARDWARE_SETTINGS), HardwareSettings.class);
276276
} catch (IOException e) {
277-
logger.error("Could not deserialize hardware settings! Loading defaults");
277+
logger.error("Could not deserialize hardware settings! Loading defaults", e);
278278
hardwareSettings = new HardwareSettings();
279279
}
280280

@@ -283,7 +283,7 @@ public void load() {
283283
JacksonUtils.deserialize(
284284
getOneConfigFile(conn, GlobalKeys.NETWORK_CONFIG), NetworkConfig.class);
285285
} catch (IOException e) {
286-
logger.error("Could not deserialize network config! Loading defaults");
286+
logger.error("Could not deserialize network config! Loading defaults", e);
287287
networkConfig = new NetworkConfig();
288288
}
289289

@@ -292,7 +292,7 @@ public void load() {
292292
JacksonUtils.deserialize(
293293
getOneConfigFile(conn, GlobalKeys.ATFL_CONFIG_FILE), AprilTagFieldLayout.class);
294294
} catch (IOException e) {
295-
logger.error("Could not deserialize apriltag layout! Loading defaults");
295+
logger.error("Could not deserialize apriltag layout! Loading defaults", e);
296296
try {
297297
atfl = AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);
298298
} catch (UncheckedIOException e2) {

photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NTDataPublisher.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import edu.wpi.first.math.geometry.Transform3d;
2121
import edu.wpi.first.networktables.NetworkTable;
2222
import edu.wpi.first.networktables.NetworkTableEvent;
23-
import edu.wpi.first.util.WPIUtilJNI;
23+
import edu.wpi.first.networktables.NetworkTablesJNI;
2424
import java.util.List;
2525
import java.util.function.BooleanSupplier;
2626
import java.util.function.Consumer;
@@ -146,13 +146,19 @@ public void accept(CVPipelineResult result) {
146146
List.of(),
147147
result.inputAndOutputFrame);
148148
else acceptedResult = result;
149-
var now = WPIUtilJNI.now();
150-
var captureMicros = MathUtils.nanosToMicros(acceptedResult.getImageCaptureTimestampNanos());
149+
var now = NetworkTablesJNI.now();
150+
var captureMicros = MathUtils.nanosToMicros(result.getImageCaptureTimestampNanos());
151+
152+
var offset = NetworkTablesManager.getInstance().getOffset();
153+
154+
// Transform the metadata timestamps from the local nt::Now timebase to the Time Sync Server's
155+
// timebase
151156
var simplified =
152157
new PhotonPipelineResult(
153158
acceptedResult.sequenceID,
154-
captureMicros,
155-
now,
159+
captureMicros + offset,
160+
now + offset,
161+
NetworkTablesManager.getInstance().getTimeSinceLastPong(),
156162
TrackedTarget.simpleFromTrackedTargets(acceptedResult.targets),
157163
acceptedResult.multiTagResult);
158164

0 commit comments

Comments
 (0)