Sensor API - seurat-atreides/Sonoff-Tasmota GitHub Wiki
This wiki page is an attempt to document the Tasmota sensor API for sensor driver development.
- There are several I2C sensor examples you can take from the development codebase when writing your own and you are encouraged to do this as it is a quick and easy way to see how things fit together.
- The Tasmota firmware is essentially intended for Sonoff devices and commits to the main development branch will be subject to review based on whether or not what you intend to develop or add to the existing code is relevant to the general Sonoff device users.
- That being said, there is a lot of development going into the firmware which extends the functionality of standard off the shelf Sonoff devices, the firmware in itself is also useful to other ESP8266 based boards such as the WeMos ESP8266 boards and more technically inclined individuals who use generic ESP8266 modules in their own circuits which provides more access to pins and the ability to add more sensors and hardware external to the Sonoff device or the generic ESP8266 module circuits.
- The resources on the ESP8266 are finite. Most of the Sonoff devices ship with 1Mbyte SPI flash chips which means for the generic Sonoff device users the code generally needs to be less than 502Kbytes to ensure that OTA (Over The Air) flash functionality (which is the main reason why people use this firmware) remains available. RAM is also limited to an absolute maximum of 80Kbytes. This memory is divided into heap (used by global variables and Strings) and stack (used by local variables) where stack space is just 4Kbytes.
- Given the above resource constraints its important to keep your code as small as possible, as fast running as possible, and use as little RAM as possible.
- You need to think about these resource constraints all the time whilst doing any development you wish to add to the firmware functionality - Face the fact that microcontroller development isn't as close a relative to standard computer programming as you'd expect.
- You will be adding code to an existing framework which requires you to adhere to some simple but strict rules such as not having any infinite loops like you would have in your generic Arduino code and try to avoid using the delay() functions when writing your code as this will cause the entire firmware to be subjected to these delay()'s you have added - Infinite loops will cause the firmware to lock up completely!
- If your sensor has configuration options please make these available by using the
SensorXX
framework which is already incorporated in the base code - This may not stop you from using a web-based configuration interface but since web-based configuration takes up a lot of code space in flash it is very important to make this optional by means of a compiler directive or a #define in the configuration file and as such something you need to keep in mind during your development and debugging - The more progressively optional additional features are in your driver the smaller the basic codebase can be for minimalist implementations. - Whilst developing drivers for devices that use the I2C bus always consider other devices already supported in the codebase which may use the same address range. This could mean you need to find a unique way of differentiating your device detection from other devices on the same address range (e.g. querying a model-specific register) and/or disabling by #undef existing devices if yours is selected with a #define statement and in such cases always provide a warning to the user during compile time using the #warning pragma such as including
#warning **** Turned off conflicting drivers SHT and VEML6070 ****
in your code. - DO NOT ADD WEB INTERFACE FOR SENSOR CONFIGURATION if your sensor requires additional user configuration. The reason for this is the additional program memory required but most importantly the amount of RAM required to even create minimal user interfaces. Running out of RAM during runtime will lead to abnormal behaviour of your driver and/or other drivers or the entire firmware! See sensors such as the MCP23008/MCP23017 driver on more information on how to implement
SensorXX
commands instead! - While developing you might want to enable additional debugging provided by file
xdrv_95_debug.ino
using define USE_DEBUG_DRIVER which provides some commands for managing configuration settings and CPU timing. In addition you can enable define PROFILE_XSNS_SENSOR_EVERY_SECOND to profile your drivers duration. - Do not assume others will know immediately how to use your addition and know that you will need to write a Wiki for it in the end.
Sensor libraries are located in the lib/
directory. Sensor drivers are located in the sonoff/
directory. The filename of the sensor driver is xsns_<driver_ID>_<driver_name>.ino
, e.g. xsns_05_ds18b20.ino
where <driver_ID>
is a unique number between 01 and 90 and <driver_name>
is a human-readable name of the driver.
Using generic libraries from external sources for sensors should be avoided as far as possible as they usually include code for other platforms and are not always written in an optimised way.
Conditional compiling of a sensor driver is achieved by adding a pre-processor directive of the scheme USE_<driver_name>
in user_config_override.h
. Accordingly the driver code has to be wrapped in #ifdef USE_<driver_name> ... #endif // USE_<driver_name>
. Any Sensor driver must contain a pre-processor directive defining the driver ID by the scheme #define XSNS_<driver_ID>
.
Any sensor driver needs a callback function following the scheme
// Conditional compilation of driver
#ifdef USE_<driver_name>
// Define driver ID
#define XSNS_<driver_ID> <driver_ID>
/**
* The callback function Xsns<driver_ID>() interfaces Tasmota with the sensor driver.
*
* It provides the Tasmota callback IDs.
*
* @param byte callback_id Tasmota function ID.
* @return boolean Return value.
* @pre None.
* @post None.
*
*/
boolean Xsns<driverID>(byte callback_id) {
// Set return value to `false`
boolean result = false;
// Check if I2C interface mode
// if(i2c_flg) {
// Check which callback ID is called by Tasmota
switch (callback_id) {
case FUNC_INIT:
break;
case FUNC_EVERY_50_MSECOND:
break;
case FUNC_EVERY_SECOND:
break;
case FUNC_JSON_APPEND:
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_APPEND:
break;
#endif // USE_WEBSERVER
case FUNC_SAVE_BEFORE_RESTART:
break;
case FUNC_COMMAND:
break;
}
// } // if(i2c_flg)
// Return boolean result
return result;
}
#endif // USE_<driver_name>
This callback ID is called when sensor drivers should be initialized.
This callback ID is called every 50 milliseconds, e.g. for near real-time operation
This callback ID is called every second.
It can be useful for anything that you need to do on a per second basis and is commonly used as an entry point to detect a driver or initialize an externally driven device such as a sensor, relay board or other forms of input/output required by your driver.
You would normally want to make sure you've detected and initialised before it is used by JSON_APPEND etc so that its ready to serve data.
The generally accepted way to use this would be to detect your sensor and once this is done set a sensor value accordingly so that the function does not use unnecessary resources during future calls, for example:
void MySensorDetect()
{
if (MySensorDetected) { return; }
/*
* Perform the code which needs to be completed to
* detect your sensor and then set MySensorDetected to
* a non-zero value which will prevent this section
* of your code to re-run every time the function is
* called.
*
* Under normal circumstances you'd not need to do
* re-detect or initialise your sensor once it has been
* done
*/
}
Setting a flag that the driver was successful in detecting the attached chip/board via I2C or SPI will prevent it from continuously trying to initialize an already initialized device.
When writing your function responsible for detecting an externally connected I2C device try to create a method by which you read or write to specific registers that would be applicable to that specific I2C device only as to confirm a positive detect for the device. If this is not done extensively it will lead to some drivers getting false detects for a different device type simply because it shares the same I2C address.
Unless your driver is specifically going to use the entire array of addresses provisioned by the manufacturer please consider using a #define USE_MYCHIPNAME_ADDR in the user_config_override.h
so that the user may specify the address on which to expect the device. This is of course only applicable to drivers that are not enabled by default in any of the pre-built binaries.
I2C address auto-detection example
#define MPR121_I2C_ADDR_1ST 0x5A /** 1st I2C address of sensor model **/
#define MPR121_I2C_ADDR_NUM 4 /** Number of sensors/I2C addresses **/
#define MPR121_I2C_ID_REG 0x5D /** Sensor model specific ID register **/
#define MPR121_I2C_ID_VAL 0x24 /** Sensor model specific ID register value **/
/* Sensor data struct type declaration/default definition */
typedef struct {
bool connected = false; /** Status if sensor is connected at I2C address */
bool running = false; /** Running state of sensor */
.
.
.
} mpr121;
// Declare array of sensor data structs
mpr121 mpr121[MPR121_I2C_ADDR_NUM];
// Sensor specific init function
void mpr121_init() {
// Loop through I2C addresses
for (uint8_t i = 0; i < MPR121_I2C_ADDR_NUM); i++) {
// Check if sensor is connected on I2C address
mpr121[i].connected = (MPR121_I2C_ID_VAL == I2cRead8(MPR121_I2C_ADDR_1ST + i, MPR121_I2C_ID_REG);
if(mpr121[i].connected) {
// Log sensor found
snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_I2C "MPR121-%d " D_FOUND_AT " 0x%X"), i, MPR121_I2C_ADDR_1ST + i);
AddLog(LOG_LEVEL_INFO);
// Initialize sensor
.
.
.
// Set running to true
mpr121[i].running = true;
}
}
if(!(mpr121[0].connected || mpr121[1].connected || mpr121[2].connected || mpr121[3].connected)){
snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_I2C "MPR121: No sensors found"));
AddLog(LOG_LEVEL_INFO);
}
}
Four advanced methods to use FUNC_EVERY_SECOND (Food for thought) :
- If a sensor needs an action which takes a long time, like more than 100mS, the action will be started here for a future follow-up. Using the uptime variable for testing like (uptime &1) will happen every 2 seconds. An example is the DS18B20 driver where readings (conversions they call it) can take up to 800mS from the initial request.
- If a sensor needed the previous action it is now time to gather the information and store it in a safe place to be used by FUNC_JSON_APPEND and/or FUNC_WEB_APPEND. Using the else function of the previous test (uptime &1) will happen every 2 seconds too but just 1 second later than the previous action.
- If a sensor does not respond for 10 times the sensor detection flag could be reset which will stop further processing until the sensor is re-detected. This is currently not being used actively as some users complain about disappearing sensors for whatever reason - Could be hardware related but easier to make Tasmota a little more flexible.
- Making re-detection of a sensor possible by executing this once every 100 seconds (94 == (uptime %100)) a re-attached sensor can be detected without a restart of Tasmota. The 94 given in this example should be different for every sensor driver to make sure not all sensors start detection at the same time. Using the drivers index number should be a good starting point.
NOTE: This callback ID is deprecated as sensors should prepare for more regular updates due to "realtime" rule execution. Use FUNC_EVERY_SECOND instead. See examples used in xsns_05_ds18x20.ino and xsns_09_bmp.ino where updated sensor data is stored in preparation to calls to FUNC_JSON_APPEND and FUNC_WEB_APPEND.
This callback ID is called when TelePeriod
is due to append telemetry data to the MQTT JSON string or at approximately every 2 seconds when a rule is checked, e.g.
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"MPR121%c\":{\"Button%i\":%i}}"), pS->id[i], j, BITC(i,j));
This callback ID is called every millisecond when HTML code should be added to the Tasmota web-interface main page, e.g.
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s{s}MPR121%c Button%d{m}%d{e}"), mqtt_data, pS->id[i], j, BITC(i,j));
It should be wrapped in #ifdef USE_WEBSERVER ... #endif // USE_WEBSERVER
This callback ID is called to allow a sensor to prepare for saving configuration changes. To be used to save volatile data just before a restart. Variables can be appended to struct SYSCFG {} Settings
in file sonoff/settings.h
.
This callback ID is called when a sensor specific command Sensor<xx>
or Driver<xx>
is executed where xx is the sensor index.
case FUNC_COMMAND:
if (XSNS_<driver_ID> == XdrvMailbox.index) {
result = <driver_name>Command() { ... }; // Return true on success
}
break;
// Data struct of FUNC_COMMAND ID
struct XDRVMAILBOX {
uint16_t valid; // ???
uint16_t index; // Sensor index
uint16_t data_len; // Length of command string
uint16_t payload16; // 16 bit unsigned int of payload if it could be converted, otherwise 0
int16_t payload; // 16 bit signed int of payload if it could be converted, otherwise 0
uint8_t grpflg; // ???
uint8_t notused; // ???
char *topic; // Command topic
char *data; // Command string/value - length of which is defined by data_len
} XdrvMailbox;
If your driver needs to accept multiple parameters for SensorXX
and/or DriverXX please consider using comma delimited formatting and use the already written subStr() function declared in support.ino to parse through the parameters you need.
An example of those could be
SensorXX reset // The reset parameter may be intercepted using:
if (!strcmp(subStr(sub_string, XdrvMailbox.data, ",", 1),"RESET")) { // Note 1 used for param number
MyDriverName_Reset();
return serviced;
}
Or in the case of multiple parameters
SensorXX mode,1
if (!strcmp(subStr(sub_string, XdrvMailbox.data, ",", 1),"RESET")) { // Note 1 used for param number
uint8_t mode = atoi(subStr(sub_string, XdrvMailbox.data, ",", 2); // Note 2 used for param number
}
This function publishes MQTT messages immediately, e.g.
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"MPR121%c\":{\"Button%i\":%i}}"), pS->id[i], j, BITC(i,j));
MqttPublishPrefixTopic_P(RESULT_OR_STAT, mqtt_data);
This function adds log messages stored in log_data
to the local logging system, e.g.
snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_I2C "MPR121(%c) " D_FOUND_AT " 0x%X"), pS->id[i], pS->i2c_addr[i]);
AddLog(LOG_LEVEL_INFO);
This function adds a log message to the local logging system dumping the serial buffer as hex information, e.g.
AddLogSerial(LOG_LEVEL_INFO);
This function adds a log message to the local logging system about missed sensor reads.
These functions return true
if 1, 2, 3 or size
bytes can be read from the I2C address addr
and register reg
into *data
.
Functions with a S
read signed data types while functions without a S
read unsigned data types.
Functions with LE read little-endian byte order while functions without LE read machine byte order.
These functions return 1, 2 or 3 bytes from the I2C address addr
and register reg
.
Functions with a S
read signed data types while functions without a S
read unsigned data types.
Functions with LE read little endian byte order while functions without LE read machine byte order.
These functions return true after successfully writing 1, 2 or size
bytes to the I2C address addr
and register reg
.
These functions copy len
bytes from/to *reg_data
starting at I2C address addr
and register reg
.
This functions writes a list of I2C addresses in use into the string *dev
with maximum length devs_len
.
This functions checks if the I2C address addr
is in use.
This pre-processor directive saves RAM by storing strings in flash instead of RAM.
This pre-processor directive saves RAM by storing strings in flash instead of RAM.
You may then reference them directly (if the type matches the parameter required) or force it to 4 byte alignment by using the variable as FPSTR(MyTextStaticVariable)