Skip to content

Commit 0669d4f

Browse files
rfpronkclaudeRAR
authored
Feature: Support lilygo t can485 board (without PSRAM) (#44)
* fix(monitor): emit platform: tigo_monitor in generated sensor YAML The "Generate YAML Config" button printed `- platform: tigo_server` for every discovered device, but the sensor platform lives in components/tigo_monitor/sensor.py and is named `tigo_monitor`. Pasting the output verbatim failed config validation. Barely noticeable on boards with the web UI, where the node table is a page you can read. On a board without one, that button is the only panel-discovery workflow there is, so its output has to paste in clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(monitor): skip the PSRAM probe when CONFIG_SPIRAM is unset psram_malloc() called heap_caps_malloc(MALLOC_CAP_SPIRAM) on every allocation and fell back to the internal heap when it failed. On a build with no PSRAM that attempt can never succeed, so it ran on every single allocation all day, and logged a "PSRAM unavailable" warning for anything over 1KB. Guard it on the IDF's CONFIG_SPIRAM, which is set only when PSRAM is actually configured, and include sdkconfig.h explicitly so a silently missing header cannot compile PSRAM out on boards that do have it. No behaviour change where PSRAM exists. Also reword the setup() logging: "No PSRAM detected" is no longer a warning, because a sensors-only build without tigo_server is a supported configuration, and the pre-allocated 16KB serial buffer is not a "PSRAM buffer" on such a board. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(boards): add LilyGO T-CAN485 for sensors-only builds The T-CAN485 is a plain ESP32 with 4MB flash and no PSRAM. It cannot have any: the board wires GPIO16/17 to its RS485 front end, which is exactly where a WROVER's PSRAM would sit. It does carry an isolated MAX13487E transceiver on GPIO21/22, which makes it a natural fit for a Home-Assistant-only install. PSRAM was never a requirement of tigo_monitor itself. The device and node tables run roughly 600 bytes per optimizer, so 20 of them cost ~12KB of the ~180-200KB an ESP32 has free once WiFi is up. It is tigo_server that needs PSRAM, and it stays out of these configs. esp32-lilygo-t-can485.yaml drives the three transceiver enable lines high at boot (5V_EN, /RE, SHDN) as the MAX13487E datasheet requires, and example-t-can485.yaml is ready to flash. Verified with esphome compile: 53% of a 1.75MB OTA slot, 15% static RAM, no warnings. Note the MAX13487E has no DE pin, so unlike the MAX485 wiring in the docs there is no way to strap this board driver-off in hardware. Read-only rests on tigo_monitor having no UART write calls; the board file documents that and how to harden it further. This does not reopen the Config Builder to PSRAM-less boards. The builder emits configs containing tigo_server, so its guard is unchanged and still enforced by test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(boards): free 40KB IRAM and drop pre-3.1 workarounds on T-CAN485 Two ESP32-classic-only framework options for the T-CAN485: sram1_as_iram hands the SRAM1 region — previously reserved as bootloader DRAM — to IRAM, worth +40KB. That pairs directly with CONFIG_UART_ISR_IN_IRAM, which spends IRAM to keep the UART ISR off flash so Tigo frames are not dropped while flash is busy. This is where that budget comes from. minimum_chip_revision: '3.1' drops the IDF's workaround code for older silicon bugs. Flash falls from 973,263 to 969,243 bytes; still 52.8% of the 1.75MB OTA slot. Both fail closed rather than degrading, so both are documented in the board file and in boards/README.md: - sram1_as_iram needs an ESP-IDF v5.1+ bootloader. A USB flash updates the bootloader automatically, an OTA does not, so this board must be flashed over USB the first time or it will not boot. - the bootloader hard-checks the minimum revision; a chip older than v3.1 halts at boot. The boot log prints the actual revision, and the value needs lowering if it reports less. Verified with esphome compile: CONFIG_ESP32_REV_MIN_FULL=301 and CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM=y in the generated sdkconfig, no warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(monitor): AUTO_LOAD sensor, text_sensor and binary_sensor tigo_monitor.h includes sensor.h, text_sensor.h and binary_sensor.h unconditionally, unlike time/button which sit behind USE_TIME/USE_BUTTON. ESPHome only copies a component's sources into the build when that component appears in the config, so any hand-written YAML that did not declare all three died at compile time: tigo_monitor.h:8:10: fatal error: esphome/components/text_sensor/text_sensor.h: No such file or directory The fix was an empty `text_sensor:` stanza you had to already know to write, and it was easy to miss because the error names only whichever header the compiler reached first — adding one at a time just moves the failure to the next line. AUTO_LOAD pulls the three in whenever tigo_monitor is used, which is what it is for. This loads ESPHome's core sensor/text_sensor/binary_sensor components, not this package's same-named platform files; those still load only when a `- platform: tigo_monitor` entry asks for them. Verified by compiling a config with no text_sensor:/binary_sensor: keys — it failed on the include before, and builds after. A config that does declare them is unaffected: example-t-can485.yaml still produces a byte-identical 969,243-byte image. Also drops the now-obsolete instructions from the troubleshooting guide and CLAUDE.md, and corrects the "required" comments on the test targets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(boards): wire CCA panel-name sync into the T-CAN485 example The example shipped without any mention of cca_ip, which made it look as though panel names needed the web server. They do not: cca_ip is already a tigo_monitor option, and sync_from_cca() -> query_cca_config() -> match_cca_to_uart() all live in tigo_monitor with no tigo_server dependency. The HTTP read buffer goes through psram_malloc() and is 4KB, with the response accumulator capped at 64KB, so it is comfortable on the internal heap of a board with no PSRAM. Adds the "Sync from CCA" button and a commented cca_ip block documenting the endpoint (GET /cgi-bin/summary_config, Basic Tigo:$olar) and a curl one-liner to verify it answers before enabling. Also records what the BLE CCA bridge actually does, since the naming invites the opposite assumption: it only issues DEVICE_PING and DEVICE_INFO for the CCA Info page. match_cca_to_uart() has exactly one caller, the HTTP handler, so BLE never contributes panel layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * tweaking * Change upstream repo * fix(t-can485): require chip rev 3.0, not 3.1, or ECO3 boards bootloop minimum_chip_revision: '3.1' excluded every ESP32 ECO3 part — the common revision — from a board config named after hardware people already own. ESPHome sets CONFIG_ESP32_REV_MIN_* for the chosen revision and clears the rest, so this is a hard floor enforced by the bootloader: it fails at boot, not at build, and presents as an unexplained bootloop. It bought nothing. Every revision-gated conditional in IDF 5.5.5 tests CONFIG_ESP32_REV_MIN_FULL >= 300, which 3.0 satisfies; nothing in the tree tests >= 301, and REV_MIN_3 and REV_MIN_3_1 select an identical option set. Verified by building both: byte-for-byte identical output, RAM 47792 and flash 856267 either way, with CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM=y unaffected — the SRAM1 option depends only on !ESP32_TRAX and never on chip revision. Also document in boards/README.md that the first flash must be over USB (sram1_as_iram needs a v5.1+ bootloader and OTA does not update it; the board file said so already, the board comparison table did not), and soften the device-count claim to what was actually measured — 18 tested, 40 expected — noting the real ceiling on a PSRAM-less board is heap fragmentation across many small per-device allocations, not total bytes. Claude-Session: https://claude.ai/code/session_01RgSnMCa3JQigphGnPbazdw --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Andrew Rankin <andrew@eiknet.com>
1 parent d790870 commit 0669d4f

11 files changed

Lines changed: 521 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **LilyGO T-CAN485 support, for Home-Assistant-only installs.** A board with no PSRAM can now run a sensors-only build — `tigo_monitor` without `tigo_server` — and feed every value to Home Assistant over the native API. PSRAM was never a requirement of the monitor itself: the device and node tables run about 600 bytes per optimizer, so twenty of them cost ~12 KB of the ~180-200 KB an ESP32 has free once WiFi is up. It is the web server that needs PSRAM, and it stays out of these configs. `boards/esp32-lilygo-t-can485.yaml` sets up the board's built-in isolated RS485 transceiver (GPIO21/22, plus the three enable lines LilyGO's own example drives high at boot), and `boards/example-t-can485.yaml` is ready to flash. This does not reopen the Config Builder to PSRAM-less boards — the builder emits configs containing `tigo_server`, so its guard is unchanged and still enforced by test. What you give up: the dashboard, the REST API, on-flash history, CCA/cloud import, and BLE.
1112
- **Docs screenshots generate themselves.** The guides now show the real device UI — dashboard, history, topology, node table, diagnostics — rendered during the docs build by driving the actual `app.html` against synthetic API responses. No firmware, no hardware, and nothing committed: the images are rebuilt from the current UI every time, so they cannot drift from it. All data is invented, with IPs and MACs from the IETF documentation ranges and a reserved fake-serial prefix, enforced by a test so a real serial, SSID or address can't reach a published page.
1213

