diff --git a/photon-core/src/main/java/org/photonvision/common/logging/KernelLogLogger.java b/photon-core/src/main/java/org/photonvision/common/logging/KernelLogLogger.java
new file mode 100644
index 0000000000..2335e5a24f
--- /dev/null
+++ b/photon-core/src/main/java/org/photonvision/common/logging/KernelLogLogger.java
@@ -0,0 +1,73 @@
+/*
+ * 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.common.logging;
+
+import edu.wpi.first.util.RuntimeDetector;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.photonvision.common.util.TimedTaskManager;
+import org.photonvision.jni.QueuedFileLogger;
+
+/**
+ * Listens for and reproduces Linux kernel logs, from /var/log/kern.log, into the Photon logger
+ * ecosystem
+ */
+public class KernelLogLogger {
+ private static KernelLogLogger INSTANCE;
+
+ public static KernelLogLogger getInstance() {
+ if (INSTANCE == null) {
+ INSTANCE = new KernelLogLogger();
+ }
+ return INSTANCE;
+ }
+
+ QueuedFileLogger listener = null;
+ Logger logger = new Logger(KernelLogLogger.class, LogGroup.General);
+
+ public KernelLogLogger() {
+ if (RuntimeDetector.isLinux()) {
+ logger.info("Listening for klogs on /var/log/dmesg ! Boot logs:");
+
+ try {
+ var bootlog = Files.readAllLines(Path.of("/var/log/dmesg"));
+ for (var line : bootlog) {
+ logger.log(line, LogLevel.DEBUG);
+ }
+ } catch (IOException e) {
+ logger.error("Couldn't read /var/log/dmesg - not printing boot logs");
+ }
+
+ listener = new QueuedFileLogger("/var/log/kern.log");
+ } else {
+ System.out.println("NOT for klogs");
+ }
+
+ // arbitrary frequency to grab logs. The underlying native buffer will grow unbounded without
+ // this, lol
+ TimedTaskManager.getInstance().addTask("outputPrintk", this::outputNewPrintks, 1000);
+ }
+
+ public void outputNewPrintks() {
+ for (var msg : listener.getNewlines()) {
+ // We currently set all logs to debug regardless of their actual level
+ logger.log(msg, LogLevel.DEBUG);
+ }
+ }
+}
diff --git a/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java b/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java
index 0e2a1d8824..f68691cc01 100644
--- a/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java
+++ b/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java
@@ -26,4 +26,5 @@ public enum LogGroup {
Config,
CSCore,
NetworkTables,
+ System,
}
diff --git a/photon-core/src/main/java/org/photonvision/common/logging/Logger.java b/photon-core/src/main/java/org/photonvision/common/logging/Logger.java
index dc0ebefe72..7bba651b53 100644
--- a/photon-core/src/main/java/org/photonvision/common/logging/Logger.java
+++ b/photon-core/src/main/java/org/photonvision/common/logging/Logger.java
@@ -30,8 +30,34 @@
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
import org.photonvision.common.util.TimedTaskManager;
-@SuppressWarnings("unused")
+/** TODO: get rid of static {} blocks and refactor to singleton pattern */
public class Logger {
+ private static final HashMap levelMap = new HashMap<>();
+ private static final List currentAppenders = new ArrayList<>();
+
+ private static final UILogAppender uiLogAppender = new UILogAppender();
+
+ // // TODO why's the logger care about this? split it out
+ // private static KernelLogLogger klogListener = null;
+
+ static {
+ levelMap.put(LogGroup.Camera, LogLevel.INFO);
+ levelMap.put(LogGroup.General, LogLevel.INFO);
+ levelMap.put(LogGroup.WebServer, LogLevel.INFO);
+ levelMap.put(LogGroup.Data, LogLevel.INFO);
+ levelMap.put(LogGroup.VisionModule, LogLevel.INFO);
+ levelMap.put(LogGroup.Config, LogLevel.INFO);
+ levelMap.put(LogGroup.CSCore, LogLevel.TRACE);
+ levelMap.put(LogGroup.NetworkTables, LogLevel.DEBUG);
+ levelMap.put(LogGroup.System, LogLevel.DEBUG);
+
+ currentAppenders.add(new ConsoleLogAppender());
+ currentAppenders.add(uiLogAppender);
+ addFileAppender(PathManager.getInstance().getLogPath());
+
+ cleanLogs(PathManager.getInstance().getLogsDir());
+ }
+
public static final String ANSI_RESET = "\u001B[0m";
public static final String ANSI_BLACK = "\u001B[30m";
public static final String ANSI_RED = "\u001B[31m";
@@ -50,8 +76,6 @@ public class Logger {
private static final List> uiBacklog = new ArrayList<>();
private static boolean connected = false;
- private static final UILogAppender uiLogAppender = new UILogAppender();
-
private final String className;
private final LogGroup group;
@@ -89,27 +113,6 @@ public static String format(
return builder.toString();
}
- private static final HashMap levelMap = new HashMap<>();
- private static final List currentAppenders = new ArrayList<>();
-
- static {
- levelMap.put(LogGroup.Camera, LogLevel.INFO);
- levelMap.put(LogGroup.General, LogLevel.INFO);
- levelMap.put(LogGroup.WebServer, LogLevel.INFO);
- levelMap.put(LogGroup.Data, LogLevel.INFO);
- levelMap.put(LogGroup.VisionModule, LogLevel.INFO);
- levelMap.put(LogGroup.Config, LogLevel.INFO);
- levelMap.put(LogGroup.CSCore, LogLevel.TRACE);
- levelMap.put(LogGroup.NetworkTables, LogLevel.DEBUG);
- }
-
- static {
- currentAppenders.add(new ConsoleLogAppender());
- currentAppenders.add(uiLogAppender);
- addFileAppender(PathManager.getInstance().getLogPath());
- cleanLogs(PathManager.getInstance().getLogsDir());
- }
-
@SuppressWarnings("ResultOfMethodCallIgnored")
public static void addFileAppender(Path logFilePath) {
var file = logFilePath.toFile();
diff --git a/photon-server/build.gradle b/photon-server/build.gradle
index eaf82419a5..80cf3a344a 100644
--- a/photon-server/build.gradle
+++ b/photon-server/build.gradle
@@ -11,6 +11,9 @@ apply from: "${rootDir}/shared/common.gradle"
dependencies {
implementation project(':photon-core')
+ // Zip
+ implementation 'org.zeroturnaround:zt-zip:1.14'
+
// Needed for Javalin Runtime Logging
implementation "org.slf4j:slf4j-simple:2.0.7"
}
diff --git a/photon-server/src/main/java/org/photonvision/Main.java b/photon-server/src/main/java/org/photonvision/Main.java
index a88a9ca166..a3834abe5e 100644
--- a/photon-server/src/main/java/org/photonvision/Main.java
+++ b/photon-server/src/main/java/org/photonvision/Main.java
@@ -33,6 +33,7 @@
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.PiVersion;
import org.photonvision.common.hardware.Platform;
+import org.photonvision.common.logging.KernelLogLogger;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.LogLevel;
import org.photonvision.common.logging.Logger;
@@ -437,6 +438,10 @@ public static void main(String[] args) {
Logger.setLevel(LogGroup.General, logLevel);
logger.info("Logging initialized in debug mode.");
+ // Add Linux kernel log->Photon logger
+ KernelLogLogger.getInstance();
+
+ // Add CSCore->Photon logger
PvCSCoreLogger.getInstance();
logger.debug("Loading ConfigManager...");
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 2b1df67433..1b17c68eb4 100644
--- a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java
+++ b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java
@@ -53,6 +53,7 @@
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.processes.VisionModuleManager;
+import org.zeroturnaround.zip.ZipUtil;
public class RequestHandler {
// Treat all 2XX calls as "INFO"
@@ -422,20 +423,34 @@ public static void onLogExportRequest(Context ctx) {
try {
ShellExec shell = new ShellExec();
var tempPath = Files.createTempFile("photonvision-journalctl", ".txt");
- shell.executeBashCommand("journalctl -u photonvision.service > " + tempPath.toAbsolutePath());
+ var tempPath2 = Files.createTempFile("photonvision-kernelogs", ".txt");
+ shell.executeBashCommand(
+ "journalctl -u photonvision.service > "
+ + tempPath.toAbsolutePath()
+ + " && journalctl -k > "
+ + tempPath2.toAbsolutePath());
while (!shell.isOutputCompleted()) {
// TODO: add timeout
}
if (shell.getExitCode() == 0) {
- // Wrote to the temp file! Add it to the ctx
- var stream = new FileInputStream(tempPath.toFile());
- ctx.contentType("text/plain");
- ctx.header("Content-Disposition", "attachment; filename=\"photonvision-journalctl.txt\"");
- ctx.status(200);
+ // Wrote to the temp file! Zip and yeet it to the client
+
+ var out = Files.createTempFile("photonvision-logs", "zip").toFile();
+
+ try {
+ ZipUtil.packEntries(new File[] {tempPath.toFile(), tempPath2.toFile()}, out);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ var stream = new FileInputStream(out);
+ ctx.contentType("application/zip");
+ ctx.header("Content-Disposition", "attachment; filename=\"photonvision-logs.zip\"");
ctx.result(stream);
- logger.info("Uploading settings with size " + stream.available());
+ ctx.status(200);
+ logger.info("Outputting log ZIP with size " + stream.available());
} else {
ctx.status(500);
ctx.result("The journalctl service was unable to export logs");
diff --git a/photon-targeting/build.gradle b/photon-targeting/build.gradle
index bddca08c36..1215c062e4 100644
--- a/photon-targeting/build.gradle
+++ b/photon-targeting/build.gradle
@@ -220,3 +220,6 @@ nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore")
nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal")
+nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore")
+nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get())
+nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag")
diff --git a/photon-targeting/src/main/java/org/photonvision/jni/QueuedFileLogger.java b/photon-targeting/src/main/java/org/photonvision/jni/QueuedFileLogger.java
new file mode 100644
index 0000000000..eade168774
--- /dev/null
+++ b/photon-targeting/src/main/java/org/photonvision/jni/QueuedFileLogger.java
@@ -0,0 +1,60 @@
+/*
+ * 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.jni;
+
+public class QueuedFileLogger {
+ long m_handle = 0;
+
+ public QueuedFileLogger(String path) {
+ m_handle = QueuedFileLogger.create(path);
+ }
+
+ public String[] getNewlines() {
+ String newBuffer = null;
+
+ synchronized (this) {
+ if (m_handle == 0) {
+ System.err.println("QueuedFileLogger use after free");
+ return new String[0];
+ }
+
+ newBuffer = QueuedFileLogger.getNewLines(m_handle);
+ }
+
+ if (newBuffer == null) {
+ return new String[0];
+ }
+
+ return newBuffer.split("\n");
+ }
+
+ public void stop() {
+ synchronized (this) {
+ if (m_handle != 0) {
+ QueuedFileLogger.destroy(m_handle);
+ m_handle = 0;
+ }
+ }
+ }
+
+ private static native long create(String path);
+
+ private static native void destroy(long handle);
+
+ private static native String getNewLines(long handle);
+}
diff --git a/photon-targeting/src/main/native/jni/FileLoggerExtrasJNI.cpp b/photon-targeting/src/main/native/jni/FileLoggerExtrasJNI.cpp
new file mode 100644
index 0000000000..c7a45e2de3
--- /dev/null
+++ b/photon-targeting/src/main/native/jni/FileLoggerExtrasJNI.cpp
@@ -0,0 +1,103 @@
+/*
+ * 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 .
+ */
+
+#include
+#include
+#include
+
+#include
+
+#include "jni_utils.h"
+#include "org_photonvision_jni_QueuedFileLogger.h"
+
+struct QueuedFileLogger {
+ // ew ew ew ew ew ew ew ew
+ std::vector m_data{};
+
+ std::mutex m_mutex;
+
+ wpi::FileLogger logger;
+
+ explicit QueuedFileLogger(std::string_view file)
+ : logger{file, std::bind(&QueuedFileLogger::callback, this,
+ std::placeholders::_1)} {
+ // fmt::println("Watching {}", file);
+ }
+
+ void callback(std::string_view newline) {
+ std::lock_guard lock{m_mutex};
+ // fmt::println("FileLogger got: {}", newline);
+ m_data.insert(m_data.end(), newline.begin(), newline.end());
+ }
+
+ std::vector SwapData() {
+ std::vector ret;
+ {
+ std::lock_guard lock{m_mutex};
+ ret.swap(m_data);
+ }
+
+ return ret;
+ }
+};
+
+extern "C" {
+
+/*
+ * Class: org_photonvision_jni_QueuedFileLogger
+ * Method: create
+ * Signature: (Ljava/lang/String;)J
+ */
+JNIEXPORT jlong JNICALL
+Java_org_photonvision_jni_QueuedFileLogger_create
+ (JNIEnv* env, jclass, jstring name)
+{
+ const char* c_name{env->GetStringUTFChars(name, 0)};
+ std::string cpp_name{c_name};
+ jlong ret{reinterpret_cast(new QueuedFileLogger(cpp_name))};
+ env->ReleaseStringUTFChars(name, c_name);
+ return ret;
+}
+
+/*
+ * Class: org_photonvision_jni_QueuedFileLogger
+ * Method: destroy
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL
+Java_org_photonvision_jni_QueuedFileLogger_destroy
+ (JNIEnv*, jclass, jlong handle)
+{
+ CHECK_PTR(handle);
+ delete reinterpret_cast(handle);
+}
+
+/*
+ * Class: org_photonvision_jni_QueuedFileLogger
+ * Method: getNewLines
+ * Signature: (J)Ljava/lang/String;
+ */
+JNIEXPORT jstring JNICALL
+Java_org_photonvision_jni_QueuedFileLogger_getNewLines
+ (JNIEnv* env, jclass, jlong handle)
+{
+ CHECK_PTR_RETURN(handle, nullptr);
+ QueuedFileLogger* logger = reinterpret_cast(handle);
+
+ return env->NewStringUTF(logger->SwapData().data());
+}
+} // extern "C"
diff --git a/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp b/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp
index 8b982865a5..82f3599635 100644
--- a/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp
+++ b/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp
@@ -20,21 +20,11 @@
#include
#include
+#include "jni_utils.h"
#include "net/TimeSyncClient.h"
using namespace wpi::tsp;
-#define CHECK_PTR(ptr) \
- if (!ptr) { \
- fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \
- return; \
- }
-#define CHECK_PTR_RETURN(ptr, default) \
- if (!ptr) { \
- fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \
- return default; \
- }
-
/**
* Finds a class and keeps it as a global reference.
*
diff --git a/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp b/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp
index 2991ac5676..444a7ddd62 100644
--- a/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp
+++ b/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp
@@ -20,21 +20,11 @@
#include
+#include "jni_utils.h"
#include "net/TimeSyncServer.h"
using namespace wpi::tsp;
-#define CHECK_PTR(ptr) \
- if (!ptr) { \
- fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \
- return; \
- }
-#define CHECK_PTR_RETURN(ptr, default) \
- if (!ptr) { \
- fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \
- return default; \
- }
-
extern "C" {
/*
diff --git a/photon-targeting/src/main/native/jni/jni_utils.h b/photon-targeting/src/main/native/jni/jni_utils.h
new file mode 100644
index 0000000000..036af25196
--- /dev/null
+++ b/photon-targeting/src/main/native/jni/jni_utils.h
@@ -0,0 +1,29 @@
+/*
+ * 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 .
+ */
+
+#pragma once
+
+#define CHECK_PTR(ptr) \
+ if (!ptr) { \
+ fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \
+ return; \
+ }
+#define CHECK_PTR_RETURN(ptr, default) \
+ if (!ptr) { \
+ fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \
+ return default; \
+ }
diff --git a/photon-targeting/src/test/java/net/TimeSyncTest.java b/photon-targeting/src/test/java/net/TimeSyncTest.java
index f5ea486f73..a5fd7b4ccb 100644
--- a/photon-targeting/src/test/java/net/TimeSyncTest.java
+++ b/photon-targeting/src/test/java/net/TimeSyncTest.java
@@ -36,6 +36,8 @@ public static void load_wpilib() throws UnsatisfiedLinkError, IOException {
if (!PhotonTargetingJniLoader.load()) {
fail();
}
+
+ HAL.initialize(1000, 0);
}
@AfterAll
@@ -45,8 +47,6 @@ public static void teardown() {
@Test
public void smoketest() throws InterruptedException {
- HAL.initialize(1000, 0);
-
// NetworkTableInstance.getDefault().stopClient();
// NetworkTableInstance.getDefault().startServer();
diff --git a/photon-targeting/src/test/java/wpiutil_extras/FileLoggerTest.java b/photon-targeting/src/test/java/wpiutil_extras/FileLoggerTest.java
new file mode 100644
index 0000000000..adc88e3f7b
--- /dev/null
+++ b/photon-targeting/src/test/java/wpiutil_extras/FileLoggerTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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 wpiutil_extras;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import edu.wpi.first.hal.HAL;
+import java.io.IOException;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.photonvision.jni.PhotonTargetingJniLoader;
+import org.photonvision.jni.QueuedFileLogger;
+import org.photonvision.jni.WpilibLoader;
+
+public class FileLoggerTest {
+ @BeforeAll
+ public static void load_wpilib() throws UnsatisfiedLinkError, IOException {
+ if (!WpilibLoader.loadLibraries()) {
+ fail();
+ }
+ if (!PhotonTargetingJniLoader.load()) {
+ fail();
+ }
+
+ HAL.initialize(1000, 0);
+ }
+
+ @AfterAll
+ public static void teardown() {
+ HAL.shutdown();
+ }
+
+ @Test
+ public void smoketest() throws InterruptedException {
+ var logger = new QueuedFileLogger("/var/log/kern.log");
+ for (int i = 0; i < 100; i++) {
+ Thread.sleep(1000);
+
+ for (var line : logger.getNewlines()) {
+ System.out.println(" ->:" + line);
+ }
+ }
+
+ logger.stop();
+ }
+}