001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.jni;
019
020import edu.wpi.first.networktables.NetworkTablesJNI;
021
022/**
023 * Send ping-pongs to estimate server time, relative to nt::Now. The underlying implementation does
024 * technically allow us to provide a different source, but all photon code assumes nt::Now is used
025 */
026public class TimeSyncClient {
027    public static class PingMetadata {
028        // offset, us
029        public long offset;
030        // outgoing count
031        public long pingsSent;
032        // incoming count
033        public long pongsReceived;
034        // when we last heard back from the server, uS, in local time base
035        public long lastPongTime;
036        // RTT2, time from ping send to pong receive at the client, uS
037        public long rtt2;
038
039        public PingMetadata(
040                long offset, long pingsSent, long pongsReceived, long lastPongTime, long rtt2) {
041            this.offset = offset;
042            this.pingsSent = pingsSent;
043            this.pongsReceived = pongsReceived;
044            this.lastPongTime = lastPongTime;
045            this.rtt2 = rtt2;
046        }
047
048        @Override
049        public String toString() {
050            return "PingMetadata [offset="
051                    + offset
052                    + ", pingsSent="
053                    + pingsSent
054                    + ", pongsReceived="
055                    + pongsReceived
056                    + ", lastPongTime="
057                    + lastPongTime
058                    + ", rtt2="
059                    + rtt2
060                    + "]";
061        }
062
063        /**
064         * How long, in us, since we last heard back from the server
065         *
066         * @return Time between last pong RX and now, or Long.MAX_VALUE if we have heard zero pongs
067         */
068        public long timeSinceLastPong() {
069            // If no pongs, it's been forever
070            if (pongsReceived < 1) {
071                return Long.MAX_VALUE;
072            }
073
074            return NetworkTablesJNI.now() - lastPongTime;
075        }
076    }
077
078    private final Object mutex = new Object();
079
080    private long handle;
081    private String server;
082    private int port;
083    private double interval;
084
085    public TimeSyncClient(String server, int port, double interval) {
086        this.server = server;
087        this.port = port;
088        this.interval = interval;
089
090        this.handle = TimeSyncClient.create(server, port, interval);
091        TimeSyncClient.start(handle);
092    }
093
094    public void setServer(String newServer) {
095        if (!server.equals(newServer)) {
096            synchronized (mutex) {
097                stop();
098                this.handle = TimeSyncClient.create(newServer, port, interval);
099                TimeSyncClient.start(handle);
100                this.server = newServer;
101            }
102        }
103    }
104
105    public void stop() {
106        synchronized (mutex) {
107            if (handle != 0) {
108                TimeSyncClient.stop(handle);
109                handle = 0;
110            }
111        }
112    }
113
114    /**
115     * This offset, when added to the current value of nt::now(), yields the timestamp in the timebase
116     * of the TSP Server
117     *
118     * @return
119     */
120    public long getOffset() {
121        synchronized (mutex) {
122            if (handle != 0) {
123                return TimeSyncClient.getOffset(handle);
124            }
125
126            System.err.println("TimeSyncClient: use after free?");
127            return 0;
128        }
129    }
130
131    /**
132     * Best estimate of the current timestamp at the TSP server
133     *
134     * @return The current time estimate, in microseconds, at the TSP server
135     */
136    public long currentServerTimestamp() {
137        return NetworkTablesJNI.now() + getOffset();
138    }
139
140    public PingMetadata getPingMetadata() {
141        synchronized (mutex) {
142            if (handle != 0) {
143                return TimeSyncClient.getLatestMetadata(handle);
144            }
145
146            System.err.println("TimeSyncClient: use after free?");
147            return new PingMetadata(0, 0, 0, 0, 0);
148        }
149    }
150
151    public String getServer() {
152        return server;
153    }
154
155    private static native long create(String serverIP, int serverPort, double pingIntervalSeconds);
156
157    private static native void start(long handle);
158
159    private static native void stop(long handle);
160
161    private static native long getOffset(long handle);
162
163    private static native PingMetadata getLatestMetadata(long handle);
164}