Adventures in Moteino : Modular Communication for CuPID Remote (UniMote)

Introduction

So we got to the end of our last post, about getting a battery-powered Moteino into a tiny little waterproof box (see posts here and here), and we needed to do a demo. What this means for us is getting our data read from our remote into a CuPID Controller, where we can plot it and watch it from, you know, anywhere. We looked at our message formatting and were at that point where we could hack something together to make it work … or we could just start writing it the right way. Guess which we chose? We wanted the data coming through to seamlessly integrate into our IO structure. Asynchronously, we want our serial monitor running in the background, cataloguing remote messages as they come in from the RF unit. We then want our IO read and update scripts to come by, grab this data, and make it available as actionable IO.

More important, we want our Moteino programming to be modular. For most or all applications, we want the same code to run on the ATMega, so that we can just reconfigure IO, control units, and other functions by remove parameter changes — without reflashing the code if at all possible.

The Project

We set out with the following goals:

  • Create an ATMega sketch that allows read/write of all available Moteino IO in all allowable formats by change of variable values, i.e. no code change required
  • Allow OneWire read operations on all digital IO  (only for DS18B20s here)
  • Configure IO for read and report (broadcast) on remotely adjustable schedule
  • Configure channel control, with configurable setpoint and process values and positive and negative feedback
  • Set all key program parameters remotely (via radio) and locally (on serial)
  • Adjustable, metadata-containing, report format for IO, channels, and system parameters
  • Save system configuration to EEPROM and restore upon resume after loss of power

Phew. Is that all?

By the way, we’ll cover the CuPID Read, Database and Logging UI in another post. This thing just got too long.

Main()

So we’re going to do some reading, some writing, some RF sending/receiving. Let’s block this out to see what it looks like. We also want to determine how to handle variables. Some will be static, some will be stored in EEPROM, and we’ll need to be able to reprogram them all remotely. Again, we want to be able to only reflash a program via the bootloader if we are upgrading firmware, not just changing the read and/or control parameters.

 Program Structure
Initialize {
  if (INIT) {
    Initialize variables to init values
  }
  else {
    Read values from EEPROM
  }
  Initialize unstored values
}
Loop {
  Read values
  Report values

  Run Channels algorithms
  Report Channels

  wait for sleep command or timeout
  if (sleep) {
    power down for LOOPPERIOD
  }
  else {
    delay for LOOPPERIOD
  }
}

That should about do it. We’ll revisit this later.

Storing everything

The one caveat about user-programmable variables is that you need to find a way to retain them. Hardcoding control parameters and values is something that is part and parcel of most avr coding. If we set them in real-time, however, we have to be able to retrieve them as-modified.

Variables

We want to store the really important stuff in non-volatile memory, should we momentarily lose power. For control applications, the important stuff needs to stay put. For this reason, we’ll put in in EEPROM. We only have 512 bytes to work with, so we’ll be cheap with it. Many of the below values, because they are stored as bytes, will be cast into a format that makes most sense. For example, a setpoint value will have a built-in range limit of 0-255, and float setpoints will be cast as byte integers. Unsigned long values such as read frequencies, which are natively unsigned longs with a range of 0 to 4,294,967,295, are cast into seconds and capped with a maximum value of 255. This reduces the total range from 0 to 256,000 with increments of 1000. You get the picture. These values are defined further in the sections below.

Item bytes EEPROM location
NodeID 1 0
NetworkID 1 1
GatewayID 1 2
loopperiod 1 3
sleepmode 1 4
sleepdelay 1 5
serialrfecho 1 6
Encryption Key 16 10-25
iomode 13 30-42
ioenabled 13 43-55
ioreadfreq 13 56-68
ioreportenabled 13 69-81
chanenabled 8 82-89
chanmode 8 90-97
chanposfdbk 8 98-105
channegfdbk 8 106-113
chandeadband 8 114-121
chanpvindex 8 122-129
chansv 8 130-137

Other variables will be initialized as follows

  • ioreportenabled[13] — all set to values for ioreadenabled
  • ioreportfreq[13] — all set to 0, meaning report whenever read
  • chanstate[8] — initialized to zero (no action)
  • chanpv[8] — initialized to zero
  • ioreporttimer[13], ioreadtimer[13], chanreporttimer[8] — all zero
The Mechanics

Alright, so how do we actually do this? It’s pretty simple. With an include of EEPROM.h, we just drop in chunks of code like this:

For storage:

void storeparams() {
  EEPROM.write(0,NODEID);
  EEPROM.write(1,NETWORKID);
  EEPROM.write(2,GATEWAYID);
  
   // update object
//    radio.writeReg(REG_SYNCVALUE2, NETWORKID);
//    radio.setAddress(NODEID);
    
  // maximum initialized loop period is 256
  // in 10ms increments, this is 2.5s
  if (LOOPPERIOD/10 > 256){
    EEPROM.write(3,256);
  }
  else {
    EEPROM.write(3,LOOPPERIOD/10);
  }
  
  EEPROM.write(4,SLEEPMODE);
  EEPROM.write(5,SLEEPDELAY);
  EEPROM.write(6,serialrfecho);
  
  int i;
  for (i=0;i<16;i++) {
//      EEPROM.write(i+10,ENCRYPTKEY[i]);
  }
  byte mybyte;
  for (i=0;i<13;i++){    
    EEPROM.write(i+30,iomode[i]);
    EEPROM.write(i+43,ioenabled[i]);
    EEPROM.write(i+69,ioreportenabled[i]);
    if (ioreadfreq[i]/1000 > 256){
      EEPROM.write(i+56,256);
    }
    else {
      mybyte = ioreadfreq[i] / 1000;
      EEPROM.write(i+56,mybyte);
    }
  } // for num io
  
  // channels data
  for (i=0;i<8;i++) {
    EEPROM.write(i+82,chanenabled[i]);
    EEPROM.write(i+90,chanmode[i]);
    EEPROM.write(i+98,chanposfdbk[i]);
    EEPROM.write(i+106,channegfdbk[i]);
    EEPROM.write(i+114,chandeadband[i]);
    EEPROM.write(i+122,chanpvindex[i]);
    EEPROM.write(i+130,chansv[i]);
  } // for channels
}

