Skip to content

Commit 380320a

Browse files
author
Maksym Shtembuliak
committed
Release 0.1.7: separate STATUS and COM LEDs, com noise-gate
- Add com_led_pin for a dedicated inverter-communication activity LED alongside status_led_pin. Two pins -> each drives one LED; only status_led_pin (single-LED dev boards) shows both (solid when connected+idle, flicker on inverter traffic); only com_led_pin -> a pure activity light. Per-pin inverted: is supported. - The COM indicator flickers (toggles ~10 Hz) instead of holding lit and reflects real inverter transactions only: armed by a forward and the reply while it is awaiting a response, never by HA<->bridge TCP chatter. - Idle-line RX noise no longer lights the COM LED: bytes arriving with no forward pending (electrical noise on a floating/unwired RX, or an idle bus) are ignored, so a bridge connected to HA before its inverter is wired stays solid instead of flickering constantly. - Bump BRIDGE_VERSION to 0.1.7; update FLASHING (en+uk), README (en+uk), the examples (commented com_led_pin) and the changelog.
1 parent 797a8c3 commit 380320a

14 files changed

Lines changed: 196 additions & 51 deletions

‎CHANGELOG.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ All notable changes to this project are documented here. The format is based on
44
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
55
[Semantic Versioning](https://semver.org/).
66

7+
## [0.1.7] - 2026-07-02
8+
9+
### Added
10+
- **Separate STATUS and COM LEDs, like a factory collector.** A new `com_led_pin`
11+
drives an inverter-communication activity LED alongside the existing `status_led_pin`
12+
connection-state LED. With both pins, each LED has one job. With only `status_led_pin`
13+
(single-LED dev boards) that LED shows both — solid when the bridge is connected and
14+
idle, flickering while the inverter is talked to. With only `com_led_pin` it is a pure
15+
activity light. Each pin takes the full pin schema, so per-pin `inverted:` is available.
16+
17+
### Changed
18+
- **The activity (COM) indicator now flickers instead of holding lit, and reflects real
19+
inverter transactions only.** It is armed by inverter UART traffic — a forward and the
20+
reply *while that forward is awaiting a response* — never by HA↔bridge TCP chatter, and
21+
it *toggles* (capped at ~10 Hz) rather than staying lit for an activity window, so a
22+
heavy read/write load reads as a fast flicker and never a solid glow.
23+
- **Idle-line RX noise no longer lights the COM LED.** Bytes arriving on the inverter UART
24+
when no forward is pending (electrical noise on a floating/unwired RX, or an idle bus)
25+
are ignored, so a bridge connected to HA before its inverter is wired or powered on
26+
stays solid instead of flickering constantly.
27+
- `BRIDGE_VERSION` bumped to `0.1.7`.
28+
729
## [0.1.6] - 2026-07-01
830

931
### Changed
@@ -74,6 +96,7 @@ Home Assistant integration. Highlights:
7496
- Status LED indication and the `AT+VDTU` virtual-bridge capability probe.
7597
- Web installer (GitHub Pages) for the ESP8266 and ESP32 presets.
7698

99+
[0.1.7]: https://github.com/groove-max/esp-eybond-collector/compare/v0.1.6...v0.1.7
77100
[0.1.6]: https://github.com/groove-max/esp-eybond-collector/compare/v0.1.5...v0.1.6
78101
[0.1.5]: https://github.com/groove-max/esp-eybond-collector/compare/v0.1.4...v0.1.5
79102
[0.1.0]: https://github.com/groove-max/esp-eybond-collector/releases/tag/v0.1.0

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The bridge stays on your LAN and does not connect to SmartESS cloud.
2929
- ESPHome features: OTA updates, logs, captive portal, and optional ESPHome entities.
3030
- Wi-Fi management from EyeBond Local where supported.
3131
- Optional runtime UART baud-rate select on ESP8266/ESP32 when you add the board through the ESPHome integration.
32-
- Built-in status LED indication on the release presets.
32+
- Built-in status LED indication on the release presets, with an optional `com_led_pin` for a separate inverter-communication LED.
3333

3434
What you do **not** get:
3535

@@ -152,6 +152,8 @@ Release presets use one pinout for all physical links:
152152

153153
For TTL and RS232, use TX/RX/GND and leave the DE/RE pin unconnected. For RS485, connect DE and RE together to the listed DE/RE pin.
154154

155+
The onboard LED in the table is `status_led_pin`; on a single LED it shows both connection state and an inverter-communication flicker. Add an optional `com_led_pin` for a dedicated communication LED — see [Status and COM LEDs](docs/FLASHING.md#status-and-com-leds).
156+
155157
---
156158

157159
## Adding it to Home Assistant

‎README.uk.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
- Можливості ESPHome: OTA-оновлення, журнали, сторінка налаштування Wi-Fi і додаткові сутності ESPHome.
3030
- Керування Wi-Fi з EyeBond Local там, де це підтримується.
3131
- Необов'язковий перемикач швидкості UART на ESP8266/ESP32, якщо додати плату через інтеграцію ESPHome.
32-
- Індикацію стану через вбудований світлодіод плати.
32+
- Індикацію стану через вбудований світлодіод плати, з необов'язковим `com_led_pin` для окремого світлодіода зв'язку з інвертором.
3333

3434
Чого тут **немає**:
3535

@@ -152,6 +152,8 @@ select:
152152

153153
Для TTL і RS232 використовуйте TX/RX/GND і не підключайте DE/RE. Для RS485 з'єднайте DE та RE разом і підключіть до вказаного DE/RE-піна.
154154

155+
Світлодіод у таблиці — це `status_led_pin`; на одному світлодіоді він показує і стан підключення, і мерехтіння зв'язку з інвертором. Додайте необов'язковий `com_led_pin` для окремого світлодіода зв'язку — див. [Світлодіоди стану (STATUS) і зв'язку (COM)](docs/FLASHING.uk.md#світлодіоди-стану-status-і-звязку-com).
156+
155157
---
156158

157159
## Додавання в Home Assistant

‎components/eybond_collector/__init__.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
CONF_COLLECTOR_ADDR = "collector_addr"
3232
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
3333
CONF_STATUS_LED_PIN = "status_led_pin"
34+
CONF_COM_LED_PIN = "com_led_pin"
3435
CONF_BLE_PROVISIONING = "ble_provisioning"
3536

3637
eybond_collector_ns = cg.esphome_ns.namespace("eybond_collector")
@@ -65,6 +66,7 @@ def _validate_pn(value):
6566
cv.Optional(CONF_COLLECTOR_ADDR, default=0x01): cv.hex_uint8_t,
6667
cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema,
6768
cv.Optional(CONF_STATUS_LED_PIN): pins.gpio_output_pin_schema,
69+
cv.Optional(CONF_COM_LED_PIN): pins.gpio_output_pin_schema,
6870
cv.Optional(CONF_BLE_PROVISIONING, default=False): cv.boolean,
6971
}
7072
)
@@ -118,6 +120,9 @@ async def to_code(config):
118120
if CONF_STATUS_LED_PIN in config:
119121
pin = await cg.gpio_pin_expression(config[CONF_STATUS_LED_PIN])
120122
cg.add(var.set_status_led_pin(pin))
123+
if CONF_COM_LED_PIN in config:
124+
pin = await cg.gpio_pin_expression(config[CONF_COM_LED_PIN])
125+
cg.add(var.set_com_led_pin(pin))
121126
if config[CONF_BLE_PROVISIONING]:
122127
cg.add_define("USE_EYBOND_BLE")
123128
cg.add(var.set_ble_provisioning(True))

