MyExtension



  • 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);
    }
    

  • Hardware Contributor

    Most of my nodes run on battery so I can't really use this but it definitely sounds nice!



  • Hi @LastSamurai! When I put this together I had a battery powered project in mind, to actually facilitate the development of it and make interactions with sleeping sensors easier. What do you see here preventing this to work on nodes running on batteries? As said I'm both a bad programmer and new to mysensors so I wouldn't be surprised if there is something that I've missed :)

    Thanks!


  • Hardware Contributor

    Well the node has to be able to answer messages from the gateway if I understand it correctly. Battery powered sensors sleep most of the time so they aren't able to receive messages. Only way around this would be something like smartsleep (which I haven't used yet and needs controllers to support it).



  • Oh I see what you are saying. This was actually the idea, to wrap around smart sleep (which plays a central role in the extension) to make easier for a controller or a user to use it and interact with the sensor. A key requirement for my project is to be able to interact with a sensor without a physical access to it so in addition to smart sleep (which just gives the possibility for a controller to send something in a short time), I needed a way to force the sensor to stop sleeping, do something (e.g. configuration change) and then putting it to sleep once again remotely.
    But agree with you, in case you are not using smart sleep you will just get automatic battery level reports out of this.
    Thanks!


  • Hardware Contributor

    Ok then it should work as you intended (if smartsleep already works without errors). Currently I don't need to do stuff like this but when I do I will come back to your library :)



  • Nice work but please considering giving it a better name than "MyExtension" ;)


Log in to reply
 

Looks like your connection to MySensors Forum was lost, please wait while we try to reconnect.