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.server; 019 020import io.javalin.Javalin; 021import io.javalin.plugin.bundled.CorsPluginConfig; 022import java.net.InetSocketAddress; 023import java.util.List; 024import java.util.StringJoiner; 025import org.photonvision.common.dataflow.DataChangeDestination; 026import org.photonvision.common.dataflow.DataChangeService; 027import org.photonvision.common.dataflow.DataChangeSource; 028import org.photonvision.common.dataflow.DataChangeSubscriber; 029import org.photonvision.common.dataflow.events.DataChangeEvent; 030import org.photonvision.common.logging.LogGroup; 031import org.photonvision.common.logging.Logger; 032 033public class Server { 034 private static final Logger logger = new Logger(Server.class, LogGroup.WebServer); 035 036 private static Javalin app = null; 037 038 static class RestartSubscriber extends DataChangeSubscriber { 039 private RestartSubscriber() { 040 super(DataChangeSource.AllSources, List.of(DataChangeDestination.DCD_WEBSERVER)); 041 } 042 043 @Override 044 public void onDataChangeEvent(DataChangeEvent<?> event) { 045 if (event.propertyName.equals("restartServer")) { 046 Server.restart(); 047 } 048 } 049 } 050 051 public static void initialize(int port) { 052 DataChangeService.getInstance().addSubscriber(new RestartSubscriber()); 053 054 start(port); 055 } 056 057 private static void start(int port) { 058 app = 059 Javalin.create( 060 javalinConfig -> { 061 javalinConfig.showJavalinBanner = false; 062 javalinConfig.staticFiles.add("web"); 063 javalinConfig.plugins.enableCors( 064 corsContainer -> { 065 corsContainer.add(CorsPluginConfig::anyHost); 066 }); 067 068 javalinConfig.requestLogger.http( 069 (ctx, ms) -> { 070 StringJoiner joiner = 071 new StringJoiner(" ") 072 .add("Handled HTTP request of type") 073 .add(ctx.req().getMethod()) 074 .add("from endpoint") 075 .add(ctx.path()) 076 .add("of req size") 077 .add(Integer.toString(ctx.contentLength())) 078 .add("bytes & type") 079 .add(ctx.contentType()) 080 .add("with return code") 081 .add(Integer.toString(ctx.res().getStatus())) 082 .add("for host") 083 .add(ctx.req().getRemoteHost()) 084 .add("in") 085 .add(ms.toString()) 086 .add("ms"); 087 088 logger.debug(joiner.toString()); 089 }); 090 javalinConfig.requestLogger.ws( 091 ws -> { 092 ws.onMessage(ctx -> logger.debug("Got WebSockets message: " + ctx.message())); 093 ws.onBinaryMessage( 094 ctx -> 095 logger.trace( 096 () -> { 097 var remote = (InetSocketAddress) ctx.session.getRemoteAddress(); 098 var host = 099 remote.getAddress().toString() + ":" + remote.getPort(); 100 return "Got WebSockets binary message from host: " + host; 101 })); 102 }); 103 }); 104 105 /* Web Socket Events for Data Exchange */ 106 var dsHandler = DataSocketHandler.getInstance(); 107 app.ws( 108 "/websocket_data", 109 ws -> { 110 ws.onConnect(dsHandler::onConnect); 111 ws.onClose(dsHandler::onClose); 112 ws.onBinaryMessage(dsHandler::onBinaryMessage); 113 }); 114 115 /* API Events */ 116 // Settings 117 app.post("/api/settings", RequestHandler::onSettingsImportRequest); 118 app.get("/api/settings/photonvision_config.zip", RequestHandler::onSettingsExportRequest); 119 app.post("/api/settings/hardwareConfig", RequestHandler::onHardwareConfigRequest); 120 app.post("/api/settings/hardwareSettings", RequestHandler::onHardwareSettingsRequest); 121 app.post("/api/settings/networkConfig", RequestHandler::onNetworkConfigRequest); 122 app.post("/api/settings/aprilTagFieldLayout", RequestHandler::onAprilTagFieldLayoutRequest); 123 app.post("/api/settings/general", RequestHandler::onGeneralSettingsRequest); 124 app.post("/api/settings/camera", RequestHandler::onCameraSettingsRequest); 125 app.post("/api/settings/camera/setNickname", RequestHandler::onCameraNicknameChangeRequest); 126 app.get("/api/settings/camera/getCalibImages", RequestHandler::onCameraCalibImagesRequest); 127 128 // Utilities 129 app.post("/api/utils/offlineUpdate", RequestHandler::onOfflineUpdateRequest); 130 app.post( 131 "/api/utils/importObjectDetectionModel", 132 RequestHandler::onImportObjectDetectionModelRequest); 133 app.get("/api/utils/photonvision-journalctl.txt", RequestHandler::onLogExportRequest); 134 app.post("/api/utils/restartProgram", RequestHandler::onProgramRestartRequest); 135 app.post("/api/utils/restartDevice", RequestHandler::onDeviceRestartRequest); 136 app.post("/api/utils/publishMetrics", RequestHandler::onMetricsPublishRequest); 137 app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest); 138 app.get("/api/utils/getCalSnapshot", RequestHandler::onCalibrationSnapshotRequest); 139 app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest); 140 app.post("/api/utils/nukeConfigDirectory", RequestHandler::onNukeConfigDirectory); 141 app.post("/api/utils/nukeOneCamera", RequestHandler::onNukeOneCamera); 142 app.post("/api/utils/activateMatchedCamera", RequestHandler::onActivateMatchedCameraRequest); 143 app.post("/api/utils/assignUnmatchedCamera", RequestHandler::onAssignUnmatchedCameraRequest); 144 app.post("/api/utils/unassignCamera", RequestHandler::onUnassignCameraRequest); 145 146 // Calibration 147 app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest); 148 app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest); 149 150 app.start(port); 151 } 152 153 /** 154 * Seems like if we change the static IP of this device, Javalin refuses to tell us when new 155 * Websocket clients connect. As a hack, we can restart the server every time we change static IPs 156 */ 157 public static void restart() { 158 logger.info("Web server going down for restart"); 159 int oldPort = app.port(); 160 app.stop(); 161 start(oldPort); 162 } 163}