Tasmota/tasmota/tasmota_xdrv_driver/xdrv_65_tuyamcubr.ino

1127 lines
25 KiB
C++

/*
* xdrv_65_tuyamcubr.ino - TuyaMCU Bridge support for Tasmota
*/
/*
* Copyright (C) 2023 David Gwynne <david@gwynne.id.au>
*
* 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 <TasmotaSerial.h>
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