Likewise for retrieval:

void getparams() {
    Serial.println(F("Getting parameters"));
    NODEID = EEPROM.read(0);
    NETWORKID = EEPROM.read(1);
    GATEWAYID = EEPROM.read(2);    
    
    // update object
    // We DON'T do this because it may not be instantiated yet.
    
//    Serial.println(REG_SYNCVALUE2);
//    radio.writeReg(REG_SYNCVALUE2, NETWORKID);
//    radio.setAddress(NODEID);
    
    LOOPPERIOD = EEPROM.read(3) * 10;
    SLEEPMODE = EEPROM.read(4);
    SLEEPDELAY = EEPROM.read(5);
    serialrfecho = EEPROM.read(6);
 
    // io
    int i;
    for (i=0;i<13;i++){
      iomode[i] = EEPROM.read(i+30);
      ioenabled[i] = EEPROM.read(i+43);
      ioreadfreq[i] = EEPROM.read(i+56)*1000;  
      ioreportenabled[i] = EEPROM.read(i+69);    
    } // for io 
    
    //channels
    for (i=0;i<8;i++) {
      chanenabled[i] = EEPROM.read(i+82);
      chanmode[i] = EEPROM.read(i+90);
      chanposfdbk[i] = EEPROM.read(i+98);
      channegfdbk[i] = EEPROM.read(i+106);
      chandeadband[i] = EEPROM.read(i+114);
      chanpvindex[i] = EEPROM.read(i+122);
      chansv[i] = EEPROM.read(i+130);
    } // for channels
}

Piece of cake!

Moteino Modular

Taking a quick look at the pinout on the R4, which we're using here, gives us the following list of available pins and functions:

Label Pin Name Function Available
0 PD0 D0 RX N
1 PD1 D1 TX N
2 PC0 D2 DI/DO N
3 PD3 D3 INT1 Y
4 PD4 D4 DI/DO Y
5 PD5 D5 DI/DO/PWM Y
6 PD6 D6 DI/DO/PWM Y
7 PD7 D7 DI/DO Y
8 PB0 D8 FLASH N
9 PB1 D9 LED N
10 PB2 D10 XFM N
11 PB3 D11 XFM N
12 PB4 D12 XFM N
13 PB5 D13 XFM N
A0 PC0 D14/A0 DI/DO/AIN Y
A1 PC1 D15/A1 DI/DO/AIN Y
A2 PC2 D16/A2 DI/DO/AIN Y
A3 PC3 D17/A3 DI/DO/AIN Y
A4 PC4 D18/A4 DI/DO/AIN/SDA Y
A5 PC5 D19/A5 DI/DO/AIN/SCL Y
A6 ADC6 A6 AIN Y
A7 ADC7 A7 AIN Y

We'll not implement I2C for now, and simply assume the following possible modes for pins above that are available, leaving us with 13 available IO:

UniMote Label Pin Name Function Available
0 3 PD3 D3 INT1 Y
1 4 PD4 D4 DI/DO Y
2 5 PD5 D5 DI/DO/PWM Y
3 6 PD6 D6 DI/DO/PWM Y
4 8 PD7 D7 DI/DO Y
5 A0 PC0 D14/A0 DI/DO/AIN Y
6 A1 PC1 D15/A1 DI/DO/AIN Y
7 A2 PC2 D16/A2 DI/DO/AIN Y
8 A3 PC3 D17/A3 DI/DO/AIN Y
9 A4 PC4 D18/A4 DI/DO/AIN/SDA Y
10 A5 PC5 D19/A5 DI/DO/AIN/SCL Y
11 A6 ADC6 A6 AIN Y
12 A7 ADC7 A7 AIN Y

That's pretty manageable. We've got 13 possible IO, with four possible modes. We can use the digital pins as 1Wire buses, so we'll call that a fifth mode, and define them:

Mode Function
0 Digital Input (DI)
1 Digital Output (DO)
2 Analog Input (AIN)
3 Pulse Width Modulation (PWM)
4 1Wire Master (OW)

Next for each IO item, we want to set a number of variables:

  • ioenabled : whether to process the pin
  • iomode : which of the above modes is set
  • ioreadfreq : how often to read the pin value into local memory
  • ioreportenabled : whether or not to report the pin value over RF
  • ioreportfreq : how often to broadcast the pin value stored in local memory over RF and serial. If this value is zero, the IO value will be reported each time it is read

We'll store the pin values as recorded into an array aptly named iovalue[13], which we'll write to when reading pins, and read from when writing pins (for digital outputs).

Globally, we'll have a number of variables that control the entire process structure, which we've noted above in the EEPROM storage:

  • GATEWAYID
  • NODEID
  • NETWORKID
  • ENCRYPTKEY[16]
  • SLEEPMODE
  • SLEEPDELAY
  • LOOPPERIOD

 Mote, let's talk

Alright, so we've really laid the foundation here, but to debug (and to, you know, send messages on Serial and RF), we'll need a message passing interface. Originally built into the gateway and mote sketches is a nice foundation for reading serial input and responding to single character commands, e.g. the character 'r' dumps all register values, 'd' flash values, etc. We decided to leave these commands in place, should anyone decide they need them. We structure our message as follows:

