> "$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 }}
diff --git a/docs/flasher/index.html b/docs/flasher/index.html
index 6c6316a..30c7d6e 100644
--- a/docs/flasher/index.html
+++ b/docs/flasher/index.html
@@ -191,6 +191,15 @@ Flash firmware in 60 seconds
loading...
+
+
+
+
+
+
@@ -217,7 +226,7 @@ Flash firmware in 60 seconds
I2S Mic + DSP
LoRa TX (SX1262)
AES-128-GCM
- Heltec WiFi LoRa 32 V3
+ Heltec WiFi LoRa 32 V3 / V4
@@ -226,7 +235,7 @@
Flash firmware in 60 seconds
LoRa RX (SX1262)
SSD1306 OLED
MQTT + HA Discovery
- Heltec WiFi LoRa 32 V3
+ Heltec WiFi LoRa 32 V3 / V4
@@ -558,6 +567,7 @@
Wait & Done
/* ---- 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");
@@ -602,6 +612,11 @@ Wait & Done
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();
}
@@ -630,7 +645,10 @@ Wait & Done
}
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() {
@@ -838,6 +856,7 @@ Wait & Done
});
select.addEventListener("change", () => updateManifests());
+ boardSelect.addEventListener("change", () => updateManifests());
/* ---- Initial setup ---- */
updateManifests();
diff --git a/docs/flasher/manifest_detector_v3.json b/docs/flasher/manifest_detector_v3.json
new file mode 100644
index 0000000..b46496f
--- /dev/null
+++ b/docs/flasher/manifest_detector_v3.json
@@ -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
+ }
+ ]
+ }
+ ]
+}
diff --git a/docs/flasher/manifest_detector_v4.json b/docs/flasher/manifest_detector_v4.json
new file mode 100644
index 0000000..77959f6
--- /dev/null
+++ b/docs/flasher/manifest_detector_v4.json
@@ -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
+ }
+ ]
+ }
+ ]
+}
diff --git a/docs/flasher/manifest_gateway_v3.json b/docs/flasher/manifest_gateway_v3.json
new file mode 100644
index 0000000..5e3f3e5
--- /dev/null
+++ b/docs/flasher/manifest_gateway_v3.json
@@ -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
+ }
+ ]
+ }
+ ]
+}
diff --git a/docs/flasher/manifest_gateway_v4.json b/docs/flasher/manifest_gateway_v4.json
new file mode 100644
index 0000000..541da5c
--- /dev/null
+++ b/docs/flasher/manifest_gateway_v4.json
@@ -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
+ }
+ ]
+ }
+ ]
+}
diff --git a/main/battery.c b/main/battery.c
index 1062da4..f76df88 100644
--- a/main/battery.c
+++ b/main/battery.c
@@ -1,9 +1,10 @@
/*
* battery.c — ADC-oneshot readback of a resistor-divided VBAT rail
*
- * Heltec V3/V4 wire VBAT through a ~4.9× divider into GPIO1 (ADC1_CH0) gated
- * by GPIO37 (ADC_Ctrl, active low). We drive the gate low for each sample to
- * avoid a constant ~100 µA drain through the divider between reads.
+ * Heltec V3/V4 wire VBAT through a ~4.9x divider into GPIO1 (ADC1_CH0) gated
+ * by GPIO37 (ADC_Ctrl). V3: active LOW (drive low to connect divider).
+ * V4: active HIGH (drive high to connect divider).
+ * We connect the divider only while sampling to avoid a constant ~100 uA drain.
*/
#include "battery.h"
@@ -35,11 +36,27 @@ static const char *TAG = "battery";
#define VBAT_ADC_ATTEN ADC_ATTEN_DB_12
#define VBAT_ADC_BITWIDTH ADC_BITWIDTH_DEFAULT
#define VBAT_SAMPLES 8
-#define VBAT_SETTLE_MS 2
-static adc_oneshot_unit_handle_t s_adc;
-static adc_cali_handle_t s_cali;
-static bool s_ready;
+/* ADC_Ctrl (GPIO37) polarity differs between board revisions:
+ * V3 — active LOW: drive LOW to connect the divider, HIGH to disconnect.
+ * V4 — active HIGH: drive HIGH to connect the divider, LOW to disconnect.
+ */
+#if defined(CONFIG_BATEAR_BOARD_HELTEC_V4)
+#define VBAT_CTRL_CONNECT 1
+#define VBAT_CTRL_DISCONNECT 0
+#else
+#define VBAT_CTRL_CONNECT 0
+#define VBAT_CTRL_DISCONNECT 1
+#endif
+
+/* Settle time after enabling the divider before sampling (ms) */
+#ifndef VBAT_SETTLE_MS
+#define VBAT_SETTLE_MS 5
+#endif
+
+static adc_oneshot_unit_handle_t s_adc = NULL;
+static adc_cali_handle_t s_cali = NULL;
+static bool s_ready = false;
static bool cali_init(void)
{
@@ -47,25 +64,20 @@ static bool cali_init(void)
adc_cali_curve_fitting_config_t cfg = {
.unit_id = VBAT_ADC_UNIT,
.chan = VBAT_ADC_CHANNEL,
- .atten = VBAT_ADC_ATTEN,
+ .atten = VBAT_ADC_ATTEN,
.bitwidth = VBAT_ADC_BITWIDTH,
};
- if (adc_cali_create_scheme_curve_fitting(&cfg, &s_cali) == ESP_OK) {
- return true;
- }
-#endif
-#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
- adc_cali_line_fitting_config_t cfg2 = {
+ return adc_cali_create_scheme_curve_fitting(&cfg, &s_cali) == ESP_OK;
+#elif ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
+ adc_cali_line_fitting_config_t cfg = {
.unit_id = VBAT_ADC_UNIT,
- .atten = VBAT_ADC_ATTEN,
+ .atten = VBAT_ADC_ATTEN,
.bitwidth = VBAT_ADC_BITWIDTH,
};
- if (adc_cali_create_scheme_line_fitting(&cfg2, &s_cali) == ESP_OK) {
- return true;
- }
-#endif
- s_cali = NULL;
+ return adc_cali_create_scheme_line_fitting(&cfg, &s_cali) == ESP_OK;
+#else
return false;
+#endif
}
void battery_init(void)
@@ -73,16 +85,15 @@ void battery_init(void)
if (s_ready) {
return;
}
-
gpio_config_t ctrl_cfg = {
- .pin_bit_mask = (1ULL << PIN_VBAT_CTRL),
- .mode = GPIO_MODE_OUTPUT,
- .pull_up_en = GPIO_PULLUP_DISABLE,
- .pull_down_en = GPIO_PULLDOWN_DISABLE,
- .intr_type = GPIO_INTR_DISABLE,
+ .pin_bit_mask = (1ULL << PIN_VBAT_CTRL),
+ .mode = GPIO_MODE_OUTPUT,
+ .pull_up_en = GPIO_PULLUP_DISABLE,
+ .pull_down_en = GPIO_PULLDOWN_DISABLE,
+ .intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&ctrl_cfg);
- gpio_set_level((gpio_num_t)PIN_VBAT_CTRL, 1); /* divider disconnected */
+ gpio_set_level((gpio_num_t)PIN_VBAT_CTRL, VBAT_CTRL_DISCONNECT); /* divider disconnected */
adc_oneshot_unit_init_cfg_t unit_cfg = {
.unit_id = VBAT_ADC_UNIT,
@@ -93,7 +104,6 @@ void battery_init(void)
ESP_LOGE(TAG, "adc_oneshot_new_unit: %s", esp_err_to_name(err));
return;
}
-
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = VBAT_ADC_ATTEN,
.bitwidth = VBAT_ADC_BITWIDTH,
@@ -103,7 +113,6 @@ void battery_init(void)
ESP_LOGE(TAG, "adc_oneshot_config_channel: %s", esp_err_to_name(err));
return;
}
-
/* cppcheck cannot see ESP-IDF's ADC_CALI_SCHEME_*_SUPPORTED macros
* (defined via soc_caps.h from sdkconfig), so under its preprocessor
* view both #if branches in cali_init() compile out and it concludes
@@ -113,7 +122,6 @@ void battery_init(void)
if (!cali_init()) {
ESP_LOGW(TAG, "ADC calibration unavailable — readings will be rough");
}
-
s_ready = true;
ESP_LOGI(TAG, "battery monitor ready (adc=GPIO%d ctrl=GPIO%d ratio=%.2f)",
PIN_VBAT_ADC, PIN_VBAT_CTRL, (double)BOARD_VBAT_DIVIDER_RATIO);
@@ -124,10 +132,8 @@ uint32_t battery_read_mv(void)
if (!s_ready) {
return 0;
}
-
- gpio_set_level((gpio_num_t)PIN_VBAT_CTRL, 0);
+ gpio_set_level((gpio_num_t)PIN_VBAT_CTRL, VBAT_CTRL_CONNECT);
vTaskDelay(pdMS_TO_TICKS(VBAT_SETTLE_MS));
-
uint32_t raw_sum = 0;
int raw = 0;
int taken = 0;
@@ -137,34 +143,28 @@ uint32_t battery_read_mv(void)
taken++;
}
}
-
- gpio_set_level((gpio_num_t)PIN_VBAT_CTRL, 1);
-
+ gpio_set_level((gpio_num_t)PIN_VBAT_CTRL, VBAT_CTRL_DISCONNECT);
if (taken == 0) {
return 0;
}
-
- int raw_avg = (int)(raw_sum / (uint32_t)taken);
-
+ int raw_avg = (int)(raw_sum / (uint32_t)taken);
int mv_at_pin = 0;
if (s_cali) {
if (adc_cali_raw_to_voltage(s_cali, raw_avg, &mv_at_pin) != ESP_OK) {
mv_at_pin = 0;
}
} else {
- /* Rough fallback: 12-bit ADC @ 12 dB atten → ~3100 mV full scale. */
mv_at_pin = (raw_avg * 3100) / 4095;
}
-
float vbat_mv = (float)mv_at_pin * BOARD_VBAT_DIVIDER_RATIO;
- if (vbat_mv < 0.0f) vbat_mv = 0.0f;
+ if (vbat_mv < 0.0f) vbat_mv = 0.0f;
if (vbat_mv > 65535.0f) vbat_mv = 65535.0f;
return (uint32_t)(vbat_mv + 0.5f);
}
-#else /* BOARD_HAS_VBAT */
+#else /* !BOARD_HAS_VBAT */
-void battery_init(void) {}
+void battery_init(void) { }
uint32_t battery_read_mv(void) { return 0; }
-#endif
+#endif /* BOARD_HAS_VBAT */
diff --git a/sdkconfig.detector b/sdkconfig.detector
index ed5cc4c..1c43bcb 100644
--- a/sdkconfig.detector
+++ b/sdkconfig.detector
@@ -1,6 +1,6 @@
# Board (determines set-target chip and flash size)
-CONFIG_BATEAR_BOARD_HELTEC_V3=y
-CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
+CONFIG_BATEAR_BOARD_HELTEC_V4=y
+CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
# Role
CONFIG_BATEAR_ROLE_DETECTOR=y
diff --git a/sdkconfig.detector_v3 b/sdkconfig.detector_v3
new file mode 100644
index 0000000..1eb6daa
--- /dev/null
+++ b/sdkconfig.detector_v3
@@ -0,0 +1,15 @@
+# Board (Heltec V3, 8MB flash)
+CONFIG_BATEAR_BOARD_HELTEC_V3=y
+CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
+
+# Role
+CONFIG_BATEAR_ROLE_DETECTOR=y
+CONFIG_BATEAR_DEVICE_ID=1
+
+# Network (must match between all detectors and gateway)
+CONFIG_BATEAR_NET_KEY="DEADBEEFCAFEBABE13374200F00DAA55"
+CONFIG_BATEAR_LORA_FREQ=915000
+CONFIG_BATEAR_LORA_SYNC_WORD=0x12
+
+# Telemetry heartbeat (jittered +-10% in firmware)
+CONFIG_BATEAR_TELEMETRY_HEARTBEAT_MIN=30
diff --git a/sdkconfig.detector_v4 b/sdkconfig.detector_v4
new file mode 100644
index 0000000..e95f007
--- /dev/null
+++ b/sdkconfig.detector_v4
@@ -0,0 +1,15 @@
+# Board (Heltec V4, 16MB flash)
+CONFIG_BATEAR_BOARD_HELTEC_V4=y
+CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
+
+# Role
+CONFIG_BATEAR_ROLE_DETECTOR=y
+CONFIG_BATEAR_DEVICE_ID=1
+
+# Network (must match between all detectors and gateway)
+CONFIG_BATEAR_NET_KEY="DEADBEEFCAFEBABE13374200F00DAA55"
+CONFIG_BATEAR_LORA_FREQ=915000
+CONFIG_BATEAR_LORA_SYNC_WORD=0x12
+
+# Telemetry heartbeat (jittered +-10% in firmware)
+CONFIG_BATEAR_TELEMETRY_HEARTBEAT_MIN=30
diff --git a/sdkconfig.gateway b/sdkconfig.gateway
index 774fe2c..6e23f66 100644
--- a/sdkconfig.gateway
+++ b/sdkconfig.gateway
@@ -1,6 +1,6 @@
# Board (determines set-target chip and flash size)
-CONFIG_BATEAR_BOARD_HELTEC_V3=y
-CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
+CONFIG_BATEAR_BOARD_HELTEC_V4=y
+CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
# Role
CONFIG_BATEAR_ROLE_GATEWAY=y
diff --git a/sdkconfig.gateway_v3 b/sdkconfig.gateway_v3
new file mode 100644
index 0000000..15bd02f
--- /dev/null
+++ b/sdkconfig.gateway_v3
@@ -0,0 +1,14 @@
+# Board (Heltec V3, 8MB flash)
+CONFIG_BATEAR_BOARD_HELTEC_V3=y
+CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
+
+# Role
+CONFIG_BATEAR_ROLE_GATEWAY=y
+
+# Network (must match between all detectors and gateway)
+CONFIG_BATEAR_NET_KEY="DEADBEEFCAFEBABE13374200F00DAA55"
+CONFIG_BATEAR_LORA_FREQ=915000
+CONFIG_BATEAR_LORA_SYNC_WORD=0x12
+
+# Telemetry heartbeat (jittered +-10% in firmware)
+CONFIG_BATEAR_TELEMETRY_HEARTBEAT_MIN=30
diff --git a/sdkconfig.gateway_v4 b/sdkconfig.gateway_v4
new file mode 100644
index 0000000..44ce339
--- /dev/null
+++ b/sdkconfig.gateway_v4
@@ -0,0 +1,14 @@
+# Board (Heltec V4, 16MB flash)
+CONFIG_BATEAR_BOARD_HELTEC_V4=y
+CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
+
+# Role
+CONFIG_BATEAR_ROLE_GATEWAY=y
+
+# Network (must match between all detectors and gateway)
+CONFIG_BATEAR_NET_KEY="DEADBEEFCAFEBABE13374200F00DAA55"
+CONFIG_BATEAR_LORA_FREQ=915000
+CONFIG_BATEAR_LORA_SYNC_WORD=0x12
+
+# Telemetry heartbeat (jittered +-10% in firmware)
+CONFIG_BATEAR_TELEMETRY_HEARTBEAT_MIN=30