‎components/eybond_collector/core.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ class CollectorCore {
138138
bool connected() const { return link_state_ == LinkState::CONNECTED; }
139139
const std::string &server_host() const { return server_host_; }
140140
uint16_t server_port() const { return server_port_; }
141+
// True only while a forwarded request is awaiting its inverter reply, so a "com"
142+
// activity LED lights for real transactions and not idle-line noise on a floating
143+
// or disconnected UART RX (which a connected bridge would otherwise show as constant
144+
// flicker whenever the inverter is unwired or powered off).
145+
bool awaiting_uart_response() const { return bridge_state_ == BridgeState::WAITING_RESPONSE; }
141146

142147
private:
143148
enum class LinkState : uint8_t { DISCONNECTED, CONNECTING, CONNECTED };

‎components/eybond_collector/eybond_collector.cpp‎

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,20 @@ namespace eybond_collector {
3939

4040
static const char *const TAG = "eybond_collector";
4141

42+
// Status/com LED timing.
43+
static const uint32_t STATUS_FAST_BLINK_MS = 250; // no Wi-Fi
44+
static const uint32_t STATUS_SLOW_BLINK_MS = 1000; // Wi-Fi up, bridge not connected
45+
static const uint32_t COM_FLICKER_MS = 50; // com toggle cap -> ~10 Hz flicker, never solid
46+
static const uint32_t COM_HOLD_MS = 120; // treat as "data flowing" for this long after an event
47+
4248
void EybondCollector::setup() {
4349
if (status_led_pin_ != nullptr) {
4450
status_led_pin_->setup();
45-
this->write_status_led_(false);
51+
this->write_led_(status_led_pin_, &status_led_state_, false);
52+
}
53+
if (com_led_pin_ != nullptr) {
54+
com_led_pin_->setup();
55+
this->write_led_(com_led_pin_, &com_led_state_, false);
4656
}
4757

4858
baud_pref_ = global_preferences->make_preference<uint32_t>(BAUD_PREF_HASH);
@@ -117,7 +127,7 @@ void EybondCollector::loop() {
117127
core_->on_tcp_closed(now);
118128
}
119129
this->process_reboot_(now);
120-
this->update_status_led_(now);
130+
this->update_leds_(now);
121131
return;
122132
}
123133

@@ -158,7 +168,6 @@ void EybondCollector::loop() {
158168
if (read_len <= 0) {
159169
break;
160170
}
161-
this->note_activity_(now);
162171
core_->on_tcp_data(buffer, static_cast<size_t>(read_len), now);
163172
available = tcp_.available();
164173
}
@@ -170,7 +179,11 @@ void EybondCollector::loop() {
170179
if (!this->read_byte(&byte)) {
171180
break;
172181
}
173-
this->note_activity_(millis());
182+
// Only inverter bytes answering a pending forward are real comms; idle-line
183+
// noise on a floating/disconnected RX must not light the com LED.
184+
if (core_->awaiting_uart_response()) {
185+
this->note_activity_(millis());
186+
}
174187
core_->on_uart_data(&byte, 1, millis());
175188
}
176189

@@ -179,7 +192,7 @@ void EybondCollector::loop() {
179192
this->process_wifi_apply_(millis());
180193
this->process_reboot_(millis());
181194
this->poll_wifi_scan_(millis());
182-
this->update_status_led_(millis());
195+
this->update_leds_(millis());
183196
}
184197

185198
bool EybondCollector::query_param(uint8_t parameter, std::string *out) {
@@ -668,7 +681,6 @@ void EybondCollector::tcp_connect(const std::string &host, uint16_t port) {
668681

669682
void EybondCollector::tcp_send(const uint8_t *data, size_t len) {
670683
if (tcp_up_) {
671-
this->note_activity_(millis());
672684
tcp_.write(data, len);
673685
}
674686
}
@@ -698,45 +710,77 @@ void EybondCollector::uart_send(const uint8_t *data, size_t len) {
698710
}
699711

700712
void EybondCollector::note_activity_(uint32_t now) {
701-
status_led_activity_until_ms_ = now + 90;
702-
}
703-
704-
void EybondCollector::update_status_led_(uint32_t now) {
705-
if (status_led_pin_ == nullptr) {
713+
// Armed only by inverter UART traffic (forward TX + inverter reply), never by
714+
// HA<->bridge TCP chatter, so the com indicator means real inverter comms.
715+
last_activity_ms_ = now;
716+
}
717+
718+
// Two logical indicators, like the factory collector's STATUS and COM LEDs:
719+
// * STATUS = connection state: fast blink (no Wi-Fi), slow blink (Wi-Fi but no
720+
// bridge), solid (bridge connected).
721+
// * COM = inverter communication: flickers while data flows on the UART, dark
722+
// when idle. It TOGGLES (capped at COM_FLICKER_MS) rather than holding lit, so
723+
// heavy load reads as a fast flicker and never as a solid glow.
724+
// Two pins -> each drives its own LED. Only status_led_pin (single-LED dev boards)
725+
// -> the status LED also carries com: solid when connected+idle, flickering while
726+
// inverter data flows. Only com_led_pin -> a pure activity light.
727+
void EybondCollector::update_leds_(uint32_t now) {
728+
const bool wifi_up = network::is_connected();
729+
const bool linked = wifi_up && tcp_up_;
730+
const bool active = linked && (now - last_activity_ms_) < COM_HOLD_MS;
731+
732+
const bool have_status = status_led_pin_ != nullptr;
733+
const bool have_com = com_led_pin_ != nullptr;
734+
if (!have_status && !have_com) {
706735
return;
707736
}
708737

709-
if (!network::is_connected()) {
710-
// Wi-Fi is not connected yet: quick blink so the board is visibly alive.
711-
if (now - status_led_last_toggle_ms_ >= 250) {
712-
this->write_status_led_(!status_led_state_);
713-
status_led_last_toggle_ms_ = now;
738+
// COM flicker phase.
739+
if (active) {
740+
if (now - com_last_toggle_ms_ >= COM_FLICKER_MS) {
741+
com_phase_ = !com_phase_;
742+
com_last_toggle_ms_ = now;
714743
}
744+
} else {
745+
com_phase_ = false;
746+
}
747+
if (have_com) {
748+
this->write_led_(com_led_pin_, &com_led_state_, com_phase_);
749+
}
750+
751+
if (!have_status) {
715752
return;
716753
}
717754

718-
if (!tcp_up_) {
719-
// Wi-Fi is up, but EyeBond Local is not connected yet: slow blink.
720-
if (now - status_led_last_toggle_ms_ >= 1000) {
721-
this->write_status_led_(!status_led_state_);
755+
// STATUS LED (also carries com when it is the only LED).
756+
bool target;
757+
if (!wifi_up) {
758+
if (now - status_led_last_toggle_ms_ >= STATUS_FAST_BLINK_MS) {
759+
status_led_phase_ = !status_led_phase_;
722760
status_led_last_toggle_ms_ = now;
723761
}
724-
return;
762+
target = status_led_phase_;
763+
} else if (!linked) {
764+
if (now - status_led_last_toggle_ms_ >= STATUS_SLOW_BLINK_MS) {
765+
status_led_phase_ = !status_led_phase_;
766+
status_led_last_toggle_ms_ = now;
767+
}
768+
target = status_led_phase_;
769+
} else if (have_com) {
770+
target = true; // dedicated com LED exists -> status LED is a pure link light
771+
} else {
772+
target = active ? com_phase_ : true; // single LED: solid when idle, flicker on traffic
773+
status_led_phase_ = target;
725774
}
726-
727-
// Connected: a combined RX+TX activity light — lit while data is flowing on
728-
// either the network (TCP) or the inverter (UART) side, dark when idle. The
729-
// activity window is (re)armed by note_activity_() on every send/receive, so a
730-
// single packet gives a ~90 ms flash and a burst holds the LED on.
731-
this->write_status_led_(static_cast<int32_t>(status_led_activity_until_ms_ - now) > 0);
775+
this->write_led_(status_led_pin_, &status_led_state_, target);
732776
}
733777

734-
void EybondCollector::write_status_led_(bool on) {
735-
if (status_led_pin_ == nullptr || status_led_state_ == on) {
778+
void EybondCollector::write_led_(GPIOPin *pin, bool *state, bool on) {
779+
if (pin == nullptr || *state == on) {
736780
return;
737781
}
738-
status_led_pin_->digital_write(on);
739-
status_led_state_ = on;
782+
pin->digital_write(on);
783+
*state = on;
740784
}
741785

742786
std::string EybondCollector::wifi_rssi() {

‎components/eybond_collector/eybond_collector.h‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class EybondCollector : public Component, public uart::UARTDevice, private eybon
7171
void set_collector_addr(uint8_t addr) { core_config_.collector_addr = addr; }
7272
void set_flow_control_pin(GPIOPin *pin) { flow_control_pin_ = pin; }
7373
void set_status_led_pin(GPIOPin *pin) { status_led_pin_ = pin; }
74+
void set_com_led_pin(GPIOPin *pin) { com_led_pin_ = pin; }
7475
void set_ble_provisioning(bool enabled) { ble_enabled_ = enabled; }
7576

7677
// Runtime inverter-UART re-baud (select entity, AT+UART=, FC=3 param 34).
@@ -104,8 +105,8 @@ class EybondCollector : public Component, public uart::UARTDevice, private eybon
104105
void poll_wifi_scan_(uint32_t now);
105106
void configure_fallback_ap_(const std::string &pn);
106107
void note_activity_(uint32_t now);
107-
void update_status_led_(uint32_t now);
108-
void write_status_led_(bool on);
108+
void update_leds_(uint32_t now);
109+
void write_led_(GPIOPin *pin, bool *state, bool on);
109110
std::string uart_settings_string_() const;
110111
void restore_persisted_baud_rate_();
111112
void persist_baud_rate_(uint32_t baud);
@@ -143,8 +144,16 @@ class EybondCollector : public Component, public uart::UARTDevice, private eybon
143144
GPIOPin *flow_control_pin_{nullptr};
144145
GPIOPin *status_led_pin_{nullptr};
145146
bool status_led_state_{true};
146-
uint32_t status_led_activity_until_ms_{0};
147+
bool status_led_phase_{false};
147148
uint32_t status_led_last_toggle_ms_{0};
149+
150+
// Communication (com) LED: its own pin, or overlaid on the status LED when no
151+
// com pin is set (single-LED dev boards). Flickers on inverter UART traffic.
152+
GPIOPin *com_led_pin_{nullptr};
153+
bool com_led_state_{true};
154+
bool com_phase_{false};
155+
uint32_t com_last_toggle_ms_{0};
156+
uint32_t last_activity_ms_{0};
148157
std::string pn_override_;
149158
uint16_t udp_port_{58899};
150159
std::string static_server_host_;

‎components/eybond_collector/profile.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace eybond {
99

1010
// Bridge firmware version, embedded in the FC=2 param-6 hardware_version marker
1111
// ("esp-collector/<version>/<platform>") the integration keys the bridge on.
12-
constexpr const char *BRIDGE_VERSION = "0.1.6";
12+
constexpr const char *BRIDGE_VERSION = "0.1.7";
1313

1414
struct CollectorProfile {
1515
std::string pn; // set at startup (config override or synthesized from MAC)

‎docs/FLASHING.md‎

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Open the YAML and check:
103103
- `board:` matches your ESP board;
104104
- `tx_pin` and `rx_pin` match your wiring;
105105
- `flow_control_pin` is set when you use a classic RS485 module with DE/RE;
106-
- `status_led_pin` matches the board's built-in LED if you want status indication;
106+
- `status_led_pin` / `com_led_pin` drive the status and communication LEDs (see below);
107107
- `baud_rate` matches the inverter family.
108108

109109
The ready-made browser firmware starts at `9600`. Typical baud rates:
@@ -204,24 +204,49 @@ On BK72xx / LibreTiny, runtime baud-rate switching is not available. Change `bau
204204

205205
The ESP32 BLE profile is YAML-only in the first release. BLE + Wi-Fi is large, so to fit the default 4 MB app partition the profile omits `api:`, `captive_portal:` and the baud `select:` (BLE provisioning replaces the captive portal; the EyeBond Local integration uses its own protocol, not the ESPHome API). OTA still works. To add those back, use a board with more flash and a larger app-partition layout — otherwise the image silently overflows and the board boot-loops.
206206

207-
## Status LED
207+
## Status and COM LEDs
208208

209-
The release presets use the board's built-in LED:
209+
The bridge drives two logical indicators, like a factory collector's **STATUS** and
210+
**COM** LEDs — configured with `status_led_pin` and `com_led_pin` (either or both):
211+
212+
- **STATUS** — connection state.
213+
- **COM** — inverter communication activity.
214+
215+
The release presets wire a single built-in LED to `status_led_pin`:
210216

211217
- Wemos D1 mini: GPIO2 / D4, inverted.
212218
- ESP-WROOM-32 DevKit: GPIO2.
213219

214220
On ESP32, GPIO2 is a boot strapping pin. The built-in LED is fine on typical DevKit boards, but do not add external pull-up/down resistors or heavy loads to GPIO2.
215221

216-
Indication:
222+
How the configured pins map to behaviour:
223+
224+
| Configured pins | Behaviour |
225+
|---|---|
226+
| `status_led_pin` only (single-LED dev boards) | One LED shows **both**: the connection state, plus a com flicker overlaid whenever the inverter is talked to. |
227+
| `status_led_pin` + `com_led_pin` | STATUS and COM each drive their own LED. |
228+
| `com_led_pin` only | A pure activity light: dark when idle, flickers on inverter traffic. |
229+
230+
STATUS indication:
217231

218-
| LED | State |
232+
| STATUS LED | State |
219233
|---|---|
220234
| Fast blink | The board is not connected to Wi-Fi yet. |
221235
| Slow blink | Wi-Fi is connected, but EyeBond Local has not connected to the bridge yet. |
222-
| Dark, flickers on traffic | The bridge is connected: the LED stays off when idle and flickers in time with data moving between Home Assistant, the bridge, and the inverter. |
236+
| Solid on | The bridge is connected and idle. |
237+
238+
COM indication:
239+
240+
| COM LED | State |
241+
|---|---|
242+
| Dark | No inverter traffic. |
243+
| Flicker | Data is flowing to/from the inverter (a forward and its reply). The LED *toggles* rather than holding lit, so heavy load reads as a fast flicker and never a solid glow. |
244+
245+
In the single-LED combined mode, the "solid on" connected state is briefly interrupted
246+
by the com flicker whenever the inverter is polled.
223247

224-
If your board uses another LED pin or inverted logic, adjust `status_led_pin` in YAML.
248+
Each pin takes the full pin schema, so per-pin inversion is available, e.g.
249+
`status_led_pin: { number: GPIO2, inverted: true }`.
225250

226251
## Troubleshooting
227252

0 commit comments

Comments
 (0)