~command;arg1;arg2;arg3

It's pretty simple. A tilda (~) tells our Mote that we are issuing a command, and we follow the command character with a command and a series of optional arguments. The key chunk of parsing code is as follows:

void processcmdstring(String cmdstring){
  Serial.println(F("processing cmdstring"));
  Serial.println(cmdstring);
  
  int i;
  String str1="";
  String str2="";
  String str3="";
  String str4="";
  if (cmdstring[0] == '~'){
    Serial.println(F("Command character received"));
    int strord=1;
    for (i=1;i<cmdstring.length();i++){
      if (cmdstring[i] != ';' || strord == 4){
        if (cmdstring[i] != '\n' && cmdstring[i] != '\0' && cmdstring[i] != '\r'){
          if (strord == 1){
            str1+=cmdstring[i];
          }
          else if (strord == 2){
            str2+=cmdstring[i];
          }
          else if (strord == 3){
            str3+=cmdstring[i];
          }
          else if (strord == 4){
            str4+=cmdstring[i];
          }
          else {
            Serial.println(F("Error in argument string parse"));
          }
        }
      } // cmdstring is not ;
      else { // cmdstring is ;
        strord ++;
      }  //cmdstring is ;
    } // for each character  
     
    if (str1 == "listparams") {
      listparams(0,0);
    }
    else if (str1 == "rlistparams") {
      listparams(1,str2.toInt());
    }
    else if (str1 == "reset") {
      resetMote();
    }
    else if (str1 == "modparam") {
      if (str2 == "loopperiod") {
        long newvalue = str3.toInt()*1000;
        if (newvalue >= 0 && newvalue < 600000){
          Serial.print(F("Modifying loopperiod to "));
          Serial.println(newvalue);
          // deliver in seconds, translate to ms
          LOOPPERIOD = newvalue;
          storeparams();
        }
        else {
          Serial.println("Value out of range");
        }
      } // loopperiod
      if (str2 == "nodeid") {
        long newvalue = str3.toInt();
        if (newvalue >= 2 && newvalue < 256){
          Serial.print(F("Modifying nodeid to "));
          Serial.println(newvalue);
          // deliver in seconds, translate to ms
          NODEID = newvalue;
          radio.setAddress(NODEID);
          storeparams();
        }
        else {
          Serial.println("Value out of range");
        }
      } // nodeid
      if (str2 == "networkid") {
        long newvalue = str3.toInt();
        if (newvalue >= 1 && newvalue < 256){
          Serial.print(F("Modifying networkid to "));
          Serial.println(newvalue);
          // deliver in ~seconds, translate to ms
          NETWORKID = newvalue;
          radio.writeReg(REG_SYNCVALUE2, NETWORKID);
          storeparams();
        }
        else {
          Serial.println("Value out of range");
        }
      } // network
      if (str2 == "gatewayid") {
        long newvalue = str3.toInt();
        if (newvalue >= 1 && newvalue < 256){
          Serial.print(F("Modifying gatewayid to "));
          Serial.println(newvalue);
          // deliver in seconds, translate to ms
          GATEWAYID = newvalue;
          storeparams();
        }
        else {
          Serial.println("Value out of range");
        }
      } // network
      else if (str2 == "iomode") {
        int ionumber = str3.toInt();
        if (ionumber >=0 && ionumber <14) {
          Serial.print(F("Modifying iomode: "));
          Serial.println(ionumber);
          int newvalue = str4.toInt();
          if (newvalue >= 0 && newvalue <5) {
            Serial.print(F("Changing iomode to: "));
            Serial.println(newvalue);
            iomode[ionumber]=newvalue;
            ioreadtimer[ionumber] = 9999999; // read now
            storeparams();
          }
          else {
            Serial.print(F("Value out of range: "));
            Serial.println(newvalue);
          }
        }
        else {
          Serial.println(F("IO number out of range"));
        }
      } // iomode
      else if (str2 == "ioenabled") {
        int ionumber = str3.toInt();
        if (ionumber >=0 && ionumber <14) {
          Serial.print(F("Modifying ioenabled: "));
          Serial.println(ionumber);
          int newvalue = str4.toInt();
          if (newvalue == 0) {
            Serial.println(F("Disabling io."));
            ioenabled[ionumber] = 0;
            ioreadtimer[ionumber] = 9999999; // read now
            storeparams();
          }
          else if (newvalue == 1) {
            Serial.println(F("Enabling io."));
            ioenabled[ionumber] = 1;
            ioreadtimer[ionumber] = 9999999; // read now
            storeparams();
          }
          else {
            Serial.print(F("Value out of range: "));
            Serial.println(newvalue);
          }
        }
        else {
          Serial.println(F("IO number out of range"));
        }
      } // ioenabled
      else if (str2 == "ioreadfreq") {
        int ionumber = str3.toInt();
        if (ionumber >=0 && ionumber <14) {
          Serial.print(F("Modifying io: "));
          Serial.println(ionumber);
          long newvalue = str4.toInt()*1000;
          if (newvalue >= 0 && newvalue <600000) {
            Serial.print(F("Changing io readfreq to"));
            Serial.println(newvalue);
            ioreadfreq[ionumber]=newvalue;
            storeparams();
          }
          Serial.print(F("Value out of range: "));
            Serial.println(newvalue);
        }
        else {
          Serial.println(F("IO number out of range"));
        }
      } // ioreadfreq
      else if (str2 == "ioreportenabled") {
        int ionumber = str3.toInt();
        if (ionumber >=0 && ionumber <14) {
          Serial.print(F("Modifying io: "));
          Serial.println(ionumber);
          int newvalue = str4.toInt();
          if (newvalue = 0) {
            Serial.print(F("Disabling ioreporting."));
            ioreportenabled[ionumber] = 0;
            storeparams();
          }
          else if (newvalue == 0) {
            Serial.print(F("Enabling ioreporting."));
            ioreportenabled[ionumber] = 1;
            storeparams();
          }
          else {
            Serial.println(F("Value out of range"));
          }
        }
        else {
          Serial.println(F("IO number out of range"));
        }
      } // ioreport enabled
      else if (str2 == "ioreportfreq") {
        int ionumber = str3.toInt();
        if (ionumber >=0 && ionumber <14) {
          Serial.print(F("Modifying io: "));
          Serial.println(ionumber);
          long newvalue = str4.toInt()*1000;
          if (newvalue >= 0 && newvalue <600000) {
            Serial.print(F("Changing io reportfreq to: "));
            Serial.println(newvalue);
            ioreportfreq[ionumber]=newvalue;
            storeparams();
          }
          else {
            Serial.println(F("Value out of range"));
          }
        }
        else {
          Serial.println(F("IO number out of range"));
        }
      } // ioreportfreq
      else if (str2 == "iovalue") {
        int ionumber = str3.toInt();
        if (ionumber >=0 && ionumber <14) {
          Serial.print(F("Modifying iovalue: "));
          Serial.println(ionumber);
          
          int newvalue = str4.toInt();
          if (newvalue >= 0) {
            Serial.print(F("Changing io value to: "));
            Serial.println(newvalue);
            iovalue[ionumber]=newvalue;
            storeparams();
          }
          else {
            Serial.print(F("Value out of range: "));
            Serial.println(newvalue);
          }
        }
        else {
          Serial.println(F("IO number out of range"));
        }
      } // iovalues
      else if (str2 == "chansv") {
        int channumber = str3.toInt();
        if (channumber >=0 && channumber <=8) {
          Serial.print(F("Modifying channel: "));
          Serial.println(channumber);
          // need to allow for floats
          int newvalue = str4.toInt();      
          Serial.print(F("Changing sv to"));
          Serial.println(newvalue);
          chansv[channumber]=newvalue;
          storeparams();
        }
        else {
          Serial.println(F("chan number out of range"));
        }
      }// chansv
      else if (str2 == "chanenabled") {
        int channumber = str3.toInt();
        if (channumber >=0 && channumber <=8) {
          Serial.print(F("Modifying channel: "));
          Serial.println(channumber);
          int newvalue = str4.toInt();
          if (newvalue == 0) {
            Serial.print(F("Disabling channel."));
            chanenabled[channumber] = 0;
            storeparams();
          }
          else if (newvalue == 1) {
            Serial.print(F("Enabling channel."));
            chanenabled[channumber] = 1;
            storeparams();
          }
          else {
            Serial.println(F("Value out of range"));
          }
        }
        else {
          Serial.println(F("chan number out of range"));
        }
      } // chanenabled
    } // modparams
    else if (str1 =="sendmsg"){
      Serial.println(F("sending message: "));
      Serial.print(F("to dest id: "));
      Serial.println(str2.toInt());
      Serial.print(F("Reserved parameter is: "));
      Serial.println(str3);
      Serial.print(F("Message is: "));
      Serial.println(str4);
      Serial.print(F("Length: "));
      Serial.println(str4.length());
      str4.toCharArray(buff,str4.length()+1);
      radio.sendWithRetry(str2.toInt(), buff, str4.length()+1);
    }
    else if (str1 == "flashid"){
      Serial.println(F("Flash ID: "));
      for (i=0;i<8;i++){
        Serial.print(flash.UNIQUEID[i],HEX);
      }
    }
    else{
      Serial.println(F("unrecognized command"));
      Serial.println(str1);
      Serial.print(F("of length: "));
      Serial.println(str1.length());
      for (i=0;i<str1.length();i++){
        Serial.println(str1[i]);
      }
    }
  } // first character indicates command sequence 
  else { // first character is not command
    if (cmdstring[0] == 'r') //d=dump register values
      radio.readAllRegs();
      //if (input == 'E') //E=enable encryption
      //  radio.encrypt(KEY);
      //if (input == 'e') //e=disable encryption
      //  radio.encrypt(null);

    if (cmdstring[0] == 'd') //d=dump flash area
    {
      Serial.println("Flash content:");
      uint16_t counter = 0;

      Serial.print("0-256: ");
      while(counter<=256){
        Serial.print(flash.readByte(counter++), HEX);
        Serial.print('.');
      }
      while(flash.busy());
      Serial.println();
    }
    if (cmdstring[0] == 'e')
    {
      Serial.print(F("Erasing Flash chip ... "));
      flash.chipErase();
      while(flash.busy());
      Serial.println(F("DONE"));
    }
    if (cmdstring[0] == 'i')
    {
      Serial.print(F("DeviceID: "));
      word jedecid = flash.readDeviceId();
      Serial.println(jedecid, HEX);
    }
  }
}

