Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ESP-NOW Wireless Remote Control #3237

Merged
merged 11 commits into from
Jun 22, 2023
14 changes: 14 additions & 0 deletions wled00/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,13 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(retainMqttMsg, if_mqtt[F("rtn")]);
#endif

#ifndef WLED_DISABLE_ESPNOW
JsonObject remote = doc["remote"];
CJSON(enable_espnow_remote, remote[F("remote_enabled")]);
getStringFromJson(linked_remote, remote[F("linked_remote")], 13);
#endif


#ifndef WLED_DISABLE_HUESYNC
JsonObject if_hue = interfaces["hue"];
CJSON(huePollingEnabled, if_hue["en"]);
Expand Down Expand Up @@ -895,6 +902,13 @@ void serializeConfig() {
if_mqtt_topics[F("group")] = mqttGroupTopic;
#endif

#ifndef WLED_DISABLE_ESPNOW
JsonObject remote = doc.createNestedObject(F("remote"));
remote[F("remote_enabled")] = enable_espnow_remote;
remote[F("linked_remote")] = linked_remote;
#endif


#ifndef WLED_DISABLE_HUESYNC
JsonObject if_hue = interfaces.createNestedObject("hue");
if_hue["en"] = huePollingEnabled;
Expand Down
11 changes: 11 additions & 0 deletions wled00/data/settings_wifi.htm
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ <h3>Experimental</h3>
Disable WiFi sleep: <input type="checkbox" name="WS"><br>
<i>Can help with connectivity issues.<br>
Do not enable if WiFi is working correctly, increases power consumption.</i>

<div id="remd">
<h3>Wireless Remote</h3>
<i>Listen for events over ESP-NOW<br>
Keep disabled if not using a remote, increases power consumption.<br></i>

Enable Remote: <input type="checkbox" name="RE"><br>
Hardware MAC: <input type="text" name="RMAC"><br>
Last Seen: <span class="rlid">None</span> <br>
</div>

<div id="ethd">
<h3>Ethernet Type</h3>
<select name="ETH">
Expand Down
4 changes: 4 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,16 @@ void serializePlaylist(JsonObject obj);
void initPresetsFile();
void handlePresets();
bool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE);
void applyPresetWithFallback(uint8_t presetID, uint8_t callMode, uint8_t effectID = 0, uint8_t paletteID = 0);
inline bool applyTemporaryPreset() {return applyPreset(255);};
void savePreset(byte index, const char* pname = nullptr, JsonObject saveobj = JsonObject());
inline void saveTemporaryPreset() {savePreset(255);};
void deletePreset(byte index);
bool getPresetName(byte index, String& name);

//remote.cpp
void handleRemote();

//set.cpp
bool isAsterisksOnly(const char* str, byte maxLen);
void handleSettingsSet(AsyncWebServerRequest *request, byte subPage);
Expand Down
316 changes: 162 additions & 154 deletions wled00/html_settings.h

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions wled00/ir.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,9 @@ void decBrightness()
}
}

// apply preset or fallback to a effect and palette if it doesn't exist
void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is now just a duplicate of applyPresetWithFallback(), we should remove it

{
applyPreset(presetID, CALL_MODE_BUTTON_PRESET);
//these two will be overwritten if preset exists in handlePresets()
effectCurrent = effectID;
effectPalette = paletteID;
applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID);
}

byte relativeChange(byte property, int8_t amount, byte lowerBoundary, byte higherBoundary)
Expand Down
9 changes: 9 additions & 0 deletions wled00/presets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ bool applyPreset(byte index, byte callMode)
return true;
}

// apply preset or fallback to a effect and palette if it doesn't exist
void applyPresetWithFallback(uint8_t index, uint8_t callMode, uint8_t effectID, uint8_t paletteID)
{
applyPreset(index, callMode);
//these two will be overwritten if preset exists in handlePresets()
effectCurrent = effectID;
effectPalette = paletteID;
}

