/* * Copyright (C) Photon Vision. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.photonvision.server; import io.javalin.Javalin; import io.javalin.plugin.bundled.CorsPluginConfig; import java.net.InetSocketAddress; import java.util.List; import java.util.StringJoiner; import org.photonvision.common.dataflow.DataChangeDestination; import org.photonvision.common.dataflow.DataChangeService; import org.photonvision.common.dataflow.DataChangeSource; import org.photonvision.common.dataflow.DataChangeSubscriber; import org.photonvision.common.dataflow.events.DataChangeEvent; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.Logger; public class Server { private static final Logger logger = new Logger(Server.class, LogGroup.WebServer); private static Javalin app = null; static class RestartSubscriber extends DataChangeSubscriber { private RestartSubscriber() { super(DataChangeSource.AllSources, List.of(DataChangeDestination.DCD_WEBSERVER)); } @Override public void onDataChangeEvent(DataChangeEvent event) { if (event.propertyName.equals("restartServer")) { Server.restart(); } } } public static void initialize(int port) { DataChangeService.getInstance().addSubscriber(new RestartSubscriber()); start(port); } private static void start(int port) { app = Javalin.create( javalinConfig -> { javalinConfig.showJavalinBanner = false; javalinConfig.staticFiles.add("web"); javalinConfig.plugins.enableCors( corsContainer -> { corsContainer.add(CorsPluginConfig::anyHost); }); // Increase the upload size limit (arbitrary, but need to be able to deal with large // calibration JSONs) javalinConfig.http.maxRequestSize = (long) (50 * 1e6); javalinConfig.requestLogger.http( (ctx, ms) -> { StringJoiner joiner = new StringJoiner(" ") .add("Handled HTTP request of type") .add(ctx.req().getMethod()) .add("from endpoint") .add(ctx.path()) .add("for host") .add(ctx.req().getRemoteHost()) .add("in") .add(ms.toString()) .add("ms"); logger.debug(joiner.toString()); }); javalinConfig.requestLogger.ws( ws -> { ws.onMessage(ctx -> logger.debug("Got WebSockets message: " + ctx.message())); ws.onBinaryMessage( ctx -> logger.trace( () -> { var remote = (InetSocketAddress) ctx.session.getRemoteAddress(); var host = remote.getAddress().toString() + ":" + remote.getPort(); return "Got WebSockets binary message from host: " + host; })); }); }); /*Web Socket Events for Data Exchange */ var dsHandler = DataSocketHandler.getInstance(); app.ws( "/websocket_data", ws -> { ws.onConnect(dsHandler::onConnect); ws.onClose(dsHandler::onClose); ws.onBinaryMessage(dsHandler::onBinaryMessage); }); /*API Events*/ // Settings app.post("/api/settings", RequestHandler::onSettingsImportRequest); app.get("/api/settings/photonvision_config.zip", RequestHandler::onSettingsExportRequest); app.post("/api/settings/hardwareConfig", RequestHandler::onHardwareConfigRequest); app.post("/api/settings/hardwareSettings", RequestHandler::onHardwareSettingsRequest); app.post("/api/settings/networkConfig", RequestHandler::onNetworkConfigRequest); app.post("/api/settings/aprilTagFieldLayout", RequestHandler::onAprilTagFieldLayoutRequest); app.post("/api/settings/general", RequestHandler::onGeneralSettingsRequest); app.post("/api/settings/camera", RequestHandler::onCameraSettingsRequest); app.post("/api/settings/camera/setNickname", RequestHandler::onCameraNicknameChangeRequest); app.get("/api/settings/camera/getCalibImages", RequestHandler::onCameraCalibImagesRequest); // Utilities app.post("/api/utils/offlineUpdate", RequestHandler::onOfflineUpdateRequest); app.get("/api/utils/photonvision-journalctl.txt", RequestHandler::onLogExportRequest); app.post("/api/utils/restartProgram", RequestHandler::onProgramRestartRequest); app.post("/api/utils/restartDevice", RequestHandler::onDeviceRestartRequest); app.post("/api/utils/publishMetrics", RequestHandler::onMetricsPublishRequest); app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest); app.get("/api/utils/getCalSnapshot", RequestHandler::onCalibrationSnapshotRequest); app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest); // Calibration app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest); app.post( "/api/calibration/importFromCalibDB", RequestHandler::onCalibDBCalibrationImportRequest); app.post("/api/calibration/importFromData", RequestHandler::onDataCalibrationImportRequest); app.start(port); } /** * Seems like if we change the static IP of this device, Javalin refuses to tell us when new * Websocket clients connect. As a hack, we can restart the server every time we change static IPs */ public static void restart() { logger.info("Web server going down for restart"); int oldPort = app.port(); app.stop(); start(oldPort); } }