Simple, unoptimized, and verbose, but functional. A nice feature of the subroutine is that we can run it on both serial arguments and also commands we receive on RF. So, for example, if we run the command ~listparams, we get the following:

~listparams
Command character received
NODEID:0,
GATEWAYID:0,
NETWORKID:0,
LOOPPERIOD:2000,
SLEEPMODE:0,
iomode:[0,0,0,0,0,3,0,3,0,0,0,0,0],
ioenabled:[0,0,0,0,0,0,0,0,0,0,0,0,0],
ioreadfreq:[10000,10000,10000,10000,10000,10000,10000,10000,10000,10000,10000,10000,10000],
ioreportenabled:[0,0,0,0,0,0,0,0,0,0,0,0,0],
ioreportfreq:[0,0,0,0,0,0,0,0,0,0,0,0,0],
chanenabled:[0,0,0,0,0,0,0,0],
chanmode:[0,0,0,0,0,0,0,0],
chanposfdbk:[0,0,0,0,0,0,0,0],
channegfdbk:[-1,0,0,0,0,0,0,0],
chandeadband:[0,0,0,0,0,0,0,0],
chanpvindex:[5,0,0,0,0,0,0,0],
chansv:[15.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00]

That will work just fine. We'd get the same thing if we broadcast a command string over RF (we'll demo this later).
We've got a basic set of commands to manage internal parameters. We'll detail a few here below.

