Skip to content

Commit 015f46c

Browse files
author
Maksym Shtembuliak
committed
Prepare ESP collector 0.1.4 release
1 parent 1bdabf2 commit 015f46c

15 files changed

Lines changed: 203 additions & 37 deletions

‎CONTRIBUTING.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ The behavioral specification is the integration's own fake collector:
4848
- **Discovery**: UDP `58899`, `set>server=IP:PORT;` → reply `rsp>server=2;` + reverse TCP to Home Assistant.
4949
- **Heartbeat**: FC=1 on connect, then periodic (default 60 s); payload is the PN truncated to 14 bytes. A server FC=1 is answered with its own `tid`.
5050
- **FC=2** (query collector): the core answers parameters it knows from its own state (PN, firmware, current endpoint, baud); the glue answers the rest (hardware, IP, SSID, RSSI, Wi-Fi scan list). Unknown parameters → `01 <param>` (refused), like the factory fake.
51-
- **FC=3** (set collector): writes are *staged* and committed together. Param 41/43 stage the target Wi-Fi SSID/password; param 21 stages the server endpoint; param 29 (`system_operation`) **commits everything staged** — endpoint and/or Wi-Fi — and is refused only when nothing is staged. Param 34 (baud) applies immediately. Everything else is refused `01 <param>`. The committed endpoint is persisted to NVS (so the bridge reconnects to the same HA across reboots / can be moved to another HA); incoming discovery still overrides it live. `AT+CLDSRVHOST1=` applies an endpoint immediately (no staging) and retargets the link.
51+
- **FC=3** (set collector): writes are *staged* and committed together. Param 41/43 stage the target Wi-Fi SSID/password; param 21 stages the server endpoint; param 29 (`system_operation`) **commits everything staged** — endpoint and/or Wi-Fi. If nothing is staged, param 29 is treated as **Restart Collector** and the reboot is delayed until after the response frame is sent. Param 34 (baud) applies immediately. Everything else is refused `01 <param>`. The committed endpoint is persisted to NVS (so the bridge reconnects to the same HA across reboots / can be moved to another HA); incoming discovery still overrides it live. `AT+CLDSRVHOST1=` applies an endpoint immediately (no staging) and retargets the link.
5252
- **FC=4** (forward): payload → UART as-is; the inverter reply (end detected by a `response_gap` of silence) → frame with the same `tid`/`devcode`/`devaddr`. **Inverter silence → no reply at all** — the integration distinguishes a timeout from a zero-filled answer, so this is load-bearing.
5353
- **AT lines** (`AT+...\n`) are interleaved with binary frames on the same TCP stream; they are told apart by the 3-byte `AT+` prefix.
5454
- Inverter requests are serialized: one in flight, with a `command_spacing` pause (factory ~850 ms) between commands.
@@ -62,10 +62,10 @@ The PN is synthetic: `V00` + the 15-digit decimal MAC → the PN18 format `^[A-Z
6262
The firmware answers `AT+VDTU?` with a capability string:
6363

6464
```
65-
AT+VDTU:esp-collector,<semver>;features=local_only,no_cloud,wifi_params,endpoint_write;uart=<baud,data,stop,parity>;spacing_ms=<n>;queue=<n>
65+
AT+VDTU:esp-collector,<semver>;features=local_only,no_cloud,wifi_params,endpoint_write,reboot;uart=<baud,data,stop,parity>;spacing_ms=<n>;queue=<n>
6666
```
6767

68-
EyeBond Local probes this on link-up. The `esp-collector,` prefix identifies a virtual bridge so cloud-only flows can be hidden and an honest device name/version can be shown. Factory collectors do not return the prefix. For every other command the bridge behavior remains compatible with the factory collector path. When the capability string is unset (core without glue), the reply is an empty value, like the factory reference for unknown commands.
68+
EyeBond Local probes this on link-up. The `esp-collector,` prefix identifies a virtual bridge so cloud-only flows can be hidden and an honest device name/version can be shown. Factory collectors do not return the prefix. Features advertise optional collector-side operations such as endpoint writes and restart. For every other command the bridge behavior remains compatible with the factory collector path.
6969

7070
---
7171

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ select:
137137
restore_value: true
138138
```
139139
140-
The normal way to change speed is in EyeBond Local: open the ESP bridge collector **Configure** menu, choose **Change inverter UART speed**, select the speed, and confirm. The inverter connection may drop briefly while the speed changes.
140+
The normal way to change speed is in EyeBond Local: open the ESP bridge collector **Configure** menu, choose **Change inverter UART speed**, select the speed, and confirm. On ESP8266/ESP32, the selected speed is saved and restored after reboot or power loss. The inverter connection may drop briefly while the speed changes.
141141
142142
This ESPHome select appears under the ESPHome device in Home Assistant. It is useful for diagnostics, but normal users should prefer the EyeBond Local action.
143143
@@ -170,7 +170,7 @@ After setup, Home Assistant may show two relevant devices:
170170
- the EyeBond Local collector/inverter devices;
171171
- the ESPHome device for firmware-level features such as logs, OTA, and optional baud-rate select.
172172