void handlePresets()
{
if (presetToSave) {
Expand Down
200 changes: 200 additions & 0 deletions wled00/remote.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#include "wled.h"

#define ESP_NOW_STATE_UNINIT 0
#define ESP_NOW_STATE_ON 1
#define ESP_NOW_STATE_ERROR 2

#define NIGHT_MODE_DEACTIVATED -1
#define NIGHT_MODE_BRIGHTNESS 5

#define WIZMOTE_BUTTON_ON 1
#define WIZMOTE_BUTTON_OFF 2
#define WIZMOTE_BUTTON_NIGHT 3
#define WIZMOTE_BUTTON_ONE 16
#define WIZMOTE_BUTTON_TWO 17
#define WIZMOTE_BUTTON_THREE 18
#define WIZMOTE_BUTTON_FOUR 19
#define WIZMOTE_BUTTON_BRIGHT_UP 9
#define WIZMOTE_BUTTON_BRIGHT_DOWN 8

#ifdef WLED_DISABLE_ESPNOW
void handleRemote(){}
#else

// This is kind of an esoteric strucure because it's pulled from the "Wizmote"
// product spec. That remote is used as the baseline for behavior and availability
// since it's broadly commercially available and works out of the box as a drop-in
typedef struct message_structure {
uint8_t program; // 0x91 for ON button, 0x81 for all others
uint8_t seq[4]; // Incremetal sequence number 32 bit unsigned integer LSB first
uint8_t byte5 = 32; // Unknown
uint8_t button; // Identifies which button is being pressed
uint8_t byte8 = 1; // Unknown, but always 0x01
uint8_t byte9 = 100; // Unnkown, but always 0x64

uint8_t byte10; // Unknown, maybe checksum
uint8_t byte11; // Unknown, maybe checksum
uint8_t byte12; // Unknown, maybe checksum
uint8_t byte13; // Unknown, maybe checksum
} message_structure;

static int esp_now_state = ESP_NOW_STATE_UNINIT;
static uint32_t last_seq = -1;
static int brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED;
static message_structure incoming;

// Pulled from the IR Remote logic but reduced to 10 steps with a constant of 3
const byte brightnessSteps[] = {
6, 9, 14, 22, 33, 50, 75, 113, 170, 255
};
const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t);

bool nightModeActive() {
return brightnessBeforeNightMode != NIGHT_MODE_DEACTIVATED;
}

void activateNightMode() {
brightnessBeforeNightMode = bri;
bri = NIGHT_MODE_BRIGHTNESS;
}

bool resetNightMode() {
if (!nightModeActive()) {
return false;
}
bri = brightnessBeforeNightMode;
brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED;
return true;
}

// increment `bri` to the next `brightnessSteps` value
void brightnessUp() {
if (nightModeActive()) { return; }
// dumb incremental search is efficient enough for so few items
for (uint8_t index = 0; index < numBrightnessSteps; ++index) {
if (brightnessSteps[index] > bri) {
bri = brightnessSteps[index];
break;
}
}
}

// decrement `bri` to the next `brightnessSteps` value
void brightnessDown() {
if (nightModeActive()) { return; }
// dumb incremental search is efficient enough for so few items
for (int index = numBrightnessSteps - 1; index >= 0; --index) {
if (brightnessSteps[index] < bri) {
bri = brightnessSteps[index];
break;
}
}
}

void setOn() {
if (resetNightMode()) {
stateUpdated(CALL_MODE_BUTTON);
}
if (!bri) {
toggleOnOff();
}
}

void setOff() {
if (resetNightMode()) {
stateUpdated(CALL_MODE_BUTTON);
}
if (bri) {
toggleOnOff();
}
}

void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) {
applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID);
}

// Callback function that will be executed when data is received
#ifdef ESP8266
void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) {
#else
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
#endif

sprintf (last_signal_src, "%02x%02x%02x%02x%02x%02x",
mac [0], mac [1], mac [2], mac [3], mac [4], mac [5]);

if (strcmp(last_signal_src, linked_remote) != 0) {
DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: "));
DEBUG_PRINTLN(last_signal_src);
return;
}

if (len != sizeof(incoming)) {
DEBUG_PRINT(F("Unknown incoming ESP Now message received of length "));
DEBUG_PRINTLN(len);
return;
}

memcpy(&(incoming.program), incomingData, sizeof(incoming));
uint32_t cur_seq = incoming.seq[0] | (incoming.seq[1] << 8) | (incoming.seq[2] << 16) | (incoming.seq[3] << 24);

if (cur_seq == last_seq) {
return;
}


DEBUG_PRINT(F("Incoming ESP Now Packet["));
DEBUG_PRINT(cur_seq);
DEBUG_PRINT(F("] from sender["));
DEBUG_PRINT(last_signal_src);
DEBUG_PRINT(F("] button: "));
DEBUG_PRINTLN(incoming.button);
switch (incoming.button) {
case WIZMOTE_BUTTON_ON : setOn(); stateUpdated(CALL_MODE_BUTTON); break;
case WIZMOTE_BUTTON_OFF : setOff(); stateUpdated(CALL_MODE_BUTTON); break;
case WIZMOTE_BUTTON_ONE : presetWithFallback(1, FX_MODE_STATIC, 0); resetNightMode(); break;
case WIZMOTE_BUTTON_TWO : presetWithFallback(2, FX_MODE_BREATH, 0); resetNightMode(); break;
case WIZMOTE_BUTTON_THREE : presetWithFallback(3, FX_MODE_FIRE_FLICKER, 0); resetNightMode(); break;
case WIZMOTE_BUTTON_FOUR : presetWithFallback(4, FX_MODE_RAINBOW, 0); resetNightMode(); break;
case WIZMOTE_BUTTON_NIGHT : activateNightMode(); stateUpdated(CALL_MODE_BUTTON); break;
case WIZMOTE_BUTTON_BRIGHT_UP : brightnessUp(); stateUpdated(CALL_MODE_BUTTON); break;
case WIZMOTE_BUTTON_BRIGHT_DOWN : brightnessDown(); stateUpdated(CALL_MODE_BUTTON); break;
default: break;

}

last_seq = cur_seq;
}