Command Set

~listparams
~rlistparams;destinationid

This command is as detailed above. It list all key mote parameters in a JSON-like format. ~rlistparams is the same, except it sends to a specified node id. Typically, this will be the gateway requesting station identification. We've not fleshed out all of the message formatting for this yet, as we'll need to break it up in chunks small enough to fit within the 61 character limit, but those are just details.

~modparam;paramname;index;value

This is the workhorse command. It will allow us to set all of our internal parameters, from Serial or radio. It's really as simple as it looks. For example, observe enabling and setting our input to digital output mode:

~modparam;ioenabled;1;1
Command character received
Modifying ioenabled: 1
Enabling io.
Time to check data for pin 4
Digital input configured for pin 4
SENDING
iopin:04,iomode:01,iovalue:0
SEND COMPLETE

We'll break out some examples later on on reading and processing IO.

~sendmsg;destinationid;reserved;message

This is a pretty basic send message command. We've reserved the third argument, both because we'll need it, and also for the following reason: because the final argument is not parsed by semicolon delimiter, you can send a full command as the final argument, as long as it fits in the 61-character message size. Here are a couple examples:

Simple message send, on the mote:

~sendmsg;1;0;hello there! 
Command character received
sending message: 
to dest id: 1
Reserved parameter is: 0
Message is: hello there! 
Length: 13

And on the gateway:

BEGIN RECEIVED
nodeid:4, hello there!,RX_RSSI:-28
END RECEIVED
 - ACK sent.

Note, importantly, that if we send data, our gateway will simply insert it into its parsed message, which it will eventually pass on to python, which will turn it into a dictionary. From the below, it's pretty obvious how our gateway will perform as an arbitrator: it takes the data, tacks on metadata, and passes it on.

Command on node:

~sendmsg;1;0;d1:val1,d2:val2

On the gateway:

BEGIN RECEIVED
nodeid:2, d1:val1,d2:val2,RX_RSSI:-28
END RECEIVED

Ok, now remember the part where we can send a command as the last part of a sendmsg command? Let's send a command by RF from the gateway to the node. Let's start with something simple. Let's tell it to list parameters on its serial output:

~sendmsg;2;0;~listparams

Sure enough, we see the list of output on the node:

BEGIN RECEIVED
nodeid:1, ~listparams ,RX_RSSI:-32
END RECEIVED
processing cmdstring
~listparams
Command character received
NODEID:2,
GATEWAYID:1,
NETWORKID:100,
LOOPPERIOD:1000,
SLEEPMODE:0,
...

Pretty simple. Now let's send it a message telling it to send us a message back!

On the gateway:

~sendmsg;2;0;~sendmsg;1;0;ping!
Command character received
sending message: 
to dest id: 2
Reserved parameter is: 0
Message is: ~sendmsg;1;0;ping!
Length: 18

On the mote:

BEGIN RECEIVED
nodeid:1, ~sendmsg;1;0;ping!,RX_RSSI:-32
END RECEIVED
processing cmdstring
~sendmsg;1;0;ping!
Command character received
sending message:
to dest id: 1
Reserved parameter is: 0
Message is: ping!
Length: 5

And again on the gateway:

BEGIN RECEIVED
nodeid:2, ping!
END RECEIVED

Pretty cool! If we didn't have that pesky 61 character message length limit, we could bounce messages back and forth indefinitely!

A Quick Example: The Power Supply Monitor

Just to demonstrate how easy this is to set up, we're going to add on a feature we've been meaning to put into our motes, and one that's been the topic of some discussion: how to monitor your power supply voltage. For our motes, this is especially important, as they're running off of battery power. For the example, we'll simulate the battery with a power supply at about 9V.  We went the KISS route and threw down 1M and 470k resistors attached to power and ground, respectively, and to each other, of course. We hooked up our divider to A6 on our Mote, and let's get to enabling read and report:

Change io to analog input:

~modparam;iomode;11;2
Command character received
Modifying iomode: 11
Changing iomode to: 2

Enable reporting:

~modparam;ioreportenabled;11;1
Command character received
Modifying io: 11
Enabling ioreporting.

And finally, enabling the IO:

~modparam;ioenabled;11;1
Command character received
Modifying ioenabled: 11
Enabling io.
Time to check data for pin 20, io number 11
Analog input configured for pin 20
Value: 950.00
SENDING TO 1
iopin:20,iomode:02,iovalue:0941
SEND COMPLETE

We've got about 9.3V on our voltage divider, and we're measuring about 2.9V, which upon our Vcc of ~3.3V into 1024 parts gives us about 900, so we're in the ballpark. Remember, this ADC reference is relative, and we've got no caps in place. All things considered, not too bad. Over on our gateway, we're reading loud and clear:

BEGIN RECEIVED
nodeid:2, iopin:20,iomode:02,iovalue:0949,RX_RSSI:-28
END RECEIVED

