diff --git a/CHANGELOG.md b/CHANGELOG.md index 095a71b..c2c3dd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ Date-based versions use `YYYYMMDD`. +## 20260603 + +### Added + +- Added a GLEDOPTO GL-RC-001WL ESP-NOW remote example using Berry + `espnow.rx(filter, callback)`. +- Added a single-ID mapping helper for `toggl`, `brigh`, and `tempe` + EventStates. +- Added a direct/indirect mapping helper that preserves the previous + ESPNOWRADIO remote behavior while keeping normal Controller-to-Controller + ESP-NOW enabled. +- Added copyable TNGL usage snippets for both variants. + +### Impact + +- Technicians can connect the GLEDOPTO remote to Spectoda EventStates without + disabling normal ESP-NOW communication between controllers. +- The public example now documents the native `size`/`magic` raw ESP-NOW filter + and the optional MAC lock for one physical remote. + ## 20260527 ### Added diff --git a/README.md b/README.md index c8eaca0..28a411a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ developer can see why the pieces are connected that way. - `controller-toggle-button-hold-dim-dali/` - a single digital toggle button pattern that ramps DALI brightness while pressed or latched on, stops on release, and reverses dimming direction on the next press. +- `gledopto-gl-rc-001wl-espnow-remote/` - Berry `espnow.rx` examples that map + the GLEDOPTO GL-RC-001WL remote to either one Spectoda ID or direct/indirect + light IDs. ## Example Rules diff --git a/gledopto-gl-rc-001wl-espnow-remote/README.md b/gledopto-gl-rc-001wl-espnow-remote/README.md new file mode 100644 index 0000000..a9272bb --- /dev/null +++ b/gledopto-gl-rc-001wl-espnow-remote/README.md @@ -0,0 +1,121 @@ +# GLEDOPTO GL-RC-001WL ESP-NOW Remote + +This example maps the GLEDOPTO GL-RC-001WL / WLED-style ESP-NOW remote to +Spectoda EventStates through the native Berry `espnow.rx(filter, callback)` +API. + +It replaces the older `ESPNOWRADIO` pattern for installations where normal +Controller-to-Controller ESP-NOW communication must stay enabled. + +## Files + +- `gledopto-single-id.be` - maps the remote to one Spectoda ID. +- `gledopto-direct-indirect.be` - maps the remote to two IDs, typically direct + and indirect light. +- `usage-single-id.tngl` - copyable TNGL usage for one ID. +- `usage-direct-indirect.tngl` - copyable TNGL usage for direct/indirect IDs. + +## Firmware Assumptions + +Use firmware `0.12.11` or newer with the native raw ESP-NOW Berry API: + +```berry +espnow.rx(filter, callback) +espnow.tx(mac, data) +``` + +Do not disable the normal ESP-NOW connector. `espnow.enable` is enabled by the +controller runtime by default; this example does not require adding an +`espnow.enable` flag to controller config. + +The example uses a native RX filter: + +```berry +{ + "size": 13, + "magic": [129, 145] +} +``` + +For this remote: + +- `13` is the observed payload size. +- `129` is `0x81`, one observed program prefix. +- `145` is `0x91`, the other observed program prefix. + +Add `"mac": "AA:BB:CC:DD:EE:FF"` when the project should accept only one +physical remote. The MAC address in the snippets is synthetic; replace it with +your remote MAC or set `"macFilter": false` while testing. + +## Button Mapping + +Both variants deduplicate packets by the remote sequence in payload bytes +`1..4`, because one physical button press is repeated across Wi-Fi channels. + +Common buttons: + +- ON (`1`) sets `toggl` to `100%`. +- OFF (`2`) sets `toggl` to `0%`. +- Brightness + (`9`) increases `brigh`. +- Brightness - (`8`) decreases `brigh`. +- Preset 3 (`18`) makes `tempe` warmer. +- Preset 4 (`19`) makes `tempe` colder. + +Single ID behavior: + +- Night (`3`) turns the ID on and sets `tempe` warm. +- Preset 1 (`16`) toggles the ID. +- Preset 2 (`17`) also toggles the ID. + +Direct/indirect behavior: + +- Night (`3`) turns direct off, indirect on, and sets both `tempe` values warm. +- Preset 1 (`16`) toggles indirect. +- Preset 2 (`17`) toggles direct. + +## Config Map + +Single ID: + +```berry +GledoptoRemoteOne({ + "id": ID1, + "mac": "AA:BB:CC:DD:EE:FF", + "macFilter": true, + "brigh": 50, + "brighStep": 10, + "tempeStep": 10, + "debug": false +}) +``` + +Direct/indirect: + +```berry +GledoptoRemoteDirectIndirect({ + "direct": ID1, + "indirect": ID2, + "mac": "AA:BB:CC:DD:EE:FF", + "macFilter": true, + "brigh": 50, + "brighStep": 10, + "tempeStep": 10, + "debug": false +}) +``` + +`brigh` is the real Spectoda brightness label. The shortened spelling is +intentional and should stay consistent with EventState labels. + +## Copy Pattern + +1. Copy the matching `.be` helper into a Berry block or controller script. +2. Copy one of the `usage-*.tngl` snippets into the controller where the remote + should be received. +3. Replace `AA:BB:CC:DD:EE:FF` with the remote MAC, or temporarily set + `"macFilter": false` during bring-up. +4. Press the remote buttons and watch `toggl`, `brigh`, and `tempe` EventStates + for the selected ID or IDs. + +Set `"debug": true` only during smoke testing. The remote sends several raw +packets for one button press, so serial output can get noisy quickly. diff --git a/gledopto-gl-rc-001wl-espnow-remote/gledopto-direct-indirect.be b/gledopto-gl-rc-001wl-espnow-remote/gledopto-direct-indirect.be new file mode 100644 index 0000000..6e967fb --- /dev/null +++ b/gledopto-gl-rc-001wl-espnow-remote/gledopto-direct-indirect.be @@ -0,0 +1,180 @@ +import espnow + +# GLEDOPTO GL-RC-001WL -> direct + indirect Spectoda IDs. +# Short locals and numeric literals keep Berry bytecode/RAM small. Comments +# act as the readable symbol table and are stripped by Studio preprocessing. +def GledoptoRemoteDirectIndirect(S) + if S == nil + S = {} + end + + # Public config: direct/indirect are the two Spectoda IDs controlled by + # the remote. mac locks the script to one physical remote when macFilter is + # true. debug prints accepted packets and decoded buttons. + var d = S.find("direct", 1) + var i = S.find("indirect", 2) + var m = S.find("mac", "") + var mf = S.find("macFilter", m != "") + var dbg = S.find("debug", false) + + if mf && m == "" + mf = false + end + + # td/bd/cd expand to toggleDirect/brighDirect/tempeDirect. + # "brigh" is the real Spectoda brightness label, not a typo. + var td = EVS("toggl", d) + var bd = EVS("brigh", d) + var cd = EVS("tempe", d) + + # ti/bi/ci expand to toggleIndirect/brighIndirect/tempeIndirect. + var ti = EVS("toggl", i) + var bi = EVS("brigh", i) + var ci = EVS("tempe", i) + + # bs/ts are brighStep/tempeStep. od/oi cache direct/indirect ON states. + # b caches shared brigh. t/u cache direct/indirect tempe. ls deduplicates + # repeated remote packets from one physical button press. + var bs = S.find("brighStep", 10) + var ts = S.find("tempeStep", 10) + var od = 100 + var oi = 100 + var b = S.find("brigh", 50) + var t = 0 + var u = 0 + var ls = -1 + + # z is clamp(v, lo, hi). + def z(v, lo, hi) + if v < lo + return lo + elif v > hi + return hi + end + return v + end + + # rx args: a = sender MAC, x = payload bytes, r = RSSI, ch = Wi-Fi channel. + # The native ESP-NOW filter below handles MAC when mf is true; avoid a + # second Berry string compare so lowercase config MACs still work. + def rx(a, x, r, ch) + # 13 = GLEDOPTO payload size. + if x.size() != 13 + return + end + + # Byte 0 is program/magic. 129/145 are 0x81/0x91. + var p = x.get(0) + if p != 129 && p != 145 + return + end + + # Bytes 1..4 are the sequence used to ignore duplicate channel sends. + var s = x.get(1, 4) + if s == ls + return + end + ls = s + + # Byte 6 is the button code. + var k = x.get(6) + + if dbg + print("GLEDOPTO di", a, "btn", k, "rssi", r, "ch", ch, x.tohex()) + end + + # 30 below is PERCENTAGE value type. + if k == 1 + # 1 = ON: direct + indirect ON. + od = 100 + oi = 100 + td.set(100, 30) + ti.set(100, 30) + + elif k == 2 + # 2 = OFF: direct + indirect OFF. + od = 0 + oi = 0 + td.set(0, 30) + ti.set(0, 30) + + elif k == 9 + # 9 = brightness plus for both zones, then both zones ON. + b = z(b + bs, 0, 100) + bd.set(b, 30) + bi.set(b, 30) + od = 100 + oi = 100 + td.set(100, 30) + ti.set(100, 30) + + elif k == 8 + # 8 = brightness minus for both zones, clamped to 0..100%. + b = z(b - bs, 0, 100) + bd.set(b, 30) + bi.set(b, 30) + od = 100 + oi = 100 + td.set(100, 30) + ti.set(100, 30) + + elif k == 3 + # 3 = night: direct OFF, indirect ON, both tempe warm. + od = 0 + oi = 100 + td.set(0, 30) + ti.set(100, 30) + t = 100 + u = 100 + cd.set(100, 30) + ci.set(100, 30) + + elif k == 16 + # 16 = preset 1: toggle indirect. + oi = oi > 0 ? 0 : 100 + ti.set(oi, 30) + + elif k == 17 + # 17 = preset 2: toggle direct. + od = od > 0 ? 0 : 100 + td.set(od, 30) + + elif k == 18 + # 18 = preset 3: warmer, tempe range -100..100. + t = z(t + ts, -100, 100) + u = z(u + ts, -100, 100) + cd.set(t, 30) + ci.set(u, 30) + + elif k == 19 + # 19 = preset 4: colder. + t = z(t - ts, -100, 100) + u = z(u - ts, -100, 100) + cd.set(t, 30) + ci.set(u, 30) + + elif dbg + print("GLEDOPTO di unknown", k, x.tohex()) + end + end + + # C++ filter: only 13-byte packets whose first byte is 0x81 or 0x91. + var f = { + "size": 13, + "magic": [129, 145] + } + if mf + f = { + "mac": m, + "size": 13, + "magic": [129, 145] + } + end + + # espnow.rx returns h(), an unsubscribe function for this one registration. + var h = espnow.rx(f, rx) + + return Plugin(def() + # All work is done by rx(). + end) +end diff --git a/gledopto-gl-rc-001wl-espnow-remote/gledopto-single-id.be b/gledopto-gl-rc-001wl-espnow-remote/gledopto-single-id.be new file mode 100644 index 0000000..63a60c7 --- /dev/null +++ b/gledopto-gl-rc-001wl-espnow-remote/gledopto-single-id.be @@ -0,0 +1,160 @@ +import espnow + +# GLEDOPTO GL-RC-001WL -> one Spectoda ID. +# The source uses short locals and numeric literals to keep Berry bytecode/RAM +# small. Comments provide the semantic names that would otherwise be constants. +# Studio tnglPreprocessing strips comments before upload, so the context here +# does not cost Controller RAM. +def GledoptoRemoteOne(S) + if S == nil + S = {} + end + + # Public config: + # id = Spectoda ID controlled by the whole remote. + # mac = optional physical remote MAC. macFilter defaults to true when mac + # is set. debug prints accepted packets and decoded button numbers. + var id = S.find("id", 1) + var m = S.find("mac", "") + var mf = S.find("macFilter", m != "") + var dbg = S.find("debug", false) + + if mf && m == "" + mf = false + end + + # tg/bg/tp expand to toggle/brigh/tempe EventState setters for one ID. + # "brigh" is the real Spectoda brightness label, not a typo. + var tg = EVS("toggl", id) + var bg = EVS("brigh", id) + var tp = EVS("tempe", id) + + # bs/ts are brighStep/tempeStep. b caches brigh. t caches tempe. + # o caches ON state for local toggle buttons. ls deduplicates repeated + # remote packets for one physical press. + var bs = S.find("brighStep", 10) + var ts = S.find("tempeStep", 10) + var b = S.find("brigh", 50) + var t = 0 + var o = 100 + var ls = -1 + + # z is clamp(v, lo, hi). Short name keeps repeated branches small. + def z(v, lo, hi) + if v < lo + return lo + elif v > hi + return hi + end + return v + end + + # rx args: a = sender MAC, x = payload bytes, r = RSSI, ch = Wi-Fi channel. + # The native ESP-NOW filter below already checks payload size/magic before + # this Berry callback runs. When mf is true, it also checks MAC natively; + # avoid a second Berry string compare so lowercase config MACs still work. + def rx(a, x, r, ch) + # 13 = GLEDOPTO payload size. + if x.size() != 13 + return + end + + # Byte 0 is program/magic. 129/145 are 0x81/0x91. + var p = x.get(0) + if p != 129 && p != 145 + return + end + + # Bytes 1..4 are the sequence used to ignore duplicate channel sends. + var s = x.get(1, 4) + if s == ls + return + end + ls = s + + # Byte 6 is the button code. + var k = x.get(6) + + if dbg + print("GLEDOPTO one", a, "btn", k, "rssi", r, "ch", ch, x.tohex()) + end + + # 30 below is PERCENTAGE value type. + if k == 1 + # 1 = ON: single ID ON. + o = 100 + tg.set(100, 30) + + elif k == 2 + # 2 = OFF: single ID OFF. + o = 0 + tg.set(0, 30) + + elif k == 9 + # 9 = brightness plus. Brightness buttons also force toggle ON. + b = z(b + bs, 0, 100) + bg.set(b, 30) + o = 100 + tg.set(100, 30) + + elif k == 8 + # 8 = brightness minus. brigh remains clamped to 0..100%. + b = z(b - bs, 0, 100) + bg.set(b, 30) + o = 100 + tg.set(100, 30) + + elif k == 3 + # 3 = night. With one ID there is no separate indirect zone, so + # the closest useful behavior is ON + warm tempe. + o = 100 + t = 100 + tg.set(100, 30) + tp.set(100, 30) + + elif k == 16 + # 16 = preset 1. In one-ID mode it toggles the single light. + o = o > 0 ? 0 : 100 + tg.set(o, 30) + + elif k == 17 + # 17 = preset 2. Also toggles the single light for symmetry with + # direct/indirect mode, where preset 2 toggles direct. + o = o > 0 ? 0 : 100 + tg.set(o, 30) + + elif k == 18 + # 18 = preset 3: warmer. tempe uses -100..100 percentage space. + t = z(t + ts, -100, 100) + tp.set(t, 30) + + elif k == 19 + # 19 = preset 4: colder. + t = z(t - ts, -100, 100) + tp.set(t, 30) + + elif dbg + print("GLEDOPTO one unknown", k, x.tohex()) + end + end + + # C++ filter: only 13-byte packets whose first byte is 0x81 or 0x91. + var f = { + "size": 13, + "magic": [129, 145] + } + if mf + f = { + "mac": m, + "size": 13, + "magic": [129, 145] + } + end + + # espnow.rx returns h(), an unsubscribe function for this one registration. + var h = espnow.rx(f, rx) + + return Plugin(def() + # All work is done by rx(). + end) +end diff --git a/gledopto-gl-rc-001wl-espnow-remote/usage-direct-indirect.tngl b/gledopto-gl-rc-001wl-espnow-remote/usage-direct-indirect.tngl new file mode 100644 index 0000000..0645965 --- /dev/null +++ b/gledopto-gl-rc-001wl-espnow-remote/usage-direct-indirect.tngl @@ -0,0 +1,21 @@ +// Paste gledopto-direct-indirect.be before this snippet. +// Replace AA:BB:CC:DD:EE:FF with your remote MAC, or set macFilter to false +// during the first smoke test. + +#define ID_DIRECT ID1 +#define ID_INDIRECT ID2 + +defController($SC_REMOTE, { + BERRY(` + GledoptoRemoteDirectIndirect({ + "direct": ID_DIRECT, + "indirect": ID_INDIRECT, + "mac": "AA:BB:CC:DD:EE:FF", + "macFilter": true, + "brigh": 50, + "brighStep": 10, + "tempeStep": 10, + "debug": false + }) + `); +}); diff --git a/gledopto-gl-rc-001wl-espnow-remote/usage-single-id.tngl b/gledopto-gl-rc-001wl-espnow-remote/usage-single-id.tngl new file mode 100644 index 0000000..d5ca007 --- /dev/null +++ b/gledopto-gl-rc-001wl-espnow-remote/usage-single-id.tngl @@ -0,0 +1,19 @@ +// Paste gledopto-single-id.be before this snippet. +// Replace AA:BB:CC:DD:EE:FF with your remote MAC, or set macFilter to false +// during the first smoke test. + +#define ID_LAMP ID1 + +defController($SC_REMOTE, { + BERRY(` + GledoptoRemoteOne({ + "id": ID_LAMP, + "mac": "AA:BB:CC:DD:EE:FF", + "macFilter": true, + "brigh": 50, + "brighStep": 10, + "tempeStep": 10, + "debug": false + }) + `); +});