Tasmota/tasmota/tasmota_xdrv_driver/xdrv_65_tuyamcubr.ino

981 lines
22 KiB
Arduino
Raw Normal View History

WIP Tuya MCU Bridge driver alternative to the TuyaMCU driver (#17626) * WIP Tuya MCU Bridge driver alternative to the TuyaMCU driver The main difference is this driver does not try and wire MCU data points (Dps) into the tasmota power/light/etc controls. Instead each Dp ends up being relayed directly to MQTT and the rules subsystem. If you want to change the state of something wired up to the MCU, you send tuyamcu specific commands to manipulate the Dp. Each Dp gets a type and id specific topic that is sent to MQTT. eg, Dp id 1 type bool looks like tele/%topic%/TUYAMCUBOOL1. To change state you send a TuyaMCUBool1 command (ie, the command index value is used as the DpId, which is nice and symmetrical) with the new value. Currently Rules operate on TuyaMCU#TypeDpid things, eg, "rule1 on TuyaMCU#Bool1 do power %value% endon" toggle the power on the tasmota device when the state of the thing on the MCU changes too. The most obviously missing stuff at the moment is: - better relaying of the wifi/mqtt status to the MCU - handling wifi reset requests from the MCU - low power stuff? - support for sending status updates and device info queries. - restarting the tuya mcu state machine? - restarting the rx state machine when no bytes are rxed for a period of time - time sync * shorten the log prefix to TYB (3 chars). requested by arendst * use the local definition for the SET_DP command. reaching back to the existing tuyamcu code isnt reliable. pointed out by arendst * put the todo list in the code so it can be tracked * check the wifi/mqtt state every second and update the mcu if it changes. * fix rule processing when Dp state is changed from a cmnd. rule processing was done as part of publishing the state, but publishing the state when it was updated by a command only happened if So59 was set. split rule processing out of publish and call them separately as needed. publish is now called from teleperiod, status updates from the MCU, and from cmnds if so59 is set. rules are called from status updates from the MCU and from cmnds. Co-authored-by: David Gwynne <dlg@defeat.lan.animata.net>
2023-01-08 16:35:45 +00:00
/*
* 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
* - time 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_SET_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 */
/* 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 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 },
};
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);
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)
{
tuyamcubr_cmnd_data(tuyamcubr_sc, TUYAMCUBR_DATA_TYPE_BOOL);
}
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_tick(struct tuyamcubr_softc *sc, unsigned int ms)
{
int diff;
sc->sc_clock += ms;
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();
/* 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
*/
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;
case FUNC_COMMAND:
result = DecodeCommand(tuyamcubr_cmnd_names, tuyamcubr_cmnds);
break;
}
return (result);
}
#endif // USE_TUYAMCUBR