Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7572506
fix: use HELTEC_V4 + 16MB flash for Detector (fixes battery_v=0 on V4…
popshark1 Jun 2, 2026
3f4a145
ci: fix Detector flash_size 16MB for Heltec V4 (was 8MB causing V3 bu…
popshark1 Jun 2, 2026
1fd932d
flasher: point REPO to popshark1/batear for V4-fixed releases
popshark1 Jun 2, 2026
58ea1d1
fix(gateway): Heltec V4 board config + 16MB flash size
popshark1 Jun 2, 2026
735c0df
feat(flasher): add V3/V4 manifests and board selection UI
popshark1 Jun 2, 2026
7f8e914
feat(flasher): add manifest_detector_v4.json
popshark1 Jun 2, 2026
9534ec2
feat(flasher): add manifest_gateway_v3.json
popshark1 Jun 2, 2026
dd0860e
feat(flasher): add manifest_gateway_v4.json
popshark1 Jun 2, 2026
d37e625
feat: add sdkconfig.detector.v3 (Heltec V3, 8MB flash)
popshark1 Jun 2, 2026
361c933
feat: add sdkconfig.gateway.v3 (Heltec V3, 8MB flash)
popshark1 Jun 2, 2026
8f89f0b
Add V3/V4 matrix builds for detector and gateway
popshark1 Jun 2, 2026
78fcf12
ci: expand matrix to V3/V4 for detector and gateway (5 roles)
popshark1 Jun 2, 2026
5bf9da6
Add sdkconfig for Heltec V3 detector board
popshark1 Jun 2, 2026
95e97db
Add sdkconfig for Heltec V4 detector board
popshark1 Jun 2, 2026
f154c90
Add initial configuration for Heltec V3 gateway
popshark1 Jun 2, 2026
37e13d0
Add initial sdkconfig for Heltec V4 gateway
popshark1 Jun 2, 2026
e06efa8
ci: fix flash sizes (V3=8MB, V4=16MB) in matrix
popshark1 Jun 2, 2026
a474e42
ci: fix env variable syntax for SDKCONFIG_DEFAULTS
popshark1 Jun 3, 2026
1564b5d
docs: update role specs to show V3/V4 support
popshark1 Jun 3, 2026
10f127b
Update Heltec WiFi LoRa 32 version references
popshark1 Jun 3, 2026
08f1a92
Add board version selection to firmware page
popshark1 Jun 3, 2026
019ca6e
Update visibility of board version selector
popshark1 Jun 3, 2026
14c3090
fix(battery): handle V3/V4 ADC_Ctrl polarity difference for VBAT read
popshark1 Jun 9, 2026
0c61064
flasher: revert fork refs, restore role panels, drop duplicate sdkconfig
TN666 Jun 16, 2026
c969178
ci: restore firmware-size delta vs firmware-latest; fix battery cppcheck
TN666 Jun 16, 2026
4c6023e
ci: skip PR-size comment on fork PRs (read-only token)
TN666 Jun 16, 2026
f16b068
ci: comment firmware sizes on fork PRs via workflow_run
TN666 Jun 16, 2026
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
107 changes: 46 additions & 61 deletions .github/workflows/esp-idf-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ jobs:
strategy:
matrix:
include:
- role: detector
- role: detector_v3
sdkconfig_extra: sdkconfig.detector_v3
flash_size: 8MB
- role: gateway
- role: detector_v4
sdkconfig_extra: sdkconfig.detector_v4
flash_size: 16MB
- role: gateway_v3
sdkconfig_extra: sdkconfig.gateway_v3
flash_size: 8MB
- role: gateway_v4
sdkconfig_extra: sdkconfig.gateway_v4
flash_size: 16MB
- role: wired_detector
sdkconfig_extra: sdkconfig.wired_detector
flash_size: 16MB

steps:
- name: Checkout code
uses: actions/checkout@v5
Expand All @@ -38,7 +46,7 @@ jobs:
run: |
. $IDF_PATH/export.sh
rm -rf build sdkconfig
env SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.${{ matrix.role }}" idf.py set-target esp32s3
env SDKCONFIG_DEFAULTS="sdkconfig.defaults;${{ matrix.sdkconfig_extra }}" idf.py set-target esp32s3
idf.py build

- name: Capture size
Expand All @@ -51,7 +59,7 @@ jobs:
. $IDF_PATH/export.sh
esptool.py --chip esp32s3 merge_bin -o merged_${{ matrix.role }}.bin \
--flash_mode dio --flash_size ${{ matrix.flash_size }} \
0x0 build/bootloader/bootloader.bin \
0x0 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0x10000 build/batear.bin

Expand All @@ -68,7 +76,6 @@ jobs:
needs: build
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest

steps:
- name: Download artifacts
uses: actions/download-artifact@v4
Expand All @@ -81,81 +88,57 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
CUR_DET=$(stat -c %s merged_detector.bin)
CUR_GW=$(stat -c %s merged_gateway.bin)
CUR_WD=$(stat -c %s merged_wired_detector.bin)
set -euo pipefail
ROLES="detector_v3 detector_v4 gateway_v3 gateway_v4 wired_detector"
REPO="${{ github.repository }}"
R=/tmp/size_report.md
sign() { local v=$1; [ "$v" -gt 0 ] && echo "+${v}" || echo "${v}"; }

echo "## Firmware Size Report" > "$R"
echo "" >> "$R"

if curl -sfL -H "Authorization: Bearer ${GITHUB_TOKEN}" \
"https://github.com/${REPO}/releases/download/firmware-latest/merged_detector.bin" \
-o /tmp/prev_detector.bin && \
curl -sfL -H "Authorization: Bearer ${GITHUB_TOKEN}" \
"https://github.com/${REPO}/releases/download/firmware-latest/merged_gateway.bin" \
-o /tmp/prev_gateway.bin; then
PREV_DET=$(stat -c %s /tmp/prev_detector.bin)
PREV_GW=$(stat -c %s /tmp/prev_gateway.bin)
DET_DELTA=$(sign $((CUR_DET - PREV_DET)))
GW_DELTA=$(sign $((CUR_GW - PREV_GW)))

# Wired detector may not exist in previous release
echo "| Role | Current | Previous | Delta |" >> "$R"
echo "|------|--------:|---------:|------:|" >> "$R"
for role in $ROLES; do
# A missing artifact means the build job dropped a binary — fail loudly.
CUR=$(stat -c %s "merged_${role}.bin")
if curl -sfL -H "Authorization: Bearer ${GITHUB_TOKEN}" \
"https://github.com/${REPO}/releases/download/firmware-latest/merged_wired_detector.bin" \
-o /tmp/prev_wired_detector.bin; then
PREV_WD=$(stat -c %s /tmp/prev_wired_detector.bin)
WD_DELTA=$(sign $((CUR_WD - PREV_WD)))
"https://github.com/${REPO}/releases/download/firmware-latest/merged_${role}.bin" \
-o "/tmp/prev_${role}.bin"; then
PREV=$(stat -c %s "/tmp/prev_${role}.bin")
echo "| ${role} | ${CUR} B | ${PREV} B | $(sign $((CUR - PREV))) B |" >> "$R"
else
PREV_WD="—"
WD_DELTA="new"
echo "| ${role} | ${CUR} B | — | new |" >> "$R"
fi

echo "| Role | Current | Previous | Delta |" >> "$R"
echo "|------|--------:|--------:|------:|" >> "$R"
echo "| Detector | $CUR_DET B | $PREV_DET B | $DET_DELTA B |" >> "$R"
echo "| Gateway | $CUR_GW B | $PREV_GW B | $GW_DELTA B |" >> "$R"
echo "| Wired Detector | $CUR_WD B | $PREV_WD B | $WD_DELTA B |" >> "$R"
echo "" >> "$R"
echo "> Compared against [\`firmware-latest\`](https://github.com/${REPO}/releases/tag/firmware-latest)." >> "$R"
else
echo "| Role | Size |" >> "$R"
echo "|------|-----:|" >> "$R"
echo "| Detector | $CUR_DET B |" >> "$R"
echo "| Gateway | $CUR_GW B |" >> "$R"
echo "| Wired Detector | $CUR_WD B |" >> "$R"
echo "" >> "$R"
echo "> No previous release found for comparison." >> "$R"
fi

done
echo "" >> "$R"
echo "> Compared against [\`firmware-latest\`](https://github.com/${REPO}/releases/tag/firmware-latest)." >> "$R"
echo "" >> "$R"
echo "<details><summary>idf.py size details</summary>" >> "$R"
echo "" >> "$R"
echo "**Detector:** \`$(cat size_detector.txt)\`" >> "$R"
echo "**Gateway:** \`$(cat size_gateway.txt)\`" >> "$R"
echo "**Wired Detector:** \`$(cat size_wired_detector.txt)\`" >> "$R"
for role in $ROLES; do
echo "**${role}:** \`$(cat size_${role}.txt)\`" >> "$R"
done
echo "" >> "$R"
echo "</details>" >> "$R"
# Hand the report and PR number off to the pr-size-comment workflow,
# which runs in the base-repo context and can comment on fork PRs
# (the fork-PR GITHUB_TOKEN here is read-only and cannot post).
cp "$R" size_report.md
echo "${{ github.event.pull_request.number }}" > pr_number.txt

{
echo "body<<ENDOFBODY"
cat "$R"
echo "ENDOFBODY"
} >> "$GITHUB_OUTPUT"

- name: Comment PR with firmware sizes
uses: thollander/actions-comment-pull-request@v3
- name: Upload size report
uses: actions/upload-artifact@v4
with:
message: ${{ steps.size_report.outputs.body }}
name: size-report
path: |
size_report.md
pr_number.txt

release:
name: Publish Firmware
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'

steps:
- name: Checkout code
if: startsWith(github.ref, 'refs/tags/')
Expand Down Expand Up @@ -190,6 +173,8 @@ jobs:
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }}
make_latest: ${{ startsWith(github.ref, 'refs/tags/') }}
files: |
merged_detector.bin
merged_gateway.bin
merged_detector_v3.bin
merged_detector_v4.bin
merged_gateway_v3.bin
merged_gateway_v4.bin
merged_wired_detector.bin
49 changes: 49 additions & 0 deletions .github/workflows/pr-size-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: PR Size Comment

