Skip to content

Commit 99e138f

Browse files
author
smartghar-sync[bot]
committed
sync(rx): rx-v2.8.6
1 parent fe47bf2 commit 99e138f

30 files changed

Lines changed: 1777 additions & 218 deletions

File tree

README.md

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# TankSync — reliable smart water monitoring
22

3-
[![Pre-order Developer Edition](https://img.shields.io/badge/Pre--order-Developer%20Edition-success.svg?style=flat)](https://shop.smartghar.org)
43
[![Firmware: AGPL-3.0](https://img.shields.io/badge/Firmware-AGPL--3.0-blue.svg)](LICENSE)
54
[![Hardware: CC BY-SA 4.0](https://img.shields.io/badge/Hardware-CC%20BY--SA%204.0-orange.svg)](hardware/LICENSE)
65
[![ESP-IDF](https://img.shields.io/badge/ESP--IDF-v5.4-red.svg)](https://docs.espressif.com/projects/esp-idf/)
@@ -17,17 +16,6 @@
1716
<sub><em>The indoor hub (left), the solar tank sensor with non-contact ultrasonic measurement (centre), and the custom circular TX PCB (right) — current production hardware, REV 2.2 (May 2026), tested through Delhi summer at 45°C ambient.</em></sub>
1817
</p>
1918

20-
## Watch the story — Episode 1
21-
22-
<p align="center">
23-
<a href="https://www.youtube.com/watch?v=ZZt6cZbWM0g">
24-
<img src="https://img.youtube.com/vi/ZZt6cZbWM0g/maxresdefault.jpg" width="70%" alt="TankSync Episode 1 — Smart Home That Works Without Internet" />
25-
</a>
26-
</p>
27-
<p align="center">
28-
<sub><em>Why I built TankSync, the local-first philosophy, and how the rooftop sensor + indoor hub stay reliable when the internet doesn't. <a href="https://www.youtube.com/watch?v=ZZt6cZbWM0g">Watch on YouTube →</a></em></sub>
29-
</p>
30-
3119
## Try the in-browser flasher first
3220

3321
👉 **[tanksync.smartghar.org/firmware/](https://tanksync.smartghar.org/firmware/)**
@@ -194,26 +182,6 @@ This is the open-source TankSync firmware + hardware mirror. The hosted cloud da
194182

195183
The firmware works fully **without** the cloud — local web UI on the hub gives you tank levels, settings, OTA updates, Home Assistant integration. Cloud is opt-in convenience, never a dependency.
196184

197-
## Developer Edition hardware
198-
199-
If you'd rather skip sourcing parts, flashing firmware, printing enclosures, and assembling hardware yourself, prebuilt TankSync Developer Edition kits are available for preorder.
200-
201-
Each kit includes:
202-
- assembled RX hub
203-
- assembled TX sensor node
204-
- flashed firmware
205-
- LoRa modules preconfigured
206-
- waterproof enclosure set
207-
- OLED display + local web UI
208-
- access to the hosted TankSync PWA experience
209-
210-
The firmware and hardware files remain fully open for self-build deployments.
211-
212-
Preorders:
213-
https://shop.smartghar.org
214-
215-
First batch is currently planned for end of July / early August 2026.
216-
217185
## Licenses
218186

219187
| Component | License | What this means |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.8.5
1+
2.8.6
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
idf_component_register(
2+
SRCS "log_buffer.c"
3+
INCLUDE_DIRS "include"
4+
REQUIRES
5+
log
6+
freertos
7+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* log_buffer — captures ESP_LOG output into an in-RAM ring buffer so the TX
3+
* web UI can show live boot/runtime logs without a USB cable attached.
4+
*
5+
* Usage:
6+
* log_buffer_init(); // call once early in app_main
7+
* // ... all subsequent ESP_LOGI/W/E calls are also appended to the ring
8+
*
9+
* size_t cursor = 0;
10+
* char out[2048];
11+
* size_t n = log_buffer_read(out, sizeof(out), &cursor);
12+
* // out contains up to n bytes of log text; cursor advances so the next
13+
* // call returns only newly-appended bytes.
14+
*
15+
* Thread-safety: vprintf callback + read both take a FreeRTOS mutex so the
16+
* HTTP handler can safely read while the LoRa / sensor tasks are logging.
17+
*/
18+
19+
#pragma once
20+
21+
#include <stdint.h>
22+
#include <stddef.h>
23+
#include "esp_err.h"
24+
25+
#ifndef LOG_BUFFER_BYTES
26+
#define LOG_BUFFER_BYTES 4096 // ~50 typical log lines
27+
#endif
28+
29+
/**
30+
* Install the vprintf hook so every ESP_LOG* call also lands in the ring
31+
* buffer. The original vprintf (stdout via USB-Serial-JTAG / UART) keeps
32+
* working — both paths receive the same bytes.
33+
* Safe to call multiple times; subsequent calls are no-ops.
34+
*/
35+
esp_err_t log_buffer_init(void);
36+
37+
/**
38+
* Copy the bytes appended since *cursor into out_buf.
39+
*
40+
* @param out_buf destination
41+
* @param out_sz max bytes to copy (capped at out_sz - 1; output is NUL-terminated)
42+
* @param cursor in: last-seen byte count; out: new byte count after this read
43+
* pass *cursor=0 on first call to get everything currently buffered.
44+
* If the reader fell behind (cursor < total - LOG_BUFFER_BYTES),
45+
* cursor is silently advanced to the oldest available byte and a
46+
* "<gap of N bytes>" marker is prepended to out_buf so the UI can
47+
* show that some lines were dropped.
48+
* @return number of bytes written to out_buf (excluding the NUL).
49+
*/
50+
size_t log_buffer_read(char *out_buf, size_t out_sz, size_t *cursor);
51+
52+
/**
53+
* Drop everything currently in the ring. Useful if the UI has a "Clear" button.
54+
* Cursor values held by clients become stale but they'll catch up on next read
55+
* via the gap-marker path.
56+
*/
57+
void log_buffer_clear(void);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* log_buffer — see log_buffer.h for design notes.
3+
*
4+
* Implementation: byte-based circular buffer protected by a FreeRTOS mutex.
5+
* Each ESP_LOG call routes through esp_log_set_vprintf(), where we vsnprintf
6+
* the formatted line into a small stack buffer, copy bytes into the ring,
7+
* and forward the same bytes to the original vprintf so the USB-Serial-JTAG
8+
* console still works.
9+
*/
10+
11+
#include "log_buffer.h"
12+
#include "freertos/FreeRTOS.h"
13+
#include "freertos/semphr.h"
14+
#include "esp_log.h"
15+
#include <stdarg.h>
16+
#include <stdio.h>
17+
#include <string.h>
18+
19+
static char s_ring[LOG_BUFFER_BYTES];
20+
static size_t s_head = 0; // next write index
21+
static size_t s_total = 0; // monotonic byte count
22+
static SemaphoreHandle_t s_mu = NULL;
23+
static vprintf_like_t s_prev_vprintf = NULL;
24+
static bool s_inited = false;
25+
26+
static int log_buffer_vprintf(const char *fmt, va_list args) {
27+
// Format into a local stack buffer first so we hold the mutex for the
28+
// minimum time. 256 chars handles every ESP_LOGI / LOGW / LOGE the
29+
// codebase emits without truncation in practice.
30+
char line[256];
31+
va_list args_copy;
32+
va_copy(args_copy, args);
33+
int n = vsnprintf(line, sizeof(line), fmt, args_copy);
34+
va_end(args_copy);
35+
if (n < 0) n = 0;
36+
if (n >= (int)sizeof(line)) n = sizeof(line) - 1;
37+
38+
if (s_mu && xSemaphoreTake(s_mu, pdMS_TO_TICKS(5)) == pdTRUE) {
39+
for (int i = 0; i < n; i++) {
40+
s_ring[s_head] = line[i];
41+
s_head = (s_head + 1) % sizeof(s_ring);
42+
s_total++;
43+
}
44+
xSemaphoreGive(s_mu);
45+
}
46+
47+
// Forward to the original sink (USB-Serial-JTAG / UART console) so the
48+
// serial monitor still works when a cable is attached.
49+
if (s_prev_vprintf) return s_prev_vprintf(fmt, args);
50+
return vprintf(fmt, args);
51+
}
52+
53+
esp_err_t log_buffer_init(void) {
54+
if (s_inited) return ESP_OK;
55+
s_mu = xSemaphoreCreateMutex();
56+
if (!s_mu) return ESP_ERR_NO_MEM;
57+
s_prev_vprintf = esp_log_set_vprintf(log_buffer_vprintf);
58+
s_inited = true;
59+
return ESP_OK;
60+
}
61+
62+
size_t log_buffer_read(char *out_buf, size_t out_sz, size_t *cursor) {
63+
if (!out_buf || out_sz < 2 || !cursor) return 0;
64+
if (!s_mu) { out_buf[0] = '\0'; return 0; }
65+
66+
size_t written = 0;
67+
if (xSemaphoreTake(s_mu, pdMS_TO_TICKS(50)) != pdTRUE) {
68+
out_buf[0] = '\0';
69+
return 0;
70+
}
71+
72+
size_t total = s_total;
73+
size_t oldest = (total > sizeof(s_ring)) ? (total - sizeof(s_ring)) : 0;
74+
75+
// Detect and report gap for clients that fell behind.
76+
if (*cursor < oldest) {
77+
size_t lost = oldest - *cursor;
78+
int marker = snprintf(out_buf, out_sz, "<gap of %u bytes>\n",
79+
(unsigned)lost);
80+
if (marker > 0) {
81+
written = (size_t)marker;
82+
if (written >= out_sz) written = out_sz - 1;
83+
}
84+
*cursor = oldest;
85+
}
86+
87+
// Copy from cursor to total, respecting buffer wrap.
88+
while (*cursor < total && written < out_sz - 1) {
89+
size_t idx = *cursor % sizeof(s_ring);
90+
out_buf[written++] = s_ring[idx];
91+
(*cursor)++;
92+
}
93+
out_buf[written] = '\0';
94+
95+
xSemaphoreGive(s_mu);
96+
return written;
97+
}
98+
99+
void log_buffer_clear(void) {
100+
if (!s_mu) return;
101+
if (xSemaphoreTake(s_mu, pdMS_TO_TICKS(50)) == pdTRUE) {
102+
s_head = 0;
103+
s_total = 0;
104+
memset(s_ring, 0, sizeof(s_ring));
105+
xSemaphoreGive(s_mu);
106+
}
107+
}

firmware/Receiver-ESP32-DevKit/components/lora_rylr998/lora_rylr998.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ static bool parse_rcv(const char *line, lora_rx_packet_t *pkt) {
270270
pkt->power_mw = 0;
271271
pkt->charging = false;
272272
pkt->sensor_status = 'u';
273+
pkt->sensor_kind[0] = '\0';
273274

274275
char *mode_s = strtok_r(NULL, ":\r\n", &sp2);
275276
if (mode_s && mode_s[0] != '\0') {
@@ -295,6 +296,15 @@ static bool parse_rcv(const char *line, lora_rx_packet_t *pkt) {
295296
if (s == 'o' || s == 'e') pkt->sensor_status = s;
296297
}
297298

299+
// Optional 11th field: sensor_kind tag (since TX v2.0.15) — "sr04" |
300+
// "ld2413" | "?" sentinel. Older TX firmware that omits this leaves the
301+
// field empty so the dashboard knows "TX too old to report".
302+
char *skind_s = strtok_r(NULL, ":\r\n", &sp2);
303+
if (skind_s && skind_s[0] != '\0' && skind_s[0] != '?') {
304+
strncpy(pkt->sensor_kind, skind_s, sizeof(pkt->sensor_kind) - 1);
305+
pkt->sensor_kind[sizeof(pkt->sensor_kind) - 1] = '\0';
306+
}
307+
298308
pkt->charging = (pkt->power_mode == 'i' && pkt->current_ma < 0);
299309

300310
return true;

firmware/Receiver-ESP32-DevKit/components/lora_rylr998/lora_rylr998.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ typedef struct {
5858
// but 'e' is an explicit failure signal (paint a sensor-error badge), while
5959
// 'u' just means "TX is too old to tell us either way."
6060
char sensor_status;
61+
62+
// Sensor driver kind the TX is currently RUNNING (since TX firmware
63+
// v2.0.15). Empty string if absent. Compared against the registry's
64+
// queued sensor_kind so the dashboard can show Active vs Queued.
65+
char sensor_kind[12];
6166
} lora_rx_packet_t;
6267

6368
// ── Hardware state ────────────────────────────────────────────────────────────

firmware/Receiver-ESP32-DevKit/components/mqtt_client/mqtt_manager.c

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ static void handle_cmd(const char *command, const char *payload) {
193193
cJSON *pwr_j = cJSON_GetObjectItem(j, "lora_pwr");
194194
uint8_t pwr = pwr_j ? (uint8_t)cJSON_GetNumberValue(pwr_j) : 0;
195195
if (pwr > 22) pwr = 22;
196+
// Optional sensor_kind ("sr04" | "ld2413"). Absent or empty = leave
197+
// whatever the TX currently has. Validation is in registry_set_sensor_kind.
198+
const char *sk_ptr = cJSON_GetStringValue(cJSON_GetObjectItem(j, "sensor_kind"));
199+
char sk_buf[12] = {0};
200+
if (sk_ptr) { strncpy(sk_buf, sk_ptr, sizeof(sk_buf) - 1); }
196201
cJSON_Delete(j);
197202

198203
const char *name = name_ptr ? name_buf : NULL;
@@ -209,14 +214,21 @@ static void handle_cmd(const char *command, const char *payload) {
209214
if (sleep_s >= 60) {
210215
registry_set_remote_config(addr, sleep_s, samples > 0 ? samples : 5, pwr);
211216
}
217+
if (sk_buf[0]) {
218+
if (!registry_set_sensor_kind(addr, sk_buf)) {
219+
pub(result_topic, "{\"ok\":false,\"err\":\"bad_sensor_kind\"}", 0);
220+
return;
221+
}
222+
}
212223
// Republish config immediately so cloud sees the new state without waiting
213224
// for the next periodic publish cycle.
214225
int idx = registry_find(addr);
215226
if (idx >= 0) mqtt_publish_tank(idx);
216227

217228
pub(result_topic, "{\"ok\":true}", 0);
218-
ESP_LOGI(TAG, "CMD set_config: addr=%u sleep=%lu samples=%u pwr=%u",
219-
(unsigned)addr, (unsigned long)sleep_s, (unsigned)samples, (unsigned)pwr);
229+
ESP_LOGI(TAG, "CMD set_config: addr=%u sleep=%lu samples=%u pwr=%u sensor=%s",
230+
(unsigned)addr, (unsigned long)sleep_s, (unsigned)samples, (unsigned)pwr,
231+
sk_buf[0] ? sk_buf : "(unchanged)");
220232

221233
} else if (strcmp(command, "ota_check") == 0) {
222234
// PWA-triggered manifest check. Non-blocking — actual fetch runs in
@@ -800,6 +812,16 @@ void mqtt_publish_tank(int idx) {
800812
make_topic(topic, sizeof(topic), slug, "lora_pwr");
801813
pub(topic, val, 1);
802814

815+
// sensor_kind — what RX has QUEUED for this TX (the user's choice, what
816+
// gets pushed in the next SET frame). Empty = no preference recorded.
817+
make_topic(topic, sizeof(topic), slug, "sensor_kind");
818+
pub(topic, info.sensor_kind, 1);
819+
820+
// active_sensor — what the TX is ACTUALLY running, reported in TANK
821+
// packets (since TX v2.0.15). Empty if TX firmware doesn't declare it.
822+
make_topic(topic, sizeof(topic), slug, "active_sensor");
823+
pub(topic, data.active_sensor, 1);
824+
803825
// TX firmware version — published only when TX has actually reported one.
804826
// Empty/zero means pre-power-telemetry TX or never received the version
805827
// packet yet; suppress to avoid clobbering a previously-known value.
@@ -821,7 +843,7 @@ void mqtt_unpublish_tank(uint16_t addr) {
821843
"sensor_error",
822844
"power_mode", "current_ma", "power_mw", "charging",
823845
"name", "min_dist", "max_dist", "capacity", "sleep_s", "samples",
824-
"lora_pwr", "fw",
846+
"lora_pwr", "sensor_kind", "active_sensor", "fw",
825847
};
826848
char topic[MAX_TOPIC_LEN];
827849
for (size_t i = 0; i < sizeof(retained_fields) / sizeof(retained_fields[0]); i++) {

0 commit comments

Comments
 (0)