void handleRemote() {
if (enable_espnow_remote) {
if (esp_now_state == ESP_NOW_STATE_UNINIT) {
DEBUG_PRINTLN(F("Initializing ESP_NOW listener"));
// Init ESP-NOW
if (esp_now_init() != 0) {
DEBUG_PRINTLN(F("Error initializing ESP-NOW"));
esp_now_state = ESP_NOW_STATE_ERROR;
}

#ifdef ESP8266
esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
#endif

esp_now_register_recv_cb(OnDataRecv);
esp_now_state = ESP_NOW_STATE_ON;
}
} else {
if (esp_now_state == ESP_NOW_STATE_ON) {
DEBUG_PRINTLN(F("Disabling ESP-NOW Remote Listener"));
if (esp_now_deinit() != 0) {
DEBUG_PRINTLN(F("Error de-initializing ESP-NOW"));
}
esp_now_state = ESP_NOW_STATE_UNINIT;
} else if (esp_now_state == ESP_NOW_STATE_ERROR) {
//Clear any error states (allows retrying by cycling)
esp_now_state = ESP_NOW_STATE_UNINIT;
}
}
}

#endif
8 changes: 8 additions & 0 deletions wled00/set.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)

noWifiSleep = request->hasArg(F("WS"));

#ifndef WLED_DISABLE_ESPNOW
enable_espnow_remote = request->hasArg(F("RE"));
strlcpy(linked_remote,request->arg(F("RMAC")).c_str(), 13);

//Normalize MAC format to lowercase
strlcpy(linked_remote,strlwr(linked_remote), 13);
#endif

#ifdef WLED_USE_ETHERNET
ethernetType = request->arg(F("ETH")).toInt();
WLED::instance().initEthernet();
Expand Down
1 change: 1 addition & 0 deletions wled00/wled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ void WLED::loop()
handleIR(); // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too
#endif
handleConnection();
handleRemote();
handleSerial();
handleNotifications();
handleTransitions();
Expand Down
15 changes: 15 additions & 0 deletions wled00/wled.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
#define WLED_ENABLE_WEBSOCKETS
#endif

//#define WLED_DISABLE_ESPNOW // Removes dependence on esp now

#define WLED_ENABLE_FS_EDITOR // enable /edit page for editing FS content. Will also be disabled with OTA lock

// to toggle usb serial debug (un)comment the following line
Expand Down Expand Up @@ -73,6 +75,9 @@
{
#include <user_interface.h>
}
#ifndef WLED_DISABLE_ESPNOW
#include <espnow.h>
#endif
#else // ESP32
#include <HardwareSerial.h> // ensure we have the correct "Serial" on new MCUs (depends on ARDUINO_USB_MODE and ARDUINO_USB_CDC_ON_BOOT)
#include <WiFi.h>
Expand All @@ -89,6 +94,10 @@
#include <LittleFS.h>
#endif
#include "esp_task_wdt.h"

#ifndef WLED_DISABLE_ESPNOW
#include <esp_now.h>
#endif
#endif
#include <Wire.h>
#include <SPI.h>
Expand Down Expand Up @@ -444,6 +453,12 @@ WLED_GLOBAL bool hueApplyColor _INIT(true);

WLED_GLOBAL uint16_t serialBaud _INIT(1152); // serial baud rate, multiply by 100

#ifndef WLED_DISABLE_ESPNOW
WLED_GLOBAL bool enable_espnow_remote _INIT(false);
WLED_GLOBAL char linked_remote[13] _INIT("");
WLED_GLOBAL char last_signal_src[13] _INIT("");
#endif

// Time CONFIG
WLED_GLOBAL bool ntpEnabled _INIT(false); // get internet time. Only required if you use clock overlays or time-activated macros
WLED_GLOBAL bool useAMPM _INIT(false); // 12h/24h clock format
Expand Down
21 changes: 21 additions & 0 deletions wled00/xml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@ void getSettingsJS(byte subPage, char* dest)
sappend('v',SET_F("AC"),apChannel);
sappend('c',SET_F("WS"),noWifiSleep);

#ifndef WLED_DISABLE_ESPNOW
sappend('c',SET_F("RE"),enable_espnow_remote);
sappends('s',SET_F("RMAC"),linked_remote);
#else
//hide remote settings if not compiled
oappend(SET_F("document.getElementById('remd').style.display='none';"));
#endif

#ifdef WLED_USE_ETHERNET
sappend('v',SET_F("ETH"),ethernetType);
#else
Expand Down Expand Up @@ -360,6 +368,19 @@ void getSettingsJS(byte subPage, char* dest)
{
sappends('m',SET_F("(\"sip\")[1]"),(char*)F("Not active"));
}

#ifndef WLED_DISABLE_ESPNOW
if (last_signal_src[0] != 0) //Have seen an ESP-NOW Remote
{
sappends('m',SET_F("(\"rlid\")[0]"),last_signal_src);
} else if (!enable_espnow_remote)
{
sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("(Enable remote to listen)"));
} else
{
sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("None"));
}
#endif
}

if (subPage == SUBPAGE_LEDS)
Expand Down