# The ESP-IDF Build CI's size-report job runs in the (possibly fork) PR context,
# where GitHub forces GITHUB_TOKEN to read-only and commenting is impossible.
# This workflow is triggered by that run's completion; because workflow_run runs
# in the base-repo context it gets a write token and can comment on fork PRs.
# NOTE: workflow_run only ever uses the copy of this file on the default branch,
# so it takes effect for fork PRs only after it is merged to main.

on:
workflow_run:
workflows: ["ESP-IDF Build CI"]
types: [completed]

permissions:
pull-requests: write

jobs:
comment:
name: Comment firmware sizes
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: Download size report
uses: actions/download-artifact@v4
with:
name: size-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Read PR number and report
id: report
run: |
set -euo pipefail
echo "number=$(cat pr_number.txt)" >> "$GITHUB_OUTPUT"
{
echo "body<<ENDOFBODY"
cat size_report.md
echo "ENDOFBODY"
} >> "$GITHUB_OUTPUT"

- name: Comment PR with firmware sizes
uses: thollander/actions-comment-pull-request@v3
with:
pr-number: ${{ steps.report.outputs.number }}
comment-tag: firmware-size
message: ${{ steps.report.outputs.body }}
25 changes: 22 additions & 3 deletions docs/flasher/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,15 @@ <h1>Flash firmware <span>in 60 seconds</span></h1>
</select>
<span id="versionLoading" class="loading">loading...</span>
</div>

