diff --git a/.github/workflows/upstream-watch.yml b/.github/workflows/upstream-watch.yml index e781cdc..d91092f 100644 --- a/.github/workflows/upstream-watch.yml +++ b/.github/workflows/upstream-watch.yml @@ -1,5 +1,5 @@ -# Has anything we build on changed? Weekly (and from the Actions tab), checks PhotonVision, the 4143 -# CUDA fork, frc971/bos, aos, allwpilib and NVIDIA's L4T against what this repo pins, tries our +# Has anything we build on changed? Weekly (and from the Actions tab), checks PhotonVision (releases, and the +# main commit we build on), frc971/bos, aos, allwpilib and NVIDIA's L4T against what this repo pins, tries our # patches on the newest code, and keeps one issue per upstream (label upstream-watch). See # scripts/ci/upstream_watch.py. A few minutes a run; free on a public repo. name: Upstream watch diff --git a/README.md b/README.md index 9cd7dcd..ea308d8 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,11 @@ Built by Spectrum 3847 with Claude Opus 5.5 (Anthropic) in Claude Code, which di *Last updated September 24, 2026.* +> **Experimental alpha-7 branch:** this branch ports the SpectrumJetson CUDA and Jetson features to +> PhotonVision source that targets WPILib `2027.0.0-alpha-7`. It has not passed a build on the target +> Jetson or a real robot-network test. Do not install it in place of the tested JetPack 6 image. +> See [CUDA13-MIGRATION.md](docs/CUDA13-MIGRATION.md) for the exact pins and acceptance checklist. + ## Headlines Measured on the bench: @@ -57,7 +62,7 @@ A camera frame goes over USB into PhotonVision on the Jetson. PhotonVision finds ```mermaid flowchart LR - CAM["2-4x Thriftiest Cam
USB 2.0, MJPEG 1280x800"] --> PV["PhotonVision 2026 fork
(4143 CUDA version + our patches)"] + CAM["2-4x Thriftiest Cam
USB 2.0, MJPEG 1280x800"] --> PV["PhotonVision alpha-7 source
(CUDA features, unverified)"] PV --> DET["971 CUDA AprilTag
detector on the GPU"] DET --> PV PV -->|NetworkTables| SC["SystemCore
2027 alpha-6 robot code"] @@ -71,7 +76,7 @@ flowchart LR | Power mode | MAXN SUPER (the fastest mode, which gives the board its "Super" name) | | Cameras | 2 Thrifty Bot [Thriftiest Cams](https://www.thethriftybot.com/products/thriftiest-cam) ([docs](https://docs.thethriftybot.com/electrical/thriftiest-cam/latest/overview)) for AprilTags: OV9281, mono, global shutter, 1280x800, USB 2.0. On the bench also: 2 "Global Shutter Camera" (32e4:0144) and a colour USB camera (32e4:62f0) for game pieces | | Power | From the robot through a 15 V boost regulator board, which kept the Jetson running down to a 5 V input (tested 2026-09-25) | -| Vision software | FRC-Team-4143's PhotonVision fork (2026 version), merged with upstream PhotonVision v2026.3.4, plus our patches | +| Vision software | PhotonVision source `1f419c9d` targets WPILib `2027.0.0-alpha-7`. The CUDA features are ported in this branch but remain unverified on Jetson. | | Tag detector | Austin Schuh's current CUDA detector (from 971 / RealtimeRoboticsGroup), built from frc971/bos | | Game pieces | YOLO models on the GPU through TensorRT 10.3 (our backend), FUEL model by Team 2826 | | Robot side | Stock PhotonLib v2027.0.0-alpha-2 in `2026-FM-SystemCore`, team 8515 | @@ -118,10 +123,10 @@ The vision stack has four parts. Two are built on the Jetson, one on the laptop, | # | Part | Built where | Script | Notes | | --- | --- | --- | --- | --- | -| 1 | PhotonVision service | Jetson (installer) | `jetson/03-photonvision.sh` | Installs the systemd service that starts PhotonVision at boot. We then replace its jar with the fork. | -| 2 | allwpilib `v2026.2.1` | Jetson | `jetson/04-build-allwpilib.sh` | Libraries the CUDA detector links against. Must be the **v2026.2.1 tag**: its `main` branch has moved on and won't compile with the detector. Took 17 minutes. | +| 1 | PhotonVision service | Jetson (installer) | `jetson/03-photonvision.sh` | Installs the systemd service that starts PhotonVision at boot. | +| 2 | allwpilib `v2027.0.0-alpha-7` | Jetson | `jetson/04-build-allwpilib.sh` | Fetches the exact tag object and commit used by the alpha-7 source. This has not been built on the target Jetson. | | 3 | CUDA detector `lib971apriltag.so` | Jetson | `jetson/07-build-bos-detector.sh`, then `08-select-detector.sh bos --mwbd 20 --jpeg nvjpg` | Austin Schuh's current code (see below) plus our JNI wrapper in `detector/`. `--jpeg nvjpg` decodes the camera JPEGs on the Jetson's JPEG hardware (`libspectrumnvjpg.so`, see Performance), gray and colour; it uses ~180 MB of memory per camera. Leave it out to decode on the CPU. | -| 4 | PhotonVision fork jar | Laptop | `host/03-build-photonvision-fork.sh`, then `jetson/06-install-fork-jar.sh` | The 4143 fork, upstream v2026.3.4 (patch 00) and our patches 01–73. It builds on the laptop in about 30 s instead of taxing the Jetson. The Jetson runs it on Java 17. The build also bundles the training site (`training/`) as the sidebar's Documentation page, so the course works on the robot with no internet. | +| 4 | PhotonVision alpha-7 jar | Laptop | `host/03-build-photonvision-fork.sh`, then `jetson/06-install-fork-jar.sh` | Applies `patches/photonvision-2027-alpha7-migration.patch` to an exact source commit and runs on Java 25. The build also bundles the training site (`training/`) as the sidebar's Documentation page, so the course works on the robot with no internet. Real Jetson and robot-network tests are still required. | | 5 | Camera driver with a bandwidth cap | Jetson | `jetson/11-uvcvideo-payload-cap.sh --install` | Needed for 3–4 cameras on the USB-A ports (see Performance). | | 6 | TensorRT backend `libspectrumtrt.so` | Jetson | built by `07-build-bos-detector.sh`; install to `/usr/lib` | Game-piece detection. Models go in with `jetson/12-install-yolo-model.sh`. | | 7 | USB controller watchdog | Jetson | `jetson/14-usb-watchdog.sh --install` | Resets the USB controller if the kernel says it died, so the cameras come back without a person. It never reboots (see Known issues). | diff --git a/detector/CMakeLists.txt b/detector/CMakeLists.txt index ed6ad05..d1b4d5e 100644 --- a/detector/CMakeLists.txt +++ b/detector/CMakeLists.txt @@ -25,6 +25,29 @@ if(NOT EXISTS "${BOS_DIR}/third_party/971apriltag/apriltag.h") message(FATAL_ERROR "BOS_DIR must point at a frc971/bos checkout") endif() +set(ALLWPILIB_DIR "" CACHE PATH "AllWPILib v2027.0.0-alpha-7 source checkout") +set(ALLWPILIB_INCLUDE_DIR "${ALLWPILIB_DIR}/wpiutil/src/main/native/include") +if(NOT EXISTS "${ALLWPILIB_INCLUDE_DIR}/wpi/util/RawFrame.h" OR + NOT EXISTS "${ALLWPILIB_INCLUDE_DIR}/wpi/util/PixelFormat.h") + message(FATAL_ERROR "ALLWPILIB_DIR must contain the v2027.0.0-alpha-7 wpiutil headers") +endif() +include(CheckCXXSourceCompiles) +set(CMAKE_REQUIRED_INCLUDES "${ALLWPILIB_INCLUDE_DIR}") +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +check_cxx_source_compiles([=[ +#include +#include +int main() { + WPI_RawFrame frame{}; + return frame.pixelFormat == WPI_PIXFMT_MJPEG ? 0 : 1; +} +]=] SPECTRUM_WPIUTIL_CXX20_HEADERS) +unset(CMAKE_REQUIRED_INCLUDES) +unset(CMAKE_TRY_COMPILE_TARGET_TYPE) +if(NOT SPECTRUM_WPIUTIL_CXX20_HEADERS) + message(FATAL_ERROR "The JNI bridge must compile as C++20 with AllWPILib alpha-7 headers") +endif() + # Same globals frc971/bos's top-level CMakeLists.txt sets for its subprojects. find_package(CUDAToolkit REQUIRED) include_directories(${CUDAToolkit_INCLUDE_DIRS}) @@ -45,10 +68,10 @@ find_package(JPEG REQUIRED) # libjpeg-turbo (libjpeg-turbo8-dev), for decodeMjp add_library(971apriltag_jni SHARED GpuDetectorJNI.cc far_search.cc fault_kernel.cu) set_target_properties(971apriltag_jni PROPERTIES OUTPUT_NAME 971apriltag) target_include_directories(971apriltag_jni PRIVATE ${BOS_DIR} ${JNI_INCLUDE_DIRS} - ${OpenCV_INCLUDE_DIRS}) + ${OpenCV_INCLUDE_DIRS} ${ALLWPILIB_INCLUDE_DIR}) # bos uses ABSL_FLAG (use_neon, debug_blob_index) but its CMake does not link absl::flags. find_package(Threads REQUIRED) -target_link_libraries(971apriltag_jni PRIVATE 971apriltag absl::flags wpiutil CUDA::cudart JPEG::JPEG +target_link_libraries(971apriltag_jni PRIVATE 971apriltag absl::flags CUDA::cudart JPEG::JPEG ${JNI_LIBRARIES} Threads::Threads ${CMAKE_DL_LIBS}) # Fail at link time, not in the JVM, if a symbol is missing. target_link_options(971apriltag_jni PRIVATE -Wl,--no-undefined) @@ -94,19 +117,19 @@ endif() # PhotonVision. Build it alone with --target fieldcal_detect. add_executable(fieldcal_detect fieldcal_detect.cc) target_include_directories(fieldcal_detect PRIVATE ${BOS_DIR}) -target_link_libraries(fieldcal_detect PRIVATE 971apriltag absl::flags wpiutil CUDA::cudart JPEG::JPEG +target_link_libraries(fieldcal_detect PRIVATE 971apriltag absl::flags CUDA::cudart JPEG::JPEG Threads::Threads ${CMAKE_DL_LIBS}) # SpectrumJetson: the far-tag search's bench test (far_search_test.cc): synthetic tags, no camera. # tests/far-search/run.sh builds and runs it on the Jetson. add_executable(far_search_test far_search_test.cc far_search.cc) target_include_directories(far_search_test PRIVATE ${BOS_DIR} ${OpenCV_INCLUDE_DIRS}) -target_link_libraries(far_search_test PRIVATE 971apriltag absl::flags wpiutil CUDA::cudart JPEG::JPEG +target_link_libraries(far_search_test PRIVATE 971apriltag absl::flags CUDA::cudart JPEG::JPEG ${OpenCV_LIBS} Threads::Threads ${CMAKE_DL_LIBS}) # SpectrumJetson: far_replay, a whole Rewind session through the detector with the far-tag search # deciding as it does live (far_replay.cc). A command-line tool; tests/far-search/replay.sh. add_executable(far_replay far_replay.cc far_search.cc) target_include_directories(far_replay PRIVATE ${BOS_DIR} ${OpenCV_INCLUDE_DIRS}) -target_link_libraries(far_replay PRIVATE 971apriltag absl::flags wpiutil CUDA::cudart JPEG::JPEG +target_link_libraries(far_replay PRIVATE 971apriltag absl::flags CUDA::cudart JPEG::JPEG ${OpenCV_LIBS} Threads::Threads ${CMAKE_DL_LIBS}) diff --git a/detector/GpuDetectorJNI.cc b/detector/GpuDetectorJNI.cc index dd15c36..70c097b 100644 --- a/detector/GpuDetectorJNI.cc +++ b/detector/GpuDetectorJNI.cc @@ -23,7 +23,6 @@ // - no per-detection std::cout; a once-per-second stats line instead #include -#include #include #include @@ -54,8 +53,8 @@ #include #include "far_search.h" #include "nvjpg_decoder.h" -#include -#include +#include +#include #include "absl/status/status.h" #include @@ -67,10 +66,46 @@ void SpectrumInjectStickyCudaFault(); // fault_kernel.cu (test only) namespace { -wpi::java::JClass detectionCls; +class GlobalJClass { + public: + GlobalJClass() = default; + GlobalJClass(JNIEnv *env, const char *name) : m_name(name) { + jclass local = env->FindClass(name); + if (!local) return; + m_class = static_cast(env->NewGlobalRef(local)); + env->DeleteLocalRef(local); + } + + void Free(JNIEnv *env) { + if (m_class) env->DeleteGlobalRef(m_class); + m_class = nullptr; + } + + explicit operator bool() const { return m_class != nullptr; } + operator jclass() const { return m_class; } + const char *name() const { return m_name; } + + private: + const char *m_name = ""; + jclass m_class = nullptr; +}; -const wpi::java::JClassInit classes[] = { - {"edu/wpi/first/apriltag/AprilTagDetection", &detectionCls}}; +struct ClassInit { + const char *name; + GlobalJClass *cls; +}; + +GlobalJClass detectionCls; + +constexpr char kDetectionClass[] = "org/wpilib/vision/apriltag/AprilTagDetection"; +const ClassInit classes[] = {{kDetectionClass, &detectionCls}}; + +// wpi::util::Now's default timebase (steady_clock, ns), which cscore frame timestamps use. +int64_t NowNs() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} // Contrast threshold for the GPU thresholding step. 4143 used 5, frc971/bos uses 4, // and RealtimeRoboticsGroup/aos 76d8f216 moved to 20 for speed. Override at launch @@ -370,41 +405,52 @@ void RecordFailure(DetectorSlot &s, jlong handle, const char *what) { } } -jobject MakeJObject(JNIEnv *env, const apriltag_detection_t *detect) { +jobject NewDetection(JNIEnv *env, const char *family_name, int id, int hamming, float margin, + const double *h, size_t h_size, double cx, double cy, const double *p) { static jmethodID constructor = env->GetMethodID(detectionCls, "", "(Ljava/lang/String;IIF[DDD[D)V"); if (!constructor) return nullptr; - wpi::java::JLocal fam{env, wpi::java::MakeJString(env, detect->family->name)}; - auto homography = detect->H; - wpi::java::JLocal harr{ - env, wpi::java::MakeJDoubleArray( - env, {reinterpret_cast(homography->data), - static_cast(homography->nrows * homography->ncols)})}; - wpi::java::JLocal carr{ - env, wpi::java::MakeJDoubleArray( - env, {reinterpret_cast(detect->p), 4 * 2})}; - - return env->NewObject(detectionCls, constructor, fam.obj(), static_cast(detect->id), - static_cast(detect->hamming), - static_cast(detect->decision_margin), harr.obj(), - static_cast(detect->c[0]), static_cast(detect->c[1]), - carr.obj()); + jstring family = env->NewStringUTF(family_name); + if (!family) return nullptr; + + jdoubleArray homography = env->NewDoubleArray(static_cast(h_size)); + if (!homography) { + env->DeleteLocalRef(family); + return nullptr; + } + env->SetDoubleArrayRegion(homography, 0, static_cast(h_size), + reinterpret_cast(h)); + + jdoubleArray corners = env->NewDoubleArray(4 * 2); + if (!corners) { + env->DeleteLocalRef(homography); + env->DeleteLocalRef(family); + return nullptr; + } + env->SetDoubleArrayRegion(corners, 0, 4 * 2, reinterpret_cast(p)); + + jobject result = env->NewObject(detectionCls, constructor, family, static_cast(id), + static_cast(hamming), static_cast(margin), + homography, static_cast(cx), static_cast(cy), + corners); + env->DeleteLocalRef(corners); + env->DeleteLocalRef(homography); + env->DeleteLocalRef(family); + return result; +} + +jobject MakeJObject(JNIEnv *env, const apriltag_detection_t *detect) { + return NewDetection(env, detect->family->name, detect->id, detect->hamming, + detect->decision_margin, detect->H->data, + static_cast(detect->H->nrows * detect->H->ncols), detect->c[0], + detect->c[1], &detect->p[0][0]); } // SpectrumJetson: the same from a copied detection (far_search.h), for results with far-search tags. jobject MakeJObject(JNIEnv *env, const far_search::Det &d) { - static jmethodID constructor = - env->GetMethodID(detectionCls, "", "(Ljava/lang/String;IIF[DDD[D)V"); - if (!constructor) return nullptr; - wpi::java::JLocal fam{env, wpi::java::MakeJString(env, d.family->name)}; - wpi::java::JLocal harr{ - env, wpi::java::MakeJDoubleArray(env, {reinterpret_cast(d.H.data()), d.H.size()})}; - wpi::java::JLocal carr{ - env, wpi::java::MakeJDoubleArray(env, {reinterpret_cast(d.p.data()), d.p.size()})}; - return env->NewObject(detectionCls, constructor, fam.obj(), static_cast(d.id), - static_cast(d.hamming), static_cast(d.margin), harr.obj(), - static_cast(d.c[0]), static_cast(d.c[1]), carr.obj()); + return NewDetection(env, d.family->name, d.id, d.hamming, d.margin, d.H.data(), d.H.size(), + d.c[0], d.c[1], d.p.data()); } // A line every 10 s while the far search has done anything since the last one. @@ -433,8 +479,13 @@ jobjectArray MakeJObjectArray(JNIEnv *env, const std::vector &d jobjectArray jarr = env->NewObjectArray(static_cast(dets.size()), detectionCls, nullptr); if (!jarr) return nullptr; for (size_t i = 0; i < dets.size(); ++i) { - wpi::java::JLocal elem{env, MakeJObject(env, dets[i])}; - env->SetObjectArrayElement(jarr, static_cast(i), elem.obj()); + jobject elem = MakeJObject(env, dets[i]); + if (!elem) { + env->DeleteLocalRef(jarr); + return nullptr; + } + env->SetObjectArrayElement(jarr, static_cast(i), elem); + env->DeleteLocalRef(elem); } return jarr; } @@ -460,8 +511,13 @@ jobjectArray MakeJObjectArray(JNIEnv *env, const zarray_t *detections) { for (int i = 0; i < n; ++i) { apriltag_detection_t *det; zarray_get(detections, i, &det); - wpi::java::JLocal elem{env, MakeJObject(env, det)}; - env->SetObjectArrayElement(jarr, i, elem.obj()); + jobject elem = MakeJObject(env, det); + if (!elem) { + env->DeleteLocalRef(jarr); + return nullptr; + } + env->SetObjectArrayElement(jarr, i, elem); + env->DeleteLocalRef(elem); } return jarr; } @@ -907,9 +963,9 @@ int NvjpgDecode(const uint8_t *jpeg, size_t size, cv::Mat &mat, bool bgr) { // A "971 jpeg" line every 10 s (not "971 stats", which health-check.sh parses per detector). // Colour frames are also counted on their own, in a clause at the end of the line. std::atomic last_timestamp_src{-1}; // WPI_TimestampSource of the last gray frame -// The capture timestamp (wpi::Now microseconds) of the last gray frame decoded on this thread; +// The capture timestamp (wpi::util::Now nanoseconds) of the last gray frame decoded on this thread; // processimage reports how old that frame is when its detection ends ("frame age at result"). -thread_local uint64_t last_capture_us = 0; +thread_local int64_t last_capture_ns = 0; // age_ms: how old the frame was when its decode started (from cscore's capture timestamp, which // the camera driver takes when the frame's first USB packet arrives), or < 0 if unknown. @@ -993,7 +1049,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { JNIEnv *env; if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; for (auto &c : classes) { - *c.cls = wpi::java::JClass(env, c.name); + *c.cls = GlobalJClass(env, c.name); if (!*c.cls) { std::cout << "971 library could not find class " << c.name << std::endl; return JNI_ERR; @@ -1032,10 +1088,9 @@ JNIEXPORT jint JNICALL Java_org_photonvision_jni_GpuDetectorJNI_decodeMjpegGray( const auto t0 = std::chrono::steady_clock::now(); const double age_ms = - frame->timestamp ? (static_cast(wpi::Now()) - static_cast(frame->timestamp)) / 1000 - : -1; + frame->timestamp ? static_cast(NowNs() - frame->timestamp) / 1e6 : -1; last_timestamp_src = frame->timestampSrc; - last_capture_us = frame->timestamp; + last_capture_ns = frame->timestamp; // Latency probe (tests only): while /tmp/spectrum-971-kmsg exists (checked every 60 frames), // log each decode start to the kernel log, next to uvcvideo's "Frame complete" trace lines. { @@ -1114,7 +1169,7 @@ JNIEXPORT jint JNICALL Java_org_photonvision_jni_GpuDetectorJNI_decodeMjpegBgr( JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *) { JNIEnv *env; if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) return; - for (auto &c : classes) c.cls->free(env); + for (auto &c : classes) c.cls->Free(env); } JNIEXPORT jlong JNICALL Java_org_photonvision_jni_GpuDetectorJNI_createGpuDetector( @@ -1478,13 +1533,13 @@ JNIEXPORT jobjectArray JNICALL Java_org_photonvision_jni_GpuDetectorJNI_processi result = MakeJObjectArray(env, all); } auto t2 = std::chrono::steady_clock::now(); - if (last_capture_us) { - const double age = (static_cast(wpi::Now()) - static_cast(last_capture_us)) / 1000; + if (last_capture_ns) { + const double age = static_cast(NowNs() - last_capture_ns) / 1e6; if (age >= 0 && age < 1000) { s->stats.age_ms += age; s->stats.ages++; } - last_capture_us = 0; // one frame, one reading + last_capture_ns = 0; // one frame, one reading } RecordStats(s->stats, handle, img, detections, failed, t0, t1, t2); return result; diff --git a/docs/CUDA13-MIGRATION.md b/docs/CUDA13-MIGRATION.md new file mode 100644 index 0000000..326e1c6 --- /dev/null +++ b/docs/CUDA13-MIGRATION.md @@ -0,0 +1,163 @@ +# WPILib 2027 alpha-7 migration contract + +Status: **UNVERIFIED. Real Jetson and robot-network testing are required.** + +This branch is a source-level port of the SpectrumJetson CUDA, Rewind, field-calibration, telemetry, +and camera-recovery features to PhotonVision source that targets WPILib `2027.0.0-alpha-7`. +It is not a tested deployment recipe. The tested JetPack 6 stack uses PhotonVision +`d8c9e8e1d3d4036f077a92f10f3a513ef4849765` and AllWPILib `v2026.2.1`. + +Do not replace the tested stack with this branch until every applicable item below passes. +Do not carry JetPack 6 performance results into the alpha-7 report. + +## Immutable source pins + +- PhotonVision source: `1f419c9de3a6a0787571648d18211537a83fdca9` +- AllWPILib tag: `v2027.0.0-alpha-7` +- AllWPILib annotated tag object: `b3232873240d60fe9f3d8ed6587e2770392f206c` +- AllWPILib commit: `83df3ee3ce1e892f76970ec21c8efdc00a104d4a` +- CUDA detector source: `frc971/bos` commit `62e93b4e1ba81bb2cab5d6a0d34346b7051fe0d7` +- Legacy detector source: `FRC-Team-4143/GpuDetectorJNI` commit + `ef9fc1ec7e43116849e71fef1ab335ba630274a7` + +There is no stable PhotonVision alpha-7 release pin in this migration. The PhotonVision source +commit above is a development snapshot, so every built jar must record its exact commit and the +SpectrumJetson commit that packaged it. + +## Source changes + +The migration applies one reproducible patch, +`patches/photonvision-2027-alpha7-migration.patch`, to the pinned PhotonVision source. It carries +the current SpectrumJetson behavior onto alpha-7 instead of replaying the old 2026 patch series. +It covers the numbered series through `photonvision-73`, as of SpectrumJetson `ab6e086`. A change +to a numbered patch on `main` must be ported into the migration patch too, and nothing enforces +that yet. + +The port includes these compatibility changes: + +- Java packages and vendordep types use the alpha-7 `org.wpilib` namespace. +- WPILib, NetworkTables and cscore times are nanoseconds. Clock sync, Rewind and frame timestamps + convert at the source; Rewind's `frames.csv` and `session.json` stay in microseconds. +- PhotonVision's JSON is Avaje Jsonb, not Jackson, and field layouts are `org.wpilib.fields.Field`. +- WPILib 2027 publishes the robot's control word only as the `/DriverStation/ControlWord` struct. + PhotonVision reads it there (`photonvision-62`), so idle and quiet mode see the robot's state. +- The shadow jar drops duplicate license entries, which made `unzip -t` reject it. +- AprilTag and CameraServer come from the matching alpha-7 vendordeps. +- The JNI bridge uses `WPI_RawFrame` and `WPI_PixelFormat` from alpha-7's C-compatible headers. +- The direct JNI method descriptor remains + `(Ljava/lang/String;IIF[DDD[D)V`. +- C++ code avoids the C++23-only `wpi/util/jni_util.hpp` implementation in the current bridge. + +These are source changes, not proof of runtime compatibility. In particular, the bridge currently +compiles its own translation unit as C++20 while AllWPILib alpha-7 requires C++23 and G++ 14.x. +The complete target must compile and link with one tested toolchain. + +## Host checks + +Run from a clean checkout of the candidate branch: + +```sh +git diff --check origin/main...HEAD +bash -n scripts/host/03-build-photonvision-fork.sh +bash -n scripts/jetson/*.sh +bash -n tests/*/run.sh +scripts/host/03-build-photonvision-fork.sh +unzip -t out/photonvision-*-linuxarm64.jar +for f in $(git ls-files 'tests/**/*.java'); do javac -proc:none -d "$(mktemp -d)" -cp out/photonvision-*-linuxarm64.jar "$f"; done +``` + +Record the exact PhotonVision source commit, AllWPILib tag object and commit, Node version, pnpm +version, Java version, Gradle version, and final jar SHA-256. + +A successful x86 host jar build does not prove that the ARM64 jar, CUDA bridge, OpenCV runtime, or +robot protocol works. + +## Target Jetson checks + +Use a spare or recoverable Jetson image. Record: + +```sh +cat /etc/nv_tegra_release +cat /etc/os-release +uname -a +gcc --version +g++ --version +/usr/local/cuda/bin/nvcc --version +/usr/lib/jvm/java-25-openjdk-arm64/bin/java -version +``` + +Alpha-7 requires C++23 and G++ 14.x. A build with an older compiler is a failed target build, not +a result to hide. Do not assume the documented JetPack 6 image supports this stack. + +From a clean candidate checkout, run: + +```sh +scripts/host/03-build-photonvision-fork.sh +scripts/jetson/04-build-allwpilib.sh +scripts/jetson/07-build-bos-detector.sh +``` + +Then install the matching jar and native libraries with the scripts in this repository. Verify: + +```sh +ldd /usr/lib/lib971apriltag.so +nm -D /usr/lib/lib971apriltag.so | grep Java_org_photonvision_jni_GpuDetectorJNI +journalctl -u photonvision +``` + +There must be no unresolved WPILib, OpenCV, JNI, CUDA, NVJPG, Abseil, or TensorRT symbols. The +PhotonVision jar and native libraries must come from the same candidate revision. + +## Jetson runtime matrix + +Run these on the target Jetson with the candidate commits recorded: + +```sh +tests/detector-frame-sizes/run.sh +tests/gpudetector-handles/run.sh 25 +tests/jpeg-hw/run.sh +tests/detector-ab/run.sh faults +tests/rewind-ab/run.sh 30 2 +tests/robot-clock/run.sh +tests/jetson-telemetry/run.sh +tests/camera-replug/run.sh 5 +tests/jvm-check.sh +tests/cpu-profile.sh +``` + +For at least 10 minutes per camera configuration, test one camera and then two cameras with: + +- AprilTagCuda enabled. +- An 8-coefficient lens calibration. +- Three-dimensional and multi-tag pose enabled. +- Resolution and pipeline changes while running. +- USB unplug and replug. +- Graceful and sticky CUDA faults. +- Rewind recording and export. +- MJPEG hardware and CPU decoding. +- TensorRT detection if the candidate includes it. + +Acceptance requires no stale frame, wrong tag ID, JNI exception, leaked detector handle, unexpected +restart, out-of-memory kill, or monotonic memory growth. Report fresh FPS, detection latency, CPU, +GPU, and memory numbers. Do not reuse the JetPack 6 measurements. + +## Robot-network gate + +Keep the robot on its tested PhotonLib release until this joint test passes. A matching alpha-7 +PhotonLib vendordep, WPILib alpha-7, SystemCore image 14, and compatible FIRST Driver Station must +be used together. + +On the real robot network: + +- Connect the SystemCore to the Jetson. +- Read `PhotonCamera.getLatestResult()` repeatedly. +- Confirm there is no PhotonLib or coprocessor message-hash mismatch. +- Confirm there is no version-mismatch warning or error. +- Verify valid single-tag and multi-tag poses. +- Verify timestamps use the timebase expected by the robot pose estimator. +- Confirm there is no 1000x timestamp offset or stale zero timestamp. +- Confirm PhotonLib can set camera enable state. +- Confirm robot-clock time sync, Rewind timestamps, and the Jetson date path remain healthy. +- Reboot each device in both orders and repeat after reconnect. + +If robot hardware is unavailable, this PR must exclude robot compatibility from its claims. diff --git a/docs/REWIND.md b/docs/REWIND.md index 35f3139..47e8657 100644 --- a/docs/REWIND.md +++ b/docs/REWIND.md @@ -135,8 +135,9 @@ Every frame has two timestamps: - `jetson_us`: the Jetson's clock, the same one PhotonVision stamps its results with, at the moment the frame arrived. - `robot_us`: the **robot's clock**, from PhotonVision's time sync with the robot. That's the - timebase of PhotonLib's result timestamps and of the AdvantageKit log. It's blank when no robot - was connected (bench recordings). + timebase of PhotonLib's result timestamps and of the AdvantageKit log, in microseconds (alpha-7 + PhotonLib reports nanoseconds: divide by 1000). It's blank when no robot was connected (bench + recordings). Both are arrival times. A result's timestamp is earlier: the middle of the exposure, half the exposure (plus `SPECTRUM_CAMERA_DELAY_US`, 0 unless set) before arrival. PhotonVision logs the diff --git a/docs/TECHNICAL.md b/docs/TECHNICAL.md index 89858af..8d58ba8 100644 --- a/docs/TECHNICAL.md +++ b/docs/TECHNICAL.md @@ -1,5 +1,10 @@ # SpectrumJetson: technical reference +> **Alpha-7 status:** this branch is an unverified migration target. The existing JetPack 6 setup +> and its measurements describe the tested `v2026.2.1` baseline. Do not apply those results to the +> alpha-7 branch until the checks in [CUDA13-MIGRATION.md](CUDA13-MIGRATION.md) pass on the real +> Jetson and robot. + Imaging and setup for a **Jetson Orin Nano Super Developer Kit** (P3768 carrier + P3767-0005 8GB module) running PhotonVision with FRC 971's CUDA AprilTag detector, for Spectrum 3847/8515. Boots from NVMe, with no SD card. @@ -121,15 +126,17 @@ fork's base and the alpha-6 era (checked in source on 2026-09-23, not yet on har > after alpha-7 (`a6167b0`, 2026-09-17) renamed the timestamp fields, which changed the > hashes, and the robot would throw against this Jetson. -Porting CUDA to 2027 PhotonVision was rejected. 2027 allwpilib needs JDK 25, C++23 -and GCC 13 (Ubuntu 24.04), while JetPack 6 has GCC 11. +The CUDA port to PhotonVision source that targets WPILib `2027.0.0-alpha-7` is a +separate experimental migration. It requires Java 25 and the alpha-7 C++ headers. Alpha-7 +requires C++23 and G++ 14.x, while the documented JetPack 6 image has GCC 11. No +JetPack 6 build or runtime result is claimed for the migration. | Piece | Version | Built on | Script | |---|---|---|---| -| allwpilib | `v2026.2.1` (not `main`: `wpi/jni_util.h` moved) | Jetson, `-j4` (more OOMs on wpimath); **17 min** in MAXN SUPER | `scripts/jetson/04-build-allwpilib.sh` | -| GpuDetectorJNI | `FRC-Team-4143` `ef9fc1e`, CUDA arch 87 → `/usr/lib/lib971apriltag.so` | Jetson | `scripts/jetson/05-build-gpudetector.sh` | -| PhotonVision fork jar | `d8c9e8e`, Java 17 target | Laptop (Node 22, pnpm 10, Temurin 17, as in CI) | `scripts/host/03-build-photonvision-fork.sh` | -| Java runtime | **17** for the fork (the PV 2027 installer made 25 the default) | systemd drop-in `photonvision.service.d/java17.conf` | `scripts/jetson/06-install-fork-jar.sh ` | +| allwpilib migration target | `v2027.0.0-alpha-7` headers, G++ 14.x and C++23 required upstream | Not yet built on the target Jetson | `scripts/jetson/04-build-allwpilib.sh` | +| GpuDetectorJNI | BOS `62e93b4` plus the local JNI bridge, CUDA arch 87 | Not yet built against alpha-7 | `scripts/jetson/07-build-bos-detector.sh` | +| PhotonVision migration jar | source `1f419c9d` plus one Spectrum migration patch, Java 25 | Host build pending | `scripts/host/03-build-photonvision-fork.sh` | +| Java runtime | Java 25 for the alpha-7 source | Jetson install not tested | `scripts/jetson/06-install-fork-jar.sh ` | `scripts/jetson/03-photonvision.sh` installs upstream `v2027.0.0-alpha-2` (CPU only). That's a placeholder, and it provides the systemd service; the fork jar replaces its @@ -1915,7 +1922,8 @@ and only a PhotonVision restart helped. with bug fixes. JetPack 7.2.x now supports Orin, but it moves the Jetson to Ubuntu 24.04 / CUDA 13, which the fork and detector were not built for. - The flash command adds `--erase-all`, per the 36.5.2 Quick Start. -- allwpilib is pinned to `v2026.2.1` instead of `main`. +- The baseline image uses allwpilib `v2026.2.1`. The alpha-7 migration uses the exact + `v2027.0.0-alpha-7` tag object and commit shown in `docs/CUDA13-MIGRATION.md`. - CUDA isn't part of a BSP-only flash; install `nvidia-jetpack` after first boot. - The 2026 fork runs against 2027 alpha-6 robot code (see above). diff --git a/docs/UPSTREAM-PORT.md b/docs/UPSTREAM-PORT.md index ce1b5f5..d41731d 100644 --- a/docs/UPSTREAM-PORT.md +++ b/docs/UPSTREAM-PORT.md @@ -4,6 +4,11 @@ What we brought in from upstream PhotonVision on 2026-09-24, and what to watch f done by a Claude subagent in a separate clone (`~/build/pv-upstream`), then built with our real build script and bench-tested on the Jetson. For the setup itself, see the [README](../README.md). +These notes and their bench results are for the JetPack 6 build on WPILib `v2026.2.1`. The +experimental alpha-7 build replaces the numbered `photonvision-*` patches with +`patches/photonvision-2027-alpha7-migration.patch`, applied to PhotonVision `1f419c9d`. See +[CUDA13-MIGRATION.md](CUDA13-MIGRATION.md). No alpha-7 hardware result is recorded. + ## The short version - **`photonvision-00-upstream-v2026.3.4.patch`:** the full diff from the 4143 fork (`d8c9e8e`) to diff --git a/patches/photonvision-2027-alpha7-migration.patch b/patches/photonvision-2027-alpha7-migration.patch new file mode 100644 index 0000000..aa5b07f --- /dev/null +++ b/patches/photonvision-2027-alpha7-migration.patch @@ -0,0 +1,19172 @@ +diff --git a/docs/source/docs/programming/photonlib/getting-target-data.md b/docs/source/docs/programming/photonlib/getting-target-data.md +index 972f0ef9..01f8e03d 100644 +--- a/docs/source/docs/programming/photonlib/getting-target-data.md ++++ b/docs/source/docs/programming/photonlib/getting-target-data.md +@@ -154,7 +154,7 @@ You can get the {ref}`best target CeB0?n-ffI+qRvF?d06N-+%92dv*7&-rd!` +zo~ozzs$Q)oDA +zGxYJM&@sDIAX&tKKg$rc48W1G*ChnT>YYePQtwFFV1>(vAJs^|SRuUa9ouoVi1ApCl(SeCeP?soXQ{&|WpNBg-d{TeaNA}2 +z_bLmx`W^#2b%jvP0r+yx+q3z*1Vr0AQ{>=7y;UfZVAY+3U@g!0DEmD;EXXksUrh_B +z`X1^3L^Iss9&Wxa)$7070JET&HzO{LsIAqHe*`uzzl>mmUxgwU4hTD9iRoVq +z%tIawP^&H!i=1~AjR|>;y-_6WeGvrBy%7fN$33q16NS}~GLF}+Nb{sWF)0gxk9e@> +z?j6`VGsx2JUdMnb$w=;Fpi2?41q-r75lMtL$rPCM=V!atzdfgf_G$`1!N0vkbtnr+EMjNIt>#dbL(8yBbdEi0 +z{k+%xVNq^7kxRXFqrBnNF_J8UoOSfclx_5XA-8F?^Y~$gRL&? +zs@b946ysB~4h4e2il^Ma4+sZ&+Y}q9!xc|t1Gy8^F^^BCSoI48LgY{RrCpE%j%TNJN}dQgc3`_9qjfCEFpsk&EhwsPnbm9x(v=V_UEiH>=|rC#(;1 +z$`1GYZARDQ2tkrUX)Uw86=uoh`?|D{t8TB;8<=DRbFi5#h`-1(XPV@|Lvtu&qJ_qA +zydJeWh&Hz#u|-sE>9`(eWbI;a@aPdt?V?O@yAdqwDNzH`b#M!?7jSxaBV#Z;2n2UC +zb?e4iZA6~5{^xg +z>PWjHkMd#qJFpMtG+j;QpAyo2bw-4{S^v=22?nK8%c7NzzTaS`iyrsV;!E|+2ZUhS +zXN(Nsv4NFLsjvb{lvNPAh2r5VOiCj7>6KKjYma`Q`BuIXidI|cEc1C=L+S*E7w3|W +zG?FMMkC;8cz`7EFo$V;Y-ulDK4gAAx$UXQr-*5j4ffx!>4^@cR4cf(%$e0Q(!Z$mV +zkQr!+!#rgBe|?87j%h!^#oDzAdXj1C+y2O11vAH_Qxi+W&R?jiNLsF!aO?-j|MQ~l +znK{RA+cdXGj82eiGtd?G;+eW4DGt{AC-Q52k5OFl?bQ%N*`F1KJTNfUQ@u3=Uk=|M +zcSNWy?Gi`Aef*p|$ot993&ZlC1K>`9X~Qh?0?xkZr2X|QiMk-m45|L^*%()oDGq2b}4CD#4M +zp6?vmjrkkzHW^E3hAkaNv6UJ+QxqYE9a*UI;#)BGI17z()-1fPGenX|iVu%IfYFC4 +zG>^+CE*GHt_>-fgr&&;ZGJmi}vC0aZW}C_5P-NVR^HB!uWn5Tb11@lS%4ND@)Npa! +zBYW4&BJ-(DMLvH^mz|+z_&v$R65AIDgT3tmSNdm*PVNE;=riBSy&_W@l}r!nAFj#GiCWjjGS~du+ytptzpD +zo%S)fy#uBYn+<|*q9cQV_46VK@D{_cqm4D>*Z_sJ&{BQf(` +zs};R!Q(OV*D*B!p=KelH_FXNTY#3mp992Cs%sz62d|j(p@3Yf9DA^n9_zF(bif^Ck +zS(27=IgXcR(7rk37w8}*(M0Unz4g!hN5UBp^rd*SKPkh5GIwmMERd>=x$8wyREf@r +zC}#uuq%i;I)r6jfE?WU3Kj(^$sR%ishtK}=rv~~jUsFSFuo-2~;YL|JPV+)4avKL} +z^1Nbuz14<4#T142ryS{w1lE6a2IkneY1l*q1nW8a5KkEOn%F3tQw0`|N-7g-gCFtv +z+#?0FB=DoABt4y3Aaxm#4SaU%GYB;TT3XmN5F645pA3@KG)hUq6ghL1r*4kxf#3Pv +zbA8^$1wYjV*Lga!8E2dVn|Wb!Ez4^SZ|=mCHiL8ODXT;Gh&8x(+o%qk1H|oW|vYMc(X)Lux4+zpZv5c@OEnl_D1r)h<0gMr_kw=1kXh=wh?#>GF>9e|O^ +zt;ZVVCQI(J(p{W}+iVU1lT#t~Fy`qwTI3CK(g}&kpbjwk8%dh)$itd#iJfw{7K2e_ +z#+r=kAZJqSmR%kfE9EEy)ieXU(6MMNz}D*Q`D3G6zei@usqKZk +zyB~tX^FKwmKJwIq;n2F}WJTf~!H-O^-d*6h)kv)V2+BBhLtjF1O}qw_LpkQpHwn`9 +z3l26vO(5Q5f<^f$thirFdLmWZjQ9jPe8>n3eQV2KsaOP-{RzW|?^YSxqpl^bP9?+6 +z6R1%f>A2A9D!YlI<23n*5hVsf@G9lYPj|}FtL~zC_^d^<+rg}WaUJOSg)43NccVvW +z2yF4!MrT&RUKcC`J)ib33oq=%WL&|ur^TL9KrxKEA?rK8ii=7L#;Ir>b_>?iP0XQQ +zQ%*0{JT*Lw4zA?a?(D|>6#O8B0`iCDh<_*PdSv*6jI&@R)ZXAWh>HjFw+t-P36I5h +zvk`?d3L5@`gDy-!kTo#58!Fcf7I$%=%5J;W@6xobt7r->oBHWmaNe7m$<;yVo6m`23eZ|v$b=H0HH9uY* +zc(7*q4RGnHxUB%v-O+e9&Q7(ol1@2+Rzcs#kvuMeo(E_KuHgl4=lhIMxU0LdKeoxS +zn4Pl~n8Nh(Zrjv^kvaH#bbEhwpn-3#B}`q};!?~pn{E$2y1dz{{)$4a?7puF2~%Cw +zsRG}kN~$m3lyurIXNnJooz$X(=xEpSDn{f%_s>URNqRVl2+6!RK!v&%@Y;oiF7fT4 +z$_+o>f0B-kV&!Vwwlp6SHuwY@9`HV&Y3tNY5|fu_78Z_Vd$l?ud@n~#b&kCO(uxLk +zCd;##%EHHEjjvMl(;x2~|28g9pwy*NkS`AA*HTdiQmzg*eR`55Yx$Ko+ujZ)him?| +zLKO>Z;%7^gV=Zb@`sfI6b$YvAEtO~2MpL1{J=U^-{~9+%C@M+iLS`a1fQgCKCv1M{ +zIec^KDoU&1#E+GFvpZd81iltOE`JGyOq1{TCriciejt5zD{X5WWgHj+dG+#yVQ|xl +zL|q==XAeYT7SrF$VT@YwJ&ARS!U>rDxP=$zfzbM?O{R +zg2(xOsgu8%q(gRcbaZO0!ai=Svhw`=yyO13*=d`{?PO=F%mQH;g=nr+T2C5mM-=dt +zcYZS``^q~{0oh=5{NUE$Laew^5@e+L==>bU-yk6Co86yh2g{Io-|yZm?*VtyVW2jD +zp#PN!1T>w|nbGw2;^p3cPT{-qh1TQtCUK2n^!4!&();$!VNi9@5QyN#>JDPI`Lz2j +zXQz8mI1f|~V*FHI`?GX)Ib4=|+PxQK5wI(~7&zfK0D^l2yM_8_|3m?O4lr-#n&htK +zDt2!Mh1HyHUkQ5$C2j!g{j@*(U-4%pZ +zr3r(QbK#z9Fu$SFvO1T)KE?}LMbwv8wZdmIZLTDqnVP5zs{=hHj<&vf;KtS+c0^N7SA4)+A +zj95pr&DKe}(Qa9vPn67D`7aqPp;^B&RUm{(({i5%);e4f`-471i0rQ`7!<+{gLYl5 +zmxzr$fTmGyjmU+S2uZq;93+pp3uIm=i~1lI=`7LzfzZAvmrN!ynkWf(i|3QQuZJ6z$T@yLGfoAE?pIM`~ZX!w|Ri8f7Yzpn?=0sTY1cVsZS5ITrDUuyZU-*%NQd|vkI}*6Z>n)y2gr`wjxOd_%iqZ;ImV8^k|P9eS^zfbiXk9 +zG32*bZaRcefW6y#v&l#Ey29+J6k}860}2~0L5LI7uH=D(*cQfk^feUQ;1&|AeRJTc +z%ip`JJNLOfSSr}KIV|7YU-=OUQP7~I2Gyqe-y&AF6I{TUaO^S0>4XWHVIM8({kQt% +zJUwbX7|!EQ9CBzC1HLgPcZu$L)jm1K>7WOold*5B2=>qBxw%^9B+ +zNR|Th*!SzOH{a#&Q4Y8<+@(4`fIX}X$K}LaoCWK%Kn&sDH!N$8x?Pfz`pthw_a9sr|xfd^Lb6KmEM#b&>n!DfVZj-e})u`L=gNunbPj +zDvX?MN^pYJ6)Oi#TEa5^cPbK8a}Gc|q`oAVAwH>nE9!udHRqx1H=jl8UU8x5ChXtzR6MkBsQQ0_ +zVEmw0zBUZN#l`GO$^@IP?$?yE`D@xX`s|GL!%62&M;c0iU(mDe*u=sUuTM2ZVAD%b +zXo5Xq2K@a0j6yN<$Gw*spTe#EpOa@H(Sucn967PzM)#vAYL)M_#Q@u`3-}lFZt=uq +z>r=I4ueNzMp0>pwSD#G6o?HCc25K>Oh}C|a(%juTOmdAdM^X{lX>?is7op_FQo@SD +zpI~#p{4NtHkMEKDVAIEL2esz!fFTvubx=_Tu=yf&DYFM2dy3@O3LgFHp4*vs*@}U9 +z(rMv~bNY^`h!EmnSZkgC_`f~HHhu$@N$u7o?7=MT{x29@tPQ_##Z&oi7yhM32z5gP +zL&@X@pG!G*qjLfGRQc~W;|&0OJ_2`Ojcx!ndABy9Y&c#q8 +z4iT)aWC49fHZ?(LYnh$XpGiL+8@cK}81_w<=nqT=k~gx-8LF+7BGn!ljy8#HxM~|3 +zk^lNvn(==fT2RbsI$5O!z@o)6$oRE?7xtZ!-Xfv^jIEBz9@3(#nHDBSc;T}D`K9`s +z?wit&2Qpzmi+32+CIs8pHTbIvKzF@UF>rm^tcHu9lyag +zhV&UtV7`%PVb+uR(SeG_$Tjf{1X85pQp@1sj$@;2l}Txi$@9Q)J}oYSX9|V9D<5mJ +zxUd{7$k=s$L0Rr+HyzZ|{(IR;hNIuBx346bHLE~+C2;^~3?OVK?yy5e6iPR=J8f-+ +zZHo6m+>#>~SUptpXc8^bxOOA}7RiyyJB!&7;pgz%X>)$dX62EHv!9U!tNaOtOQ9Mj +zv$*?N;0@85lvH2ID{uSnxQjk^}B8qwpeE@s(Ob%mseqB||X3>83+a +z0pN{pR}7rEY4P{FcY0Ezl;>h(tn*(h2`vBOWf6x$&ZqhHe*>^@Dbrf>y&$;(WZ8%0v9*h`iq +zbiFedOO_+TW)HN$m_CQJIthMTYf+cBZ_Q!YW0h{G`v*QhD+4Ma)xSj&YNF_pod0s; +z4jH)X1NPLf3HU|bwru2$eL}Z%2dr5#R^94sFtR(w&bj3c2{jc=a!t}#oE$*FLvp(a +zXVS%t1X78rL5LBQaozU1L@k +z*D(vv19&I7queeo+Ft;qqh)|*jeW~8O$KgIWLspnY+BYWn!1rwGBVDHbjD&k=Pd&O +z;|!Vi5FiHfTLof#)6=j9b>3zD%Kh+P!(!7Z`0Wbp-w-ILo}ut6`@JDUpkP9;x`*<` +zGu7O80EA@}aFRdOuFXXH84d^=T)c0=VNKNCwcYYuY6S|KD{GIyWnvd6*R^HgZ;Gbf +zB0IRn77U1&k$x*ti^iKT0Pc*%1n{9k{`d=q03fEvOd=o_+h0034pk+UbobT3N!TE{Ku_kRh6|AtsTyGCaF7k_WC{o^<6 +zoPn9Xc2jB6sK%V_?FTrt)@KyJS}DaU$V)N8umct(p3s8VqckxcT;hY{Ii4HX={uYFtILP#%{edIpp6ojdpex*~-Y7gv*r{5-0wStBV9> +zbil+j>@@3($e{5?&oKm;4Pt+?00ETI$x;Wnc??R(7^MrXGJMt@W*-)RzZtQZe2T<1 +zD->A{SOslWcRP$XJ98G_gkrT1!_Cbpids8C;;hBOCKozF6VhQ7WP~wk48gRqkxoG} +z_&gDl`kZb!7%F=L1U{b1VE)}?>O)$6l1E0_HI>N1reqcUT7w@rC?-wgJsh!@n;*?@ +zuVjIFN63Tg0dPJU|0H}wC@d(08C^~~P{`5qL0}`6an7|4c5%S!9xQyIFMA=jZkyrz +zn=a*RUSpf0s;`xdZzJP +z;-}KT$3#h+51%=0YOYGQQH_dO{YU{cRve_o#}k4^^SCjxajHC9oo)o%{7$a@Y^32~ +z^f1_wf#_s%mk-P!4NHUzc?UX&=B(Zsv=^_>9_^!I8NmrA5Z1kdFPEZ-n%?w!1} +zM>cr~+Nf)0ab)ti$=00;SO$A#MfpW}hQJX>-MHn#jLLy@EMN8tERzw(!*Mye5OyzHDNc5fnwcSJQ90+aLv{DrSrF|5Dk&p!xQhMX3q1M83)0JKi|uZ==;I4u8~x%4c+4pE^qB5-0Tb$LrQ&C#f0wu_%hZQoM$KKK1trs@)(?8AOzwgQ*cNXe=Bi&t(T +zBJg0a>hrKoAF;YSL@J*jCdpD&c=PPiQ(D0-e+3`e-$RPwKB15pdX)op84L6jp)EE+ +zUHtQ_1zkcMCk>_0JD;*n5hz&v_gFJDq1lN*&mAWAaq#)aZek47Z*t!&apq&U;(N?H +z=2dXTVV@jdcSx-yV%O$Tv_%Q1eQ*hPW}=`vVq^y1oP`4MjGkcgR{^A0-l1xs=;T5{sh<;|UiXucM;Y*@-VKf5m#XUy +zA_dAg=PRbx%nsyJ>8I5k=ei@)&Y&FG3xlQ(K=0>zpilSv!cnrTQ);8l1A#>SqX#lW;>gDi}62tM&h+?~G;P2R3he<^}gPTVOy7dS@=lX2bz(!SaOD5qoL3bInw$tH7IE~NdyvQ+?AA+NbsrE^og +zms%tf?Qt$+LXvGQA^GiD`t9*E?N1{)XDM_vk^Ta}N +z<`_dPzqU*k^vNy_C=WlTl!=h;FYwr99}fRBWq9~lsJiM}c_l62Lo%fu9bS(#e!z=&aU})CXV25Q +ztQC}ZSXRo<2@3ZgC|E}G8E;i>zjMf>;bxRwh171^XR+<+IVUO)(y_u$>Cl{Hju2Og +z%Y7ZB?!fVl(bGx5$mFr>ZxonsJ=n)nq5`rXiB%yfl%l$!dQVbrd_x<1UF?e+9kfd% +zE-1vtmF-;xgWG#^{y>|l+@Nu5@z6}pxAA_${tVg1x8s(fAEHx}E$O#K0|uoBHJkuo +z*&}d7ZAQY7kIu32Ki_T9heFoy3||^9B@u}GTP^x0R3yjRiuYK*-J!`OJfc0!l3Wv> +ztdVGfxrh-cPPuXbAk#{(w(bzZ1^>Ozhr2MB=?D3n9(8wgtz`bxD}VO%{NSaZ!-%lEQdND|9MV=C`O-|6WFO;*e!K;G +z(l!uTgw5uPX`YvDAPvE=EJ%a2Izg2LbGQwc1! +zBT#d|p1D@h5aZ)a6!o-MhVQEA46&kB2}at4Z(6Y&^%)P!KLO9ZIVgw(={bbgSfJ^2 +z8)UhRW+!A~=-DViiD+e0k(w$buWFM|uk%epE|EL%Eno3+?T||n^Lr%dA4ob&07r|TP1Z6SF6-44dOHpr#(9$HF+a~UdKuH{wGJMfw#bJkHg&~?&nl@;v=)crHwn?%l@plq4Q +zIR$@$(@LV@ClQ1rJV*yWO<0srUf_wuwJi8 +zY9G$$IxfUkmF!9S7-Xlnjzpf6z)zZ#BkX=`*h{>hy< +z=d6k^^&&#BYyF3*Ey8AyGQQ8T%58l4tFCc +zjMKM(vM(c`icao1$8G#v7gUJ@!^#H)^RQ_l`qOqovvAo?dH_OZ0wCaWM +z2|3Z^_34Vddyf?@&7&v+Q4jiSeAAR#Ii64o)KmoM8EYOq22x+h`=?W&-kBmw?xo +z0tPOaRW-o9p7Sjb-9oa1ovnUF^uYCdp-l$k^SxHD=oI3DUHy-a7-^yO>k}Uo;cK|$ +z;+4np@Nu7Ib!19D3t72k6db;L$YYFenEk4BEdVzmTs{e(3~5*b6&^gxk%k?7$;z3t +z`5)u7f~s!wV}js9?C2zQexW{!TPn67kWa?K3wB@Jr-#?tB?RsFl?Y}*%m$PPWJHs| +zlsjjh)0~vN9ul_w!n)$%RG(s}EVHOBuiV7zmh*j?uS7I3rnJ5E;K~;{x85^)M|BnR +zDpJM$XzcI;ADwwEIhnho12yrA#wt2CKK~0M-6E1jYV|Id;qRbl|CxJ$4Bdf!&? +zxP*oaQdJw&xRH031HCg19f=-B8!N(WBECAODP3-HTW#Ru?RS4-#|)h2dMd(tN1c&r +zRj`O5a9!vSqnca1r>v*t{ZT6uLol|AFYxyAl2xl* +z&%cV#7HVNv92Ft_#7WQ0t_|_rW$p^z!4V#9e^xmV(aSPdz${D7{0_DlakC2LD5mSC +zQ2br+)9e07g+t88?jElz_bOVh#AC)VT`!v|8k}O5{idl}ymHh2H3rB12WBEatmJER6 +zKOBxar>!uhb@a8Q!=lcQg$b3v-b~x&wA8P9N{G6T6%JEB%LH&D@L#0HYGF?OpA2w%p${UN0Dc2h#jf)}LFnG2s3OUbiz~69;UHzGBdcW0o^%2E*n6=mD`gIQqH3RV+*HBAq9$7jJ +z2=|1cttqO$9rW%K%2`gZnkN0DuwYdI=cayit|ggKeYTN%Rh57AqpkfuLMEpy5dC!% +zVI?*aY)Wkol0n`WNxfwS9QsnWr +zylBBZPwu#y>+7O#&8-Kc4CWui_UYI*LOWqg;y>2+P$UUFCx(5xX5+uu_rq6Pe(x6D +zYOf`pV;ywwSoeTebwu?n$Xdvh9XHG`P#_l+s4i`W$sHGi5o2WSc7=mGxFU|1Pz&3i00!~jD-Uor7kV>j +zPP*^u&%o39bdAdGFk31JbzuaV*pTdbK1WI-@q}SjZkhlrU>Rj3u^h;zQXfp@G(q}7 +z(mz4i%WiIjz@W>EBrQTtuG{80@U2|+-~2mg5RpBN~hYMNhV8vbDE=?};Hfv^b^&K_XRkffj>8qc7Z?(e( +ziJvFU*YMDhGB+4xiXEU``<+PsUVh>&1{fCoU~qN(&6Wiby*mlit^ysl4ua4h1I*7u +zdXh#C2n^Ey%E%4QdT2O892B90IWmH#Z(L%|{(>X#fqtFh7FPq#2nt(I5!Yhkif_ba~LuFdS_aVf*&{HkqY`5C$#CsgVMZ+Wi*&1l{3 +z9pM&a&nA-)R!5fG{@2`n(NtBI9!Ifk(V1Z(b~vYk@AqMIb|{9k*+abT?_x3c_j- +z_nX%06gvy1MrPmjt)MN|80{2d06=I+W-tHl@FFq7nM3_s)GA^lATpw|-0AnvEcz`i +zn3uDU`=$rMHQrjI?h^jVGUfPS0yny9EKWT_Hou5B_vw~5-x1lVJAG?ZBMm3rCYHiu +zh+zFR6^zY296czrJkZN#k0SL8t;&t9HG#xR7g%?=fVIKVC14L5E#|TiCr)xzzg$O> +zacPH5JW&S&vdaubNQk&Zvpzr~Pr*e^A#(^B+Y&_;rz +z1^(pZmNu0M(@$pH0qZ +z9495(IG1&Fc^5dl))A0wzGSpXoK;63Ft+cu0)z?B;qGrjFBG{fHsmn=c73dJDE5oh +zcE>zx7Q7l6Bl0e +z(sGJ-)No9yf6m1di*tDrgrpucXsej4r^QbQZ-i0+Hvyb)bz$qSwT0P>vUvZR8}zTI +z+#h}$AnWU4bZ+x|l5DaQjpQrda&VO +zn2p=sQjy)#AhWCbgWysfPpI|juG5I3>WT#AuS(%vn_UrXCf{tvh8y`fzWq~{(pB?l +f0Y4EY>})c5cnoqcpB#PtgY0O0=trBOQC + +literal 0 +HcmV?d00001 + +diff --git a/photon-client/src/assets/styles/variables.scss b/photon-client/src/assets/styles/variables.scss +index 77850717..80ba538b 100644 +--- a/photon-client/src/assets/styles/variables.scss ++++ b/photon-client/src/assets/styles/variables.scss +@@ -14,7 +14,7 @@ html { + } + + .v-row-group__header { +- background: #005281 !important; ++ background: #4e3a8c !important; + } + .v-table + > .v-table__wrapper +@@ -72,3 +72,44 @@ html { + .rounded-12 { + border-radius: 12px; + } ++ ++// Light theme: white cards. Many components hard-code white text for the old blue cards, so ++// inside cards and dialogs that text follows the theme's text colour instead. ++.v-theme--LightTheme { ++ .v-card, ++ &.v-card { ++ .text-white, ++ [style*="color: white"], ++ .v-table th, ++ .v-table td, ++ .metric-item { ++ color: rgb(var(--v-theme-on-surface)) !important; ++ } ++ .v-icon[style*="rgb(197, 197, 197)"] { ++ color: rgba(var(--v-theme-on-surface), 0.6) !important; ++ } ++ // Camera Matching's status words and stream links (bright green / yellow) ++ .active-status { ++ color: #2e7d32 !important; ++ } ++ .stream-link { ++ color: rgb(var(--v-theme-primary)) !important; ++ } ++ .mismatch-status { ++ color: #b26a00 !important; ++ } ++ .v-btn.v-btn--disabled { ++ background-color: #e6e1f0 !important; ++ } ++ .v-divider, ++ .metrics-table { ++ border-color: rgba(var(--v-theme-on-surface), 0.25) !important; ++ } ++ } ++ .v-row-group__header { ++ color: white; ++ } ++ .v-table > .v-table__wrapper > table > tbody > tr:hover { ++ background: rgba(107, 82, 181, 0.1) !important; ++ } ++} +diff --git a/photon-client/src/components/app/photon-3d-visualizer.vue b/photon-client/src/components/app/photon-3d-visualizer.vue +index cde1d3c7..5e0cfb66 100644 +--- a/photon-client/src/components/app/photon-3d-visualizer.vue ++++ b/photon-client/src/components/app/photon-3d-visualizer.vue +@@ -31,7 +31,7 @@ const theme = useTheme(); + const calibrationCoeffs = useCameraSettingsStore().getCalibrationCoeffs( + useCameraSettingsStore().currentCameraSettings.validVideoFormats[ + useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex +- ].resolution ++ ]?.resolution + ); + + const props = defineProps<{ +diff --git a/photon-client/src/components/app/photon-camera-stream.vue b/photon-client/src/components/app/photon-camera-stream.vue +index df5cfeda..0dbf6f67 100644 +--- a/photon-client/src/components/app/photon-camera-stream.vue ++++ b/photon-client/src/components/app/photon-camera-stream.vue +@@ -15,10 +15,18 @@ const props = defineProps<{ + + const backendHostname = inject("backendHostname"); + const emptyStreamSrc = "//:0"; ++// SpectrumJetson (photonvision-48): a hidden tab (another tab in front, or the window minimised) ++// drops its stream connections, so the Jetson stops encoding frames nobody sees (photonvision-15 ++// encodes only while a stream has a viewer). They reconnect when the tab is shown again. ++const pageVisible = ref(document.visibilityState !== "hidden"); ++const onVisibilityChange = () => { ++ pageVisible.value = document.visibilityState !== "hidden"; ++}; ++document.addEventListener("visibilitychange", onVisibilityChange); + const streamSrc = computed(() => { + const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"]; + +- if (!useStateStore().backendConnected || port === 0) { ++ if (!useStateStore().backendConnected || port === 0 || !pageVisible.value) { + return emptyStreamSrc; + } + +@@ -49,7 +57,11 @@ const rotatedResolution = computed<{ width: number; height: number } | null>(() + return null; + } + const resolution = +- props.cameraSettings.validVideoFormats[props.cameraSettings.pipelineSettings.cameraVideoModeIndex].resolution; ++ props.cameraSettings.validVideoFormats[props.cameraSettings.pipelineSettings.cameraVideoModeIndex]?.resolution; ++ // photonvision-42: an out-of-range mode index (camera activating) would throw here ++ if (!resolution) { ++ return { aspectRatio: "1/1" }; ++ } + const rotation = props.cameraSettings.pipelineSettings.inputImageRotationMode; + return rotation === 1 || rotation === 3 + ? { width: resolution.height, height: resolution.width } +@@ -463,6 +475,7 @@ const handleStreamError = () => { + }; + + onBeforeUnmount(() => { ++ document.removeEventListener("visibilitychange", onVisibilityChange); + if (!mjpgStream.value) return; + mjpgStream.value.src = emptyStreamSrc; + }); +diff --git a/photon-client/src/components/app/photon-log-view.vue b/photon-client/src/components/app/photon-log-view.vue +index 0f9a17b7..e0497c23 100644 +--- a/photon-client/src/components/app/photon-log-view.vue ++++ b/photon-client/src/components/app/photon-log-view.vue +@@ -20,12 +20,16 @@ const selectedLogLevels = ref>({ + [LogLevel.DEBUG]: false + }); + +-const logs = computed(() => +- useStateStore() ++// SpectrumJetson (photonvision-73): only while the log window is open. This component is always ++// mounted, and the filter went over every line kept at each new line, with the window closed too. ++const logs = computed(() => { ++ if (!useStateStore().showLogModal) return []; ++ const query = searchQuery.value?.toLowerCase() || ""; ++ return useStateStore() + .logMessages.filter( + (message) => + selectedLogLevels.value[message.level] && +- message.message.toLowerCase().includes(searchQuery.value?.toLowerCase() || "") && ++ (query === "" || message.message.toLowerCase().includes(query)) && + (timeInput.value === undefined || + message.timestamp.getTime() >= + new Date().setHours( +@@ -34,8 +38,8 @@ const logs = computed(() => + parseInt(timeInput.value.substring(6, 8)) + )) + ) +- .map((item, index) => ({ ...item, index: index })) +-); ++ .map((item, index) => ({ ...item, index: index })); ++}); + + watch(logs, () => { + if (!logList.value) return; +diff --git a/photon-client/src/components/app/photon-sidebar.vue b/photon-client/src/components/app/photon-sidebar.vue +index deb43d6b..2ba68a19 100644 +--- a/photon-client/src/components/app/photon-sidebar.vue ++++ b/photon-client/src/components/app/photon-sidebar.vue +@@ -24,10 +24,13 @@ const renderCompact = computed(() => compact.value || !mdAndUp.value); + +