Tuya: Fix init sequence and handle wifi test command (#820)

* Handle WiFi test command

Also rename commands to match Tuya protocol docs

* Fix init sequence and product info check

* Fix clang-format suggestions

* Additional changes based on code review

* Fix temp command buffer scope

* Let the interval timer fire the first heatbeat

* Fix init steps; add logging

* Lint

* Remove setup_priority override

* Add delay to dump_config

* Refactor dump sequence

* Fix verbose logging

* Fix lints

* Don't bother suppressing duplicate config dumps

* nolint


Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
Brandon Davidson 2019-11-14 04:43:44 -08:00 committed by Otto Winter
parent 531428b8b0
commit c5c42c4338
No known key found for this signature in database
GPG key ID: DB66C0BE6013F97E
2 changed files with 80 additions and 37 deletions

View file

@ -8,7 +8,6 @@ namespace tuya {
static const char *TAG = "tuya"; static const char *TAG = "tuya";
void Tuya::setup() { void Tuya::setup() {
this->send_empty_command_(TuyaCommandType::MCU_CONF);
this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
} }
@ -22,8 +21,12 @@ void Tuya::loop() {
void Tuya::dump_config() { void Tuya::dump_config() {
ESP_LOGCONFIG(TAG, "Tuya:"); ESP_LOGCONFIG(TAG, "Tuya:");
if ((gpio_status_ != -1) || (gpio_reset_ != -1)) if (this->init_state_ != TuyaInitState::INIT_DONE) {
ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!"); ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", // NOLINT
this->init_state_);
ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device.");
return;
}
for (auto &info : this->datapoints_) { for (auto &info : this->datapoints_) {
if (info.type == TuyaDatapointType::BOOLEAN) if (info.type == TuyaDatapointType::BOOLEAN)
ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool));
@ -36,9 +39,11 @@ void Tuya::dump_config() {
else else
ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id);
} }
if (this->datapoints_.empty()) { if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) {
ESP_LOGCONFIG(TAG, " Received no datapoints! Please make sure this is a supported Tuya device."); ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_,
this->gpio_reset_);
} }
ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str());
this->check_uart_settings(9600); this->check_uart_settings(9600);
} }
@ -89,8 +94,8 @@ bool Tuya::validate_message_() {
// valid message // valid message
const uint8_t *message_data = data + 6; const uint8_t *message_data = data + 6;
ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version, ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT
hexencode(message_data, length).c_str()); hexencode(message_data, length).c_str(), this->init_state_);
this->handle_command_(command, version, message_data, length); this->handle_command_(command, version, message_data, length);
// return false to reset rx buffer // return false to reset rx buffer
@ -105,41 +110,58 @@ void Tuya::handle_char_(uint8_t c) {
} }
void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) {
uint8_t c;
switch ((TuyaCommandType) command) { switch ((TuyaCommandType) command) {
case TuyaCommandType::HEARTBEAT: case TuyaCommandType::HEARTBEAT:
ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]);
if (buffer[0] == 0) { if (buffer[0] == 0) {
ESP_LOGI(TAG, "MCU restarted"); ESP_LOGI(TAG, "MCU restarted");
this->send_empty_command_(TuyaCommandType::QUERY_STATE); this->init_state_ = TuyaInitState::INIT_HEARTBEAT;
}
if (this->init_state_ == TuyaInitState::INIT_HEARTBEAT) {
this->init_state_ = TuyaInitState::INIT_PRODUCT;
this->send_empty_command_(TuyaCommandType::PRODUCT_QUERY);
} }
break; break;
case TuyaCommandType::QUERY_PRODUCT: { case TuyaCommandType::PRODUCT_QUERY: {
// check it is a valid string // check it is a valid string made up of printable characters
bool valid = false; bool valid = true;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
if (buffer[i] == 0x00) { if (!std::isprint(buffer[i])) {
valid = true; valid = false;
break; break;
} }
} }
if (valid) { if (valid) {
ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast<const char *>(buffer)); this->product_ = std::string(reinterpret_cast<const char *>(buffer), len);
} else {
this->product_ = R"({"p":"INVALID"})";
}
if (this->init_state_ == TuyaInitState::INIT_PRODUCT) {
this->init_state_ = TuyaInitState::INIT_CONF;
this->send_empty_command_(TuyaCommandType::CONF_QUERY);
} }
break; break;
} }
case TuyaCommandType::MCU_CONF: case TuyaCommandType::CONF_QUERY: {
if (len >= 2) { if (len >= 2) {
gpio_status_ = buffer[0]; gpio_status_ = buffer[0];
gpio_reset_ = buffer[1]; gpio_reset_ = buffer[1];
} }
// set wifi state LED to off or on depending on the MCU firmware if (this->init_state_ == TuyaInitState::INIT_CONF) {
// but it shouldn't be blinking // If we were following the spec to the letter we would send
c = 0x3; // state updates until connected to both WiFi and API/MQTT.
this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1); // Instead we just claim to be connected immediately and move on.
this->send_empty_command_(TuyaCommandType::QUERY_STATE); uint8_t c[] = {0x04};
this->init_state_ = TuyaInitState::INIT_WIFI;
this->send_command_(TuyaCommandType::WIFI_STATE, c, 1);
}
break; break;
}
case TuyaCommandType::WIFI_STATE: case TuyaCommandType::WIFI_STATE:
if (this->init_state_ == TuyaInitState::INIT_WIFI) {
this->init_state_ = TuyaInitState::INIT_DATAPOINT;
this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
}
break; break;
case TuyaCommandType::WIFI_RESET: case TuyaCommandType::WIFI_RESET:
ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled");
@ -147,14 +169,22 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
case TuyaCommandType::WIFI_SELECT: case TuyaCommandType::WIFI_SELECT:
ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled");
break; break;
case TuyaCommandType::SET_DATAPOINT: case TuyaCommandType::DATAPOINT_DELIVER:
break; break;
case TuyaCommandType::STATE: { case TuyaCommandType::DATAPOINT_REPORT:
if (this->init_state_ == TuyaInitState::INIT_DATAPOINT) {
this->init_state_ = TuyaInitState::INIT_DONE;
this->set_timeout("datapoint_dump", 1000, [this] { this->dump_config(); });
}
this->handle_datapoint_(buffer, len); this->handle_datapoint_(buffer, len);
break; break;
} case TuyaCommandType::DATAPOINT_QUERY:
case TuyaCommandType::QUERY_STATE:
break; break;
case TuyaCommandType::WIFI_TEST: {
uint8_t c[] = {0x00, 0x00};
this->send_command_(TuyaCommandType::WIFI_TEST, c, 2);
break;
}
default: default:
ESP_LOGE(TAG, "invalid command (%02x) received", command); ESP_LOGE(TAG, "invalid command (%02x) received", command);
} }
@ -214,8 +244,6 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
} }
if (!found) { if (!found) {
this->datapoints_.push_back(datapoint); this->datapoints_.push_back(datapoint);
// New datapoint found, reprint dump_config after a delay.
this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); });
} }
// Run through listeners // Run through listeners
@ -227,9 +255,12 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) {
uint8_t len_hi = len >> 8; uint8_t len_hi = len >> 8;
uint8_t len_lo = len >> 0; uint8_t len_lo = len >> 0;
this->write_array({0x55, 0xAA, uint8_t version = 0;
0x00, // version
(uint8_t) command, len_hi, len_lo}); ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT
hexencode(buffer, len).c_str(), this->init_state_);
this->write_array({0x55, 0xAA, version, (uint8_t) command, len_hi, len_lo});
if (len != 0) if (len != 0)
this->write_array(buffer, len); this->write_array(buffer, len);
@ -278,7 +309,7 @@ void Tuya::set_datapoint_value(TuyaDatapoint datapoint) {
buffer.push_back(data.size() >> 8); buffer.push_back(data.size() >> 8);
buffer.push_back(data.size() >> 0); buffer.push_back(data.size() >> 0);
buffer.insert(buffer.end(), data.begin(), data.end()); buffer.insert(buffer.end(), data.begin(), data.end());
this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size()); this->send_command_(TuyaCommandType::DATAPOINT_DELIVER, buffer.data(), buffer.size());
} }
void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func) { void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func) {

View file

@ -34,19 +34,29 @@ struct TuyaDatapointListener {
enum class TuyaCommandType : uint8_t { enum class TuyaCommandType : uint8_t {
HEARTBEAT = 0x00, HEARTBEAT = 0x00,
QUERY_PRODUCT = 0x01, PRODUCT_QUERY = 0x01,
MCU_CONF = 0x02, CONF_QUERY = 0x02,
WIFI_STATE = 0x03, WIFI_STATE = 0x03,
WIFI_RESET = 0x04, WIFI_RESET = 0x04,
WIFI_SELECT = 0x05, WIFI_SELECT = 0x05,
SET_DATAPOINT = 0x06, DATAPOINT_DELIVER = 0x06,
STATE = 0x07, DATAPOINT_REPORT = 0x07,
QUERY_STATE = 0x08, DATAPOINT_QUERY = 0x08,
WIFI_TEST = 0x0E,
};
enum class TuyaInitState : uint8_t {
INIT_HEARTBEAT = 0x00,
INIT_PRODUCT,
INIT_CONF,
INIT_WIFI,
INIT_DATAPOINT,
INIT_DONE,
}; };
class Tuya : public Component, public uart::UARTDevice { class Tuya : public Component, public uart::UARTDevice {
public: public:
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::LATE; }
void setup() override; void setup() override;
void loop() override; void loop() override;
void dump_config() override; void dump_config() override;
@ -62,8 +72,10 @@ class Tuya : public Component, public uart::UARTDevice {
void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len);
void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); }
TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
int gpio_status_ = -1; int gpio_status_ = -1;
int gpio_reset_ = -1; int gpio_reset_ = -1;
std::string product_ = "";
std::vector<TuyaDatapointListener> listeners_; std::vector<TuyaDatapointListener> listeners_;
std::vector<TuyaDatapoint> datapoints_; std::vector<TuyaDatapoint> datapoints_;
std::vector<uint8_t> rx_message_; std::vector<uint8_t> rx_message_;