/* * Copyright (C) Photon Vision. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.photonvision.jni; import edu.wpi.first.networktables.NetworkTablesJNI; /** * Send ping-pongs to estimate server time, relative to nt::Now. The underlying implementation does * technically allow us to provide a different source, but all photon code assumes nt::Now is used */ public class TimeSyncClient { public static class PingMetadata { // offset, us public long offset; // outgoing count public long pingsSent; // incoming count public long pongsReceived; // when we last heard back from the server public long lastPongTime; // RTT2, time from ping send to pong recieve at the client public long rtt2; public PingMetadata( long offset, long pingsSent, long pongsReceived, long lastPongTime, long rtt2) { this.offset = offset; this.pingsSent = pingsSent; this.pongsReceived = pongsReceived; this.lastPongTime = lastPongTime; this.rtt2 = rtt2; } @Override public String toString() { return "PingMetadata [offset=" + offset + ", pingsSent=" + pingsSent + ", pongsReceived=" + pongsReceived + ", lastPongTime=" + lastPongTime + ", rtt2=" + rtt2 + "]"; } /** * How long, in us, since we last heard back from the server * * @return Time between last pong RX and now, or Long.MAX_VALUE if we have heard zero pongs */ public long timeSinceLastPong() { // If no pongs, it's been forever if (pongsReceived < 1) { return Long.MAX_VALUE; } return NetworkTablesJNI.now() - lastPongTime; } } private final Object mutex = new Object(); private long handle; private String server; private int port; private double interval; public TimeSyncClient(String server, int port, double interval) { this.server = server; this.port = port; this.interval = interval; synchronized (mutex) { this.handle = TimeSyncClient.create(server, port, interval); TimeSyncClient.start(handle); } } public void setServer(String newServer) { if (!server.equals(newServer)) { synchronized (mutex) { stop(); this.handle = TimeSyncClient.create(newServer, port, interval); TimeSyncClient.start(handle); this.server = newServer; } } } public void stop() { synchronized (mutex) { if (handle > 0) { TimeSyncClient.stop(handle); handle = 0; } } } /** * This offset, when added to the current value of nt::now(), yields the timestamp in the timebase * of the TSP Server * * @return */ public long getOffset() { synchronized (mutex) { return TimeSyncClient.getOffset(handle); } } /** * Best estimate of the current timestamp at the TSP server * * @return The current time estimate, in microseconds, at the TSP server */ public long currentServerTimestamp() { return NetworkTablesJNI.now() + getOffset(); } public PingMetadata getPingMetadata() { synchronized (mutex) { return TimeSyncClient.getLatestMetadata(handle); } } public String getServer() { return server; } private static native long create(String serverIP, int serverPort, double pingIntervalSeconds); private static native void start(long handle); private static native void stop(long handle); private static native long getOffset(long handle); private static native PingMetadata getLatestMetadata(long handle); }