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%En