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:
Chris Gerth
2021-12-03 22:08:51 -06:00
committed by GitHub
parent dbd631da61
commit c944967476
10 changed files with 203 additions and 16 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -49,6 +49,7 @@
return {
selectedTab: 0,
snack: false,
calibrationInProgress: false,
snackbar: {
color: "accent",
text: ""

View File

@@ -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 {

View File

@@ -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%"

View File

@@ -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;
}
}

View File

@@ -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 =

View File

@@ -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);

View File

@@ -1 +1 @@
<p>UI has not been copied!</p>
<p>UI has not been copied!</p>

View File

@@ -59,4 +59,4 @@
"dependencies": [],
"foldername": "simposeest"
}
]
]