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}