Zee Channels

Now, we've already built a pretty flexible IO node. But one of the beautiful elements of a super-reliable remote node is that you can program it to control something simply and without connectivity. If you lose power or have to do those operating system things like reboot or update the system, mission critical processes don't spill, light on fire, or ruin your beer.

For this purpose, we built in eight channels with simple on/off control. The variables used for these channels are below, and their EEPROM assignments were listed previously.

  • chanenabled : whether the channel is enabled
  • chanpvindex : which indexed io is the process value
  • chansv : what the current setpoint value is for the channel
  • chanpv : what the current process value is for the channel (this is redundant and included for error-correction)
  • chanposfdbk : the io used for positive feedback (-1 = none)
  • channegfdbk : the io used for negative feedback (-1 = none)
  • chanmode : reserved for future use for different algorithms
  • chandeadband : deadback for sv-pv before action is taken
  • chanstate : status of the channel's control, e.g. -1 for negative feedback, +1 for positive feedback

We think that, with the above, the algorithm is straightforward and not worth discussing here. Instead, we'll get straight into an example. We'll configure the following:

  • D14/A0, UniMote 5 - 1Wire DS18B20 temperature sensor
  • D4, UniMote 2 - Red LED, Positive Feedback
  • D5, UniMote 3 - Blue LED, Negative Feedback

This mimics  a pretty common control scenario: controlling temperature with positive and negative feedback mechanisms. Even more control systems are comprised of a single feedback. Hot water heaters, brew kettles, glycol fermenter jackets, boilers, and so many more.

So let's set up a channel. 1Wire master is mode 4:

~modparam;iomode;5;4
Command character received
Modifying iomode: 5
Changing iomode to: 4

Get it reading:

~modparam;ioenabled;5;1
Command character received
Modifying ioenabled: 5
Enabling io.
Time to check data for pin 14, io number 5
1Wire configured for pin 14
dsaddress:2811FDE203000092,
temperature:24.31

Set our channel inputs and outputs:

~modparam;chanpvindex;0;5
Command character received
Modifying channel: 0
Changing pvindex to 5
~modparam;chanposfdbk;0;1
Command character received
Modifying channel: 0
Changing posfdbk to 2

~modparam;channegfdbk;0;2
Command character received
Modifying channel: 0
Changing posfdbk to3

Set the outputs to the correct modes (this could easily be automated, but you should be smart about it anyway):

~modparam;iomode;2;1
Command character received
Modifying iomode: 2
Changing iomode to: 1

~modparam;iomode;3;1
Command character received
Modifying iomode: 3
Changing iomode to: 1

Finally, let's enable the channel, and then enable the outputs:

~modparam;chanenabled;0;1
 Command character received
 Modifying channel: 0
 Enabling channel.
 Channel 0 value: 
 24.19
 Setpoint value: 
 15.00
 Setting negative action
 Neg feedback: 2

Looks good. Enabling outputs:

~modparam;ioenabled;1;1
Command character received
Modifying ioenabled: 1
Enabling io.
~modparam;ioenabled;2;1 
Command character received
Modifying ioenabled: 2
Enabling io.

Neat. We've got negative feedback on, indicated by our red LED, trying to reach the setpoint value of 15.00, with a process value of 24.19. Let's get that up to something above its current value so we can watch the action:

~modparam;chansv;0;28
Command character received
Modifying channel: 0
Changing sv to 28

Now let's play with it to ensure it works. Sure enough, if we grab the temperature sensor until the temperature exceeds 28, the control switches to negative feedback, and our blue LED comes on. Take the heat away, and it returns to positive feedback with the red LED lit. Success!

 

Sleep, but listen first!

Alright, so we want to conserve power for our remote nodes. It's not as if the 'duino is power-hungry, but when you are running on battery, everything counts. Fortunately, there are many libraries and loads of examples on how to do this. The tricky part, however, has to do with our communications sequence. The key part is that all communications are initiated by the remote node. The reason for this is, in fact, that the motes will sleep for periods of time, pop up, report, and go back to sleep. If communication were one-way (only from mote to gateway), this is not an issue. If we want to reprogram our node's parameters, such as report frequency, whether channels are enabled, how long it sleeps for, etc., we'll need to account for that in the remote's 'wake sequence'. To accomplish this, we'll build in a short period after the node processes its IO and channels, but before it sleeps, during which the gateway can issue commands. Alternatively, if the gateway has no commands, it can just tell the node to go to sleep. Revisiting our controls structure from above:

loop {
  read IO
  read channels

  sleepdelaytimer = 0
  while (sleepdelay <= sleepdelaytimer) {
    listen to radio
    if (message received) {
      process message
      // reset wait time
      sleepdelaytimer = 0 
    }
  }
  sleep (LOOPPERIOD)
}

We'll add in a command called 'gosleep', which will max out the sleepdelaytimer and send our mote to sleep. Now, recognizing that this will slow down the loop, we will only use the SLEEPDELAY if SLEEPMODE is enabled. This means that if SLEEPMODE is not enabled, the LOOPPERIOD will determine how often IO and channels are processed. If SLEEPMODE is enabled, however, it means that the mote will wait for SLEEPDELAY to go to sleep, and then sleep for LOOPPERIOD. This means that the device IO poll, when in SLEEPMODE, will actually only take place every SLEEPDELAY + LOOPPERIOD. Just something to be aware of. The other option would be to subtract SLEEPDELAY from LOOPPERIOD and only sleep for that long, but we prefer for code simplicity and semantics to leave it as is. We just have to remember this fact and that when SLEEPMODE is enabled LOOPPERIOD can be interpreted as the sleep period.