1314
### Changed
@@ -20,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2021
- **Boards without PSRAM are no longer offered.** The Config Builder listed the M5Stack AtomS3/AtomS3 Lite and a generic ESP32 DevKit, both of which lack PSRAM — so it could hand you a ready-to-flash YAML for a board the project doesn't support. Both are gone from the builder, along with their `boards/*.yaml` reference configs, and a test now blocks a PSRAM-less board from being added back.
2122

2223
### Fixed
24+
- **A config without a `text_sensor:` section failed to compile.** `tigo_monitor.h` includes `sensor.h`, `text_sensor.h` and `binary_sensor.h` unconditionally, but ESPHome only copies a component's sources into the build when that component appears in the config — so any YAML that didn't happen to declare all three died on `fatal error: esphome/components/text_sensor/text_sensor.h: No such file or directory`. The documented fix was an empty `text_sensor:` stanza, which you had to already know about to write. `AUTO_LOAD` now pulls the three in whenever `tigo_monitor` is used, so hand-written configs work without the incantation. Existing configs that declare the stanzas are unaffected.
25+
- **The "Generate YAML Config" button printed a platform name that doesn't exist.** Every generated sensor block said `platform: tigo_server`; the sensor platform is `platform: tigo_monitor`, so pasting the output verbatim failed validation. Harmless on boards with the web UI, where the node table is a page you can read — but on a board without one, that button *is* the panel-discovery workflow, and its output has to paste in clean.
26+
- **PSRAM allocation no longer probes a heap that isn't there.** `psram_malloc()` tried `heap_caps_malloc(MALLOC_CAP_SPIRAM)` on every allocation and fell back to the internal heap when it failed. On a board with no PSRAM that attempt can never succeed, so it ran — and logged a "PSRAM unavailable" warning for anything over 1 KB — on every single allocation, all day. It is now compiled out when the IDF's `CONFIG_SPIRAM` is unset. No change on boards that have PSRAM.
2327
- **The flash-write crash appears to be fixed, not just made rarer.** The device now executes from PSRAM (`execute_from_psram`), and firmware configures this itself — you don't have to know it exists. Every history commit takes an ESP-IDF lock that disables the instruction cache across both cores; with code running from flash, that stall raced the WiFi/BLE radio ISRs and faulted. ESP-IDF skips the cache-disable entirely when instructions and read-only data live in PSRAM instead, which removes the race rather than shrinking its window. Previous builds survived 13.5, 42.0 and 14.0 hours before faulting; the current one has run **142 hours and counting** with no movement in any memory watermark, and with the per-panel databases already at full size — so it has been running at the worst-case flash cost the whole time, not easing into it. Costs about 1.7 MB of the 8 MB PSRAM, which moves out of the heap to hold the relocated code. Applies to ESP32-S3 boards with PSRAM configured; the reference config and the Config Builder set the flag explicitly as well.
2428

