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.common.dataflow.networktables;
019
020import edu.wpi.first.cscore.CameraServerJNI;
021import edu.wpi.first.networktables.IntegerPublisher;
022import edu.wpi.first.networktables.NetworkTable;
023import edu.wpi.first.networktables.NetworkTableInstance;
024import org.photonvision.common.configuration.NetworkConfig;
025import org.photonvision.common.logging.LogGroup;
026import org.photonvision.common.logging.Logger;
027import org.photonvision.common.util.TimedTaskManager;
028import org.photonvision.jni.PhotonTargetingJniLoader;
029import org.photonvision.jni.TimeSyncClient;
030import org.photonvision.jni.TimeSyncServer;
031
032public class TimeSyncManager {
033    private static final Logger logger = new Logger(TimeSyncManager.class, LogGroup.NetworkTables);
034
035    private TimeSyncClient m_client = null;
036    private TimeSyncServer m_server = null;
037
038    private NetworkTableInstance ntInstance;
039    IntegerPublisher m_offsetPub;
040    IntegerPublisher m_rtt2Pub;
041    IntegerPublisher m_pingsPub;
042    IntegerPublisher m_pongsPub;
043    IntegerPublisher m_lastPongTimePub;
044
045    public TimeSyncManager(NetworkTable kRootTable) {
046        if (!PhotonTargetingJniLoader.isWorking) {
047            logger.error("PhotonTargetingJNI was not loaded! Cannot do time-sync");
048        }
049
050        this.ntInstance = kRootTable.getInstance();
051
052        // Need this subtable to be unique per coprocessor. TODO: consider using MAC address or
053        // something similar for metrics?
054        var timeTable = kRootTable.getSubTable(".timesync").getSubTable(CameraServerJNI.getHostname());
055        m_offsetPub = timeTable.getIntegerTopic("offset_us").publish();
056        m_rtt2Pub = timeTable.getIntegerTopic("rtt2_us").publish();
057        m_pingsPub = timeTable.getIntegerTopic("ping_tx_count").publish();
058        m_pongsPub = timeTable.getIntegerTopic("pong_rx_count").publish();
059        m_lastPongTimePub = timeTable.getIntegerTopic("pong_rx_time_us").publish();
060
061        // default to being a client
062        logger.debug("Starting TimeSyncClient on localhost (for now)");
063        m_client = new TimeSyncClient("127.0.0.1", 5810, 1.0);
064    }
065
066    // Since we're spinning off tasks in a new thread, be careful and start it seperately
067    public void start() {
068        if (!PhotonTargetingJniLoader.isWorking) {
069            logger.error("PhotonTargetingJNI was not loaded! Cannot start");
070        }
071
072        TimedTaskManager.getInstance().addTask("TimeSyncManager::tick", this::tick, 1000);
073    }
074
075    public synchronized long getOffset() {
076        if (!PhotonTargetingJniLoader.isWorking) {
077            return 0;
078        }
079
080        // if we're a client, return the offset to server time
081        if (m_client != null) return m_client.getOffset();
082        // if we're a server, our time (nt::Now) is the same as network time
083        if (m_server != null) return 0;
084
085        // ????? should never hit
086        logger.error("Client and server and null?");
087        return 0;
088    }
089
090    synchronized void setConfig(NetworkConfig config) {
091        if (!PhotonTargetingJniLoader.isWorking) {
092            return;
093        }
094
095        if (m_client == null && m_server == null) {
096            throw new RuntimeException("Neither client nor server are null?");
097        }
098
099        // if not already running a server, set it up
100        if (config.runNTServer && m_server == null) {
101            // tear down anything old
102            if (m_client != null) {
103                logger.debug("Tearing down old client");
104                m_client.stop();
105                m_client = null;
106            }
107
108            logger.debug("Starting TimeSyncServer");
109            m_server = new TimeSyncServer(5810);
110            m_server.start();
111        } else
112        // if not already running a client, set it up
113        if (m_client == null) {
114            // tear down anything old
115            if (m_server != null) {
116                logger.debug("Tearing down old server");
117                m_server.stop();
118                m_server = null;
119            }
120
121            // Guess at IP -- tick will take care of changing this (may take up to 1 second)
122            logger.debug("Starting TimeSyncClient on localhost (for now)");
123            m_client = new TimeSyncClient("127.0.0.1", 5810, 1.0);
124        }
125    }
126
127    synchronized void tick() {
128        if (m_client != null) {
129            var conns = ntInstance.getConnections();
130
131            if (conns.length > 0) {
132                var newServer = conns[0].remote_ip;
133                if (!m_client.getServer().equals(newServer)) {
134                    logger.debug("Changing TimeSyncClient server to " + newServer);
135                    m_client.setServer(newServer);
136                }
137            }
138
139            if (m_client != null) {
140                var m = m_client.getPingMetadata();
141
142                m_offsetPub.set(m.offset);
143                m_rtt2Pub.set(m.rtt2);
144                m_pingsPub.set(m.pingsSent);
145                m_pongsPub.set(m.pongsReceived);
146                m_lastPongTimePub.set(m.lastPongTime);
147            }
148        }
149    }
150
151    public synchronized long getTimeSinceLastPong() {
152        if (m_client != null) {
153            return m_client.getPingMetadata().timeSinceLastPong();
154        } else if (m_server != null) {
155            return 0;
156        } else {
157            // ????
158            return 0;
159        }
160    }
161
162    /** Restart our timesync client if NT just connected */
163    public synchronized void reportNtConnected() {
164        if (m_client != null) {
165            // restart (in java code; we could just add a reset metrics function...)
166            logger.debug(
167                    "NT (re)connected -- restarting Time Sync Client at " + m_client.getServer() + ":5810");
168            m_client.stop();
169            m_client = new TimeSyncClient(m_client.getServer(), 5810, 1.0);
170        }
171    }
172}