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.apriltag.AprilTagFieldLayout; 021import edu.wpi.first.networktables.LogMessage; 022import edu.wpi.first.networktables.NetworkTable; 023import edu.wpi.first.networktables.NetworkTableEvent; 024import edu.wpi.first.networktables.NetworkTableEvent.Kind; 025import edu.wpi.first.networktables.NetworkTableInstance; 026import edu.wpi.first.networktables.StringSubscriber; 027import java.io.IOException; 028import java.util.EnumSet; 029import java.util.HashMap; 030import org.photonvision.PhotonVersion; 031import org.photonvision.common.configuration.ConfigManager; 032import org.photonvision.common.configuration.NetworkConfig; 033import org.photonvision.common.dataflow.DataChangeService; 034import org.photonvision.common.dataflow.events.OutgoingUIEvent; 035import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration; 036import org.photonvision.common.hardware.HardwareManager; 037import org.photonvision.common.logging.LogGroup; 038import org.photonvision.common.logging.LogLevel; 039import org.photonvision.common.logging.Logger; 040import org.photonvision.common.scripting.ScriptEventType; 041import org.photonvision.common.scripting.ScriptManager; 042import org.photonvision.common.util.TimedTaskManager; 043import org.photonvision.common.util.file.JacksonUtils; 044 045public class NetworkTablesManager { 046 private static final Logger logger = 047 new Logger(NetworkTablesManager.class, LogGroup.NetworkTables); 048 049 private final NetworkTableInstance ntInstance = NetworkTableInstance.getDefault(); 050 private final String kRootTableName = "/photonvision"; 051 private final String kFieldLayoutName = "apriltag_field_layout"; 052 public final NetworkTable kRootTable = ntInstance.getTable(kRootTableName); 053 054 private boolean m_isRetryingConnection = false; 055 056 private StringSubscriber m_fieldLayoutSubscriber = 057 kRootTable.getStringTopic(kFieldLayoutName).subscribe(""); 058 059 private final TimeSyncManager m_timeSync = new TimeSyncManager(kRootTable); 060 061 private NetworkTablesManager() { 062 ntInstance.addLogger( 063 LogMessage.kInfo, LogMessage.kCritical, this::logNtMessage); // to hide error messages 064 ntInstance.addConnectionListener(true, this::checkNtConnectState); // to hide error messages 065 066 ntInstance.addListener( 067 m_fieldLayoutSubscriber, EnumSet.of(Kind.kValueAll), this::onFieldLayoutChanged); 068 069 // Get the UI state in sync with the backend. NT should fire a callback when it first connects 070 // to the robot 071 broadcastConnectedStatus(); 072 } 073 074 public void registerTimedTasks() { 075 m_timeSync.start(); 076 TimedTaskManager.getInstance().addTask("NTManager", this::ntTick, 5000); 077 } 078 079 private static NetworkTablesManager INSTANCE; 080 081 public static NetworkTablesManager getInstance() { 082 if (INSTANCE == null) INSTANCE = new NetworkTablesManager(); 083 return INSTANCE; 084 } 085 086 private void logNtMessage(NetworkTableEvent event) { 087 String levelmsg = "DEBUG"; 088 LogLevel pvlevel = LogLevel.DEBUG; 089 if (event.logMessage.level >= LogMessage.kCritical) { 090 pvlevel = LogLevel.ERROR; 091 levelmsg = "CRITICAL"; 092 } else if (event.logMessage.level >= LogMessage.kError) { 093 pvlevel = LogLevel.ERROR; 094 levelmsg = "ERROR"; 095 } else if (event.logMessage.level >= LogMessage.kWarning) { 096 pvlevel = LogLevel.WARN; 097 levelmsg = "WARNING"; 098 } else if (event.logMessage.level >= LogMessage.kInfo) { 099 pvlevel = LogLevel.INFO; 100 levelmsg = "INFO"; 101 } 102 103 logger.log( 104 "NT: " 105 + levelmsg 106 + " " 107 + event.logMessage.level 108 + ": " 109 + event.logMessage.message 110 + " (" 111 + event.logMessage.filename 112 + ":" 113 + event.logMessage.line 114 + ")", 115 pvlevel); 116 } 117 118 public void checkNtConnectState(NetworkTableEvent event) { 119 var isConnEvent = event.is(Kind.kConnected); 120 var isDisconnEvent = event.is(Kind.kDisconnected); 121 122 if (isDisconnEvent) { 123 var msg = 124 String.format( 125 "NT lost connection to %s:%d! (NT version %d). Will retry in background.", 126 event.connInfo.remote_ip, 127 event.connInfo.remote_port, 128 event.connInfo.protocol_version); 129 logger.error(msg); 130 HardwareManager.getInstance().setNTConnected(false); 131 132 getInstance().broadcastConnectedStatus(); 133 } else if (isConnEvent && event.connInfo != null) { 134 var msg = 135 String.format( 136 "NT connected to %s:%d! (NT version %d)", 137 event.connInfo.remote_ip, 138 event.connInfo.remote_port, 139 event.connInfo.protocol_version); 140 logger.info(msg); 141 HardwareManager.getInstance().setNTConnected(true); 142 143 ScriptManager.queueEvent(ScriptEventType.kNTConnected); 144 getInstance().broadcastVersion(); 145 getInstance().broadcastConnectedStatus(); 146 147 m_timeSync.reportNtConnected(); 148 } else if (isConnEvent) { 149 logger.warn("Got connection event with no connection info??"); 150 } else { 151 logger.warn("Got a non-sensical connection message that is neither connect nor disconnect?"); 152 } 153 } 154 155 public NetworkTableInstance getNTInst() { 156 return ntInstance; 157 } 158 159 private void onFieldLayoutChanged(NetworkTableEvent event) { 160 var atfl_json = event.valueData.value.getString(); 161 try { 162 System.out.println("Got new field layout!"); 163 var atfl = JacksonUtils.deserialize(atfl_json, AprilTagFieldLayout.class); 164 ConfigManager.getInstance().getConfig().setApriltagFieldLayout(atfl); 165 ConfigManager.getInstance().requestSave(); 166 DataChangeService.getInstance() 167 .publishEvent( 168 new OutgoingUIEvent<>( 169 "fullsettings", 170 UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig()))); 171 } catch (IOException e) { 172 logger.error("Error deserializing atfl!"); 173 logger.error(atfl_json); 174 } 175 } 176 177 public void broadcastConnectedStatus() { 178 TimedTaskManager.getInstance().addOneShotTask(this::broadcastConnectedStatusImpl, 1000L); 179 } 180 181 private void broadcastConnectedStatusImpl() { 182 HashMap<String, Object> map = new HashMap<>(); 183 var subMap = new HashMap<String, Object>(); 184 185 subMap.put("connected", ntInstance.isConnected()); 186 if (ntInstance.isConnected()) { 187 var connections = ntInstance.getConnections(); 188 if (connections.length > 0) { 189 subMap.put("address", connections[0].remote_ip + ":" + connections[0].remote_port); 190 } 191 subMap.put("clients", connections.length); 192 } 193 194 map.put("ntConnectionInfo", subMap); 195 DataChangeService.getInstance() 196 .publishEvent(new OutgoingUIEvent<>("networkTablesConnected", map)); 197 } 198 199 private void broadcastVersion() { 200 kRootTable.getEntry("version").setString(PhotonVersion.versionString); 201 kRootTable.getEntry("buildDate").setString(PhotonVersion.buildDate); 202 } 203 204 public void setConfig(NetworkConfig config) { 205 if (config.runNTServer) { 206 setServerMode(); 207 } else { 208 setClientMode(config.ntServerAddress); 209 } 210 211 m_timeSync.setConfig(config); 212 213 broadcastVersion(); 214 } 215 216 public long getOffset() { 217 return m_timeSync.getOffset(); 218 } 219 220 private void setClientMode(String ntServerAddress) { 221 ntInstance.stopServer(); 222 ntInstance.startClient4("photonvision"); 223 try { 224 int t = Integer.parseInt(ntServerAddress); 225 if (!m_isRetryingConnection) logger.info("Starting NT Client, server team is " + t); 226 ntInstance.setServerTeam(t); 227 } catch (NumberFormatException e) { 228 if (!m_isRetryingConnection) 229 logger.info("Starting NT Client, server IP is \"" + ntServerAddress + "\""); 230 ntInstance.setServer(ntServerAddress); 231 } 232 ntInstance.startDSClient(); 233 broadcastVersion(); 234 } 235 236 private void setServerMode() { 237 logger.info("Starting NT Server"); 238 ntInstance.stopClient(); 239 ntInstance.startServer(); 240 broadcastVersion(); 241 } 242 243 // So it seems like if Photon starts before the robot NT server does, and both aren't static IP, 244 // it'll never connect. This hack works around it by restarting the client/server while the nt 245 // instance 246 // isn't connected, same as clicking the save button in the settings menu (or restarting the 247 // service) 248 private void ntTick() { 249 if (!ntInstance.isConnected() 250 && !ConfigManager.getInstance().getConfig().getNetworkConfig().runNTServer) { 251 setConfig(ConfigManager.getInstance().getConfig().getNetworkConfig()); 252 } 253 254 if (!ntInstance.isConnected() && !m_isRetryingConnection) { 255 m_isRetryingConnection = true; 256 logger.error( 257 "[NetworkTablesManager] Could not connect to the robot! Will retry in the background..."); 258 } 259 } 260 261 public long getTimeSinceLastPong() { 262 return m_timeSync.getTimeSinceLastPong(); 263 } 264}