Hi,
Even if I'm not very good with programming and terrible with C/C++, I tried to put together a sort of extension to the core engine providing additional functionalities but without impacting the core code. The idea was to embed within the sketch (better if in the future in a library), a sort of additional service taking care of common tasks automatically so to simplify the development. The "extension" hooks up (well, manually) into setup(), presentation(), loop() and receive(), present itself as a child-id node type S_CUSTOM to the controller and communicates with it with V_CUSTOM messages. A summary of what it does is:
- Report battery level periodically and automatically, the period is configurable
- Able to reboot the board on demand with the default bootloader, by connecting one of the pins to RST
- Support multiple sleep mode: no sleep, sleep once and sleep cycle
- Sleep mode and sleep time can be configured remotely with V_CUSTOM messages
- Sleep mode and sleep time are made persistent within the EEPROM once changed
- A pin can be configure to wake up a sleeping board when connected to ground
- Can report battery level on demand
- Allow changing the node_id remotely
- Stop sleeping when a WAKEUP message is sent just after a smart sleeping cycle
- Calculate battery from internal vcc without requiring a pin and the resistors
- Can calculate the battery percentage using custom boundaries
- Return the battery voltage through the custom service
A message to the service would look like 254;200;2;0;48;HELLO and the language (kind of inspired by the old ciseco LLAP) used in the payload of the S_CUSTOM message is the following:
- BATT: return the battery level
- HELLO: hello request
- NOSLEEP: set and save sleeping mode to no sleep
- SLEEPONCE: set and save sleeping mode to sleep once and go to sleep just after
- SLEEPCYCLE: set and save sleeping mode to sleep cycle and got to sleep just after
- SLEEPnnnX: set and save the sleep interval to nnn where X is S=Seconds, M=mins, H=Hours, D=Days. E.g. SLEEP010M would be 10 minutes
- MYxxx: change the node id to the provided one. E.g. MY025: change the node id to 25. Requires a reboot/restart
- WAKEUP: when received after a sleeping cycle, abort cycle sleeping since the controller needs us awake
- REBOOT: reboot the board
- RESET: clear the user's eeprom
- VERSION: send back the extension's version
Now, this is just a proof of concept not suitable for production but I would like to hear your feedback. Does the idea make some sense? Do you see anything that could be interesting to have in the core engine instead? Should I go on developing the idea or trash everything?
Thanks in advance!
// Enable debug prints to serial monitor
#define MY_DEBUG
// Enable and select radio type attached
#define MY_RADIO_NRF24
//#define MY_RADIO_RFM69
#include <MySensors.h>
/*
* MyExtension
*/
// sample request messages
/*
254;200;2;0;48;BATT
254;200;2;0;48;HELLO
254;200;2;0;48;SLEEP005S
254;200;2;0;48;WAKEUP
254;200;2;0;48;NOSLEEP
254;200;2;0;48;SLEEPONCE
254;200;2;0;48;SLEEPCYCLE
254;200;2;0;48;SLEEP010M
254;200;2;0;48;REBOOT
254;200;2;0;48;VERSION
*/
// define supported sleeping mode
#define MY_EXT_NO_SLEEP 0
#define MY_EXT_SLEEP_CYCLE 1
#define MY_EXT_SLEEP_ONCE 2
#define MY_EXT_SLEEP_UNIT_S 0
#define MY_EXT_SLEEP_UNIT_M 1
#define MY_EXT_SLEEP_UNIT_H 2
#define MY_EXT_SLEEP_UNIT_D 3
// define eeprom addresses
#define MY_EXT_EEPROM_SLEEP_SAVED 0
#define MY_EXT_EEPROM_SLEEP_MODE 1
#define MY_EXT_EEPROM_SLEEP_TIME_MAJOR 2
#define MY_EXT_EEPROM_SLEEP_TIME_MINOR 3
#define MY_EXT_EEPROM_SLEEP_UNIT 4
// version
#define MY_EXT_VERSION 1.0
/*
* configuration settings
*/
// define if the board has to sleep every time entering loop(). It can be MY_EXT_NO_SLEEP (no sleep), MY_EXT_SLEEP_ONCE (sleep only once) or MY_EXT_SLEEP_CYCLE (sleep at every cycle)
#define MY_EXT_SLEEP_MODE MY_EXT_NO_SLEEP
// define for how long the board will sleep
#define MY_EXT_SLEEP_TIME 0
// define the unit of MY_EXT_SLEEP_TIME. It can be MY_EXT_SLEEP_UNIT_S (seconds), MY_EXT_SLEEP_UNIT_M (minutes), MY_EXT_SLEEP_UNIT_H (hours), MY_EXT_SLEEP_UNIT_D (days)
#define MY_EXT_SLEEP_UNIT MY_EXT_SLEEP_UNIT_S
// if defined, allow modifying sleep mode and interval remotely with NOSLEEP/SLEEPONCE/SLEEPCYCLE and SLEEPnnnX
#define MY_EXT_SLEEP_REMOTE_CONFIGURATION
// if defined, reset the board with the configured pin when receiving a REBOOT message
#define MY_EXT_SOFT_REBOOT
// the pin to connect to the RST pin to reboot the board
#define MY_EXT_SOFT_REBOOT_PIN 4
// if defined, wakeup from sleep when the configured pin changes
#define MY_EXT_SLEEP_INTERRUPT
// if defined, when waking up from the interrupt, the board stops sleeping. Disable it when attaching e.g. a motion sensor
#define MY_EXT_SLEEP_INTERRUPT_ABORT_SLEEP
// the pin to connect sued for waking up a sleeping board. Can be either 2, 3 or a different pin provided is reconfigured to act as an interrupt pin
#define MY_EXT_SLEEP_INTERRUPT_PIN 3
// the interrupt mode. Can be either FALLING or RISING
#define MY_EXT_SLEEP_INTERRUPT_MODE FALLING
// the expected vcc when the batter is fully charged, used to calculate the percentage
#define MY_EXT_BATTERY_MAX 3.3
// the expected vcc when the batter is fully discharged, used to calculate the percentage
#define MY_EXT_BATTERY_MIN 2.8
// if defined, report automatically the battery level back to the controller after the given number of hours (if the board sleeping it will be reported when awake next)
#define MY_EXT_REPORT_BATTERY
// how frequently (in hours) to report the battery level to the controller
#define MY_EXT_REPORT_BATTERY_H 1
// if defined, enable debug of the extension on serial port
#define MY_EXT_DEBUG
// the sensor child id used by the extension to send messages to the controller
#define MY_EXT_CHILD_ID 200
/*
* class definition
*/
class MyExtension {
public:
MyExtension();
// functions to be explicitely called within the sketch allow the extension hooking into the flow
void setup();
void presentation();
void loop();
void receive(const MyMessage &);
private:
// define if and how to sleep
int _sleep_mode = MY_EXT_SLEEP_MODE;
// define for how long to sleep (time)
int _sleep_time = MY_EXT_SLEEP_TIME;
// define for how long to sleep (unit)
int _sleep_unit = MY_EXT_SLEEP_UNIT;
// store last time the battery level has been reported
long _battery_last_reported = millis();
// implement custom sleep
void _sleep();
// process an incoming message/command
void _process(const char *);
// the container of the message used to respond to any request
MyMessage msg;
};
/*
* implementation
*/
// constructor
inline MyExtension::MyExtension() {
// initialize the response message container
msg = MyMessage(MY_EXT_CHILD_ID,V_CUSTOM);
}
// what to do when called within setup()
inline void MyExtension::setup() {
#ifdef MY_EXT_SOFT_REBOOT
// setup the reboot pin
digitalWrite(MY_EXT_SOFT_REBOOT_PIN,HIGH);
pinMode(MY_EXT_SOFT_REBOOT_PIN,OUTPUT);
#endif
#ifdef MY_EXT_SLEEP_INTERRUPT
// setup the sleep interrupt pin
pinMode(MY_EXT_SLEEP_INTERRUPT_PIN,INPUT);
digitalWrite(MY_EXT_SLEEP_INTERRUPT_PIN,HIGH ? MY_EXT_SLEEP_INTERRUPT_MODE == FALLING : LOW);
#endif
#ifdef MY_EXT_SLEEP_REMOTE_CONFIGURATION
// restore sleep configuration from eeprom
if (loadState(MY_EXT_EEPROM_SLEEP_SAVED) == 1) {
// sleep settings found in the eeprom, restore them
_sleep_mode = loadState(MY_EXT_EEPROM_SLEEP_MODE);
_sleep_time = loadState(MY_EXT_EEPROM_SLEEP_TIME_MINOR);
int major = loadState(MY_EXT_EEPROM_SLEEP_TIME_MAJOR);
if (major == 1) _sleep_time = _sleep_time + 250;
if (major == 2) _sleep_time = _sleep_time + 250*2;
if (major == 3) _sleep_time = _sleep_time + 250*3;
_sleep_unit = loadState(MY_EXT_EEPROM_SLEEP_UNIT);
#ifdef MY_EXT_DEBUG
Serial.print("Restored from eeprom sleep settings: sleep_mode=");
Serial.print(_sleep_mode);
Serial.print(" sleep_time=");
Serial.print(_sleep_time);
Serial.print(" sleep_unit=");
Serial.println(_sleep_unit);
#endif
}
#endif
}
// what to do when called within presentation()
inline void MyExtension::presentation() {
// present the extension as a custom sensor to the controller
present(MY_EXT_CHILD_ID,S_CUSTOM);
#ifdef MY_EXT_REPORT_BATTERY
// report battery level
MyExtension::_process("BATT");
#endif
}
// what to do when called within loop()
inline void MyExtension::loop() {
// continue/start sleeping if requested
if (_sleep_mode != MY_EXT_NO_SLEEP) MyExtension::_sleep();
}
// wrapper of smart sleep
inline void MyExtension::_sleep() {
// calculate the seconds to sleep
long sleep_sec = _sleep_time;
if (_sleep_unit == MY_EXT_SLEEP_UNIT_M) sleep_sec = sleep_sec*60;
else if (_sleep_unit == MY_EXT_SLEEP_UNIT_H) sleep_sec = sleep_sec*3600;
else if (_sleep_unit == MY_EXT_SLEEP_UNIT_D) sleep_sec = sleep_sec*43200;
long sleep_ms = sleep_sec*1000;
#ifdef MY_EXT_DEBUG
Serial.print("Going to sleep for ");
Serial.print(sleep_sec);
Serial.println("s");
#endif
// notify the controller I'm going to sleep
send(msg.set("SLEEPING"));
// go in smart sleep for the requested sleep interval
#ifdef MY_EXT_SLEEP_INTERRUPT
// sleep and wakeup when the interrupt triggers
int ret = sleep(digitalPinToInterrupt(MY_EXT_SLEEP_INTERRUPT_PIN),MY_EXT_SLEEP_INTERRUPT_MODE,sleep_ms,true);
#else
// sleep without any interrupt
int ret = sleep(sleep_ms,true);
#endif
#ifdef MY_EXT_SLEEP_INTERRUPT
if (ret > -1) {
#ifdef MY_EXT_DEBUG
Serial.println("Woke up by interrupt");
#endif
#ifdef MY_EXT_SLEEP_INTERRUPT_ABORT_SLEEP
// when waking up from an interrupt on the wakup pin, stop sleeping
_sleep_mode = MY_EXT_NO_SLEEP;
#endif
// restore the sleep interrupt pin
digitalWrite(MY_EXT_SLEEP_INTERRUPT_PIN,HIGH ? MY_EXT_SLEEP_INTERRUPT_MODE == FALLING : LOW);
}
#endif
// coming out of sleep
#ifdef MY_EXT_DEBUG
Serial.println("Awake");
#endif
// if supposed to sleep once, reset sleep mode
if (_sleep_mode == MY_EXT_SLEEP_ONCE) _sleep_mode = MY_EXT_NO_SLEEP;
// notify the controller I am awake
send(msg.set("AWAKE"));
#ifdef MY_EXT_REPORT_BATTERY
if ( (millis()-_battery_last_reported)/1000/3600 > MY_EXT_REPORT_BATTERY_H) {
// time to report the battery level again
MyExtension::_process("BATT");
_battery_last_reported = millis();
}
#endif
}
// process a message/command
inline void MyExtension::_process(const char *message) {
// BATT: return the battery level
if (strcmp(message,"BATT") == 0) {
// measure the provided vcc
float volt = (float)hwCPUVoltage()/1000;
// send the measure back with a custom message
char reply[25] = "BATT";
dtostrf(volt,1,2,&reply[strlen(reply)]);
send(msg.set(reply));
// calculate the percentage
int percentage = 100-(MY_EXT_BATTERY_MAX-volt/(MY_EXT_BATTERY_MAX-MY_EXT_BATTERY_MIN));
if (percentage > 100) percentage = 100;
if (percentage < 0) percentage = 0;
// send battery level percentage
sendBatteryLevel(percentage);
}
// HELLO: hello request
else if (strcmp(message,"HELLO") == 0) {
send(msg.set("HELLO"));
}
#ifdef MY_EXT_SLEEP_REMOTE_CONFIGURATION
// NOSLEEP: set and save sleeping mode to no sleep
else if (strcmp(message,"NOSLEEP") == 0) {
// go to sleep
_sleep_mode = MY_EXT_NO_SLEEP;
// save it to the eeprom
saveState(MY_EXT_EEPROM_SLEEP_SAVED,1);
saveState(MY_EXT_EEPROM_SLEEP_MODE,MY_EXT_NO_SLEEP);
send(msg.set(message));
}
// SLEEPONCE: set and save sleeping mode to sleep once and go to sleep just after
else if (strcmp(message,"SLEEPONCE") == 0) {
// go to sleep
_sleep_mode = MY_EXT_SLEEP_ONCE;
// save it to the eeprom
saveState(MY_EXT_EEPROM_SLEEP_SAVED,1);
saveState(MY_EXT_EEPROM_SLEEP_MODE,MY_EXT_SLEEP_ONCE);
}
// SLEEPCYCLE: set and save sleeping mode to sleep cycle and got to sleep just after
else if (strcmp(message,"SLEEPCYCLE") == 0) {
// go to sleep
_sleep_mode = MY_EXT_SLEEP_CYCLE;
// save it to the eeprom
saveState(MY_EXT_EEPROM_SLEEP_SAVED,1);
saveState(MY_EXT_EEPROM_SLEEP_MODE,MY_EXT_SLEEP_CYCLE);
}
// SLEEPnnnX: set and save the sleep interval to nnn where X is S=Seconds, M=mins, H=Hours, D=Days. E.g. SLEEP010M would be 10 minutes
else if (strlen(message) == 9 && strncmp("SLEEP",message,strlen("SLEEP")) == 0) {
// parse and set the sleep interval
int offset = 5;
// extract the unit (S=secs, M=mins, H=hours, D=Days)
char unit[2];
sprintf(unit,"%c",message[3+offset]);
unit[1] = '\0';
if (strcmp(unit,"S") == 0) _sleep_unit = MY_EXT_SLEEP_UNIT_S;
else if (strcmp(unit,"M") == 0) _sleep_unit = MY_EXT_SLEEP_UNIT_M;
else if (strcmp(unit,"H") == 0) _sleep_unit = MY_EXT_SLEEP_UNIT_H;
else if (strcmp(unit,"D") == 0) _sleep_unit = MY_EXT_SLEEP_UNIT_D;
else return;
// extract the requested time
char s[4];
s[0] = message[0+offset];
s[1] = message[1+offset];
s[2] = message[2+offset];
s[3] = '\0';
_sleep_time = atoi(s);
#ifdef MY_EXT_DEBUG
Serial.print("Set sleep_interval to ");
Serial.print(_sleep_time);
Serial.print(" ");
Serial.println(_sleep_unit);
#endif
// save it to eeprom
saveState(MY_EXT_EEPROM_SLEEP_UNIT,_sleep_unit);
// encode sleep time
int major = 0;
if (_sleep_time > 750) major = 3;
else if (_sleep_time > 500) major = 2;
else if (_sleep_time > 250) major = 1;
int minor = _sleep_time - 250*major;
saveState(MY_EXT_EEPROM_SLEEP_SAVED,1);
saveState(MY_EXT_EEPROM_SLEEP_TIME_MINOR,minor);
saveState(MY_EXT_EEPROM_SLEEP_TIME_MAJOR,major);
// interval set, reply back with the same message to acknowledge. Wait for SLEEP to start sleeping
send(msg.set(message));
}
#endif
// MYxxx: change the node id to the provided one. E.g. MY025: change the node id to 25. Requires a reboot/restart
else if (strlen(message) == 5 && strncmp("MY",message,strlen("MY")) == 0) {
// extract the node id
char s[4];
s[0] = message[2];
s[1] = message[3];
s[2] = message[4];
s[3] = '\0';
int node_id = atoi(s);
// Save static ID to eeprom
hwWriteConfig(EEPROM_NODE_ID_ADDRESS, (uint8_t)node_id);
_transportConfig.nodeId = (uint8_t)node_id;
}
// WAKEUP: when received after a sleeping cycle, abort cycle sleeping since the controller needs us awake
else if (strcmp(message,"WAKEUP") == 0) {
send(msg.set(message));
_sleep_mode = MY_EXT_NO_SLEEP;
}
// REBOOT: reboot the board
else if (strcmp(message,"REBOOT") == 0) {
#ifdef MY_EXT_SOFT_REBOOT
// set the reboot pin connected to RST to low so to reboot the board
send(msg.set(message));
digitalWrite(MY_EXT_SOFT_REBOOT_PIN,LOW);
#endif
}
// RESET: clear the user's eeprom
else if (strcmp(message,"RESET") == 0) {
for (int i = 0; i<=255; i++) saveState(i,0xFF);
send(msg.set(message));
}
// VERSION: send back the extension's version
else if (strcmp(message,"VERSION") == 0) {
send(msg.set(MY_EXT_VERSION,1));
}
}
// what to do when called within receive()
inline void MyExtension::receive(const MyMessage &message) {
// ignore messages not for this extension
if (message.sensor != MY_EXT_CHILD_ID) return;
#ifdef MY_EXT_DEBUG
Serial.print("Received message from=");
Serial.print(message.sender);
Serial.print(" command=");
Serial.print(message.getCommand());
Serial.print(" type=");
Serial.print(message.type);
Serial.print(" payload=");
Serial.println(message.getString());
#endif
// parse incoming req messages
if (message.getCommand() == C_REQ && message.type == V_CUSTOM) {
// process only V_CUSTOM messages
MyExtension::_process(message.getString());
}
}
// instantiate MyExtension
MyExtension extension;
//setup
void setup() {
// setup MyExtension
extension.setup();
}
// presentation
void presentation() {
// Send the sketch version information to the gateway and Controller
sendSketchInfo("Test Sketch", "1.0");
// present MyExtension
extension.presentation();
}
// loop
void loop() {
// loop for MyExtension
extension.loop();
}
// receive
void receive(const MyMessage &message) {
// receive for MyExtension
extension.receive(message);
}