mirror of https://github.com/arendst/Tasmota.git
1119 lines
25 KiB
C++
1119 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();
|
|
|
|
/* 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;
|
|
}
|
|
|
|
return (result);
|
|
}
|
|
|
|
#endif // USE_TUYAMCUBR
|