diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 07179431a..868599527 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -59,6 +59,49 @@ jobs:
- name: Build C++ examples
working-directory: photonlib-cpp-examples
run: ./gradlew build
+ playwright-tests:
+ name: "Playwright E2E tests"
+ runs-on: ubuntu-22.04
+ needs: [validation]
+ steps:
+ # Checkout code.
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Fetch tags
+ run: git fetch --tags --force
+ - name: Install Java 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: temurin
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ - name: Install mrcal deps
+ run: sudo apt-get update && sudo apt-get install -y libcholmod3 liblapack3 libsuitesparseconfig5
+ - name: Setup tests
+ working-directory: photon-client
+ run: |
+ pnpm install
+ pnpm test-setup
+ - name: Prebuild Gradle
+ run: ./gradlew photon-targeting:build photon-core:build photon-server:build -x check
+ - name: Run Playwright tests
+ working-directory: photon-client
+ run: pnpm test
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: photon-client/playwright-report/
+ retention-days: 30
build-gradle:
name: "Gradle Build"
runs-on: ubuntu-22.04
diff --git a/.gitignore b/.gitignore
index 6416c2fba..6a3ddc0b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -146,5 +146,13 @@ networktables.json
photon-server/src/main/resources/web/*
node_modules
dist
+dist-ssr
components.d.ts
photon-server/src/main/resources/web/index.html
+
+# Playwright
+photon-client/test-results/
+photon-client/playwright-report/
+photon-client/blob-report/
+photon-client/playwright/.cache/
+photon-client/playwright/.auth/
diff --git a/photon-client/.gitignore b/photon-client/.gitignore
deleted file mode 100644
index 0f57d3477..000000000
--- a/photon-client/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-node_modules
-.DS_Store
-dist
-dist-ssr
-
-# Editor directories and files
-.idea
-components.d.ts
diff --git a/photon-client/eslint.config.mjs b/photon-client/eslint.config.mjs
index 792d7dcc6..435241641 100644
--- a/photon-client/eslint.config.mjs
+++ b/photon-client/eslint.config.mjs
@@ -8,7 +8,7 @@ export default defineConfigWithVueTs(
vueTsConfigs.recommended,
skipFormattingConfig,
{
- ignores: ["**/dist/**"]
+ ignores: ["**/dist/**", "playwright-report"]
},
{
//extends: ["js/recommended"],
diff --git a/photon-client/package.json b/photon-client/package.json
index cf2485b2d..db4a7230f 100644
--- a/photon-client/package.json
+++ b/photon-client/package.json
@@ -9,9 +9,13 @@
"build": "vite build",
"build-demo": "vite build --mode demo",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
- "format": "prettier --write src/",
+ "format": "prettier --write src/ tests/",
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
- "format-ci": "prettier --check src/"
+ "format-ci": "prettier --check src/",
+ "test": "playwright test",
+ "test-ui": "playwright test --ui",
+ "test-setup": "playwright install --with-deps"
+
},
"dependencies": {
"@fontsource/prompt": "^5.2.6",
@@ -28,6 +32,7 @@
},
"devDependencies": {
"@eslint/js": "^9.31.0",
+ "@playwright/test": "^1.56.1",
"@types/node": "^22.15.14",
"@types/three": "^0.178.0",
"@vitejs/plugin-vue": "^6.0.0",
diff --git a/photon-client/playwright.config.ts b/photon-client/playwright.config.ts
new file mode 100644
index 000000000..8ce62cbda
--- /dev/null
+++ b/photon-client/playwright.config.ts
@@ -0,0 +1,83 @@
+import { defineConfig, devices } from "@playwright/test";
+import path from "path";
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// import path from 'path';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ globalSetup: "./tests/global-setup",
+ testDir: "./tests",
+ /* Run tests in files in parallel */
+ fullyParallel: false,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: 1,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: "html",
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('')`. */
+ baseURL: "http://localhost:5800",
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: "on-first-retry"
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] }
+ },
+
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] }
+ },
+
+ {
+ name: "webkit",
+ use: { ...devices["Desktop Safari"] }
+ },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: { ...devices['Pixel 5'] },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: { ...devices['iPhone 12'] },
+ // },
+
+ /* Test against branded browsers. */
+ {
+ name: "Microsoft Edge",
+ use: { ...devices["Desktop Edge"], channel: "msedge" }
+ }
+ // {
+ // name: 'Google Chrome',
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: process.platform == "win32" ? "" : "./" + "gradlew run",
+ url: "http://localhost:5800",
+ timeout: 300 * 1000,
+ reuseExistingServer: !process.env.CI,
+ cwd: path.normalize("../")
+ }
+});
diff --git a/photon-client/pnpm-lock.yaml b/photon-client/pnpm-lock.yaml
index b93de8144..380943670 100644
--- a/photon-client/pnpm-lock.yaml
+++ b/photon-client/pnpm-lock.yaml
@@ -45,6 +45,9 @@ importers:
'@eslint/js':
specifier: ^9.31.0
version: 9.31.0
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
'@types/node':
specifier: ^22.15.14
version: 22.15.14
@@ -430,6 +433,11 @@ packages:
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@playwright/test@1.56.1':
+ resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@rolldown/pluginutils@1.0.0-beta.19':
resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
@@ -1019,6 +1027,11 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1247,6 +1260,16 @@ packages:
typescript:
optional: true
+ playwright-core@1.56.1:
+ resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.56.1:
+ resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
@@ -1759,6 +1782,10 @@ snapshots:
'@pkgr/core@0.2.4': {}
+ '@playwright/test@1.56.1':
+ dependencies:
+ playwright: 1.56.1
+
'@rolldown/pluginutils@1.0.0-beta.19': {}
'@rollup/rollup-android-arm-eabi@4.40.2':
@@ -2399,6 +2426,9 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -2605,6 +2635,14 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
+ playwright-core@1.56.1: {}
+
+ playwright@1.56.1:
+ dependencies:
+ playwright-core: 1.56.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
diff --git a/photon-client/src/components/settings/ObjectDetectionCard.vue b/photon-client/src/components/settings/ObjectDetectionCard.vue
index 53435b3d8..7b9a2b987 100644
--- a/photon-client/src/components/settings/ObjectDetectionCard.vue
+++ b/photon-client/src/components/settings/ObjectDetectionCard.vue
@@ -224,6 +224,7 @@ const handleBulkImport = () => {
v-model="importVersion"
variant="underlined"
label="Model Version"
+ data-testid="import-version-select"
:items="
useSettingsStore().general.supportedBackends?.includes('RKNN')
? ['YOLOv5', 'YOLOv8', 'YOLO11']
@@ -324,7 +325,7 @@ const handleBulkImport = () => {
Info |
-
+
| {{ model.nickname }} |
{{ model.labels.join(", ") }} |
diff --git a/photon-client/tests/fixtures.ts b/photon-client/tests/fixtures.ts
new file mode 100644
index 000000000..b5bedca41
--- /dev/null
+++ b/photon-client/tests/fixtures.ts
@@ -0,0 +1,16 @@
+import { test as base } from "@playwright/test";
+import axios from "axios";
+
+export const test = base.extend({
+ page: async ({ page }, use) => {
+ // Use the page in the test (no per-test backend reset here)
+ axios.defaults.baseURL = "http://localhost:5800/api/test";
+ await use(page);
+ }
+});
+
+test.beforeAll(async () => {
+ console.log("Running before all tests: Resetting backend state...");
+ await axios.post("http://localhost:5800/api/test/resetBackend");
+ await axios.post("http://localhost:5800/api/test/activateTestMode");
+});
diff --git a/photon-client/tests/global-setup.ts b/photon-client/tests/global-setup.ts
new file mode 100644
index 000000000..cfb7a590e
--- /dev/null
+++ b/photon-client/tests/global-setup.ts
@@ -0,0 +1,7 @@
+async function globalSetup() {
+ // You can perform global setup tasks here, such as starting a server or setting environment variables
+ const path = await import("path");
+ process.env.TESTS_DIR = path.resolve(process.cwd());
+}
+
+export default globalSetup;
diff --git a/photon-client/tests/platformDependent/od.spec.ts b/photon-client/tests/platformDependent/od.spec.ts
new file mode 100644
index 000000000..2ac32a194
--- /dev/null
+++ b/photon-client/tests/platformDependent/od.spec.ts
@@ -0,0 +1,79 @@
+import { expect } from "@playwright/test";
+import { test } from "../fixtures";
+import axios from "axios";
+import path from "path";
+
+const fakeModelName = "FAKE-MODEL";
+const fakeLabels = "test, 1, woof";
+const newModelName = "foo-bar";
+const platforms = ["LINUX_RK3588_64", "LINUX_QCS6490"];
+
+for (const platform of platforms) {
+ test.describe(`Platform: ${platform}`, () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/#/settings");
+ await axios.post("/override/platform", { platform: platform });
+ await page.reload();
+ });
+
+ test("testSettingsPage", async ({ page }) => {
+ if (platform.endsWith("RK3588_64")) {
+ await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with RK3588");
+ } else if (platform.endsWith("QCS6490")) {
+ await expect(page.getByRole("main")).toContainText("Linux AARCH 64-bit with QCS6490");
+ }
+ await expect(page.getByText("Object Detection")).toBeVisible();
+ });
+
+ test("Upload model", async ({ page }) => {
+ const testsDir = process.env.TESTS_DIR;
+ if (!testsDir) {
+ throw new Error("TESTS_DIR is not set");
+ }
+
+ await page.getByRole("button", { name: "Import Model" }).click();
+ await page.getByRole("textbox", { name: "Labels" }).fill(fakeLabels);
+ await page.getByRole("spinbutton", { name: "Width" }).fill("640");
+ await page.getByRole("spinbutton", { name: "Height" }).fill("640");
+ await page.getByTestId("import-version-select").click();
+ await page.getByRole("option", { name: "YOLOv8" }).click();
+
+ const modelFile = platform.endsWith("RK3588_64") ? `${fakeModelName}.rknn` : `${fakeModelName}.tflite`;
+ await page
+ .getByRole("button", { name: "Model File Model File" })
+ .setInputFiles(path.join(testsDir, "tests/resources", modelFile));
+
+ await page.getByRole("button", { name: "Import Object Detection Model" }).click();
+
+ await page.goto("/#/settings");
+ const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
+
+ await expect(tableRow).toBeVisible();
+ await expect(tableRow).toContainText(fakeLabels);
+ });
+
+ test("Rename model", async ({ page }) => {
+ const tableRow = page.getByTestId("model-table").locator("tr", { hasText: fakeModelName });
+
+ await tableRow.getByRole("button", { name: "Rename Model" }).click();
+ await page.getByRole("textbox", { name: "New Name New Name" }).fill(newModelName);
+ await page.getByRole("button", { name: "Rename", exact: true }).click();
+
+ await page.reload();
+
+ const renamedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
+ await expect(renamedRow).toContainText(fakeLabels);
+ });
+
+ test("Delete model", async ({ page }) => {
+ const tableRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
+
+ await tableRow.getByRole("button", { name: "Delete Model" }).click();
+ await page.getByRole("button", { name: "Delete model", exact: true }).click();
+
+ await page.reload();
+ const deletedRow = page.getByTestId("model-table").locator("tr", { hasText: newModelName });
+ await expect(deletedRow).toHaveCount(0);
+ });
+ });
+}
diff --git a/photon-client/tests/resources/FAKE-MODEL.rknn b/photon-client/tests/resources/FAKE-MODEL.rknn
new file mode 100644
index 000000000..e69de29bb
diff --git a/photon-client/tests/resources/FAKE-MODEL.tflite b/photon-client/tests/resources/FAKE-MODEL.tflite
new file mode 100644
index 000000000..e69de29bb
diff --git a/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java b/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java
index d1063ded4..6b789402e 100644
--- a/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java
+++ b/photon-core/src/main/java/org/photonvision/common/configuration/NeuralNetworkModelManager.java
@@ -212,12 +212,23 @@ public class NeuralNetworkModelManager {
}
/**
- * Returns the singleton instance of the NeuralNetworkModelManager
+ * Returns the singleton instance of the NeuralNetworkModelManager. Call getInstance() to use the
+ * default (no reset), or getInstance(true) to reset.
*
* @return The singleton instance
*/
public static NeuralNetworkModelManager getInstance() {
- if (INSTANCE == null) {
+ return getInstance(false);
+ }
+
+ /**
+ * Returns the singleton instance of the NeuralNetworkModelManager, optionally resetting it.
+ *
+ * @param reset If true, resets the instance
+ * @return The singleton instance
+ */
+ public static NeuralNetworkModelManager getInstance(boolean reset) {
+ if (INSTANCE == null || reset) {
INSTANCE = new NeuralNetworkModelManager();
}
return INSTANCE;
diff --git a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java
index 67b60070a..b655cb656 100644
--- a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java
+++ b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java
@@ -74,6 +74,12 @@ public class RequestHandler {
private static final ObjectMapper kObjectMapper = new ObjectMapper();
+ private static boolean testMode = false;
+
+ public static void setTestMode(boolean isTestMode) {
+ testMode = isTestMode;
+ }
+
private record CommonCameraUniqueName(String cameraUniqueName) {}
public static void onSettingsImportRequest(Context ctx) {
@@ -662,35 +668,36 @@ public class RequestHandler {
ModelProperties modelProperties =
new ModelProperties(modelPath, nickname, labels, width, height, family, version);
- ObjectDetector objDetector = null;
-
- try {
- objDetector =
- switch (family) {
- case RUBIK -> new RubikModel(modelProperties).load();
- case RKNN -> new RknnModel(modelProperties).load();
- };
- } catch (RuntimeException e) {
- ctx.status(400);
- ctx.result("Failed to load object detection model: " + e.getMessage());
+ if (!testMode) {
+ ObjectDetector objDetector = null;
try {
- Files.deleteIfExists(modelPath);
- } catch (IOException ex) {
- e.addSuppressed(ex);
- }
+ objDetector =
+ switch (family) {
+ case RUBIK -> new RubikModel(modelProperties).load();
+ case RKNN -> new RknnModel(modelProperties).load();
+ };
+ } catch (RuntimeException e) {
+ ctx.status(400);
+ ctx.result("Failed to load object detection model: " + e.getMessage());
- logger.error("Failed to load object detection model", e);
- return;
- } finally {
- // this finally block will run regardless of what happens in try/catch
- // please see https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html
- // for a summary on how finally works
- if (objDetector != null) {
- objDetector.release();
+ try {
+ Files.deleteIfExists(modelPath);
+ } catch (IOException ex) {
+ e.addSuppressed(ex);
+ }
+
+ logger.error("Failed to load object detection model", e);
+ return;
+ } finally {
+ // this finally block will run regardless of what happens in try/catch
+ // please see https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html
+ // for a summary on how finally works
+ if (objDetector != null) {
+ objDetector.release();
+ }
}
}
-
ConfigManager.getInstance()
.getConfig()
.neuralNetworkPropertyManager()
diff --git a/photon-server/src/main/java/org/photonvision/server/Server.java b/photon-server/src/main/java/org/photonvision/server/Server.java
index ec54caf25..ee2922c26 100644
--- a/photon-server/src/main/java/org/photonvision/server/Server.java
+++ b/photon-server/src/main/java/org/photonvision/server/Server.java
@@ -162,6 +162,13 @@ public class Server {
app.post("/api/objectdetection/rename", RequestHandler::onRenameObjectDetectionModelRequest);
app.post("/api/objectdetection/nuke", RequestHandler::onNukeObjectDetectionModelsRequest);
+ /* Testing API Events */
+
+ app.post("/api/test/resetBackend", TestRequestHandler::handleResetRequest);
+
+ app.post("/api/test/activateTestMode", TestRequestHandler::testMode);
+ app.post("/api/test/override/platform", TestRequestHandler::handlePlatformOverrideRequest);
+
app.start(port);
}
diff --git a/photon-server/src/main/java/org/photonvision/server/TestRequestHandler.java b/photon-server/src/main/java/org/photonvision/server/TestRequestHandler.java
new file mode 100644
index 000000000..6f888c78a
--- /dev/null
+++ b/photon-server/src/main/java/org/photonvision/server/TestRequestHandler.java
@@ -0,0 +1,67 @@
+/*
+ * 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.http.Context;
+import org.photonvision.common.configuration.ConfigManager;
+import org.photonvision.common.configuration.NeuralNetworkModelManager;
+import org.photonvision.common.hardware.Platform;
+import org.photonvision.common.logging.LogGroup;
+import org.photonvision.common.logging.Logger;
+import org.photonvision.common.util.file.JacksonUtils;
+
+public class TestRequestHandler {
+ // Treat all 2XX calls as "INFO"
+ // Treat all 4XX calls as "ERROR"
+ // Treat all 5XX calls as "ERROR"
+
+ static Logger logger = new Logger(TestRequestHandler.class, LogGroup.WebServer);
+
+ public static void handleResetRequest(Context ctx) {
+ logger.info("Resetting Backend");
+ // Reset backend
+ ConfigManager.nukeConfigDirectory();
+ ConfigManager.getInstance().load();
+ }
+
+ private record PlatformOverrideRequest(Platform platform) {}
+
+ public static void handlePlatformOverrideRequest(Context ctx) {
+ try {
+ PlatformOverrideRequest request =
+ JacksonUtils.deserialize(ctx.body(), PlatformOverrideRequest.class);
+ Platform platform = request.platform();
+ logger.info("Overriding platform to: " + platform);
+
+ Platform.overridePlatform(platform);
+ NeuralNetworkModelManager.getInstance(true).extractModels();
+ NeuralNetworkModelManager.getInstance().discoverModels();
+ ctx.status(200);
+
+ } catch (Exception e) {
+ logger.error("Failed to parse platform override request: " + e.getMessage());
+ ctx.status(400).result("Invalid request");
+ }
+ }
+
+ public static void testMode(Context ctx) {
+ logger.info("Test mode activated");
+ RequestHandler.setTestMode(true);
+ ctx.status(200).result("Test mode activated");
+ }
+}
diff --git a/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java
index efe06760e..1080f40ab 100644
--- a/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java
+++ b/photon-targeting/src/main/java/org/photonvision/common/hardware/Platform.java
@@ -102,7 +102,8 @@ public enum Platform {
public final boolean isSupported;
// Set once at init, shouldn't be needed after.
- private static final Platform currentPlatform = getCurrentPlatform();
+ private static Platform currentPlatform = getCurrentPlatform();
+ private static boolean override = false;
Platform(
String description,
@@ -119,6 +120,11 @@ public enum Platform {
this.nativeLibraryFolderName = nativeLibFolderName;
}
+ public static void overridePlatform(Platform platform) {
+ currentPlatform = platform;
+ override = true;
+ }
+
//////////////////////////////////////////////////////
// Public API
@@ -128,14 +134,15 @@ public enum Platform {
}
public static boolean isRK3588() {
- return Platform.isOrangePi()
+ return currentPlatform == LINUX_RK3588_64
+ || Platform.isOrangePi()
|| Platform.isCoolPi4b()
|| Platform.isRock5C()
|| fileHasText("/proc/device-tree/compatible", "rk3588");
}
public static boolean isQCS6490() {
- return isRubik();
+ return currentPlatform == LINUX_QCS6490 || Platform.isRubik();
}
public static boolean isRaspberryPi() {
@@ -167,6 +174,11 @@ public enum Platform {
return runRobotFile.exists();
}
+ public static boolean isWindows() {
+ var p = getCurrentPlatform();
+ return (p == WINDOWS_32 || p == WINDOWS_64);
+ }
+
//////////////////////////////////////////////////////
// Debug info related to unknown platforms for debug help
@@ -177,6 +189,10 @@ public enum Platform {
private static final String UnknownDeviceModelString = "Unknown";
public static Platform getCurrentPlatform() {
+ if (override) {
+ return currentPlatform;
+ }
+
String OS_NAME;
if (Platform.OS_NAME != null) {
OS_NAME = Platform.OS_NAME;
@@ -319,9 +335,4 @@ public enum Platform {
return false;
}
}
-
- public static boolean isWindows() {
- var p = getCurrentPlatform();
- return (p == WINDOWS_32 || p == WINDOWS_64);
- }
}