<!-- Board Version Picker (V3/V4) -->
<div class="version-picker">
<label for="boardSelect">Board Version:</label>
<select id="boardSelect">
<option value="v3">Heltec V3 (8MB)</option>
<option value="v4">Heltec V4 (16MB)</option>
</select>
</div>
</section>

<!-- Role Tabs -->
Expand All @@ -217,7 +226,7 @@ <h1>Flash firmware <span>in 60 seconds</span></h1>
<span>I2S Mic + DSP</span>
<span>LoRa TX (SX1262)</span>
<span>AES-128-GCM</span>
<span>Heltec WiFi LoRa 32 V3</span>
<span>Heltec WiFi LoRa 32 V3 / V4</span>
</div>
</div>
<div class="role-info-panel" data-role="gateway">
Expand All @@ -226,7 +235,7 @@ <h1>Flash firmware <span>in 60 seconds</span></h1>
<span>LoRa RX (SX1262)</span>
<span>SSD1306 OLED</span>
<span>MQTT + HA Discovery</span>
<span>Heltec WiFi LoRa 32 V3</span>
<span>Heltec WiFi LoRa 32 V3 / V4</span>
</div>
</div>
<div class="role-info-panel" data-role="wired_detector">
Expand Down Expand Up @@ -558,6 +567,7 @@ <h4>Wait &amp; Done</h4>

