From a7bc62326f0ab2a6663bc97580d98caccbbf754b Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 16 Nov 2025 10:07:01 +0000 Subject: [PATCH 1/3] recorder interface: simplify code with await --- apps/recorder/interface.html | 569 ++++++++++++++++++----------------- 1 file changed, 286 insertions(+), 283 deletions(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 1f11b87615..f29d5412da 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -691,315 +691,318 @@ `; } -function getTrackList() { +async function getTrackList() { Util.showModal("Loading Track List..."); domTracks.innerHTML = ""; cleanupMaps(); cleanupCharts(); - Puck.eval(`require("Storage").list(/^recorder\\.log.*\\.csv$/,{sf:1})`,files=>{ - let trackList = []; - // Use promise chain to load tracks sequentially - this prevents overwhelming the Bangle - // with too many concurrent requests and ensures stable Bluetooth communication - let promise = Promise.resolve(); - - files.forEach(filename => { - promise = promise.then(()=>new Promise(resolve => { - const matches = filename.match(/^recorder\.log(.*)\.csv$/); - const trackNo = matches ? matches[1] : ''; - Util.showModal(`Loading Track ${trackNo}...`); - // This function runs on the Bangle to quickly scan for valid GPS data - // Many tracks start recording before GPS lock, so we search up to 100 lines - // to find the first coordinate for the track preview - Puck.eval(`(function(fn) { - var f = require("Storage").open(fn,"r"); - var headers = f.readLine().trim(); - var data = f.readLine(); - var lIdx = headers.split(",").indexOf("Latitude"); - if (lIdx >= 0) { - var tries = 100; - var l = data; - while (l && l.split(",")[lIdx]==="" && tries--) - l = f.readLine(); - if (l) data = l; - } - return {headers:headers,l:data}; -})(${JSON.stringify(filename)})`, trackInfo=>{ - if (!trackInfo || !("headers" in trackInfo)) { - showToast("Error loading track list.", "error"); - resolve(); - return; - } - trackInfo.headers = trackInfo.headers.split(","); - trackList.push({ - filename : filename, - number : trackNo, - info : trackInfo - }); - resolve(); - }); - })); + const files = await new Promise(resolve => { + Puck.eval(`require("Storage").list(/^recorder\\.log.*\\.csv$/,{sf:1})`, resolve); + }); + + let trackList = []; + + // Load tracks sequentially - don't overwhelm the Bangle with too many concurrent requests + let promise = Promise.resolve(); + + for (const filename of files) { + const matches = filename.match(/^recorder\.log(.*)\.csv$/); + const trackNo = matches ? matches[1] : ''; + + Util.showModal(`Loading Track ${trackNo}...`); + + const trackInfo = await new Promise(resolve => { + // This function runs on the Bangle to quickly scan for valid GPS data + // Many tracks start recording before GPS lock, so we search up to 100 lines + // to find the first coordinate for the track preview + Puck.eval(`(function(fn) { +var f = require("Storage").open(fn,"r"); +var headers = f.readLine().trim(); +var data = f.readLine(); +var lIdx = headers.split(",").indexOf("Latitude"); +if (lIdx >= 0) { + var tries = 100; + var l = data; + while (l && l.split(",")[lIdx]==="" && tries--) + l = f.readLine(); + if (l) data = l; +} +return {headers:headers,l:data}; +})(${JSON.stringify(filename)})`, resolve); }); - promise.then(() => { - trackList.sort((a, b) => b.number.localeCompare(a.number)); - - let html = ` -
-

GPS Tracks

`; - - if (trackList.length > 0) { - html += htmlCheckbox("select-all", "Select all"); - html += `
`; - trackList.forEach((track, index) => { - const trackData = trackLineToObject(track.info.headers, track.info.l); - const dateStr = trackData.Time ? - trackData.Time.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) + - ' at ' + trackData.Time.toLocaleTimeString() : - "No date info"; - - html += ` -
-
- ${htmlCheckbox("track-download-" + track.number, undefined, "class='select-checkbox'")} -
- - -
-
Click to load track data...
-
-
-
-
`; - }); + if (!trackInfo || !("headers" in trackInfo)) { + showToast(`Error loading ${filename}`, "error"); + continue; + } + trackInfo.headers = trackInfo.headers.split(","); + trackList.push({ + filename, + number: trackNo, + info: trackInfo + }); + } - html += `
`; - } else { - html += ` -
-
-
-
No tracks
-
No GPS tracks found
-
-
-
`; - } + trackList.sort((a, b) => b.number.localeCompare(a.number)); + + let html = ` +
+

GPS Tracks

`; + + if (trackList.length > 0) { + html += htmlCheckbox("select-all", "Select all"); + html += `
`; + trackList.forEach((track, index) => { + const trackData = trackLineToObject(track.info.headers, track.info.l); + const dateStr = trackData.Time ? + trackData.Time.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) + + ' at ' + trackData.Time.toLocaleTimeString() : + "No date info"; html += ` - `; + } else { + html += ` +
+
+
+
No tracks
+
No GPS tracks found
+
+
+
`; + } - async function confirmDelete(button, filenames) { - if (button.dataset.confirmDelete === "true") { - // Second click - proceed with deletion - for(const filename of filenames) - await deleteTrack(filename); - getTrackList(); - } else { - // First click - change to confirm state - const originalText = button.textContent; - button.textContent = "Confirm Delete"; - button.classList.add("btn-error"); - button.dataset.confirmDelete = "true"; - - // Reset after 3 seconds - setTimeout(() => { - if (button.dataset.confirmDelete === "true") { - button.textContent = originalText; - button.classList.remove("btn-error"); - delete button.dataset.confirmDelete; - } - }, 3000); - } + html += ` + +

Settings

+
+ ${htmlCheckbox( + "settings-allow-no-gps", + "Include GPX/KML entries even when there's no GPS info", + localStorage.getItem("recorder-allow-no-gps")=="true" ? "checked" : "", + "switch", + )} +
+
+ + +
+
`; // Close container div here to include everything + + domTracks.innerHTML = html; + + window.currentTrackList = trackList; + async function displayTrack(trackIndex, trackNumber) { + const trackContainer = document.getElementById(`track-content-${trackNumber}`); + if (!trackContainer || trackIndex >= trackList.length) return; + + if (trackContainer.dataset.loaded === 'true') return; + trackContainer.innerHTML = '
Loading track data...
'; + const track = trackList[trackIndex]; + const trackData = trackLineToObject(track.info.headers, track.info.l); + let trackHtml = ` +
+
+
+ ${trackData.Latitude + ? ` +
+
+ Loading... +
` + : ''} +
+
+ +
+
`; + + trackContainer.innerHTML = trackHtml; + trackContainer.dataset.loaded = 'true'; + attachTrackButtonListeners(trackContainer); + + const fullTrack = await downloadTrack(track.filename); + + const coordinates = fullTrack + .filter(hasValidGPS) + .map(pt => [parseFloat(pt.Latitude), parseFloat(pt.Longitude)]); + + if (coordinates.length > 0) { + createLeafletMap(`map-${track.number}`, coordinates, fullTrack); + + let distance = 0; + for (let i = 1; i < coordinates.length; i++) + distance += L.latLng(coordinates[i-1]).distanceTo(L.latLng(coordinates[i])); + const duration = fullTrack[fullTrack.length-1].Time - fullTrack[0].Time; + const hours = Math.floor(duration / 3600000), minutes = Math.floor((duration % 3600000) / 60000); + const statsEl = document.getElementById(`stats-${track.number}`); + if (statsEl) { + const d = convertDistance(distance/1000); + statsEl.innerHTML = `Distance: ${d.value.toFixed(2)} ${d.unit} | Duration: ${hours}h ${minutes}m | Points: ${coordinates.length}`; } + } - function attachTrackButtonListeners(container) { - const buttons = container.querySelectorAll("button[task]"); - - buttons.forEach(button => { - button.addEventListener("click", async event => { - const button = event.currentTarget; - const filename = button.getAttribute("filename"); - const trackid = button.getAttribute("trackid"); - const task = button.getAttribute("task"); - - if (!filename || !trackid) return; - - const tracks = [{ filename, number: trackid }]; - switch(task) { - case "delete": - await confirmDelete(button, [filename]); - break; - case "downloadkml": - await downloadTracks(tracks, track => saveKML(track, `Bangle.js Track ${trackid}`)); - break; - case "downloadgpx": - await downloadTracks(tracks, track => saveGPX(track, `Bangle.js Track ${trackid}`)); - break; - case "downloadcsv": - await downloadTracks(tracks, track => saveCSV(track, `Bangle.js Track ${trackid}`)); - break; - } - }); - }); - } + createChartsForTrack(track.number, fullTrack); + } - if (trackList.length > 0) { - const selectAll = document.querySelector("#select-all"); - const showOrHideBatch = checkboxes => { - const batch = document.querySelector("#batch"); - batch.classList.toggle("hidden", !checkboxes.some(b => b.checked)); - }; - const handleCheckboxCheck = () => { - const checkboxes = [...document.querySelectorAll(".select-checkbox")]; + async function confirmDelete(button, filenames) { + if (button.dataset.confirmDelete === "true") { + // Second click - proceed with deletion + for(const filename of filenames) + await deleteTrack(filename); + await getTrackList(); + } else { + // First click - change to confirm state + const originalText = button.textContent; + button.textContent = "Confirm Delete"; + button.classList.add("btn-error"); + button.dataset.confirmDelete = "true"; + + // Reset after 3 seconds + setTimeout(() => { + if (button.dataset.confirmDelete === "true") { + button.textContent = originalText; + button.classList.remove("btn-error"); + delete button.dataset.confirmDelete; + } + }, 3000); + } + } - selectAll.checked = checkboxes.every(b => b.checked); - showOrHideBatch(checkboxes); - }; + function attachTrackButtonListeners(container) { + const buttons = container.querySelectorAll("button[task]"); + + buttons.forEach(button => { + button.addEventListener("click", async event => { + const button = event.currentTarget; + const filename = button.getAttribute("filename"); + const trackid = button.getAttribute("trackid"); + const task = button.getAttribute("task"); + + if (!filename || !trackid) return; + + const tracks = [{ filename, number: trackid }]; + switch(task) { + case "delete": + await confirmDelete(button, [filename]); + break; + case "downloadkml": + await downloadTracks(tracks, track => saveKML(track, `Bangle.js Track ${trackid}`)); + break; + case "downloadgpx": + await downloadTracks(tracks, track => saveGPX(track, `Bangle.js Track ${trackid}`)); + break; + case "downloadcsv": + await downloadTracks(tracks, track => saveCSV(track, `Bangle.js Track ${trackid}`)); + break; + } + }); + }); + } - document.querySelectorAll('.accordion-header').forEach(header => { - header.addEventListener('click', e => { - const trackIndex = parseInt(header.dataset.trackIndex); - const trackNumber = trackList[trackIndex].number; - if (!document.getElementById(`accordion-track-${trackNumber}`).checked) { - setTimeout(() => displayTrack(trackIndex, trackNumber), 10); - } - }); + if (trackList.length > 0) { + const selectAll = document.querySelector("#select-all"); + const showOrHideBatch = checkboxes => { + const batch = document.querySelector("#batch"); + batch.classList.toggle("hidden", !checkboxes.some(b => b.checked)); + }; + const handleCheckboxCheck = () => { + const checkboxes = [...document.querySelectorAll(".select-checkbox")]; + + selectAll.checked = checkboxes.every(b => b.checked); + showOrHideBatch(checkboxes); + }; + + document.querySelectorAll('.accordion-header').forEach(header => { + header.addEventListener('click', e => { + const trackIndex = parseInt(header.dataset.trackIndex); + const trackNumber = trackList[trackIndex].number; + if (!document.getElementById(`accordion-track-${trackNumber}`).checked) { + setTimeout(() => displayTrack(trackIndex, trackNumber), 10); + } + }); - header.closest(".horizontal") - .querySelector(".select-checkbox") - .addEventListener('click', handleCheckboxCheck); - }); + header.closest(".horizontal") + .querySelector(".select-checkbox") + .addEventListener('click', handleCheckboxCheck); + }); - selectAll.addEventListener("click", e => { - const checkboxes = [...document.querySelectorAll(".select-checkbox")]; - const { checked } = e.target; + selectAll.addEventListener("click", e => { + const checkboxes = [...document.querySelectorAll(".select-checkbox")]; + const { checked } = e.target; - for(const b of checkboxes) - b.checked = checked; - showOrHideBatch(checkboxes); - }); - } + for(const b of checkboxes) + b.checked = checked; + showOrHideBatch(checkboxes); + }); + } - document.getElementById("settings-allow-no-gps").addEventListener("change",event=>{ - localStorage.setItem("recorder-allow-no-gps", event.target.checked); - }); + document.getElementById("settings-allow-no-gps").addEventListener("change",event=>{ + localStorage.setItem("recorder-allow-no-gps", event.target.checked); + }); - const unitsSelector = document.getElementById("settings-units"); - const currentSettings = getLocalizationSettings(); - unitsSelector.value = currentSettings.auto ? "auto" : (currentSettings.speed === 'kmh' ? "metric" : "imperial"); - unitsSelector.addEventListener("change", e => { - const val = e.target.value; - if (val === "auto") localStorage.removeItem("recorder-units"); - else saveLocalizationSettings({ - speed: val === "metric" ? 'kmh' : 'mph', - distance: val === "metric" ? 'km' : 'mi', - temperature: val === "metric" ? 'celsius' : 'fahrenheit', - elevation: val === "metric" ? 'm' : 'ft' - }); - getTrackList(); - }); - Util.hideModal(); - domTracks.querySelectorAll("button[task$='_selected']").forEach(button => { - button.addEventListener("click", async e => { - const task = e.currentTarget.getAttribute("task"); - await downloadTracks( - trackList.filter(isSelected), - task.includes('kml') ? saveKML : task.includes('gpx') ? saveGPX : saveCSV - ); - }); - }); - domTracks.querySelector("button#delete-selected").addEventListener("click", e => { - const filenames = trackList.filter(isSelected).map(track => track.filename); - confirmDelete(e.target, filenames); - }); + const unitsSelector = document.getElementById("settings-units"); + const currentSettings = getLocalizationSettings(); + unitsSelector.value = currentSettings.auto ? "auto" : (currentSettings.speed === 'kmh' ? "metric" : "imperial"); + unitsSelector.addEventListener("change", e => { + const val = e.target.value; + if (val === "auto") localStorage.removeItem("recorder-units"); + else saveLocalizationSettings({ + speed: val === "metric" ? 'kmh' : 'mph', + distance: val === "metric" ? 'km' : 'mi', + temperature: val === "metric" ? 'celsius' : 'fahrenheit', + elevation: val === "metric" ? 'm' : 'ft' + }); + getTrackList(); + }); + Util.hideModal(); + domTracks.querySelectorAll("button[task$='_selected']").forEach(button => { + button.addEventListener("click", async e => { + const task = e.currentTarget.getAttribute("task"); + await downloadTracks( + trackList.filter(isSelected), + task.includes('kml') ? saveKML : task.includes('gpx') ? saveGPX : saveCSV + ); }); }); + domTracks.querySelector("button#delete-selected").addEventListener("click", e => { + const filenames = trackList.filter(isSelected).map(track => track.filename); + confirmDelete(e.target, filenames); + }); } function onInit() { From eb6aad63cb2ff0f790a4a404cfe1b7c5430650aa Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 16 Nov 2025 10:27:28 +0000 Subject: [PATCH 2/3] recorder: only set `dataset.loaded` after loading the track --- apps/recorder/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index f29d5412da..754be72de8 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -852,10 +852,10 @@

Settings

`; trackContainer.innerHTML = trackHtml; - trackContainer.dataset.loaded = 'true'; attachTrackButtonListeners(trackContainer); const fullTrack = await downloadTrack(track.filename); + trackContainer.dataset.loaded = 'true'; const coordinates = fullTrack .filter(hasValidGPS) From 180c4a966f26ed392e25d027ff251a61b1ec0551 Mon Sep 17 00:00:00 2001 From: Rob Pilling Date: Sun, 16 Nov 2025 10:27:53 +0000 Subject: [PATCH 3/3] recorder: show map if we discover coords after fetching the whole file --- apps/recorder/interface.html | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/recorder/interface.html b/apps/recorder/interface.html index 754be72de8..dbb7969566 100644 --- a/apps/recorder/interface.html +++ b/apps/recorder/interface.html @@ -829,17 +829,14 @@

Settings

trackContainer.innerHTML = '
Loading track data...
'; const track = trackList[trackIndex]; const trackData = trackLineToObject(track.info.headers, track.info.l); - let trackHtml = ` + trackContainer.innerHTML = `
- ${trackData.Latitude - ? `
Loading... -
` - : ''} +
`; - trackContainer.innerHTML = trackHtml; attachTrackButtonListeners(trackContainer); const fullTrack = await downloadTrack(track.filename); @@ -861,8 +857,10 @@

Settings

.filter(hasValidGPS) .map(pt => [parseFloat(pt.Latitude), parseFloat(pt.Longitude)]); + const containerId = `map-${track.number}`; + if (coordinates.length > 0) { - createLeafletMap(`map-${track.number}`, coordinates, fullTrack); + createLeafletMap(containerId, coordinates, fullTrack); let distance = 0; for (let i = 1; i < coordinates.length; i++) @@ -874,6 +872,14 @@

Settings

const d = convertDistance(distance/1000); statsEl.innerHTML = `Distance: ${d.value.toFixed(2)} ${d.unit} | Duration: ${hours}h ${minutes}m | Points: ${coordinates.length}`; } + } else { + const map = trackContainer.querySelector(`#${containerId}`); + const stats = trackContainer.querySelector(".track-stats"); + + map.style.display = "none"; + stats.style.display = "none"; + + // but keep the charts } createChartsForTrack(track.number, fullTrack);