/* * xdrv_65_tuyamcubr.ino - TuyaMCU Bridge support for Tasmota */ /* * Copyright (C) 2023 David Gwynne * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #ifdef USE_TUYAMCUBR /* * Tuya MCU Bridge */ /* * TODO: * * - handling wifi reset requests from the MCU * - low power stuff? * - support for (re)sending status updates and device info queries * - supporting the raw and string Dp types * - restarting the tuya mcu state machine? * - restarting the rx state machine when no bytes are rxed for a while * - gmtime sync */ #define XDRV_65 65 #ifndef nitems #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) #endif #ifndef CTASSERT #define CTASSERT(x) extern char _ctassert[(x) ? 1 : -1 ] \ __attribute__((__unused__)) #endif #define TUYAMCUBR_LOGNAME "TYB" #define TUYAMCUBR_FMT(_fmt) PSTR(TUYAMCUBR_LOGNAME ": " _fmt) #define D_CMND_TUYAMCUBR_PREFIX "TuyaMCU" #define D_CMND_TUYAMCUBR_DATA_RAW "Raw" #define D_CMND_TUYAMCUBR_DATA_BOOL "Bool" #define D_CMND_TUYAMCUBR_DATA_VALUE "Value" #define D_CMND_TUYAMCUBR_DATA_STRING "String" #define D_CMND_TUYAMCUBR_DATA_ENUM "Enum" #include struct tuyamcubr_header { uint8_t header[2]; #define TUYAMCUBR_H_ONE 0x55 #define TUYAMCUBR_H_TWO 0xaa uint8_t version; uint8_t command; uint16_t datalen; }; CTASSERT(sizeof(struct tuyamcubr_header) == 6); #define TUYAMCUBR_CMD_HEARTBEAT 0x00 #define TUYAMCUBR_CMD_PRODUCT 0x01 #define TUYAMCUBR_CMD_MODE 0x02 #define TUYAMCUBR_CMD_WIFI_STATE 0x03 #define TUYAMCUBR_CMD_WIFI_RESET 0x04 #define TUYAMCUBR_CMD_WIFI_SELECT 0x05 #define TUYAMCUBR_CMD_SET_DP 0x06 #define TUYAMCUBR_CMD_STATE 0x07 #define TUYAMCUBR_CMD_QUERY_STATE 0x08 #define TUYAMCUBR_CMD_INIT_UPGRADE 0x0a #define TUYAMCUBR_CMD_UPGRADE_PKG 0x0b #define TUYAMCUBR_CMD_GMTIME 0x0c #define TUYAMCUBR_CMD_TIME 0x1c /* wifi state */ #define TUYAMCUBR_NETWORK_STATUS_1 0x00 /* pairing in EZ mode */ #define TUYAMCUBR_NETWORK_STATUS_2 0x01 /* pairing in AP mode */ #define TUYAMCUBR_NETWORK_STATUS_3 0x02 /* WiFi */ #define TUYAMCUBR_NETWORK_STATUS_4 0x03 /* WiFi + router */ #define TUYAMCUBR_NETWORK_STATUS_5 0x04 /* WiFi + router + cloud*/ #define TUYAMCUBR_NETWORK_STATUS_6 0x05 /* low power mode */ #define TUYAMCUBR_NETWORK_STATUS_7 0x06 /* pairing in EZ+AP mode */ /* gmtime */ struct tuyamcubr_gmtime { uint8_t valid; uint8_t year; /* + 2000 */ uint8_t month; /* 1 to 12 */ uint8_t day; /* 1 to 31 */ uint8_t hour; /* 0 to 23 */ uint8_t minute; /* 0 to 59 */ uint8_t second; /* 0 to 59 */ }; CTASSERT(sizeof(struct tuyamcubr_gmtime) == 7); /* time */ struct tuyamcubr_time { uint8_t valid; uint8_t year; /* 2000 + */ uint8_t month; /* 1 to 12 */ uint8_t day; /* 1 to 31 */ uint8_t hour; /* 0 to 23 */ uint8_t minute; /* 0 to 59 */ uint8_t second; /* 0 to 59 */ uint8_t weekday; /* 1 (monday) to 7 */ }; CTASSERT(sizeof(struct tuyamcubr_time) == 8); /* set dp */ struct tuyamcubr_data_header { uint8_t dpid; uint8_t type; uint16_t len; /* followed by len bytes */ }; CTASSERT(sizeof(struct tuyamcubr_data_header) == 4); #define TUYAMCUBR_DATA_TYPE_RAW 0x00 #define TUYAMCUBR_DATA_TYPE_BOOL 0x01 #define TUYAMCUBR_DATA_TYPE_VALUE 0x02 #define TUYAMCUBR_DATA_TYPE_STRING 0x03 #define TUYAMCUBR_DATA_TYPE_ENUM 0x04 struct tuyamcubr_data_type { const char *t_name; int t_len; uint32_t t_max; uint32_t (*t_rd)(const uint8_t *); void (*t_wr)(uint8_t *, uint32_t); }; static uint32_t tuyamcubr_rd_u8(const uint8_t *b) { return (*b); } static void tuyamcubr_wr_u8(uint8_t *b, uint32_t v) { *b = v; } static uint32_t tuyamcubr_rd_u32(const uint8_t *b) { uint32_t be32; memcpy(&be32, b, sizeof(be32)); return (ntohl(be32)); } static void tuyamcubr_wr_u32(uint8_t *b, uint32_t v) { uint32_t be32 = htonl(v); memcpy(b, &be32, sizeof(be32)); } static const struct tuyamcubr_data_type tuyamcubr_data_types[] = { [TUYAMCUBR_DATA_TYPE_RAW] = { .t_name = D_CMND_TUYAMCUBR_DATA_RAW, .t_len = -1, }, [TUYAMCUBR_DATA_TYPE_BOOL] = { .t_name = D_CMND_TUYAMCUBR_DATA_BOOL, .t_len = 1, .t_max = 1, .t_rd = tuyamcubr_rd_u8, .t_wr = tuyamcubr_wr_u8, }, [TUYAMCUBR_DATA_TYPE_VALUE] = { .t_name = D_CMND_TUYAMCUBR_DATA_VALUE, .t_len = sizeof(uint32_t), .t_max = 0xffffffff, .t_rd = tuyamcubr_rd_u32, .t_wr = tuyamcubr_wr_u32, }, [TUYAMCUBR_DATA_TYPE_STRING] = { .t_name = D_CMND_TUYAMCUBR_DATA_STRING, .t_len = -1, }, [TUYAMCUBR_DATA_TYPE_ENUM] = { .t_name = D_CMND_TUYAMCUBR_DATA_ENUM, .t_len = 1, .t_max = 0xff, .t_rd = tuyamcubr_rd_u8, .t_wr = tuyamcubr_wr_u8, }, }; static inline const struct tuyamcubr_data_type * tuyamcubr_find_data_type(uint8_t type) { const struct tuyamcubr_data_type *dt; if (type > nitems(tuyamcubr_data_types)) return (NULL); dt = &tuyamcubr_data_types[type]; if (dt->t_name == NULL) return (NULL); return (dt); } static inline uint8_t tuyamcubr_cksum_fini(uint8_t sum) { /* * "Start from the header, add up all the bytes, and then divide * the sum by 256 to get the remainder." * * If we accumulate bytes in a uint8_t, we get this for free. */ return (sum); } enum tuyamcubr_parser_state { TUYAMCUBR_P_START, TUYAMCUBR_P_HEADER, TUYAMCUBR_P_VERSION, TUYAMCUBR_P_COMMAND, TUYAMCUBR_P_LEN1, TUYAMCUBR_P_LEN2, TUYAMCUBR_P_DATA, TUYAMCUBR_P_CKSUM, TUYAMCUBR_P_SKIP, TUYAMCUBR_P_SKIP_CKSUM, }; //#ifdef ESP8266 //#define TUYAMCUBR_BUFLEN 256 //#else #define TUYAMCUBR_BUFLEN 1024 //#endif struct tuyamcubr_parser { enum tuyamcubr_parser_state p_state; unsigned int p_deadline; uint8_t p_version; uint8_t p_command; uint8_t p_sum; uint16_t p_len; uint8_t p_off; uint8_t p_data[TUYAMCUBR_BUFLEN]; }; struct tuyamcubr_dp { STAILQ_ENTRY(tuyamcubr_dp) dp_entry; uint8_t dp_id; uint8_t dp_type; uint32_t dp_value; }; STAILQ_HEAD(tuyamcubr_dps, tuyamcubr_dp); enum tuyamcubr_state { TUYAMCUBR_S_START, TUYAMCUBR_S_PROD_INFO, TUYAMCUBR_S_MODE, TUYAMCUBR_S_NET_STATUS, TUYAMCUBR_S_RUNNING, }; struct tuyamcubr_softc { TasmotaSerial *sc_serial; struct tuyamcubr_parser sc_parser; enum tuyamcubr_state sc_state; unsigned int sc_deadline; unsigned int sc_waiting; uint8_t sc_network_status; unsigned int sc_clock; struct tuyamcubr_dps sc_dps; }; static struct tuyamcubr_softc *tuyamcubr_sc = nullptr; struct tuyamcubr_recv_command { uint8_t r_command; void (*r_func)(struct tuyamcubr_softc *, uint8_t, const uint8_t *, size_t); }; static void tuyamcubr_recv_heartbeat(struct tuyamcubr_softc *, uint8_t, const uint8_t *, size_t); static void tuyamcubr_recv_product_info(struct tuyamcubr_softc *, uint8_t, const uint8_t *, size_t); static void tuyamcubr_recv_mode(struct tuyamcubr_softc *, uint8_t, const uint8_t *, size_t); static void tuyamcubr_recv_net_status(struct tuyamcubr_softc *, uint8_t, const uint8_t *, size_t); static void tuyamcubr_recv_status(struct tuyamcubr_softc *, uint8_t, const uint8_t *, size_t); static void tuyamcubr_recv_time(struct tuyamcubr_softc *, uint8_t, const uint8_t *, size_t); static const struct tuyamcubr_recv_command tuyamcubr_recv_commands[] = { { TUYAMCUBR_CMD_HEARTBEAT, tuyamcubr_recv_heartbeat }, { TUYAMCUBR_CMD_PRODUCT, tuyamcubr_recv_product_info }, { TUYAMCUBR_CMD_MODE, tuyamcubr_recv_mode }, { TUYAMCUBR_CMD_WIFI_STATE, tuyamcubr_recv_net_status }, { TUYAMCUBR_CMD_STATE, tuyamcubr_recv_status }, { TUYAMCUBR_CMD_TIME, tuyamcubr_recv_time }, }; static void tuyamcubr_recv(struct tuyamcubr_softc *sc, const struct tuyamcubr_parser *p) { const struct tuyamcubr_recv_command *r; const uint8_t *data = p->p_data; size_t len = p->p_len; size_t i; if (len > 0) { AddLog(LOG_LEVEL_DEBUG, TUYAMCUBR_FMT("recv version 0x%02x command 0x%02x: %*_H"), p->p_version, p->p_command, len, data); } else { AddLog(LOG_LEVEL_DEBUG, TUYAMCUBR_FMT("recv version 0x%02x command 0x%02x"), p->p_version, p->p_command); } for (i = 0; i < nitems(tuyamcubr_recv_commands); i++) { r = &tuyamcubr_recv_commands[i]; if (r->r_command == p->p_command) { r->r_func(sc, p->p_version, data, len); return; } } /* unhandled command? */ } static enum tuyamcubr_parser_state tuyamcubr_parse(struct tuyamcubr_softc *sc, uint8_t byte) { struct tuyamcubr_parser *p = &sc->sc_parser; enum tuyamcubr_parser_state nstate = p->p_state; switch (p->p_state) { case TUYAMCUBR_P_START: if (byte != TUYAMCUBR_H_ONE) return (TUYAMCUBR_P_START); /* reset state */ p->p_sum = 0; nstate = TUYAMCUBR_P_HEADER; break; case TUYAMCUBR_P_HEADER: if (byte != TUYAMCUBR_H_TWO) return (TUYAMCUBR_P_START); p->p_deadline = sc->sc_clock + (10 * 1000); nstate = TUYAMCUBR_P_VERSION; break; case TUYAMCUBR_P_VERSION: p->p_version = byte; nstate = TUYAMCUBR_P_COMMAND; break; case TUYAMCUBR_P_COMMAND: p->p_command = byte; nstate = TUYAMCUBR_P_LEN1; break; case TUYAMCUBR_P_LEN1: p->p_len = (uint16_t)byte << 8; nstate = TUYAMCUBR_P_LEN2; break; case TUYAMCUBR_P_LEN2: p->p_len |= (uint16_t)byte; p->p_off = 0; if (p->p_len > sizeof(p->p_data)) { AddLog(LOG_LEVEL_DEBUG, TUYAMCUBR_FMT("skipping command %02x" ", too much data %zu/%zu"), p->p_command, p->p_len, sizeof(p->p_data)); return (TUYAMCUBR_P_SKIP); } nstate = (p->p_len > 0) ? TUYAMCUBR_P_DATA : TUYAMCUBR_P_CKSUM; break; case TUYAMCUBR_P_DATA: p->p_data[p->p_off++] = byte; if (p->p_off >= p->p_len) nstate = TUYAMCUBR_P_CKSUM; break; case TUYAMCUBR_P_CKSUM: if (tuyamcubr_cksum_fini(p->p_sum) != byte) { AddLog(LOG_LEVEL_DEBUG, TUYAMCUBR_FMT("checksum failed, skipping")); return (TUYAMCUBR_P_START); } tuyamcubr_recv(sc, p); /* this message is done, wait for another */ return (TUYAMCUBR_P_START); case TUYAMCUBR_P_SKIP: if (++p->p_off >= p->p_len) return (TUYAMCUBR_P_SKIP_CKSUM); return (nstate); case TUYAMCUBR_P_SKIP_CKSUM: return (TUYAMCUBR_P_START); } p->p_sum += byte; return (nstate); } static uint8_t tuyamcubr_write(struct tuyamcubr_softc *sc, const void *data, size_t len) { TasmotaSerial *serial = sc->sc_serial; const uint8_t *bytes = (const uint8_t *)data; uint8_t cksum = 0; size_t i; for (i = 0; i < len; i++) { uint8_t b = bytes[i]; serial->write(b); cksum += b; } return (cksum); } static void tuyamcubr_send(struct tuyamcubr_softc *sc, uint8_t command, const void *data, size_t len) { TasmotaSerial *serial = sc->sc_serial; struct tuyamcubr_header h = { .header = { TUYAMCUBR_H_ONE, TUYAMCUBR_H_TWO }, .version = 0x00, .command = command, .datalen = htons(len), }; uint8_t cksum = 0; if (len) { AddLog(LOG_LEVEL_DEBUG, TUYAMCUBR_FMT("send version 0x%02x command 0x%02x: %*_H"), h.version, h.command, len, data); } else { AddLog(LOG_LEVEL_DEBUG, TUYAMCUBR_FMT("send version 0x%02x command 0x%02x"), h.version, h.command); } cksum += tuyamcubr_write(sc, &h, sizeof(h)); if (len > 0) cksum += tuyamcubr_write(sc, data, len); cksum = tuyamcubr_cksum_fini(cksum); serial->write(cksum); serial->flush(); } /* if we have polymorphic funcions then we may as well (ab)use them */ static void tuyamcubr_send(struct tuyamcubr_softc *sc, uint8_t command) { tuyamcubr_send(sc, command, NULL, 0); } static void tuyamcubr_heartbeat(struct tuyamcubr_softc *sc, unsigned int deadline) { sc->sc_deadline += deadline; tuyamcubr_send(sc, TUYAMCUBR_CMD_HEARTBEAT); } static struct tuyamcubr_dp * tuyamcubr_find_dp(struct tuyamcubr_softc *sc, uint32_t index, uint8_t type) { struct tuyamcubr_dp *dp; if (index > 0xff) return (NULL); STAILQ_FOREACH(dp, &sc->sc_dps, dp_entry) { if (dp->dp_id == index && dp->dp_type == type) return (dp); } return (NULL); } static void tuyamcubr_cmnd_data(struct tuyamcubr_softc *sc, uint8_t type) { const struct tuyamcubr_data_type *dt = &tuyamcubr_data_types[type]; struct { struct tuyamcubr_data_header h; uint8_t value[4]; /* only up to 4 bytes */ } data; size_t len = sizeof(data.h) + dt->t_len; struct tuyamcubr_dp *dp; dp = tuyamcubr_find_dp(sc, XdrvMailbox.index, type); if (dp == NULL) { ResponseCmndChar_P(PSTR("Unknown DpId")); return; } if (XdrvMailbox.data_len == 0) { ResponseCmndNumber(dp->dp_value); return; } if (XdrvMailbox.payload < 0x00 || XdrvMailbox.payload > dt->t_max) { ResponseCmndChar_P(PSTR("Invalid")); return; } dp->dp_value = XdrvMailbox.payload; data.h.dpid = dp->dp_id; data.h.type = dp->dp_type; data.h.len = htons(dt->t_len); dt->t_wr(data.value, dp->dp_value); tuyamcubr_send(sc, TUYAMCUBR_CMD_SET_DP, &data, len); tuyamcubr_rule_dp(sc, dp); ResponseCmndNumber(dp->dp_value); /* SetOption59 */ if (Settings->flag3.hass_tele_on_power) tuyamcubr_publish_dp(sc, dp); } static void tuyamcubr_cmnd_data_bool(void) { struct tuyamcubr_softc *sc = tuyamcubr_sc; struct { struct tuyamcubr_data_header h; uint8_t value[1]; } data; struct tuyamcubr_dp *dp; uint32_t value; dp = tuyamcubr_find_dp(sc, XdrvMailbox.index, TUYAMCUBR_DATA_TYPE_BOOL); if (dp == NULL) { ResponseCmndChar_P(PSTR("Unknown DpId")); return; } if (XdrvMailbox.data_len == 0) { ResponseCmndNumber(dp->dp_value); return; } switch (XdrvMailbox.payload) { case 0: case 1: value = XdrvMailbox.payload; break; case 2: value = !dp->dp_value; break; default: ResponseCmndChar_P(PSTR("Invalid")); return; } dp->dp_value = value; data.h.dpid = dp->dp_id; data.h.type = dp->dp_type; data.h.len = htons(sizeof(data.value)); data.value[0] = value; tuyamcubr_send(sc, TUYAMCUBR_CMD_SET_DP, &data, sizeof(data)); tuyamcubr_rule_dp(sc, dp); ResponseCmndNumber(dp->dp_value); /* SetOption59 */ if (Settings->flag3.hass_tele_on_power) tuyamcubr_publish_dp(sc, dp); } static void tuyamcubr_cmnd_data_value(void) { tuyamcubr_cmnd_data(tuyamcubr_sc, TUYAMCUBR_DATA_TYPE_VALUE); } static void tuyamcubr_cmnd_data_enum(void) { tuyamcubr_cmnd_data(tuyamcubr_sc, TUYAMCUBR_DATA_TYPE_ENUM); } static void tuyamcubr_rule_dp(struct tuyamcubr_softc *sc, const struct tuyamcubr_dp *dp) { const struct tuyamcubr_data_type *dt = &tuyamcubr_data_types[dp->dp_type]; /* XXX this only handles numeric types */ Response_P(PSTR("{\"%s\":{\"%s%u\":%u}}"), D_CMND_TUYAMCUBR_PREFIX, dt->t_name, dp->dp_id, dp->dp_value); XdrvRulesProcess(0); } static void tuyamcubr_publish_dp(struct tuyamcubr_softc *sc, const struct tuyamcubr_dp *dp) { const struct tuyamcubr_data_type *dt = &tuyamcubr_data_types[dp->dp_type]; char topic[64]; /* how long is a (bit of) string? */ /* XXX this only handles numeric types */ snprintf(topic, sizeof(topic), PSTR("%s%s%u"), D_CMND_TUYAMCUBR_PREFIX, dt->t_name, dp->dp_id); Response_P(PSTR("%u"), dp->dp_value); MqttPublishPrefixTopic_P(TELE, topic); } static void tuyamcubr_publish(struct tuyamcubr_softc *sc) { struct tuyamcubr_dp *dp; STAILQ_FOREACH(dp, &sc->sc_dps, dp_entry) tuyamcubr_publish_dp(sc, dp); } static void tuyamcubr_send_heartbeat(struct tuyamcubr_softc *sc, unsigned int deadline) { sc->sc_deadline += deadline; tuyamcubr_send(sc, TUYAMCUBR_CMD_HEARTBEAT); } static void tuyamcubr_recv_heartbeat(struct tuyamcubr_softc *sc, uint8_t v, const uint8_t *data, size_t datalen) { /* check the data? */ switch (sc->sc_state) { case TUYAMCUBR_S_START: sc->sc_state = TUYAMCUBR_S_PROD_INFO; tuyamcubr_send(sc, TUYAMCUBR_CMD_PRODUCT); break; case TUYAMCUBR_S_RUNNING: sc->sc_waiting = 0; break; default: AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("unexpected heartbeat in state %u"), sc->sc_state); break; } } static void tuyamcubr_recv_product_info(struct tuyamcubr_softc *sc, uint8_t v, const uint8_t *data, size_t datalen) { AddLog(LOG_LEVEL_INFO, TUYAMCUBR_FMT("MCU Product ID: %.*s"), datalen, data); switch (sc->sc_state) { case TUYAMCUBR_S_PROD_INFO: sc->sc_state = TUYAMCUBR_S_MODE; tuyamcubr_send(sc, TUYAMCUBR_CMD_MODE); break; default: AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("unexpected product info in state %u"), sc->sc_state); break; } } static void tuyamcubr_recv_mode(struct tuyamcubr_softc *sc, uint8_t v, const uint8_t *data, size_t datalen) { switch (sc->sc_state) { case TUYAMCUBR_S_MODE: switch (datalen) { case 0: AddLog(LOG_LEVEL_INFO, TUYAMCUBR_FMT("MCU Mode: Coordinated")); break; case 2: AddLog(LOG_LEVEL_INFO, TUYAMCUBR_FMT("MCU Mode" ": Status GPIO%u, Reset GPIO%u"), data[0], data[1]); sc->sc_state = TUYAMCUBR_S_RUNNING; tuyamcubr_send(sc, TUYAMCUBR_CMD_QUERY_STATE); return; default: AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("MCU Mode" ": unexpected data length %zu"), datalen); break; } sc->sc_state = TUYAMCUBR_S_NET_STATUS; tuyamcubr_send(sc, TUYAMCUBR_CMD_WIFI_STATE, &sc->sc_network_status, sizeof(sc->sc_network_status)); break; default: AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("unexpected product info in state %u"), sc->sc_state); break; } } static void tuyamcubr_recv_net_status(struct tuyamcubr_softc *sc, uint8_t v, const uint8_t *data, size_t datalen) { switch (sc->sc_state) { case TUYAMCUBR_S_NET_STATUS: sc->sc_state = TUYAMCUBR_S_RUNNING; tuyamcubr_send(sc, TUYAMCUBR_CMD_QUERY_STATE); break; default: AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("unexpected product info in state %u"), sc->sc_state); break; } } static void tuyamcubr_recv_status(struct tuyamcubr_softc *sc, uint8_t v, const uint8_t *data, size_t datalen) { const struct tuyamcubr_data_type *dt; struct tuyamcubr_dp *dp; struct tuyamcubr_data_header h; size_t len; const uint8_t *b; uint32_t value; /* take dp status updates at any time */ do { if (datalen < sizeof(h)) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("status header short %zu<%zu"), datalen, sizeof(h)); return; } memcpy(&h, data, sizeof(h)); data += sizeof(h); datalen -= sizeof(h); len = ntohs(h.len); if (datalen < len) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("status data short %zu<%zu"), datalen, len); return; } b = data; data += len; datalen -= len; dt = tuyamcubr_find_data_type(h.type); if (dt == NULL || dt->t_len == -1) { /* XXX revisit this */ AddLog(LOG_LEVEL_INFO, TUYAMCUBR_FMT("DpId %u unsupported type 0x%02x"), h.dpid, h.type); continue; } if (len != dt->t_len) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("%s%s%u: unexpected len %zu"), D_CMND_TUYAMCUBR_PREFIX, dt->t_name, len); continue; } value = dt->t_rd(b); if (value > dt->t_max) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("%s%s%u: unexpected value %u>%u"), D_CMND_TUYAMCUBR_PREFIX, dt->t_name, value, dt->t_max); continue; } dp = tuyamcubr_find_dp(sc, h.dpid, h.type); if (dp == NULL) { dp = (struct tuyamcubr_dp *)malloc(sizeof(*dp)); if (dp == NULL) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("%s%s%u no memory"), D_CMND_TUYAMCUBR_PREFIX, tuyamcubr_data_types[h.type], h.dpid); continue; } dp->dp_id = h.dpid; dp->dp_type = h.type; STAILQ_INSERT_TAIL(&sc->sc_dps, dp, dp_entry); } else if (dp->dp_value == value) { /* nop */ continue; } dp->dp_value = value; tuyamcubr_rule_dp(sc, dp); tuyamcubr_publish_dp(sc, dp); } while (datalen > 0); } static void tuyamcubr_recv_time(struct tuyamcubr_softc *sc, uint8_t v, const uint8_t *data, size_t datalen) { struct tuyamcubr_time tm; uint8_t weekday; weekday = RtcTime.day_of_week - 1; if (weekday == 0) weekday = 7; /* check datalen? should be 0 */ tm.valid = 1; /* XXX check whether time is valid */ tm.year = RtcTime.year % 100; tm.month = RtcTime.month; tm.day = RtcTime.day_of_month; tm.hour = RtcTime.hour; tm.minute = RtcTime.minute; tm.second = RtcTime.second; tm.weekday = weekday; tuyamcubr_send(sc, TUYAMCUBR_CMD_TIME, &tm, sizeof(tm)); } static void tuyamcubr_tick(struct tuyamcubr_softc *sc, unsigned int ms) { int diff; sc->sc_clock += ms; if (sc->sc_parser.p_state >= TUYAMCUBR_P_VERSION) { /* parser timeout only starts after the header */ diff = sc->sc_clock - sc->sc_parser.p_deadline; if (diff > 0) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("recv timeout")); sc->sc_parser.p_state = TUYAMCUBR_P_START; } } diff = sc->sc_clock - sc->sc_deadline; if (diff < 0) { /* deadline hasn't been reached, nothing to do */ return; } switch (sc->sc_state) { case TUYAMCUBR_S_START: tuyamcubr_send_heartbeat(sc, 3000); break; case TUYAMCUBR_S_RUNNING: tuyamcubr_send_heartbeat(sc, 15000); if (sc->sc_waiting) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("no heartbeat response")); /* XXX restart? */ } sc->sc_waiting = 1; break; } } static void tuyamcubr_every_1sec(struct tuyamcubr_softc *sc) { /* start with the assumption that wifi is configured */ uint8_t network_status = TUYAMCUBR_NETWORK_STATUS_3; if (MqttIsConnected()) { /* the device is connected to the "cloud" */ network_status = TUYAMCUBR_NETWORK_STATUS_5; } else { switch (WifiState()) { case WIFI_MANAGER: /* Pairing in AP mode */ network_status = TUYAMCUBR_NETWORK_STATUS_2; break; case WIFI_RESTART: /* WiFi + router */ network_status = TUYAMCUBR_NETWORK_STATUS_4; break; } } if (sc->sc_network_status != network_status) { sc->sc_network_status = network_status; if (sc->sc_state == TUYAMCUBR_S_RUNNING) { tuyamcubr_send(sc, TUYAMCUBR_CMD_WIFI_STATE, &network_status, sizeof(network_status)); } } } static void tuyamcubr_pre_init(void) { struct tuyamcubr_softc *sc; int baudrate; /* * SetOption97 - Set Baud rate for TuyaMCU serial communication * (0 = 9600 or 1 = 115200) */ baudrate = (Settings->flag4.tuyamcu_baudrate) ? 115200 : 9600; if (!PinUsed(GPIO_TUYAMCUBR_TX) || !PinUsed(GPIO_TUYAMCUBR_RX)) return; sc = (struct tuyamcubr_softc *)calloc(1, sizeof(*sc)); if (sc == NULL) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("unable to allocate state")); return; } sc->sc_parser.p_state = TUYAMCUBR_P_START; sc->sc_state = TUYAMCUBR_S_START; sc->sc_clock = 0; sc->sc_network_status = (WifiState() == WIFI_MANAGER) ? TUYAMCUBR_NETWORK_STATUS_2 : TUYAMCUBR_NETWORK_STATUS_3; STAILQ_INIT(&sc->sc_dps); sc->sc_serial = new TasmotaSerial(Pin(GPIO_TUYAMCUBR_RX), Pin(GPIO_TUYAMCUBR_TX), 2); if (!sc->sc_serial->begin(baudrate)) { AddLog(LOG_LEVEL_ERROR, TUYAMCUBR_FMT("unable to begin serial (baudrate %d)"), baudrate); goto del; } if (sc->sc_serial->hardwareSerial()) ClaimSerial(); #ifdef ESP32 AddLog(LOG_LEVEL_DEBUG, PSTR(TUYAMCUBR_LOGNAME ": Serial UART%d"), sc->sc_serial->getUart()); #endif /* commit */ tuyamcubr_sc = sc; /* kick the state machine off */ tuyamcubr_tick(sc, 0); return; del: delete sc->sc_serial; free: free(sc); } static void tuyamcubr_loop(struct tuyamcubr_softc *sc) { TasmotaSerial *serial = sc->sc_serial; while (serial->available()) { yield(); sc->sc_parser.p_state = tuyamcubr_parse(sc, serial->read()); } } /* * Interface */ #ifdef USE_WEBSERVER static void tuyamcubr_web_sensor(struct tuyamcubr_softc *sc) { struct tuyamcubr_dp *dp; const struct tuyamcubr_data_type *dt; STAILQ_FOREACH(dp, &sc->sc_dps, dp_entry) { dt = &tuyamcubr_data_types[dp->dp_type]; WSContentSend_PD(PSTR("{s}%s%u{m}%u{e}"), dt->t_name, dp->dp_id, dp->dp_value); } } #endif // USE_WEBSERVER static const char tuyamcubr_cmnd_names[] PROGMEM = D_CMND_TUYAMCUBR_PREFIX "|" D_CMND_TUYAMCUBR_DATA_BOOL "|" D_CMND_TUYAMCUBR_DATA_VALUE "|" D_CMND_TUYAMCUBR_DATA_ENUM ; static void (*const tuyamcubr_cmnds[])(void) PROGMEM = { &tuyamcubr_cmnd_data_bool, &tuyamcubr_cmnd_data_value, &tuyamcubr_cmnd_data_enum, }; bool Xdrv65(uint32_t function) { bool result = false; struct tuyamcubr_softc *sc; switch (function) { case FUNC_PRE_INIT: tuyamcubr_pre_init(); return (false); } sc = tuyamcubr_sc; if (sc == NULL) return (false); switch (function) { case FUNC_LOOP: tuyamcubr_loop(sc); break; #if 0 case FUNC_SET_DEVICE_POWER: result = tuyamcubr_set_power(sc); break; #endif case FUNC_EVERY_100_MSECOND: tuyamcubr_tick(sc, 100); break; case FUNC_EVERY_50_MSECOND: case FUNC_EVERY_200_MSECOND: case FUNC_EVERY_250_MSECOND: break; case FUNC_EVERY_SECOND: tuyamcubr_every_1sec(sc); break; #if 0 case FUNC_JSON_APPEND: tuyamcubr_sensor(sc); break; #endif case FUNC_AFTER_TELEPERIOD: tuyamcubr_publish(sc); break; #ifdef USE_WEBSERVER case FUNC_WEB_ADD_MAIN_BUTTON: break; case FUNC_WEB_SENSOR: tuyamcubr_web_sensor(sc); break; #endif // USE_WEBSERVER case FUNC_COMMAND: result = DecodeCommand(tuyamcubr_cmnd_names, tuyamcubr_cmnds); break; case FUNC_ACTIVE: result = true; break; } return (result); } #endif // USE_TUYAMCUBR