Here are the vital bits of the code:

// If we're in SLEEPMODE, we wait for a set period to receive packets on serial and radio
   
int millistart;
SLEEPDELAYTIMER = 0;
if (SLEEPMODE) {
  millistart = millis();
  Serial.println(F("Entering receive sequence"));
}
// failsafe. must listen.
if (SLEEPDELAY < 1000){
  SLEEPDELAY = 1000;
}
  
while (SLEEPDELAYTIMER < SLEEPDELAY){
    
  // SERIAL RECEIVE AND PROCESSING
  // Check for any received packets

  << Serial processing code >>

  // RADIO RECEIVE AND PROCESSING
  // Check for any received packets

  << Radio processing code >>

  if (SLEEPMODE) {
    if (SLEEPDELAYTIMER < 65535){
      // if timer is at 65535, means we are intentionally exiting
      SLEEPDELAYTIMER = millis() - millistart;
    }
  }
  else { // exit loop after one iteration if not in sleep mode
    SLEEPDELAYTIMER = 65535;
  } 
} // SLEEPDELAY while
if (SLEEPMODE){
 Serial.println(F("Exiting receive sequence"));
} 

 Sleep - the mechanics

As we mentioned previously, there are a number of libraries that will help put our Mote to sleep. We like the LowPower library, a favorite of the Moteino community. Also built into the RFM69 library is a simple radio.sleep() function that will put our RF to rest. A typical call to the LowPower powerDown function is shown below:

LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);

The LowPower library has a number of fixed length sleep intervals, ranging from 15ms to 8S, in multiple increments. We can choose one and loop as many times as we like. We put in a simple algorithm that tracks how much longer is remaining to sleep, grabs the next time increment down in size, and sleeps for that long. This technique will optimize our up vs. downtime at a fixed sleep period:

// Do our sleep or delay
if (SLEEPMODE) {
  Serial.println(F("Going to sleep for "));
  Serial.println(LOOPPERIOD);
  
  period_t sleepperiod;
  unsigned int sleepremaining = LOOPPERIOD;
  
  while (sleepremaining > 0) {
    if (sleepremaining > 8000) {
      // Use 8s interval
      sleepperiod = SLEEP_8S;
      sleepremaining -= 8000;
    }
    else if (sleepremaining >= 4000) {
      // Use 4s interval
      sleepremaining-=4000;
      sleepperiod = SLEEP_4S;
    }
    else if (sleepremaining >= 2000) {
      // Use 2s interval
      sleepremaining-=2000;
      sleepperiod = SLEEP_2S;
    }
    else if (sleepremaining >= 1000) {
      // Use 1s interval
     sleepremaining-=1000;
      sleepperiod = SLEEP_1S;
    }
    else if (sleepremaining >= 500) {
      // Use 500ms interval
      sleepremaining-=500;
      sleepperiod = SLEEP_500MS;
    }
    else if (sleepremaining >= 250) {
      // Use 250ms interval
     sleepremaining-=250;
      sleepperiod = SLEEP_250MS;
    }
    else if (sleepremaining >= 120) {
      // Use 120ms interval
      sleepremaining-=120;
      sleepperiod = SLEEP_120MS;
    }
    else if (sleepremaining >= 60) {
      // Use 60ms interval
      sleepremaining-=60;
      sleepperiod = SLEEP_60MS;
    }
    else if (sleepremaining >= 30) {
      // Use 30ms interval
      sleepremaining-=30;
      sleepperiod = SLEEP_30MS;
    }
    else {
      // Use 15ms interval
      sleepperiod = SLEEP_15Ms;
      sleepremaining = 0;
    }
    Serial.flush();
    radio.sleep();
    LowPower.powerDown(sleepperiod, ADC_OFF, BOD_OFF);
    Serial.print(F("Sleep remaining: "));
    Serial.println(sleepremaining);
  }
  // Sleeptime eludes millis()
  looptime += LOOPPERIOD;
} // end if

The important bit is at the end here:

    Serial.flush();
    radio.sleep();

    LowPower.powerDown(sleepperiod, ADC_OFF, BOD_OFF); 
    // Sleeptime eludes millis()
    looptime += LOOPPERIOD;

The serial flush function fixes some serial garble issues with power down (see discussion here). Also note the last bit where we add in the LOOPPERIOD. As it turns out, millis() stops when we power down the Mote. As a result, we need to manually add the time in to the looptime. We use this in the IO and channels processing, so we want to keep (relatively) accurate time. After these adds, we get the following for a sleep time of 3000:

Going to sleep for 
3000
Sleep remaining: 1000
Sleep remaining: 0

From our logic above, our delay is set to 3000, which will select a delay of 2000ms, then 1000ms after that is complete. Perfect.

Alright, we still have a ton of code cleanup to do, but the bones are here. Next, we'll get all this data into our gateway controller and work on some visualization and presentation. Eventually, we'll set up some metadata tables so we can control the Mote parameters from our UI!

Reference

The above makes use of the open source libraries available on github here. Arduino sketches are in iicontrollibs/mote

Explanation and installation here:

 

