Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .github/workflows/build_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
with:
name: eocvsim-ubuntu-latest
path: |
EOCV-Sim/build/libs/*.jar
VisionBench/build/libs/*.jar

build_linux_arm:
runs-on: ubuntu-24.04-arm
Expand All @@ -51,7 +51,7 @@ jobs:
with:
name: eocvsim-ubuntu-arm64
path: |
EOCV-Sim/build/libs/*.jar
VisionBench/build/libs/*.jar

build_windows:
runs-on: windows-latest
Expand All @@ -73,7 +73,7 @@ jobs:
with:
name: eocvsim-windows-latest
path: |
EOCV-Sim/build/libs/*.jar
VisionBench/build/libs/*.jar

build_mac:
runs-on: macos-26
Expand All @@ -96,7 +96,7 @@ jobs:
with:
name: eocvsim-macos-latest
path: |
EOCV-Sim/build/libs/*.jar
VisionBench/build/libs/*.jar

build_mac_intel:
runs-on: macos-26-intel
Expand All @@ -119,7 +119,7 @@ jobs:
with:
name: eocvsim-macos-13
path: |
EOCV-Sim/build/libs/*.jar
VisionBench/build/libs/*.jar

release:
needs: [build_linux, build_linux_arm, build_windows, build_mac, build_mac_intel]
Expand All @@ -143,7 +143,7 @@ jobs:
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
run: ./gradlew -Penv=release :Common:publishToMavenCentral :Vision:publishToMavenCentral :EOCV-Sim:publishToMavenCentral -x test -x :EOCV-Sim:shadowJar
run: ./gradlew -Penv=release :Common:publishToMavenCentral :Vision:publishToMavenCentral :VisionBench:publishToMavenCentral -x test -x :VisionBench:shadowJar

- name: Download Linux artifacts
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -188,15 +188,15 @@ jobs:
run: |
mkdir -p release-jars
# Copy the first (and should be only) jar from each platform folder
cp artifacts/ubuntu/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Linux jar found"
cp artifacts/ubuntu-arm64/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Linux ARM64 jar found"
cp artifacts/windows/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Windows jar found"
cp artifacts/macos/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No macOS jar found"
cp artifacts/macos-intel/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Intel macOS jar found"
cp artifacts/ubuntu/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Linux jar found" jar found"
cp artifacts/ubuntu-arm64/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Linux ARM64 jar found"
cp artifacts/windows/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Windows jar found"
cp artifacts/macos/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No macOS jar found"
cp artifacts/macos-intel/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Intel macOS jar found"
# Remove source jars if any were copied
rm -f release-jars/*-sources.jar
# Remove the generic jar (no platform suffix)
find release-jars -name "EOCV-Sim-*.jar" ! -name "*x86*" ! -name "*arm*" ! -name "*win*" -delete
find release-jars -name "VisionBench-*.jar" ! -name "*x86*" ! -name "*arm*" ! -name "*win*" -delete
echo "Release jars to upload:"
ls -la release-jars/

Expand Down Expand Up @@ -247,11 +247,11 @@ jobs:
run: |
mkdir -p release-jars
# Copy the first (and should be only) jar from each platform folder
cp artifacts/dev/ubuntu/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Linux jar found"
cp artifacts/dev/ubuntu-arm64/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Linux ARM64 jar found"
cp artifacts/dev/windows/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Windows jar found"
cp artifacts/dev/macos/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No macOS jar found"
cp artifacts/dev/macos-intel/EOCV-Sim-*.jar release-jars/ 2>/dev/null || echo "No Intel macOS jar found"
cp artifacts/dev/ubuntu/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Linux jar found"
cp artifacts/dev/ubuntu-arm64/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Linux ARM64 jar found"
cp artifacts/dev/windows/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Windows jar found"
cp artifacts/dev/macos/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No macOS jar found"
cp artifacts/dev/macos-intel/VisionBench-*.jar release-jars/ 2>/dev/null || echo "No Intel macOS jar found"
# Remove source jars if any were copied
rm -f release-jars/*-sources.jar
# Remove the generic jar (no platform suffix)
Expand All @@ -263,7 +263,7 @@ jobs:
uses: pyTooling/Actions/releaser@r0
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: 'Dev'
tag: ${{ github.ref_name }}
rm: true
files: |
release-jars/*.jar
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# AGENTS

This repository is the EOCV-Sim desktop simulator for OpenCV/FIRST Tech Challenge-style pipelines.

Quick orientation:

- `VisionBench/` contains the main app runtime, workspace tooling, configuration, GUI, plugins, and pipeline orchestration.
- `Common/` contains shared lifecycle/orchestration utilities, event handling, and cross-cutting runtime helpers.
- `Vision/` contains the rendering, camera compatibility, and FTC/OpenCV compatibility layer used to display and process frames.
- `TeamCode/` contains sample FTC-style pipelines and example code.

For deeper architecture and subsystem details, start from the documentation index:

- `documents/README.md` — index of architecture docs and reading order
- `documents/01-overview-and-architecture.md` — repository structure and lifecycle overview
- `documents/02-main-eocvsim.md` — main app bootstrap and runtime shell
- `documents/03-config-and-settings.md` — configuration and settings flow
- `documents/04-gui-and-visualizer.md` — GUI and viewport host
- `documents/05-pipeline-runtime.md` — pipeline execution, timeouts, and failure handling
- `documents/06-input-sources.md` — input source acquisition and runtime sources
- `documents/07-workspaces.md` — workspace compile/reload flow
- `documents/08-plugins-and-superaccess.md` — plugin/security architecture
- `documents/09-vision-and-common-libraries.md` — rendering and shared foundation layers
- `documents/10-runtime-utilities.md` — EventHandler, orchestration, and common runtime utilities
- `documents/11-tunable-fields.md` — live pipeline tuning and reflected field editing

In short: the app is organized around a lifecycle-driven runtime, a pipeline execution engine, a desktop viewport/rendering layer, and a plugin/workspace ecosystem. The docs in `documents/` are the preferred place to understand the architecture before changing code.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
2 changes: 1 addition & 1 deletion Common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
id 'signing'
id 'com.gradleup.shadow'
id 'org.photonvision.tools.WpilibTools'
id "com.vanniktech.maven.publish" version "0.30.0"
id "com.vanniktech.maven.publish"
}

apply from: '../build.common.gradle'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public Toml read(File f) {

public Toml read(InputStream in) {
try {
Map<String, Object> m = MAPPER.readValue(in, new TypeReference<Map<String, Object>>() {});
Map<String, Object> m = MAPPER.readValue(in, new TypeReference<>() {
});
return new Toml(m);
} catch (Exception e) {
throw new RuntimeException("Failed to read TOML from stream", e);
Expand Down
120 changes: 92 additions & 28 deletions Common/src/main/java/org/deltacv/eocvsim/plugin/security/Authority.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,25 @@ object AuthorityFetcher {
private val cache = mutableMapOf<String, CachedAuthority>()

fun fetchAuthority(name: String): Authority? {
validateCache()
try {
validateCache()

// Check if the authority is cached and the file is valid
cache[name]?.let { cachedAuthority ->
logger.info("Returning cached authority for $name")
return cachedAuthority.authority
// Check if the authority is cached and the file is valid
cache[name]?.let { cachedAuthority ->
logger.info("Returning cached authority for $name")
return cachedAuthority.authority
}
} catch (e: Exception) {
logger.error("Failed to validate cache", e)
}

// Load authorities from file if it exists
if (AUTHORITIES_FILE.exists() && tryLockAuthoritiesFile()) {
try {
val authoritiesToml = Toml().read(AUTHORITIES_FILE)
val authoritiesToml = readAuthoritiesFile() ?: return null
val timestamp = authoritiesToml.getLong("timestamp")

if(System.currentTimeMillis() - timestamp > TTL_DURATION_MS) {
if (System.currentTimeMillis() - (timestamp ?: 0L) > TTL_DURATION_MS) {
AUTHORITIES_FILE.delete()
} else {
val authorityData = authoritiesToml.getTable(name)
Expand Down Expand Up @@ -118,22 +122,43 @@ object AuthorityFetcher {
return
}

val currentTime = System.currentTimeMillis()
try {
val currentTime = System.currentTimeMillis()

if(!AUTHORITIES_FILE.exists()) {
AUTHORITIES_FILE.writeText("timestamp = $currentTime\n")
}
if(!AUTHORITIES_FILE.exists()) {
AUTHORITIES_FILE.writeText("timestamp = $currentTime\n")
return
}

val authoritiesToml = Toml().read(AUTHORITIES_FILE)
val timestamp = authoritiesToml.getLong("timestamp")
val authoritiesToml = readAuthoritiesFile()
val timestamp = authoritiesToml?.getLong("timestamp")

if(timestamp != null && currentTime - timestamp > TTL_DURATION_MS) {
if(timestamp != null && currentTime - timestamp > TTL_DURATION_MS) {
AUTHORITIES_FILE.delete()
logger.info("Authorities file has expired, clearing cache")
cache.clear()
}
} catch (e: Exception) {
logger.error("Authorities cache is corrupted, clearing it", e)
AUTHORITIES_FILE.delete()
logger.info("Authorities file has expired, clearing cache")
cache.clear()
} finally {
AUTHORITIES_LOCK_FILE.unlock()
}
}

private fun readAuthoritiesFile(): Toml? {
if (!AUTHORITIES_FILE.exists()) {
return null
}

AUTHORITIES_LOCK_FILE.unlock()
return try {
Toml().read(AUTHORITIES_FILE)
} catch (e: Exception) {
logger.warn("Authorities cache is invalid, deleting it and recreating later", e)
AUTHORITIES_FILE.delete()
null
}
}

private fun saveAuthorityToFile(name: String, publicKey: String) {
Expand All @@ -142,30 +167,69 @@ object AuthorityFetcher {
}

try {
val sb = StringBuilder()
val currentTime = System.currentTimeMillis()
val existingAuthorities = linkedMapOf<String, Any?>()

// Load existing authorities if the file exists
if (AUTHORITIES_FILE.exists()) {
val existingToml = AUTHORITIES_FILE.readText()
sb.append(existingToml)
} else {
sb.append("timestamp = ${System.currentTimeMillis()}\n")
val existingToml = readAuthoritiesFile() ?: Toml()
val existingMap = existingToml.toMap().filterKeys { it != "timestamp" }
existingMap.forEach { (key, value) ->
if (value is Map<*, *>) {
existingAuthorities[key] = linkedMapOf<String, Any?>().apply {
value.forEach { (nestedKey, nestedValue) -> put(nestedKey.toString(), nestedValue) }
}
} else {
existingAuthorities[key] = value
}
}

val topLevelTimestamp = existingToml.getLong("timestamp")
if (topLevelTimestamp != null) {
existingAuthorities["timestamp"] = topLevelTimestamp
}
}

// Append new authority information
sb.appendLine("[$name]")
sb.appendLine("public = \"$publicKey\"")
sb.appendLine("timestamp = ${System.currentTimeMillis()}")
existingAuthorities["timestamp"] = existingAuthorities["timestamp"] as? Number ?: currentTime
val authorityTable = linkedMapOf<String, Any?>()
authorityTable["public"] = publicKey
authorityTable["timestamp"] = currentTime
existingAuthorities[name] = authorityTable

// Write the updated content to the file
AUTHORITIES_FILE.writeText(sb.toString())
AUTHORITIES_FILE.writeText(buildAuthoritiesToml(existingAuthorities))
} catch (e: Exception) {
logger.error("Failed to save authority to file", e)
} finally {
AUTHORITIES_LOCK_FILE.unlock()
}
}

private fun buildAuthoritiesToml(authorities: Map<String, Any?>): String {
val sb = StringBuilder()
val topLevelTimestamp = authorities["timestamp"]
if (topLevelTimestamp != null) {
sb.appendLine("timestamp = $topLevelTimestamp")
}

for ((name, value) in authorities.filterKeys { it != "timestamp" }) {
if (value !is Map<*, *>) continue

sb.appendLine()
sb.appendLine("[$name]")
for ((key, entryValue) in value) {
val fieldName = key.toString()
when (entryValue) {
is Number -> sb.appendLine("$fieldName = $entryValue")
is Boolean -> sb.appendLine("$fieldName = ${entryValue.toString().lowercase(Locale.ROOT)}")
is String -> sb.appendLine("$fieldName = \"$entryValue\"")
null -> sb.appendLine("$fieldName = \"\"")
else -> sb.appendLine("$fieldName = \"$entryValue\"")
}
}
}

return sb.toString().trimEnd() + "\n"
}

private fun tryLockAuthoritiesFile(): Boolean {
val time = System.currentTimeMillis()

Expand Down
Loading
Loading