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}