/* ---- DOM refs ---- */
const select = document.getElementById("versionSelect");
const boardSelect = document.getElementById("boardSelect");
const loading = document.getElementById("versionLoading");
const btnInstall = document.getElementById("btnInstall");
const installLabel = document.getElementById("installLabel");
Expand Down Expand Up @@ -602,6 +612,11 @@ <h4>Wait &amp; Done</h4>
cfgPanels.forEach(p => p.classList.toggle("active", p.dataset.role === role));
// Wired Detector does not transmit over LoRa — hide the AES key field.
networkKeyPanel.style.display = (role === "wired_detector") ? "none" : "";
// Hide board version selector for wired_detector (only uses one firmware)
const boardVersionPicker = boardSelect.closest('.version-picker');
if (boardVersionPicker) {
boardVersionPicker.style.display = (role === "wired_detector") ? "none" : "";
}
installLabel.textContent = `Install ${ROLE_LABELS[role]}`;
validateAndUpdate();
}
Expand Down Expand Up @@ -630,7 +645,10 @@ <h4>Wait &amp; Done</h4>
}

function fwUrl(tag, role) {
return `${pageBase}firmware/${tag}/merged_${role}.bin`;
// Wired Detector ships a single firmware; LoRa roles carry a board-version suffix.
const boardVer = boardSelect?.value || 'v3';
const roleSuffix = (role === 'wired_detector') ? '' : `_${boardVer}`;
return `${pageBase}firmware/${tag}/merged_${role}${roleSuffix}.bin`;
}

function getValidKey() {
Expand Down Expand Up @@ -838,6 +856,7 @@ <h4>Wait &amp; Done</h4>
});

select.addEventListener("change", () => updateManifests());
boardSelect.addEventListener("change", () => updateManifests());

/* ---- Initial setup ---- */
updateManifests();
Expand Down
15 changes: 15 additions & 0 deletions docs/flasher/manifest_detector_v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Batear Detector (Heltec V3)",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{
"path": "firmware/firmware-latest/merged_detector_v3.bin",
"offset": 0
}
]
}
]
}
15 changes: 15 additions & 0 deletions docs/flasher/manifest_detector_v4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Batear Detector (Heltec V4)",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{
"path": "firmware/firmware-latest/merged_detector_v4.bin",
"offset": 0
}
]
}
]
}
15 changes: 15 additions & 0 deletions docs/flasher/manifest_gateway_v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Batear Gateway (Heltec V3)",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{
"path": "firmware/firmware-latest/merged_gateway_v3.bin",
"offset": 0
}
]
}
]
}
15 changes: 15 additions & 0 deletions docs/flasher/manifest_gateway_v4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Batear Gateway (Heltec V4)",
"new_install_prompt_erase": true,
"builds": [
{
"chipFamily": "ESP32-S3",
"parts": [
{
"path": "firmware/firmware-latest/merged_gateway_v4.bin",
"offset": 0
}
]
}
]
}
Loading
Loading