14 thoughts on “Adventures in Moteino : Modular Communication for CuPID Remote (UniMote)”

  1. Hi Colin,

    I’m liking your modular approach, I’ve been struggling with how to standardise messages to and from moteinos..

    Using uni_mote_2p3.ino and for any iomode other than 2 (analog), I’m finding my moteino power cycles after sending the value.

    In this serial output below, pin 20 as analog is ok, but when i make pin 3 digital in, it power cycles.

    Same happens for other pins, set to digital in, or 1-wire..

    Have tried other moteinos (with no other hardware connected). and swapped ftdi cables..

    Any thoughts would be great! thanks
    dags


    Time to check data for pin 20, io number 11
    Analog input configured for pin 20
    Value: SENDING TO 1
    iopin:20,iomode:02,ioval:128.0000
    SEND COMPLETE

    ~modparam;iomode;0;0
    processing cmdstring
    ~modparam;iomode;0;0
    Command character received
    iomode
    Modifying iomode: 0
    Changing iomode to: 0

    Time to check data for pin 20, io number 11
    Analog input configured for pin 20
    Value: SENDING TO 1
    iopin:20,iomode:02,ioval:122.0000
    SEND COMPLETE

    BEGIN RECEIVED
    nodeid:1, ACK TEST,RX_RSSI:-29
    END RECEIVED
    Free memory: 571
    processing cmdstring
    ACK TEST
    - ACK sent

    ~modparam;ioreportenabled;0;1
    Free memory: 529
    processing cmdstring
    ~modparam;ioreportenabled;0;1
    Command character received
    ioreportenabled
    Modifying io: 0
    Enabling ioreporting.

    ~modparam;ioenabled;0;1
    Free memory: 541
    processing cmdstring
    ~modparam;ioenabled;0;1
    Command character received
    ioenabled
    Modifying ioenabled: 0
    Enabling io.

    Time to check data for pin 3, io number 0
    Digital input configured for pin 3
    SENDING TO 1
    iopin:03,iomode:00,iovalue:0000
    SEND COMPLETE

    SPI Flash Init FAIL! (is chip present?)

    Transmitting at 433 Mhz...
    I am node id: 8
    Free memory: 704

    1. Hi Dargs,

      I believe what you are seeing is a memory issue. In deployment, I do a lot of commenting, and have also made sure all print strings are done with the print(F(“string literal”)) command. I’ll get a new version uploaded, also containing the 1Wire optimization reads discussed in another post.

      I’ll also be test-driving this code shortly in the Moteino Mega, so if memory is indeed the issue, the behavior you have observed should go away entirely.

      Cheers,
      Colin

  2. Hi Colin,
    Great work, i like your universal approach. I found two little bugs. First an index error on the call of handleOWIO, it should be called with the index, not with iopins[i]:
    //handleOWIO(iopins[i]);
    handleOWIO(i); // call with index, not iopin
    I’ve had the same power cycle errors on my MoteIno board. By removing the first (memoryintensif) sprintf statement in handleOWIO the problems disappeared. (//sprintf(dscharaddr… ). Now the code works just fine for my purpose (DS18B20 sensor readout). Keep up the good work!

    1. Hi Frank,

      Thanks for the compliment. You are indeed correct regarding the index. I changed this in my code and did not update this post. I was between revisions when I made the original post. I have since updated the git repo where the sketch lives: https://github.com/iinnovations/iicontrollibs/blob/master/mote/remote/uni_mote_2p3/uni_mote_2p3.ino

      I have the freeRam routine in the program, and it does not seem to indicate that memory is an issue, but the sprintf is certainly unnecessary.

      The power cycle issues I had went away when I changed the sendlength in the sendIOMessage routine (see line 1308) to account for the string termination character. I noticed everything was fine until I began reporting data, and things would occasionally go wonky. Another user pointed this out on the Moteino forum.

      With respect to the sleep routine, that has also been redone and is much cleaner. I’ll update this here post when I get a minute, but the repo is already updated. Thanks again for your notes and interest.

      Colin

  3. Hi Colin, what about the numloops variable? It is not set, just defined, and used in the for loop:
    for (i=0;i 0) {
    numloops++;
    Is this correct?

  4. Hi Colin,
    Thanks! It works fine at this moment, i have put my own STRUCT in the code. I did use LONGs for the LOOPPERIOD and looptime vars to get sleep intervals >65.767 working. Kind regards,
    Frank

    1. Cool. What did you use the struct for, if you don’t mind me asking? I’ve actually toyed with this and never gotten around to it. I’ll need to do my best to compress data on the way in and out when I start using longer messages. For example, when the gateway wants a report of all of the configuration data, it makes much more sense to compress it into binary then to send it all over ASCII. I just haven’t quite gotten to it yet.

  5. Hi Colin,

    I’m trying to use your moteino remote sketch, but I have some trouble compiling, see below:

    /home/jdp/Arduino/libraries/Flash-5/Flash.h:70:3: note: no known conversion for argument 1 from ‘const char [2]’ to ‘const int*’
    /home/jdp/Arduino/libraries/Flash-5/Flash.h:67:7: note: _FLASH_STRING::_FLASH_STRING(const _FLASH_STRING&)
    class _FLASH_STRING : public _Printable
    ^
    /home/jdp/Arduino/libraries/Flash-5/Flash.h:67:7: note: no known conversion for argument 1 from ‘const char [2]’ to ‘const _FLASH_STRING&’
    Error compiling

    So my question is what Flash library are you using?
    What IDE and libraries versions do you have?

    Best regards,
    Jacques-D. Piguet

    1. Jacques,

      The error appears to be related to an upgrade in the gcc toolset in the new versions of the Arduino IDE > 1.5. See details here: http://forum.arduino.cc/index.php?topic=272313.0

      I’ve made the necessary modifications to make the Flash library compatible with the new IDE and put it into the git repo here:
      https://github.com/iinnovations/iicontrollibs/tree/master/mote/libraries/Flash

      The version with the ‘bak’ suffix is the pre-1.5 version found elsewhere. Please feel free to contact me directly with any questions.

      Thanks for your patience!
      C

Leave a Reply

Your email address will not be published. Required fields are marked *