173-
For the custom ESP bridge, EyeBond Local also exposes **Change inverter UART speed** in the collector **Configure** menu. Use it when the bridge is found but the inverter needs another serial speed.
173+
For the custom ESP bridge, EyeBond Local also exposes **Change inverter UART speed** in the collector **Configure** menu. Use it when the bridge is found but the inverter needs another serial speed. On ESP8266/ESP32, the selected speed is saved and restored after reboot or power loss.
174174

175175
---
176176

‎README.uk.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ select:
137137
restore_value: true
138138
```
139139
140-
Звичайний спосіб змінити швидкість — через EyeBond Local: відкрийте меню **Configure** для ESP-моста, оберіть **Сменить скорость UART инвертора**, виберіть швидкість і підтвердьте дію. Під час зміни швидкості зв'язок з інвертором може коротко зникнути.
140+
Звичайний спосіб змінити швидкість — через EyeBond Local: відкрийте меню **Configure** для ESP-моста, оберіть **Сменить скорость UART инвертора**, виберіть швидкість і підтвердьте дію. На ESP8266/ESP32 вибрана швидкість зберігається і відновлюється після перезавантаження або вимкнення живлення. Під час зміни швидкості зв'язок з інвертором може коротко зникнути.
141141
142142
Цей ESPHome-перемикач з'являється на ESPHome-пристрої в Home Assistant. Він корисний для діагностики, але для звичайного використання краще користуватися дією EyeBond Local.
143143
@@ -170,7 +170,7 @@ select:
170170
- пристрої колектора/інвертора EyeBond Local;
171171
- ESPHome-пристрій для функцій прошивки: логи, OTA і необов'язковий перемикач швидкості UART.
172172

173-
Для кастомного ESP-моста EyeBond Local також показує дію **Сменить скорость UART инвертора** в меню **Configure** колектора. Використовуйте її, якщо міст знайдено, але інвертору потрібна інша швидкість serial-зв'язку.
173+
Для кастомного ESP-моста EyeBond Local також показує дію **Сменить скорость UART инвертора** в меню **Configure** колектора. Використовуйте її, якщо міст знайдено, але інвертору потрібна інша швидкість serial-зв'язку. На ESP8266/ESP32 вибрана швидкість зберігається і відновлюється після перезавантаження або вимкнення живлення.
174174

175175
---
176176

‎components/eybond_collector/at_handler.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ std::string vdtu_value(const CollectorProfile &profile) {
5151
return profile.vdtu;
5252
}
5353
return std::string("esp-collector,") + BRIDGE_VERSION +
54-
";features=local_only,no_cloud,wifi_params,endpoint_write" +
54+
";features=local_only,no_cloud,wifi_params,endpoint_write,reboot" +
5555
";uart=" + profile.uart;
5656
}
5757

‎components/eybond_collector/core.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ bool parse_uart_settings(const std::string &value, UartSettings *out) {
6363

6464
std::string build_vdtu_capabilities(const CollectorProfile &profile, const CoreConfig &config) {
6565
return std::string("esp-collector,") + BRIDGE_VERSION +
66-
";features=local_only,no_cloud,wifi_params,endpoint_write" +
66+
";features=local_only,no_cloud,wifi_params,endpoint_write,reboot" +
6767
";uart=" + profile.uart +
6868
";spacing_ms=" + std::to_string(config.command_spacing_ms) +
6969
";queue=" + std::to_string(config.forward_queue_limit);

‎components/eybond_collector/core.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
// - on connect: unsolicited FC=1 heartbeat (payload = PN[:14] NUL-padded), then periodic
1111
// - server FC=1 -> heartbeat reply with the request tid
1212
// - server FC=2 param 5 -> {0x00, 0x05} + firmware version; other params -> {0x01, param}
13-
// - server FC=3 -> {0x01, param} (sets are refused; the bridge has no cloud config)
13+
// - server FC=3 -> platform-controlled collector params (Wi-Fi/endpoint
14+
// staging, param 29 apply/reboot, UART baud) or {0x01, param} refused
1415
// - server FC=4 -> payload to UART verbatim, response framed back with the same
1516
// tid/devcode/devaddr; inverter silence -> NO reply (the integration relies on
1617
// distinguishing timeout from data)
@@ -48,7 +49,7 @@ struct CoreConfig {
4849
};
4950

5051
// Compose the AT+VDTU capability value the bridge advertises, e.g.
51-
// "esp-collector,0.2.0;features=local_only,no_cloud;uart=2400,8,1,NONE;spacing_ms=850;queue=4".
52+
// "esp-collector,0.2.0;features=local_only,no_cloud,reboot;uart=2400,8,1,NONE;spacing_ms=850;queue=4".
5253
// A future integration version can probe this once to detect the virtual bridge;
5354
// factory collectors answer the query differently (empty/garbage), so absence of
5455
// this exact "esp-collector," prefix means "factory".

‎components/eybond_collector/eybond_collector.cpp‎

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include <cstdio>
77
#include <cstdlib>
88

9+
#include <Arduino.h>
10+
911
#include "esphome/core/helpers.h"
1012
#include "esphome/core/log.h"
1113
#include "esphome/components/network/util.h"
@@ -43,6 +45,9 @@ void EybondCollector::setup() {
4345
this->write_status_led_(false);
4446
}
4547

48+
baud_pref_ = global_preferences->make_preference<uint32_t>(BAUD_PREF_HASH);
49+
this->restore_persisted_baud_rate_();
50+
4651
eybond::CollectorProfile profile;
4752
uint8_t mac[6];
4853
get_mac_address_raw(mac);
@@ -112,6 +117,7 @@ void EybondCollector::loop() {
112117
tcp_up_ = false;
113118
core_->on_tcp_closed(now);
114119
}
120+
this->process_reboot_(now);
115121
this->update_status_led_(now);
116122
return;
117123
}
@@ -172,6 +178,7 @@ void EybondCollector::loop() {
172178
core_->loop(millis());
173179
this->process_pending_connect_();
174180
this->process_wifi_apply_(millis());
181+
this->process_reboot_(millis());
175182
this->poll_wifi_scan_(millis());
176183
this->update_status_led_(millis());
177184
}
@@ -230,7 +237,11 @@ uint8_t EybondCollector::set_param(uint8_t parameter, const std::string &value)
230237
pending_endpoint_ = value;
231238
return 0;
232239
}
233-
if (parameter == 29) { // system_operation: commit ALL staged changes
240+
if (parameter == 29) { // system_operation: commit staged changes or restart
241+
bool had_staged = !pending_endpoint_.empty();
242+
#ifdef USE_WIFI
243+
had_staged = had_staged || !pending_wifi_ssid_.empty() || !pending_wifi_password_.empty();
244+
#endif
234245
bool committed = false;
235246
if (!pending_endpoint_.empty()) {
236247
std::string host;
@@ -249,7 +260,14 @@ uint8_t EybondCollector::set_param(uint8_t parameter, const std::string &value)
249260
committed = true;
250261
}
251262
#endif
252-
return committed ? 0 : 1; // refuse only when nothing was staged
263+
if (committed) {
264+
return 0;
265+
}
266+
if (had_staged) {
267+
return 1; // staged but invalid/incomplete; do not reinterpret as restart
268+
}
269+
this->request_reboot_(millis());
270+
return 0;
253271
}
254272
#ifdef USE_WIFI
255273
switch (parameter) {
@@ -330,6 +348,7 @@ void EybondCollector::apply_uart(const std::string &value) {
330348
if (!this->reconfigure_uart_(s.baud, s.data_bits, s.stop_bits, parity)) {
331349
return;
332350
}
351+
this->persist_baud_rate_(s.baud);
333352
if (core_ != nullptr) {
334353
core_->update_uart_description(this->uart_settings_string_());
335354
}
@@ -347,6 +366,7 @@ bool EybondCollector::apply_baud_rate(uint32_t baud) {
347366
if (!this->reconfigure_uart_(baud, u->get_data_bits(), u->get_stop_bits(), u->get_parity())) {
348367
return false;
349368
}
369+
this->persist_baud_rate_(baud);
350370
if (core_ != nullptr) {
351371
core_->update_uart_description(this->uart_settings_string_());
352372
}
@@ -358,6 +378,32 @@ bool EybondCollector::apply_baud_rate(uint32_t baud) {
358378

359379
uint32_t EybondCollector::current_baud_rate() const { return this->parent_->get_baud_rate(); }
360380

381+
void EybondCollector::restore_persisted_baud_rate_() {
382+
uint32_t saved = 0;
383+
if (!baud_pref_.load(&saved) || saved == 0) {
384+
return;
385+
}
386+
if (!this->baud_supported_(saved)) {
387+
ESP_LOGW(TAG, "Ignoring unsupported persisted inverter UART baud rate %u", static_cast<unsigned>(saved));
388+
return;
389+
}
390+
auto *u = this->parent_;
391+
if (!this->reconfigure_uart_(saved, u->get_data_bits(), u->get_stop_bits(), u->get_parity())) {
392+
ESP_LOGW(TAG, "Persisted inverter UART baud rate %u cannot be applied on this platform",
393+
static_cast<unsigned>(saved));
394+
return;
395+
}
396+
ESP_LOGI(TAG, "Restored inverter UART baud rate %u", static_cast<unsigned>(saved));
397+
}
398+
399+
void EybondCollector::persist_baud_rate_(uint32_t baud) {
400+
if (!baud_pref_.save(&baud)) {
401+
ESP_LOGW(TAG, "Failed to persist inverter UART baud rate %u", static_cast<unsigned>(baud));
402+
return;
403+
}
404+
global_preferences->sync();
405+
}
406+
361407
void EybondCollector::process_wifi_apply_(uint32_t now) {
362408
#ifdef USE_WIFI
363409
if (!wifi_apply_requested_ || static_cast<int32_t>(now - wifi_apply_at_ms_) < 0) {
@@ -374,6 +420,21 @@ void EybondCollector::process_wifi_apply_(uint32_t now) {
374420
#endif
375421
}
376422

423+
void EybondCollector::request_reboot_(uint32_t now) {
424+
reboot_requested_ = true;
425+
reboot_at_ms_ = now + 1500;
426+
ESP_LOGI(TAG, "Collector restart requested; rebooting after response flush");
427+
}
428+
429+
void EybondCollector::process_reboot_(uint32_t now) {
430+
if (!reboot_requested_ || static_cast<int32_t>(now - reboot_at_ms_) < 0) {
431+
return;
432+
}
433+
reboot_requested_ = false;
434+
ESP_LOGI(TAG, "Restarting collector");
435+
ESP.restart();
436+
}
437+
377438
void EybondCollector::poll_wifi_scan_(uint32_t now) {
378439
(void) now;
379440
if (!wifi_scan_running_) {

‎components/eybond_collector/eybond_collector.h‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ struct PersistedEndpoint {
4141
} PACKED;
4242

4343
static const uint32_t ENDPOINT_PREF_HASH = 0xE7B0'DC01;
44+
static const uint32_t BAUD_PREF_HASH = 0xE7B0'DC02;
4445

4546
class EybondCollector : public Component, public uart::UARTDevice, private eybond::CollectorCore::Actions
4647
#ifdef USE_EYBOND_BLE
@@ -98,12 +99,16 @@ class EybondCollector : public Component, public uart::UARTDevice, private eybon
9899

99100
void process_pending_connect_();
100101
void process_wifi_apply_(uint32_t now);
102+
void request_reboot_(uint32_t now);
103+
void process_reboot_(uint32_t now);
101104
void poll_wifi_scan_(uint32_t now);
102105
void configure_fallback_ap_(const std::string &pn);
103106
void note_activity_(uint32_t now);
104107
void update_status_led_(uint32_t now);
105108
void write_status_led_(bool on);
106109
std::string uart_settings_string_() const;
110+
void restore_persisted_baud_rate_();
111+
void persist_baud_rate_(uint32_t baud);
107112

108113
#ifdef USE_EYBOND_BLE
109114
// eybond::BleProvisioning::Actions
@@ -147,19 +152,27 @@ class EybondCollector : public Component, public uart::UARTDevice, private eybon
147152

148153
// WiFi reconfiguration via the integration's collector parameter flow:
149154
// FC=3 writes stage ssid (41) and password (43); FC=3 param 29 ("apply")
150-
// commits them. The commit is deferred to loop() so the FC=3 response
151-
// reaches HA before the STA reconnects.
155+
// commits them. With nothing staged, param 29 requests collector restart.
156+
// Deferred operations run from loop() so the FC=3 response reaches HA before
157+
// the STA reconnects or the board reboots.
152158
std::string pending_wifi_ssid_;
153159
std::string pending_wifi_password_;
154160
bool wifi_apply_requested_{false};
155161
uint32_t wifi_apply_at_ms_{0};
156162

163+
// FC=3 param 29 with no staged Wi-Fi/endpoint changes is a collector restart
164+
// request. Reboot is delayed so the SmartESS-compatible response frame reaches
165+
// HA before the TCP link drops.
166+
bool reboot_requested_{false};
167+
uint32_t reboot_at_ms_{0};
168+
157169
// Wi-Fi scan cache for FC=2 param 49 ("[ssid,rssi][ssid2,rssi2]...").
158170
std::string wifi_scan_cache_;
159171
bool wifi_scan_running_{false};
160172

161173
// Notified after a successful runtime re-baud (keeps the select entity in sync).
162174
std::function<void(uint32_t)> baud_listener_;
175+
ESPPreferenceObject baud_pref_;
163176

164177
// Server-endpoint write (FC=3 param 21 staged, committed by param 29) and its
165178
// NVS persistence.

‎docs/FLASHING.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Both presets can be used for TTL, RS232, or RS485:
4949

5050
After flashing, the board opens a setup access point if it cannot join Wi-Fi. Connect to that access point from a phone or laptop and enter your Wi-Fi credentials.
5151

52-
If your inverter needs another UART speed, add the bridge in EyeBond Local, open the collector **Configure** menu, choose **Change inverter UART speed**, select the speed, and confirm. The inverter connection may drop briefly while the speed changes.
52+
If your inverter needs another UART speed, add the bridge in EyeBond Local, open the collector **Configure** menu, choose **Change inverter UART speed**, select the speed, and confirm. On ESP8266/ESP32, the selected speed is saved and restored after reboot or power loss. The inverter connection may drop briefly while the speed changes.
5353

5454
If your board or pins are different, use the ESPHome YAML option below.
5555

@@ -190,7 +190,7 @@ Keep Home Assistant and the bridge on the same network for easiest discovery.
190190

191191
EyeBond Local creates the collector/inverter devices.
192192

193-
EyeBond Local can change the inverter UART speed for the custom ESP bridge from the collector **Configure** menu. Use **Change inverter UART speed**, choose the speed, and confirm. The option is shown only for this ESP bridge, not for factory collectors.
193+
EyeBond Local can change the inverter UART speed for the custom ESP bridge from the collector **Configure** menu. Use **Change inverter UART speed**, choose the speed, and confirm. On ESP8266/ESP32, the selected speed is saved and restored after reboot or power loss. The option is shown only for this ESP bridge, not for factory collectors.
194194

195195
The ESPHome integration can also add the ESP board as a separate device. This gives you:
196196

0 commit comments

Comments
 (0)