2529
- **Opening the Diagnostics page could reboot the device.** The page asked for `/api/tsdb/stats` every 10 seconds, and building that response was the only thing in the whole web UI that made the device read flash. On the ESP32-S3 every flash access stalls the other core, and doing it from the web server while the history writer is mid-write faults the writer. Reproduced on demand: four concurrent callers killed the device in about eleven seconds, while four concurrent callers of `/api/status` — same load, no flash — ran indefinitely. The figures now come from a snapshot the history writer takes during its own commit, so serving them touches no flash at any request rate. They can be up to one snapshot interval old, and the page now says how old ("sampled 4m ago") rather than implying they're live. This does not fix the underlying flash-versus-cache fault, which can still reboot the device roughly once a day; it removes the way the UI was provoking it.

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ PSRAM is required for 15+ devices. Custom STL-compatible allocators (`PSRAMAlloc
6262

6363
Always use PSRAM containers for large data. Internal RAM is <200KB; PSRAM is 8MB. Use `#ifdef USE_ESP_IDF` guards for PSRAM types.
6464

65+
**Boards without PSRAM** (e.g. `boards/esp32-lilygo-t-can485.yaml`) are supported for *sensors-only* builds — `tigo_monitor` without `tigo_server`. The `psram_*` aliases still compile there; `psram_malloc()` keys off the IDF's `CONFIG_SPIRAM` and resolves straight to the internal heap when it is unset, so no call site changes. Keep it that way: gate on `CONFIG_SPIRAM` inside the allocator rather than sprinkling new `#ifdef`s at usage sites, and never assume `USE_ESP_IDF` implies PSRAM exists. The web server genuinely does require PSRAM and must stay out of those configs.
66+
6567
### Web Server
6668

6769
5 HTML pages (`/`, `/nodes`, `/status`, `/yaml`, `/cca`) + JSON API endpoints (`/api/devices`, `/api/overview`, `/api/strings`, `/api/status`, `/api/health`, `/api/inverters`, `/api/energy-history`). Auth: `api_token` for API, HTTP Basic for HTML pages. All responses built in PSRAM via `PSRAMString`.
@@ -77,7 +79,7 @@ Fuzzy barcode matching (`match_barcode()`): compares last 6 chars of UART-discov
7779
- **JSON field naming**: `snake_case` in JSON, `kebab-case` in HTML IDs, `camelCase` in JavaScript
7880
- **When renaming methods** in `tigo_monitor.h`, update all call sites: header → member variable → web server → Python config → JavaScript
7981
- **When changing any `/api/*` JSON shape** (rename a field, restructure a response), update `site/screenshots/fixtures.mjs` in the same commit. The docs screenshots are rendered from the real `app.html` against those fixtures at build time — a stale fixture doesn't fail the build, it publishes screenshots of a UI rendering `undefined`. Check with `cd site && npm run screenshots` — which also refreshes the committed README images in `docs/images/`, so commit those too
80-
- **Sensor/text_sensor/binary_sensor sections** must be declared in YAML (even if empty) or compilation fails with missing header errors
82+
- **Sensor/text_sensor/binary_sensor** are pulled in by `AUTO_LOAD` in `tigo_monitor/__init__.py`, because `tigo_monitor.h` includes all three unconditionally. YAML no longer needs to declare empty stanzas for them. If you add an unconditional `#include` of another ESPHome component to that header, add it to `AUTO_LOAD` too — otherwise configs that don't happen to use it fail with "No such file or directory". Prefer a `USE_*` guard (as `time`/`button` already do) when the dependency is genuinely optional
8183
- `CONFIG_UART_ISR_IN_IRAM: "y"` in sdkconfig is required for reduced frame loss
8284

8385
## Git Workflow

boards/README.md

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
This directory contains optimized ESPHome configurations for different ESP32 boards used with the Tigo Monitor system.
44

5-
**PSRAM is required.** Only boards that have it are configured here.
5+
**PSRAM is required for the web server (`tigo_server`)**, which builds whole HTML
6+
pages and JSON API responses in memory. It is *not* required by `tigo_monitor`
7+
the device and node tables are a few KB and sit happily on the internal heap. A
8+
board without PSRAM can therefore run a sensors-only build that feeds Home
9+
Assistant over the native API; it just cannot serve the dashboard.
610

711
## Available Configurations
812

@@ -26,6 +30,42 @@ This directory contains optimized ESPHome configurations for different ESP32 boa
2630
- **UART Buffers**: 16384 RX / 1024 TX (listen-only, no transmission)
2731
- **Special optimizations**: High-frequency FreeRTOS tick, tickless idle disabled
2832

33+
### ESP32 Boards (no PSRAM — sensors only)
34+
35+
#### `esp32-lilygo-t-can485.yaml` - LilyGO T-CAN485 (no PSRAM)
36+
- **Board**: LilyGO T-CAN485 (`esp32dev`), 4MB flash
37+
- **PSRAM**: none — the board wires GPIO16/17 to its RS485 front end, which is
38+
where a WROVER's PSRAM would live, so it cannot have any
39+
- **CPU**: 240MHz (classic ESP32 defaults to 160MHz under ESP-IDF; raised here)
40+
- **RS485**: built-in isolated transceiver on GPIO21 (RX) / GPIO22 (TX), with
41+
three enable lines (GPIO16 5V boost, GPIO17 `/RE`, GPIO19 `/SHDN`) that the
42+
board file drives high at boot
43+
- **Recommended for**: Home-Assistant-only installations. Tested at 18 devices;
44+
40 should fit — each optimizer costs roughly 600 bytes across the device and
45+
node tables, so 40 is ~24KB of the ~130KB left free once WiFi is up. Those are
46+
many small heap allocations rather than one block, so on a board with no PSRAM
47+
the practical ceiling is fragmentation, not total bytes
48+
- **First flash must be over USB**: the board file enables `sram1_as_iram`, which
49+
needs an ESP-IDF v5.1+ bootloader. A USB flash updates the bootloader; an OTA
50+
does not, so OTA-ing an existing device onto this config leaves it unable to
51+
boot
52+
- **Excludes**: `tigo_server` (needs PSRAM), on-flash history (the smallest tsdb
53+
layout wants ~7MB — dual OTA slots plus a 3MB LittleFS partition — which 4MB
54+
cannot hold; Home Assistant keeps long-term history anyway), CCA/cloud import, BLE
55+
- **Ready-to-flash example**: `example-t-can485.yaml`
56+
- **⚠ Flash over USB the first time.** The config sets `sram1_as_iram` (+40KB of
57+
IRAM, which is what pays for `CONFIG_UART_ISR_IN_IRAM`), and that needs an
58+
ESP-IDF v5.1+ bootloader. A USB flash updates the bootloader automatically; an
59+
OTA does not, so an OTA-first device will fail to boot.
60+
- **⚠ Assumes chip revision 3.1.** `minimum_chip_revision: '3.1'` drops the IDF's
61+
workarounds for older silicon. The bootloader hard-checks it — an older chip
62+
halts at boot. Your boot log prints `chip revision: v3.1`; lower the value in
63+
the board file if yours reports less.
64+
65+
Because there is no web UI on this board, panel discovery happens through the
66+
**"Generate YAML Config"** button, which prints a paste-ready `sensor:` block to
67+
the logs. `example-t-can485.yaml` documents that workflow.
68+
2969
## Usage
3070

3171
To use a board configuration, include it in your main YAML file:
@@ -47,18 +87,20 @@ Or copy the relevant sections directly into your main configuration file.
4787

4888
## GPIO Pin Compatibility
4989

50-
All configurations assume the standard Tigo UART connection:
90+
Most configurations assume the standard Tigo UART connection:
5191
- **TX Pin**: GPIO1
5292
- **RX Pin**: GPIO3
5393
- **Baud Rate**: 38400
5494

55-
Adjust these in your main configuration if your hardware differs.
95+
Adjust these in your main configuration if your hardware differs. Boards with a
96+
built-in transceiver already set their own pins — the AtomS3R uses GPIO1/GPIO2,
97+
and the T-CAN485 uses GPIO22/GPIO21 plus three transceiver enable lines.
5698

5799
## Performance Notes
58100

59101
- **ESP32-P4** offers the best performance with massive PSRAM and higher CPU frequency
60102
- **ESP32-S3 with PSRAM** is the sweet spot for most installations
61-
- Boards without PSRAM are not supported — the web server and device tables have nowhere to live and the firmware goes unstable
103+
- **Boards without PSRAM** work for sensors-only builds that talk to Home Assistant over the native API. Do not add `tigo_server:` to one — it compiles, then fragments the internal heap to OOM under dashboard polling
62104

63105
## Customization
64106

0 commit comments

Comments
 (0)