Files
Tasmota/tasmota/tasmota_xdrv_driver/xdrv_62_improv.ino
2026-01-30 12:03:38 +01:00

420 lines
15 KiB
C++

/*
xdrv_62_improv.ino - IMPROV support for Tasmota
SPDX-FileCopyrightText: 2022 Theo Arends
SPDX-License-Identifier: GPL-3.0-only
*/
#ifdef USE_IMPROV
/*********************************************************************************************\
* Serial implementation of IMPROV for initial wifi configuration using esp-web-tools
*
* See https://esphome.github.io/esp-web-tools/ and https://www.improv-wifi.com/serial/
\*********************************************************************************************/
#define XDRV_62 62
#define IMPROV_WIFI_TIMEOUT 30 // Max seconds wait for wifi connection after reconfig
//#define IMPROV_DEBUG
enum ImprovError {
IMPROV_ERROR_NONE = 0x00,
IMPROV_ERROR_INVALID_RPC = 0x01,
IMPROV_ERROR_UNKNOWN_RPC = 0x02,
IMPROV_ERROR_UNABLE_TO_CONNECT = 0x03,
IMPROV_ERROR_NOT_AUTHORIZED = 0x04,
IMPROV_ERROR_UNKNOWN = 0xFF,
};
enum ImprovState {
IMPROV_STATE_STOPPED = 0x00,
IMPROV_STATE_AWAITING_AUTHORIZATION = 0x01,
IMPROV_STATE_AUTHORIZED = 0x02,
IMPROV_STATE_PROVISIONING = 0x03,
IMPROV_STATE_PROVISIONED = 0x04,
};
enum ImprovCommand {
IMPROV_UNKNOWN = 0x00,
IMPROV_WIFI_SETTINGS = 0x01,
IMPROV_GET_CURRENT_STATE = 0x02,
IMPROV_GET_DEVICE_INFO = 0x03,
IMPROV_GET_WIFI_NETWORKS = 0x04,
IMPROV_GET_SET_HOSTNAME = 0x05,
IMPROV_GET_SET_DEVICENAME = 0x06,
IMPROV_BAD_CHECKSUM = 0xFF,
};
enum ImprovSerialType {
IMPROV_TYPE_CURRENT_STATE = 0x01,
IMPROV_TYPE_ERROR_STATE = 0x02,
IMPROV_TYPE_RPC = 0x03,
IMPROV_TYPE_RPC_RESPONSE = 0x04
};
static const uint8_t IMPROV_SERIAL_VERSION = 1;
struct IMPROV {
char* serial_in_buffer;
int serial_in_counter;
uint8_t wifi_timeout;
uint8_t seriallog_level;
uint8_t version;
} Improv;
/*********************************************************************************************/
void ImprovWriteData(uint8_t* data, uint32_t size) {
data[0] = 'I';
data[1] = 'M';
data[2] = 'P';
data[3] = 'R';
data[4] = 'O';
data[5] = 'V';
data[6] = IMPROV_SERIAL_VERSION; // 0x01
uint8_t checksum = 0x00;
for (uint32_t i = 0; i < size -1; i++) {
checksum += data[i];
}
data[size -1] = checksum;
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("IMP: Send '%*_H'"), size, data);
for (uint32_t i = 0; i < size; i++) {
TasConsole.write(data[i]);
}
TasConsole.write('\n');
}
void ImprovSendCmndState(uint32_t command, uint32_t state) {
uint8_t data[11];
data[7] = command;
data[8] = 1;
data[9] = state;
ImprovWriteData(data, sizeof(data));
}
void ImprovSendState(uint32_t state) {
#ifdef IMPROV_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: State %d"), state);
#endif
RtcSettings.improv_state = state;
ImprovSendCmndState(IMPROV_TYPE_CURRENT_STATE, state); // 0x01
}
void ImprovSendError(uint32_t error) {
#ifdef IMPROV_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: Error %d"), error);
#endif
ImprovSendCmndState(IMPROV_TYPE_ERROR_STATE, error); // 0x02
}
void ImprovSendResponse(uint8_t* response, uint32_t size) {
uint8_t data[9 + size];
data[7] = IMPROV_TYPE_RPC_RESPONSE; // 0x04
data[8] = size -1;
memcpy(data +9, response, size);
data[10] = size -3; // Total length of strings following
if (data[10]) {
// Replace '\n' (= lf) with string length
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ...
// I M P R O V ve ty le co pl lf T a s m o t a lf 1 1 . 0 . 0 . 5 lf ...
// I M P R O V ve ty le co pl l1 T a s m o t a l2 1 1 . 0 . 0 . 5 lf ...
uint32_t str_pos = 11;
for (uint32_t i = 12; i < sizeof(data); i++) {
if ('\n' == data[i]) {
data[str_pos] = i - str_pos -1; // Replace lf with string length
str_pos = i;
}
}
}
ImprovWriteData(data, sizeof(data));
}
void ImprovSendSetting(uint32_t command) {
char data[100];
uint32_t len = 0;
#ifdef USE_WEBSERVER
len = ext_snprintf_P(data, sizeof(data), PSTR("01\nhttp://%_I:%d\n"), (uint32_t)WiFi.localIP(), WEB_PORT);
len -= 3;
#endif // USE_WEBSERVER
data[0] = command;
ImprovSendResponse((uint8_t*)data, len +3);
}
void ImprovReceived(void) {
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// ty le co dl sl s s i d pl p a s s w o r d cr
// ty le co dl h o s t n a m e cr
uint32_t command = Improv.serial_in_buffer[2];
switch (command) {
case IMPROV_WIFI_SETTINGS: { // 0x01
// if (RtcSettings.improv_state != IMPROV_STATE_AUTHORIZED) {
// ImprovSendError(IMPROV_ERROR_NOT_AUTHORIZED); // 0x04
// } else {
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// ty le co dl sl s s i d pl p a s s w o r d cr
uint32_t ssid_length = Improv.serial_in_buffer[4];
uint32_t ssid_end = 5 + ssid_length;
uint32_t pass_length = Improv.serial_in_buffer[ssid_end];
uint32_t pass_start = ssid_end + 1;
uint32_t pass_end = pass_start + pass_length;
Improv.serial_in_buffer[ssid_end] = '\0';
char* ssid = &Improv.serial_in_buffer[5];
Improv.serial_in_buffer[pass_end] = '\0';
char* password = &Improv.serial_in_buffer[pass_start];
#ifdef IMPROV_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: Ssid '%s', Password '%s'"), ssid, password);
#endif // IMPROV_DEBUG
Improv.wifi_timeout = IMPROV_WIFI_TIMEOUT; // Set WiFi connect timeout
ImprovSendState(IMPROV_STATE_PROVISIONING);
Settings->flag4.network_wifi = 1; // Enable WiFi
char cmnd[TOPSZ];
snprintf_P(cmnd, sizeof(cmnd), PSTR(D_CMND_BACKLOG "0 " D_CMND_SSID "1 %s;" D_CMND_PASSWORD "1 %s"), ssid, password);
ExecuteCommand(cmnd, SRC_SERIAL); // Set SSID and Password and restart
// }
break;
}
case IMPROV_GET_CURRENT_STATE: { // 0x02
// 0 1 2 3 4 5
// ty le co dl cr lf
// 03 02 02 00 E5 0A
ImprovSendState(RtcSettings.improv_state);
if (IMPROV_STATE_PROVISIONED == RtcSettings.improv_state) {
ImprovSendSetting(command);
}
break;
}
case IMPROV_GET_DEVICE_INFO: { // 0x03
// 0 1 2 3 4 5
// ty le co dl cr lf
// 03 02 03 00 E6 0A
// Tasmota Zbbridge 11.0.0.7 ESP8266EX Wemos4
// Tasmota Sensors 11.0.0.7 ESP8266EX Wemos4
// Tasmota DE 11.0.0.7 ESP8266EX Wemos4
char image_name[33];
snprintf_P(image_name, sizeof(image_name), PSTR(D_HTML_LANGUAGE));
UpperCase(image_name, image_name); // Language id
if (!strcmp_P(image_name, PSTR("EN")) && // English
strcasecmp_P("Tasmota", PSTR(CODE_IMAGE_STR))) { // Not Tasmota
snprintf_P(image_name, sizeof(image_name), PSTR(CODE_IMAGE_STR)); // English image name
image_name[0] &= 0xDF; // Make first character uppercase
}
char data[200];
uint32_t len = snprintf_P(data, sizeof(data), PSTR("01\nTasmota %s\n%s\n%s\n%s\n"),
image_name, TasmotaGlobal.version, GetDeviceHardware().c_str(), SettingsText(SET_DEVICENAME));
data[0] = command;
ImprovSendResponse((uint8_t*)data, len);
break;
}
case IMPROV_GET_WIFI_NETWORKS: { // 0x04
// 0 1 2 3 4 5
// ty le co dl cr lf
// 03 02 04 00 E7 0A
char data[200];
int n = WiFi.scanNetworks(false, false); // Wait for scan result, hide hidden
if (n) {
int indices[n];
// Sort RSSI - strongest first
for (uint32_t i = 0; i < n; i++) { indices[i] = i; }
for (uint32_t i = 0; i < n; i++) {
for (uint32_t j = i + 1; j < n; j++) {
if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
std::swap(indices[i], indices[j]);
}
}
}
// Remove duplicate SSIDs - IMPROV does not distinguish between channels so no need to keep them
for (uint32_t i = 0; i < n; i++) {
if (-1 == indices[i]) { continue; }
String cssid = WiFi.SSID(indices[i]);
for (uint32_t j = i + 1; j < n; j++) {
if (cssid == WiFi.SSID(indices[j])) {
indices[j] = -1; // Set dup aps to index -1
}
}
}
// Send networks
for (uint32_t i = 0; i < n; i++) {
if (-1 == indices[i]) { continue; } // Skip dups
String ssid_copy = WiFi.SSID(indices[i]);
if (!ssid_copy.length()) { ssid_copy = F("no_name"); }
int32_t rssi = WiFi.RSSI(indices[i]);
bool encryption = (ENC_TYPE_NONE == WiFi.encryptionType(indices[i]));
// Send each ssid separately to avoid overflowing the buffer
uint32_t len = snprintf_P(data, sizeof(data), PSTR("01\n%s\n%d\n%s\n"),
ssid_copy.c_str(), rssi, (encryption)?"NO":"YES");
data[0] = command;
ImprovSendResponse((uint8_t*)data, len);
}
}
// Send empty response to signify the end of the list.
data[0] = command;
ImprovSendResponse((uint8_t*)data, 3); // Empty string
break;
}
case IMPROV_GET_SET_HOSTNAME: // 0x05
case IMPROV_GET_SET_DEVICENAME: { // 0x06
// 0 1 2 3 4 5 6 7 8 9 10 11 12
// ty le co dl h o s t n a m e cr
uint32_t data_length = Improv.serial_in_buffer[3];
if (0 == data_length) {
char data[100];
uint32_t len = snprintf_P(data, sizeof(data), PSTR("01\n%s\n"),
(IMPROV_GET_SET_HOSTNAME == command) ? TasmotaGlobal.hostname : SettingsText(SET_DEVICENAME));
data[0] = command;
ImprovSendResponse((uint8_t*)data, len);
} else {
uint32_t data_end = 4 + data_length;
Improv.serial_in_buffer[data_end] = '\0';
char* name = &Improv.serial_in_buffer[4];
#ifdef IMPROV_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: Name '%s'"), name);
#endif // IMPROV_DEBUG
char cmnd[TOPSZ];
snprintf_P(cmnd, sizeof(cmnd), PSTR("%s %s"), (IMPROV_GET_SET_HOSTNAME == command) ? D_CMND_HOSTNAME : D_CMND_DEVICENAME, name);
ExecuteCommand(cmnd, SRC_SERIAL); // Set hostname and restart / devicename
}
break;
}
/*
case IMPROV_BAD_CHECKSUM: { // 0xFF
break;
}
*/
default:
ImprovSendError(IMPROV_ERROR_UNKNOWN_RPC); // 0x02 - Unknown payload
}
}
/*********************************************************************************************/
bool ImprovSerialInput(const char *serial_in_buffer,
int serial_in_counter,
char serial_in_byte) {
if (IMPROV_SERIAL_VERSION == Improv.version) {
// 0 1 2 3 4 1 + le +1 (Improv.serial_in_buffer)
// ty le co pl data ... \n
// 03 xx yy zz ........ 0A
Improv.serial_in_buffer[Improv.serial_in_counter++] = serial_in_byte;
if (Improv.serial_in_counter > 1) { // Wait for length
uint32_t data_len = Improv.serial_in_buffer[1];
if (Improv.serial_in_counter > 3 + data_len) { // Receive including '\n'
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("IMP: Rcvd '%*_H'"), Improv.serial_in_counter, Improv.serial_in_buffer);
uint32_t checksum_pos = Improv.serial_in_counter -2;
uint8_t checksum = 0xDE; // Offset 49 4D 50 52 4F 56 01 = IMPROV\01
for (uint32_t i = 0; i < checksum_pos; i++) {
checksum += Improv.serial_in_buffer[i];
}
if (checksum != Improv.serial_in_buffer[checksum_pos]) {
ImprovSendError(IMPROV_ERROR_INVALID_RPC); // 0x01 - CRC error
}
else if (IMPROV_TYPE_RPC == Improv.serial_in_buffer[0]) {
uint32_t data_length = Improv.serial_in_buffer[3];
if (data_length == data_len - 2) {
ImprovReceived();
}
}
Improv.version = 0; // Done
free(Improv.serial_in_buffer);
Improv.serial_in_buffer = nullptr;
TasmotaGlobal.seriallog_level = Improv.seriallog_level; // Restore seriallogging
}
}
return true;
}
else if (6 == serial_in_counter) {
// 0 1 2 3 4 5 6 (serial_in_buffer)
// I M P R O V ve
// 49 4D 50 52 4F 56 01
// Check if received data is IMPROV data
if (!strncmp_P(serial_in_buffer, PSTR("IMPROV"), 6)) {
if (IMPROV_SERIAL_VERSION == serial_in_byte) {
if (Improv.serial_in_buffer == nullptr) {
if (!(Improv.serial_in_buffer = (char*)calloc(1, 260))) {
return false;
}
}
Improv.seriallog_level = TasmotaGlobal.seriallog_level;
TasmotaGlobal.seriallog_level = 0; // Disable seriallogging interfering with IMPROV
Improv.serial_in_counter = 0;
Improv.version = IMPROV_SERIAL_VERSION;
return true;
}
}
}
return false;
}
void ImprovEverySecond(void) {
if (Improv.wifi_timeout) {
Improv.wifi_timeout--;
if (Improv.wifi_timeout < IMPROV_WIFI_TIMEOUT -3) { // Tasmota restarts after ssid or password change
if (WifiHasIP()) {
Improv.wifi_timeout = 0;
if (IMPROV_STATE_AUTHORIZED == RtcSettings.improv_state) {
RtcSettings.improv_state = IMPROV_STATE_PROVISIONED;
}
if (IMPROV_STATE_PROVISIONING == RtcSettings.improv_state) {
ImprovSendState(IMPROV_STATE_PROVISIONED);
ImprovSendSetting(IMPROV_WIFI_SETTINGS);
}
return;
}
}
if (!Improv.wifi_timeout) {
if (IMPROV_STATE_PROVISIONING == RtcSettings.improv_state) {
ImprovSendError(IMPROV_ERROR_UNABLE_TO_CONNECT); // 0x03 - WiFi connect timeout
ImprovSendState(IMPROV_STATE_AUTHORIZED);
}
}
}
}
void ImprovInit(void) {
if (!RtcSettings.improv_state || // After power on
!Settings->bootcount) { // After reset to defaults caused by GUI option ERASE
RtcSettings.improv_state = IMPROV_STATE_AUTHORIZED; // Power on state (persistent during restarts)
}
Improv.wifi_timeout = IMPROV_WIFI_TIMEOUT; // Try to update state after restart
#ifdef IMPROV_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: State %d"), RtcSettings.improv_state);
#endif // IMPROV_DEBUG
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xdrv62(uint32_t function) {
bool result = false;
switch (function) {
case FUNC_EVERY_SECOND:
ImprovEverySecond();
break;
/*
case FUNC_SERIAL:
result = ImprovSerialInput(TasmotaGlobal.serial_in_buffer,
TasmotaGlobal.serial_in_byte_counter,
(char)TasmotaGlobal.serial_in_byte);
break;
*/
case FUNC_PRE_INIT:
ImprovInit();
break;
case FUNC_ACTIVE:
result = true;
break;
}
return result;
}
#endif // USE_IMPROV