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 org.wpilib.networktables.NetworkTablesJNI;
021
022/**
023 * Send ping-pongs to estimate server time, relative to wpi::nt::Now. The underlying implementation
024 * does technically allow us to provide a different source, but all photon code assumes wpi::nt::Now
025 * is used
026 */
027public class TimeSyncClient {
028    public static class PingMetadata {
029        // offset, us
030        public long offset;
031        // outgoing count
032        public long pingsSent;
033        // incoming count
034        public long pongsReceived;
035        // when we last heard back from the server, uS, in local time base
036        public long lastPongTime;
037        // RTT2, time from ping send to pong receive at the client, uS
038        public long rtt2;
039
040        public PingMetadata(
041                long offset, long pingsSent, long pongsReceived, long lastPongTime, long rtt2) {
042            this.offset = offset;
043            this.pingsSent = pingsSent;
044            this.pongsReceived = pongsReceived;
045            this.lastPongTime = lastPongTime;
046            this.rtt2 = rtt2;
047        }
048
049        @Override
050        public String toString() {
051            return "PingMetadata [offset="
052                    + offset
053                    + ", pingsSent="
054                    + pingsSent
055                    + ", pongsReceived="
056                    + pongsReceived
057                    + ", lastPongTime="
058                    + lastPongTime
059                    + ", rtt2="
060                    + rtt2
061                    + "]";
062        }
063
064        /**
065         * How long, in us, since we last heard back from the server
066         *
067         * @return Time between last pong RX and now, or Long.MAX_VALUE if we have heard zero pongs
068         */
069        public long timeSinceLastPong() {
070            // If no pongs, it's been forever
071            if (pongsReceived < 1) {
072                return Long.MAX_VALUE;
073            }
074
075            return NetworkTablesJNI.now() - lastPongTime;
076        }
077    }
078
079    private final Object mutex = new Object();
080
081    private long handle;
082    private String server;
083    private int port;
084    private double interval;
085
086    public TimeSyncClient(String server, int port, double interval) {
087        this.server = server;
088        this.port = port;
089        this.interval = interval;
090
091        this.handle = TimeSyncClient.create(server, port, interval);
092        TimeSyncClient.start(handle);
093    }
094
095    public void setServer(String newServer) {
096        if (!server.equals(newServer)) {
097            synchronized (mutex) {
098                stop();
099                this.handle = TimeSyncClient.create(newServer, port, interval);
100                TimeSyncClient.start(handle);
101                this.server = newServer;
102            }
103        }
104    }
105
106    public void stop() {
107        synchronized (mutex) {
108            if (handle != 0) {
109                TimeSyncClient.stop(handle);
110                handle = 0;
111            }
112        }
113    }
114
115    /**
116     * This offset, when added to the current value of wpi::nt::now(), yields the timestamp in the
117     * timebase of the TSP Server
118     *
119     * @return
120     */
121    public long getOffset() {
122        synchronized (mutex) {
123            if (handle != 0) {
124                return TimeSyncClient.getOffset(handle);
125            }
126
127            System.err.println("TimeSyncClient: use after free?");
128            return 0;
129        }
130    }
131
132    /**
133     * Best estimate of the current timestamp at the TSP server
134     *
135     * @return The current time estimate, in microseconds, at the TSP server
136     */
137    public long currentServerTimestamp() {
138        return NetworkTablesJNI.now() + getOffset();
139    }
140
141    public PingMetadata getPingMetadata() {
142        synchronized (mutex) {
143            if (handle != 0) {
144                return TimeSyncClient.getLatestMetadata(handle);
145            }
146
147            System.err.println("TimeSyncClient: use after free?");
148            return new PingMetadata(0, 0, 0, 0, 0);
149        }
150    }
151
152    public String getServer() {
153        return server;
154    }
155
156    private static native long create(String serverIP, int serverPort, double pingIntervalSeconds);
157
158    private static native void start(long handle);
159
160    private static native void stop(long handle);
161
162    private static native long getOffset(long handle);
163
164    private static native PingMetadata getLatestMetadata(long handle);
165}