mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-02 02:51:40 +00:00
Offline Update (.jar replace) (#340)
Allows users to upload a new JAR to a Pi. Also bumps the pi image to increase the heap size.
This commit is contained in:
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -160,7 +160,7 @@ jobs:
|
||||
- run: |
|
||||
NEW_JAR=$(realpath $(find . -name photonvision\*.jar))
|
||||
sudo apt install unzip zip
|
||||
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
|
||||
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2021.1.4 | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
|
||||
FILE_NAME=$(ls | grep image_*.zip)
|
||||
unzip $FILE_NAME
|
||||
IMAGE_FILE=$(ls | grep *.img)
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
run: |
|
||||
NEW_JAR=$(realpath $(find . -name photonvision\*.jar))
|
||||
sudo apt install unzip zip
|
||||
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
|
||||
curl -sk https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2021.1.4 | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d '"' | wget -qi -
|
||||
FILE_NAME=$(ls | grep image_*.zip)
|
||||
unzip $FILE_NAME
|
||||
IMAGE_FILE=$(ls | grep *.img)
|
||||
|
||||
@@ -19,15 +19,16 @@
|
||||
<CVselect
|
||||
v-model="currentCameraIndex"
|
||||
name="Camera"
|
||||
select-cols="10"
|
||||
:list="$store.getters.cameraList"
|
||||
@input="handleInput('currentCamera',currentCameraIndex)"
|
||||
:select-cols="$vuetify.breakpoint.mdAndUp ? 10 : 7"
|
||||
/>
|
||||
<CVnumberinput
|
||||
v-model="cameraSettings.fov"
|
||||
:tooltip="cameraSettings.isFovConfigurable ? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.' : 'This setting is managed by a vendor'"
|
||||
name="Maximum diagonal FOV"
|
||||
:disabled="!cameraSettings.isFovConfigurable"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<br>
|
||||
<CVnumberinput
|
||||
@@ -35,6 +36,7 @@
|
||||
name="Camera pitch"
|
||||
tooltip="How many degrees above the horizontal the physical camera is tilted"
|
||||
:step="0.01"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<br>
|
||||
<v-btn
|
||||
@@ -89,26 +91,26 @@
|
||||
<CVnumberinput
|
||||
v-model="squareSizeIn"
|
||||
name="Pattern Spacing (in)"
|
||||
label-cols="5"
|
||||
tooltip="Spacing between pattern features in inches"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[v => (v > 0) || 'Size must be positive']"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
|
||||
/>
|
||||
<CVnumberinput
|
||||
v-model="boardWidth"
|
||||
name="Board width"
|
||||
label-cols="5"
|
||||
tooltip="Width of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[v => (v >= 4) || 'Width must be at least 4']"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
|
||||
/>
|
||||
<CVnumberinput
|
||||
v-model="boardHeight"
|
||||
name="Board height"
|
||||
label-cols="5"
|
||||
tooltip="Height of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[v => (v >= 4) || 'Height must be at least 4']"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? 5 : 7"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
return {
|
||||
selectedTab: 0,
|
||||
snack: false,
|
||||
calibrationInProgress: false,
|
||||
snackbar: {
|
||||
color: "accent",
|
||||
text: ""
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="3"
|
||||
md="4"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@@ -106,7 +106,7 @@
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="3"
|
||||
md="4"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@@ -120,7 +120,21 @@
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="3"
|
||||
md="4"
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="$refs.offlineUpdate.click()"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-update
|
||||
</v-icon>
|
||||
Offline Update
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="6"
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@@ -134,7 +148,7 @@
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="3"
|
||||
lg="6"
|
||||
>
|
||||
<v-btn
|
||||
color="red"
|
||||
@@ -172,6 +186,15 @@
|
||||
:href="'http://' + this.$address + '/api/settings/photonvision_config.zip'"
|
||||
download="photonvision-settings.zip"
|
||||
/>
|
||||
|
||||
<!-- Special hidden new jar upload input that gets 'clicked' when the user posts a new .jar -->
|
||||
<input
|
||||
ref="offlineUpdate"
|
||||
type="file"
|
||||
accept=".jar"
|
||||
style="display: none;"
|
||||
@change="doOfflineUpdate"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -181,6 +204,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
snack: false,
|
||||
uploadPercentage: 0.0,
|
||||
snackbar: {
|
||||
color: "success",
|
||||
text: ""
|
||||
@@ -249,6 +273,52 @@ export default {
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
doOfflineUpdate(event) {
|
||||
this.snackbar = {
|
||||
color: "secondary",
|
||||
text: "New Software Upload in Process..."
|
||||
};
|
||||
this.snack = true;
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("jarData", event.target.files[0]);
|
||||
this.axios.post("http://" + this.$address + "/api/settings/offlineUpdate", formData,
|
||||
{headers: {"Content-Type": "multipart/form-data"},
|
||||
onUploadProgress: function( progressEvent ) {
|
||||
this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded / progressEvent.total ) * 100 ) );
|
||||
if(this.uploadPercentage < 99.5){
|
||||
this.snackbar.text = "New Software Upload in Process, " + this.uploadPercentage + "% complete";
|
||||
} else {
|
||||
this.snackbar.text = "Installing uploaded software...";
|
||||
}
|
||||
|
||||
}.bind(this)
|
||||
}).then(() => {
|
||||
this.snackbar = {
|
||||
color: "success",
|
||||
text: "New .jar copied successfully! Program will now exit...",
|
||||
};
|
||||
this.snack = true;
|
||||
}).catch(err => {
|
||||
if (err.response) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading new .jar file! Could not process provided file.",
|
||||
};
|
||||
} else if (err.request) {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading new .jar file! No respond to upload attempt.",
|
||||
};
|
||||
} else {
|
||||
this.snackbar = {
|
||||
color: "error",
|
||||
text: "Error while uploading new .jar file!",
|
||||
};
|
||||
}
|
||||
this.snack = true;
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -266,6 +336,8 @@ export default {
|
||||
text-align: left;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.infoElem {
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
name="Team Number"
|
||||
:rules="[v => (v > 0) || 'Team number must be greater than zero', v => (v < 10000) || 'Team number must have fewer than five digits']"
|
||||
class="mb-4"
|
||||
:label-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<v-chip label color="red" text-color="white" v-if="parseInt(teamNumber) < 1 && !runNTServer">
|
||||
<span>
|
||||
<v-chip label color="red" v-bind:style="$vuetify.breakpoint.xsOnly ? 'height: auto;' : ''" text-color="white" v-if="parseInt(teamNumber) < 1 && !runNTServer">
|
||||
<span class="text-wrap">
|
||||
Team number not set! NetworkTables cannot connect.
|
||||
</span>
|
||||
</v-chip>
|
||||
@@ -42,6 +43,7 @@
|
||||
name="Run NetworkTables Server (Debugging Only!)"
|
||||
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
|
||||
class="mt-3 mb-3"
|
||||
:text-cols="$vuetify.breakpoint.mdAndUp ? undefined : 7"
|
||||
/>
|
||||
<v-chip label color="red" text-color="white" v-if="runNTServer">
|
||||
<span>
|
||||
@@ -60,7 +62,7 @@
|
||||
</v-btn>
|
||||
<v-divider class="mt-4 mb-4"/>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-col cols="12" sm="6">
|
||||
|
||||
<v-simple-table
|
||||
fixed-header
|
||||
@@ -86,7 +88,7 @@
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-simple-table
|
||||
fixed-header
|
||||
height="100%"
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.photonvision.common.util.file;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class ProgramDirectoryUtilities {
|
||||
private static String getJarName() {
|
||||
return new File(
|
||||
ProgramDirectoryUtilities.class
|
||||
.getProtectionDomain()
|
||||
.getCodeSource()
|
||||
.getLocation()
|
||||
.getPath())
|
||||
.getName();
|
||||
}
|
||||
|
||||
private static boolean runningFromJAR() {
|
||||
String jarName = getJarName();
|
||||
return jarName.contains(".jar");
|
||||
}
|
||||
|
||||
public static String getProgramDirectory() {
|
||||
if (runningFromJAR()) {
|
||||
return getCurrentJARDirectory();
|
||||
} else {
|
||||
return System.getProperty("user.dir");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getCurrentJARDirectory() {
|
||||
try {
|
||||
return new File(
|
||||
ProgramDirectoryUtilities.class
|
||||
.getProtectionDomain()
|
||||
.getCodeSource()
|
||||
.getLocation()
|
||||
.toURI()
|
||||
.getPath())
|
||||
.getParent();
|
||||
} catch (URISyntaxException exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,11 @@ import edu.wpi.first.math.geometry.Rotation2d;
|
||||
import io.javalin.http.Context;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
@@ -37,6 +40,7 @@ import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.util.ShellExec;
|
||||
import org.photonvision.common.util.file.ProgramDirectoryUtilities;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.target.TargetModel;
|
||||
|
||||
@@ -103,6 +107,49 @@ public class RequestHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public static void onOfflineUpdate(Context ctx) {
|
||||
logger.info("Handling offline update .jar upload...");
|
||||
var file = ctx.uploadedFile("jarData");
|
||||
logger.info("New .jar uploaded successfully.");
|
||||
|
||||
if (file != null) {
|
||||
if (Platform.isRaspberryPi()) {
|
||||
try {
|
||||
Path filePath =
|
||||
Paths.get(ProgramDirectoryUtilities.getProgramDirectory(), "photonvision.jar");
|
||||
File targetFile = new File(filePath.toString());
|
||||
var stream = new FileOutputStream(targetFile);
|
||||
|
||||
logger.info(
|
||||
"Streaming user-provided " + file.getFilename() + " into " + targetFile.toString());
|
||||
|
||||
file.getContent().transferTo(stream);
|
||||
stream.close();
|
||||
|
||||
ctx.status(200);
|
||||
logger.info("New .jar in place, going down for restart...");
|
||||
restartProgram(ctx);
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
logger.error(
|
||||
".jar of this program could not be found. How the heck this program started in the first place is a mystery.");
|
||||
ctx.status(500);
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not overwrite the .jar for this instance of photonvision.");
|
||||
ctx.status(500);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.error("Hot .jar replace currently only supported on Raspberry pi. Ignoring.");
|
||||
ctx.status(500);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.error("Couldn't read provided file for new .jar! Ignoring.");
|
||||
ctx.status(500);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void onGeneralSettings(Context context) throws JsonProcessingException {
|
||||
Map<String, Object> map =
|
||||
|
||||
@@ -71,6 +71,7 @@ public class Server {
|
||||
});
|
||||
/*API Events*/
|
||||
app.post("/api/settings/import", RequestHandler::onSettingUpload);
|
||||
app.post("/api/settings/offlineUpdate", RequestHandler::onOfflineUpdate);
|
||||
app.get("/api/settings/photonvision_config.zip", RequestHandler::onSettingsDownload);
|
||||
app.post("/api/settings/camera", RequestHandler::onCameraSettingsSave);
|
||||
app.post("/api/settings/general", RequestHandler::onGeneralSettings);
|
||||
|
||||
@@ -1 +1 @@
|
||||
<p>UI has not been copied!</p>
|
||||
<p>UI has not been copied!</p>
|
||||
@@ -59,4 +59,4 @@
|
||||
"dependencies": [],
|
||||
"foldername": "simposeest"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user