@@ -101,8 +107,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
const ZOOM_MARGIN = 10;
- import WebGLImpl from "../common/AMCImplementation_WebGL.js"
- import LayerViewImpl from "../common/AMCImplementation_LayerView.js"
+ import WebGLImpl from "@core/common/AMCImplementation_WebGL.js"
+ import LayerViewImpl from "@core/common/AMCImplementation_LayerView.js"
export default {
props: ["Application", "module"],
@@ -128,11 +134,33 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
showJumps: true,
loadingLayerData: false,
loadingScatterplot: false,
- hoverOverData: true
+ hoverOverData: true,
+ appliedCoordinateTransform: ""
}),
methods: {
+ applyCoordinateTransform: function () {
+ const platform = this.module.platform;
+ if (!platform || !this.LayerViewerInstance)
+ return;
+
+ const transform = [
+ Number(platform.transformangle) || 0,
+ Number(platform.rotationcenterx) || 0,
+ Number(platform.rotationcentery) || 0,
+ Number(platform.translationx) || 0,
+ Number(platform.translationy) || 0
+ ];
+ const transformKey = transform.join(",");
+ if (transformKey === this.appliedCoordinateTransform)
+ return;
+
+ this.LayerViewerInstance.setCoordinateTransform(...transform);
+ this.appliedCoordinateTransform = transformKey;
+ this.LayerViewerInstance.RenderScene(true);
+ },
+
onResize: function () {
let domelement = this.$refs.layerViewDiv;
if (!domelement)
@@ -157,8 +185,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
this.LayerViewerInstance.setColorMode ("laseron");
}
else if (this.LayerViewerInstance.layerPointsMode == "laseron") {
+ this.LayerViewerInstance.setColorMode ("powerramp");
+ }
+ else if (this.LayerViewerInstance.layerPointsMode == "powerramp") {
this.LayerViewerInstance.setColorMode ("uniform");
- } else {
+ }
+ else {
this.LayerViewerInstance.setColorMode ("time");
}
},
@@ -168,12 +200,26 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
this.LayerViewerInstance.updateLoadedLayer ();
},
+ onToggleShowLaserOffClick: function () {
+ this.LayerViewerInstance.showLaserOffPoints = !this.LayerViewerInstance.showLaserOffPoints;
+ this.LayerViewerInstance.updateLayerPoints();
+ },
+
onResetViewClick: function () {
let platform = this.module.platform;
if (platform) {
- this.LayerViewerInstance.CenterOnRectangle (- ZOOM_MARGIN, - ZOOM_MARGIN, platform.sizex + ZOOM_MARGIN, platform.sizey + ZOOM_MARGIN);
+ // Origin is machine-zero's location inside the plate (from lower-left),
+ // so plate corners in machine coords span -origin .. (size - origin).
+ let ox = platform.originx || 0;
+ let oy = platform.originy || 0;
+ let sx = platform.sizex;
+ let sy = platform.sizey;
+ // Optional per-axis padding (in mm) enlarging the reset zoom window.
+ let px = platform.paddingx || 0;
+ let py = platform.paddingy || 0;
+ this.LayerViewerInstance.CenterOnRectangle (- ox - ZOOM_MARGIN - px, - oy - ZOOM_MARGIN - py, (sx - ox) + ZOOM_MARGIN + px, (sy - oy) + ZOOM_MARGIN + py);
this.LayerViewerInstance.RenderScene (true);
}
},
@@ -240,6 +286,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
return "Color: LaserOn";
}
+ if (this.LayerViewerInstance.layerPointsMode == "powerramp") {
+ return "Color: Power";
+ }
+
return "Color: Uniform";
},
@@ -256,10 +306,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (pathBoundaries.radius > 0) {
- const left = pathBoundaries.center.x - pathBoundaries.radius + platform.sizex / 2;
- const right = pathBoundaries.center.x + pathBoundaries.radius + platform.sizex / 2;
- const top = pathBoundaries.center.y - pathBoundaries.radius + platform.sizey / 2;
- const bottom = pathBoundaries.center.y + pathBoundaries.radius + platform.sizey / 2;
+ const left = pathBoundaries.center.x - pathBoundaries.radius + platform.originx;
+ const right = pathBoundaries.center.x + pathBoundaries.radius + platform.originx;
+ const top = pathBoundaries.center.y - pathBoundaries.radius + platform.originy;
+ const bottom = pathBoundaries.center.y + pathBoundaries.radius + platform.originy;
this.LayerViewerInstance.CenterOnRectangle (left, top, right, bottom);
this.LayerViewerInstance.RenderScene (true);
@@ -308,7 +358,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
{
this.LayerViewerInstance.clearPoints ();
- return this.Application.axiosGetArrayBufferRequest("/ui/pointcloud/" + scatterplotuuid)
+ return this.Application.axiosGetArrayBufferRequest("/pointcloud/" + scatterplotuuid)
.then(responseData => {
let pointcoordinates = new Float32Array(responseData.data);
@@ -332,7 +382,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
{
this.LayerViewerInstance.clearPointsChannelData (pointsChannelName);
- return this.Application.axiosGetArrayBufferRequest("/ui/pointchanneldata/" + scatterplotuuid + "/" + pointsChannelName)
+ return this.Application.axiosGetArrayBufferRequest("/pointchanneldata/" + scatterplotuuid + "/" + pointsChannelName)
.then(responseData => {
const contentType = responseData.headers['content-type'];
@@ -357,6 +407,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
} else {
console.log(`${key}: ${floatArray.length}`);
}
+ } else if (key.toLowerCase() === 'power') {
+ if (this.LayerViewerInstance) {
+ this.LayerViewerInstance.loadPointsChannelData ("laser", key.toLowerCase(), floatArray);
+ } else {
+ console.log(`${key}: ${floatArray.length}`);
+ }
}
} else {
console.warn(`Key "${key}" is not an array and will be ignored.`);
@@ -389,6 +445,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
let module = this.module;
let platform = this.module.platform;
if (platform && sender) {
+ this.applyCoordinateTransform();
if (module.isActive () && (sender.uuid === module.uuid)) {
@@ -581,13 +638,15 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
const mouseX = event.clientX - renderElementPosition.left;
const mouseY = event.clientY - renderElementPosition.top;
- let pointIndex = this.LayerViewerInstance.glInstance.getRaycasterCollisions ("layerdata_points", mouseX, mouseY);
+ let rawPointIndex = this.LayerViewerInstance.glInstance.getRaycasterCollisions ("layerdata_points", mouseX, mouseY);
+ let pointIndex = this.LayerViewerInstance.resolvePointIndex (rawPointIndex);
if (pointIndex >= 0) {
let pointPosition = this.LayerViewerInstance.getPointPosition (pointIndex);
let pointVelocity = this.LayerViewerInstance.getPointVelocity (pointIndex);
let pointAcceleration = this.LayerViewerInstance.getPointAcceleration (pointIndex);
let pointJerk = this.LayerViewerInstance.getPointJerk (pointIndex);
+ let pointPower = this.LayerViewerInstance.getPointPower (pointIndex);
let infoCaption = `Point ID = ${pointIndex.toFixed(0)}\n`;
if (pointPosition) {
@@ -605,7 +664,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
let jerkinmeterspersecondsquared = pointJerk.j / 1000000.0;
infoCaption += `Jerk: ${jerkinmeterspersecondsquared.toFixed(4)} km/s³\n`;
}
-
+ if (pointPower) {
+ let powerinwatts = pointPower; // * 1000.0;
+ infoCaption += `Power: ${powerinwatts.toFixed(4)} W\n`;
+ }
this.lastMouseX = mouseX;
this.lastMouseY = mouseY;
@@ -632,8 +694,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
const laserpower = this.LayerViewerInstance.segmentProperties[lineIndex].laserpower;
const laserspeed = this.LayerViewerInstance.segmentProperties[lineIndex].laserspeed;
+ const profilename = this.LayerViewerInstance.segmentProperties[lineIndex].profilename;
let infoCaption = `Line ID = ${lineIndex.toFixed(0)}\n${x1.toFixed(3)} / ${y1.toFixed(3)} - ${x2.toFixed(3)} / ${y2.toFixed(3)} mm\n`;
infoCaption += `${laserpower.toFixed(0)}W / ${laserspeed.toFixed (1)} mm/s`;
+ if (profilename) {
+ infoCaption += `\nProfile: ${profilename}`;
+ }
infoboxDiv.innerText = infoCaption;
infoboxDiv.style.background = 'rgba(0, 0, 0, 0.7)';
}
@@ -666,24 +732,24 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
},
- created () {
- this.module.onDataHasChanged = null;
- if (this.module.platform) {
- this.module.platform.displayed_layer = 0;
- this.module.platform.displayed_build = 0;
- }
-
- this.glInstance = this.Application.retrieveWebGLInstance (module.uuid);
+ created () {
+ this.module.onDataHasChanged = null;
+ if (this.module.platform) {
+ this.module.platform.displayed_layer = 0;
+ this.module.platform.displayed_build = 0;
+ }
+
+ this.glInstance = this.Application.retrieveWebGLInstance (this.module.uuid);
- if (!this.glInstance) {
- this.glInstance = new WebGLImpl ();
+ if (!this.glInstance) {
+ this.glInstance = new WebGLImpl ();
+ this.Application.storeWebGLInstance (this.module.uuid, this.glInstance);
+ }
- this.Application.storeWebGLInstance (this.glInstance);
- this.LayerViewerInstance = new LayerViewImpl (this.glInstance);
- this.LayerViewerInstance.RenderScene (true);
-
- }
- },
+ // Always create a fresh LayerViewerInstance wrapping the (possibly cached) glInstance.
+ this.LayerViewerInstance = new LayerViewImpl (this.glInstance);
+ this.LayerViewerInstance.RenderScene (true);
+ },
unmounted ()
{
@@ -725,7 +791,15 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
}
this.LayerViewerInstance.setOrigin (platform.originx, platform.originy);
- this.LayerViewerInstance.CenterOnRectangle (- ZOOM_MARGIN, - ZOOM_MARGIN, platform.sizex + ZOOM_MARGIN, platform.sizey + ZOOM_MARGIN);
+ this.applyCoordinateTransform();
+ let ox = platform.originx || 0;
+ let oy = platform.originy || 0;
+ let sx = platform.sizex;
+ let sy = platform.sizey;
+ // Optional per-axis padding (in mm) enlarging the reset zoom window.
+ let px = platform.paddingx || 0;
+ let py = platform.paddingy || 0;
+ this.LayerViewerInstance.CenterOnRectangle (- ox - ZOOM_MARGIN - px, - oy - ZOOM_MARGIN - py, (sx - ox) + ZOOM_MARGIN + px, (sy - oy) + ZOOM_MARGIN + py);
this.viewed_platform_uuid = platform.uuid;
}
diff --git a/Client/vue2/src/modules/AMCModule_Logs.vue b/Client/vue2/src/modules/AMCModule_Logs.vue
new file mode 100644
index 000000000..61c0e9f32
--- /dev/null
+++ b/Client/vue2/src/modules/AMCModule_Logs.vue
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-arrow-collapse-down
+
+
+ Scroll to bottom
+
+
+
+
+
+
+
+
+ mdi-format-list-numbered
+
+
+ Limit shown entries ({{ effectiveLimit }})
+
+
+
+
+ {{ opt.label }}
+
+
+
+
+
+
+
+
+
+
+
+ mdi-filter-variant
+
+
+
+ {{ subsystemFilter ? 'Filter: ' + subsystemFilter : 'Filter by subsystem' }}
+
+
+
+
+
+ mdi-filter-off-outline
+
+ Clear filter
+
+
+
+ {{ sys }}
+
+
+ No entries
+
+
+
+
+
+
+
+
+
+
+ mdi-download
+
+
+ Download log
+
+
+
+
+ mdi-code-json
+ Current view – JSON
+
+
+ mdi-code-json
+ All historic – JSON
+
+
+
+ mdi-file-delimited-outline
+ Current view – CSV
+
+
+ mdi-file-delimited-outline
+ All historic – CSV
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Client/vue2/src/modules/AMCModule_Paragraph.vue b/Client/vue2/src/modules/AMCModule_Paragraph.vue
new file mode 100644
index 000000000..f84b9ce8a
--- /dev/null
+++ b/Client/vue2/src/modules/AMCModule_Paragraph.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+ {{ module.text }}
+
+
+
+
+
+
+
+
diff --git a/Client/vue2/src/modules/AMCModule_ParameterList.vue b/Client/vue2/src/modules/AMCModule_ParameterList.vue
new file mode 100644
index 000000000..87c7d5e2d
--- /dev/null
+++ b/Client/vue2/src/modules/AMCModule_ParameterList.vue
@@ -0,0 +1,501 @@
+
+
+
+
+
+
+ {{ filterSummary }}
+
+ mdi-download
+ CSV
+
+
+
+
+ mdi-tune-variant
+
+
+
+
+
+
+
+
+ {{ isFavorite(item) ? 'mdi-star' : 'mdi-star-outline' }}
+
+
+
+
+
+ mdi-check
+ mdi-close
+
+ {{ item.paramValue }}
+ {{ item.paramValue }}
+
+
+
+ No entries available
+
+
+
+
+
+ Save view preset
+
+
+
+
+
+ Cancel
+ Save
+
+
+
+
+
+
+
+
+
+
diff --git a/Client/vue2/src/modules/AMCModule_PartList.vue b/Client/vue2/src/modules/AMCModule_PartList.vue
new file mode 100644
index 000000000..8721362c1
--- /dev/null
+++ b/Client/vue2/src/modules/AMCModule_PartList.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
+
{{ loadingText }}
+
{{ errorMessage }}
+
No build selected.
+
+
+
+
+
+
+ Parts ({{ parts.length }})
+
+
+
+
+ |
+ Part name |
+ Part number |
+
+
+
+
+ | No parts in this build. |
+
+
+ | mdi-cube-outline |
+ {{ part.name || '—' }} |
+ {{ part.partnumber || '—' }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Client/src/modules/AMCModule_ContentItem_Chart.vue b/Client/vue2/src/modules/AMCModule_Separator.vue
similarity index 50%
rename from Client/src/modules/AMCModule_ContentItem_Chart.vue
rename to Client/vue2/src/modules/AMCModule_Separator.vue
index d238cd117..a98c78dc3 100644
--- a/Client/src/modules/AMCModule_ContentItem_Chart.vue
+++ b/Client/vue2/src/modules/AMCModule_Separator.vue
@@ -1,6 +1,6 @@
+
+
+
+
+
+ {{ iconName }}
+
+ {{ module.title }}
+ — {{ module.description }}
+
+
+
+
+
+
+
+
diff --git a/Client/vue2/src/modules/AMCModule_StatusList.vue b/Client/vue2/src/modules/AMCModule_StatusList.vue
new file mode 100644
index 000000000..9df60aee4
--- /dev/null
+++ b/Client/vue2/src/modules/AMCModule_StatusList.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
{{ module.title }}
+
+ {{ module.loadingtext || 'No status available.' }}
+
+
+ {{ row.label }}
+ {{ renderRow(row).text }}
+ {{ renderRow(row).text }}
+
+
+
+
+
+
+
+
diff --git a/Client/vue2/src/modules/AMCModule_TableBase.vue b/Client/vue2/src/modules/AMCModule_TableBase.vue
new file mode 100644
index 000000000..1d08df3ee
--- /dev/null
+++ b/Client/vue2/src/modules/AMCModule_TableBase.vue
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Client/src/modules/AMCModule_Tabs.vue b/Client/vue2/src/modules/AMCModule_Tabs.vue
similarity index 52%
rename from Client/src/modules/AMCModule_Tabs.vue
rename to Client/vue2/src/modules/AMCModule_Tabs.vue
index 02bb83e8c..b3a6c5d7f 100644
--- a/Client/src/modules/AMCModule_Tabs.vue
+++ b/Client/vue2/src/modules/AMCModule_Tabs.vue
@@ -30,62 +30,75 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-
-
+
+
+
+
-
{{ moduleTab.caption }}
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
-
-
+
-
+
+
+
diff --git a/Client/vue2/src/modules/AMCModule_TogglePanel.vue b/Client/vue2/src/modules/AMCModule_TogglePanel.vue
new file mode 100644
index 000000000..2e6ba1152
--- /dev/null
+++ b/Client/vue2/src/modules/AMCModule_TogglePanel.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+ {{ toggle.label }}
+ {{ stateLabel(toggle) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/Client/src/modules/AMCModule_ContentItem_Upload.vue b/Client/vue2/src/modules/AMCModule_Upload.vue
similarity index 54%
rename from Client/src/modules/AMCModule_ContentItem_Upload.vue
rename to Client/vue2/src/modules/AMCModule_Upload.vue
index cbb590850..a06d3fb44 100644
--- a/Client/src/modules/AMCModule_ContentItem_Upload.vue
+++ b/Client/vue2/src/modules/AMCModule_Upload.vue
@@ -30,51 +30,44 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
+
+
+
-
\ No newline at end of file
+
diff --git a/Client/src/modules/AMCModule_Logs.vue b/Client/vue2/src/modules/AMCModule_VideoStream.vue
similarity index 67%
rename from Client/src/modules/AMCModule_Logs.vue
rename to Client/vue2/src/modules/AMCModule_VideoStream.vue
index 77e167383..0bfb0294b 100644
--- a/Client/src/modules/AMCModule_Logs.vue
+++ b/Client/vue2/src/modules/AMCModule_VideoStream.vue
@@ -30,44 +30,38 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-
-
-
-
+
+
![Video Stream]()
+
- export default {
- props: ["Application", "module"],
-
- components: {
- },
-
- data: () => ({
- Headers: [
- { text: 'Time', value: 'logTime', width: "50pt", fixed: true },
- { text: 'Subsystem', value: 'logSubsystem', width: "50pt", fixed: true },
- { text: 'Text', value: 'logText' },
- ],
-
- logsAvailable: false,
- tab: null
- })
-
- };
-
-
\ No newline at end of file
+
diff --git a/Client/src/vuetify.js b/Client/vue2/src/vuetify.js
similarity index 85%
rename from Client/src/vuetify.js
rename to Client/vue2/src/vuetify.js
index 834fe0a4d..fe1bbf441 100644
--- a/Client/src/vuetify.js
+++ b/Client/vue2/src/vuetify.js
@@ -36,13 +36,19 @@ Vue.use(Vuetify);
export default new Vuetify({
theme: {
+ dark: false,
themes: {
light: {
- primary: '#808080',
- secondary: '#1e1e1e',
- accent: '#666666',
- error: '#DD2222',
+ primary: '#C8102E',
+ secondary: '#3C3C3C',
+ accent: '#C8102E',
+ error: '#C8102E',
+ info: '#2979FF',
+ success: '#43A047',
+ warning: '#F9A825',
+ background: '#E0E3E7',
+ surface: '#FFFFFF',
},
},
- },
+ },
});
diff --git a/Client/vue.config.js b/Client/vue2/vue.config.js
similarity index 90%
rename from Client/vue.config.js
rename to Client/vue2/vue.config.js
index 43e322eca..723a51ef8 100644
--- a/Client/vue.config.js
+++ b/Client/vue2/vue.config.js
@@ -28,8 +28,17 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+const path = require('path');
+
module.exports = {
"transpileDependencies": [
"vuetify"
- ]
+ ],
+ configureWebpack: {
+ resolve: {
+ alias: {
+ '@core': path.resolve(__dirname, '../core')
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/DevTools/CMakeLists_Distrib.txt b/DevTools/CMakeLists_Distrib.txt
index b7174787a..07fbc660e 100644
--- a/DevTools/CMakeLists_Distrib.txt
+++ b/DevTools/CMakeLists_Distrib.txt
@@ -26,7 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]
-cmake_minimum_required(VERSION 3.5)
+cmake_minimum_required(VERSION 3.10)
set(CMAKE_C_IMPLICIT_LINK_LIBRARIES "")
diff --git a/DevTools/build_clean_distrib_linux64.sh b/DevTools/build_clean_distrib_linux64.sh
index 87dcf0ed5..4ee0f0cd3 100644
--- a/DevTools/build_clean_distrib_linux64.sh
+++ b/DevTools/build_clean_distrib_linux64.sh
@@ -1,9 +1,6 @@
#!/bin/bash
set -e
-# disable go modules
-export GO111MODULE="off"
-
scriptpath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
echo "script path: $scriptpath"
cd "$scriptpath"
@@ -40,4 +37,3 @@ cmake ..
cmake --build . --config Release
cd Output
-
diff --git a/DevTools/build_clean_distrib_rpi.sh b/DevTools/build_clean_distrib_rpi.sh
index d22dabf15..0faaa1e2b 100644
--- a/DevTools/build_clean_distrib_rpi.sh
+++ b/DevTools/build_clean_distrib_rpi.sh
@@ -1,9 +1,6 @@
#!/bin/bash
set -e
-# disable go modules
-export GO111MODULE="off"
-
scriptpath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
echo "script path: $scriptpath"
cd "$scriptpath"
@@ -40,4 +37,3 @@ cmake ..
cmake --build . --config Release
cd Output
-
diff --git a/DevTools/build_clean_distrib_win64.bat b/DevTools/build_clean_distrib_win64.bat
index 948759d1d..4c1bb8fae 100644
--- a/DevTools/build_clean_distrib_win64.bat
+++ b/DevTools/build_clean_distrib_win64.bat
@@ -6,7 +6,7 @@ SET /p DEVPACKAGE=
/dev/null 2>&1; then
+ echo "lcov not found. Please install lcov/genhtml before running coverage."
+ exit 1
+fi
+
+mkdir -p "$BUILD_DIR"
+if [[ ! -f "$BUILD_DIR/githash.txt" ]]; then
+ GITHASH=""
+ if command -v git >/dev/null 2>&1; then
+ GITHASH="$(git -C "$ROOT_DIR" rev-parse --short=8 HEAD 2>/dev/null || true)"
+ fi
+ if [[ -z "$GITHASH" && -f "$ROOT_DIR/build_linux64/githash.txt" ]]; then
+ GITHASH="$(cat "$ROOT_DIR/build_linux64/githash.txt" 2>/dev/null || true)"
+ fi
+ if [[ -z "$GITHASH" && -f "$ROOT_DIR/githash.txt" ]]; then
+ GITHASH="$(cat "$ROOT_DIR/githash.txt" 2>/dev/null || true)"
+ fi
+ if [[ -z "$GITHASH" ]]; then
+ echo "githash.txt not found and git hash unavailable. Run build_clean_linux64.sh or ensure githash.txt exists."
+ exit 1
+ fi
+ printf "%s\n" "$GITHASH" > "$BUILD_DIR/githash.txt"
+fi
+
+if [[ ! -f "$BUILD_DIR/clientdirhash.txt" ]]; then
+ CLIENTHASH=""
+ if [[ -f "$ROOT_DIR/build_linux64/clientdirhash.txt" ]]; then
+ CLIENTHASH="$(cat "$ROOT_DIR/build_linux64/clientdirhash.txt" 2>/dev/null || true)"
+ fi
+ if [[ -z "$CLIENTHASH" && -f "$ROOT_DIR/clientdirhash.txt" ]]; then
+ CLIENTHASH="$(cat "$ROOT_DIR/clientdirhash.txt" 2>/dev/null || true)"
+ fi
+ if [[ -z "$CLIENTHASH" ]]; then
+ echo "clientdirhash.txt not found. Run build_clean_linux64.sh or ensure clientdirhash.txt exists."
+ exit 1
+ fi
+ printf "%s\n" "$CLIENTHASH" > "$BUILD_DIR/clientdirhash.txt"
+fi
+
+cmake -S "$ROOT_DIR" -B "$BUILD_DIR" -DINCLUDE_TESTS=ON -DENABLE_COVERAGE=ON
+
+TARGETS=()
+if [[ $RUN_UNITTEST -eq 1 ]]; then
+ TARGETS+=("amc_unittest")
+fi
+if [[ ${#TESTDEFS[@]} -gt 0 ]]; then
+ TARGETS+=("amc_test")
+fi
+if [[ ${#EXTRA_TARGETS[@]} -gt 0 ]]; then
+ TARGETS+=("${EXTRA_TARGETS[@]}")
+fi
+
+if [[ ${#TARGETS[@]} -gt 0 ]]; then
+ cmake --build "$BUILD_DIR" --target "${TARGETS[@]}"
+fi
+
+pushd "$BUILD_DIR" >/dev/null
+lcov --directory . --zerocounters
+
+if [[ $RUN_UNITTEST -eq 1 ]]; then
+ ./Output/amc_unittest
+fi
+
+if [[ ${#TESTDEFS[@]} -gt 0 ]]; then
+ CMD=(./Output/amc_test)
+ for def in "${TESTDEFS[@]}"; do
+ CMD+=(--testdefinition "$def")
+ done
+ "${CMD[@]}"
+fi
+
+lcov --capture --directory . --base-directory "$ROOT_DIR" --ignore-errors inconsistent --output-file coverage.info
+lcov --ignore-errors unused,inconsistent --remove coverage.info \
+ '/usr/*' \
+ "$ROOT_DIR/3RD_PARTY/*" \
+ "$ROOT_DIR/Framework/*" \
+ "$ROOT_DIR/Libraries/*" \
+ "$ROOT_DIR/SDK/*" \
+ "$ROOT_DIR/submodules/*" \
+ --output-file coverage.info
+genhtml coverage.info --ignore-errors inconsistent --output-directory coverage_html
+echo "Coverage report: $BUILD_DIR/coverage_html/index.html"
+popd >/dev/null
diff --git a/DevTools/executionlist_perf/README.md b/DevTools/executionlist_perf/README.md
new file mode 100644
index 000000000..67f2908d7
--- /dev/null
+++ b/DevTools/executionlist_perf/README.md
@@ -0,0 +1,72 @@
+# Execution list performance
+
+`executionlist_perf.py` seeds a large build history into a dev `amc_server` and measures the
+Build History list, for before/after comparisons of changes to it. It needs only
+Python 3 (standard library) on Linux.
+
+## Usage
+
+```bash
+python3 DevTools/executionlist_perf/executionlist_perf.py run \
+ --output build_linux64/Output --workdir /tmp/elperf --fresh [-n 2000] [--json result.json]
+```
+
+`run` does every step and then stops the server it started:
+
+1. **prepare**: creates `--workdir` from the build's `Output/`. The binaries are symlinked. The
+ MinimalSystem config is copied with a `buildhistory` page added (an `executionlist`, as in
+ `Examples/LPBFSystem/config.xml`). The server binds to 127.0.0.1:18869 (`--port`) with
+ `telemetry="off"`.
+2. Starts the server once so that it creates and migrates `data/storage.db`. This step is
+ skipped when the database already exists.
+3. **seed**: writes N jobs and executions straight into SQLite. The seed is deterministic, and
+ rerunning it replaces its own rows. About 85% of executions are finished, 10% failed and 5%
+ in process. Every 10th job is `deleted`, and every 3rd job has a thumbnail UUID. Every seeded
+ row gets an `incremental_id` (one step per row, above any existing rows), so the list head
+ is the newest seeded execution and `?since=` returns exactly the last k rows.
+4. Starts the server and **measures** each mode in `--modes` (default
+ `full,concurrent,delta,frontend`). It logs in as `test`/`test` with the same
+ `/api/auth` session-key handshake the client uses, and sends the token as a Bearer header.
+
+The steps can also run one at a time, against a server you start yourself:
+
+```bash
+executionlist_perf.py prepare --output build_linux64/Output --workdir W
+executionlist_perf.py seed W/data/storage.db -n 2000 # server stopped
+executionlist_perf.py measure --pid [--modes full,delta --single-count 20]
+```
+
+## Modes
+
+The client (`Client/core/modules/AMCModule_ExecutionList.js` with `AMCListDeltaSync`) fetches
+the full list once when the page loads, then only a `?since=` delta whenever the
+`executionlistheadid` attribute changes. It does not poll the list.
+
+| mode | what it simulates |
+|--------------|-------------------|
+| `full` | Sequential `GET /api/executions` (`--single-count`): one page load. |
+| `concurrent` | `--clients` (default 6) full fetches started at once: several tabs or devices loading together. |
+| `delta` | Sequential `GET /api/executions?since=` for each k in `--delta-sizes` (default `1,10,100`): the fetch after k executions changed. |
+| `frontend` | Sequential `GET /api/frontend?pages=buildhistory`: the scoped v2 snapshot the client polls when it has no SSE stream. The list item contributes only its head id, so this should not grow with N. |
+
+## Output
+
+- `latency_*_s`: per-request latency.
+- `rows`, `response_bytes`: list entries and JSON size of one response.
+- `wall_s` (concurrent): time until the last of the simultaneous fetches finished.
+- `errors` / `first_error`: failed requests and the server's message for the first.
+- `cpu.cpu_mean_pct` / `cpu.cpu_peak_pct`: `amc_server` CPU from `/proc//stat`, sampled
+ every 1 s over the mode. 100% means one core. Short modes finish before the first sample, so
+ only the mean is meaningful there.
+
+Measure an optimised build when you want device-like numbers. An empty `CMAKE_BUILD_TYPE`
+compiles at -O0, while device builds use `-DCMAKE_BUILD_TYPE=Release`. Compare before/after
+only between builds of the same type.
+
+## Damaged databases
+
+`--damaged K` (on `seed` and `run`) appends K executions whose job row does not exist, as in a
+damaged database. They carry the newest `incremental_id`s, so they are in the full list and in
+every delta. Before the per-entry degrade fix in LibMCData, one such row makes the whole
+`/api/executions` request fail (`errors` > 0), and the client's list stays frozen.
+
diff --git a/DevTools/executionlist_perf/executionlist_perf.py b/DevTools/executionlist_perf/executionlist_perf.py
new file mode 100755
index 000000000..ddcfb4eb0
--- /dev/null
+++ b/DevTools/executionlist_perf/executionlist_perf.py
@@ -0,0 +1,425 @@
+#!/usr/bin/env python3
+"""Seed a large build history into a dev amc_server and measure the Build History list.
+
+Before/after tooling for the execution list (N+1 queries per list request). Standard
+library only. See README.md next to this file.
+
+Subcommands
+ prepare Build a scratch server directory from a build Output dir (MinimalSystem plus a
+ "buildhistory" page with an executionlist), bound to 127.0.0.1, telemetry off.
+ seed Insert N build jobs + executions straight into storage.db (server stopped).
+ measure Fetch the execution list of a running server the way the client does and report
+ latency, response size and amc_server CPU.
+ run prepare + start/stop once to migrate the DB + seed + start + measure all modes + stop.
+
+Measure modes (measure --modes, run --modes)
+ full Sequential GET /api/executions: the full list a client fetches once per page load
+ (Client/core/modules/AMCModule_ExecutionList.js via AMCListDeltaSync).
+ concurrent --clients full fetches started at once: several tabs/devices loading together.
+ delta Sequential GET /api/executions?since= for each k in --delta-sizes: the
+ incremental fetch after k executions changed (the client sends one per head change).
+ frontend Sequential GET /api/frontend?pages=buildhistory: the scoped v2 snapshot the
+ client polls when it has no SSE stream (the list item only adds its head id).
+"""
+import argparse, hashlib, json, os, random, re, shutil, signal, socket, sqlite3, subprocess
+import sys, threading, time, urllib.error, urllib.parse, urllib.request, uuid
+from datetime import datetime, timedelta, timezone
+
+DEFAULT_PORT = 18869
+PAGE = "buildhistory"
+
+# --------------------------------------------------------------------------------------------
+# prepare
+
+BUILDHISTORY_PAGE = """
+
+
+
+
+
+
+
+"""
+BUILDHISTORY_MENU = """
+"""
+
+
+def prepare(output, workdir, port):
+ output, workdir = os.path.abspath(output), os.path.abspath(workdir)
+ server_xml = open(os.path.join(output, "amc_server.xml")).read()
+ githash = re.search(r'githash="([0-9a-f]+)"', server_xml).group(1)
+ os.makedirs(os.path.join(workdir, "data"), exist_ok=True)
+ for name in os.listdir(output):
+ if not name.startswith(githash + "_") or name.endswith("_config.xml"):
+ continue
+ link = os.path.join(workdir, name)
+ if os.path.lexists(link):
+ os.remove(link)
+ os.symlink(os.path.join(output, name), link)
+
+ config = open(os.path.join(output, githash + "_config.xml")).read()
+ if 'name="buildhistory"' not in config:
+ # Insert the page before the menu and a menu entry at the start of the menu.
+ config, n = re.subn(r"(\n\s*