Running Hamachi on Snappy Ubuntu Linaro

This is recorded here in case the source link (written for a Beaglebone) goes down. Hamachi is rad. That’s covered elsewhere.

Unfortunately, the armhf source from LogMeIn Hamachi does not work on the new shiny ARMv7 Raspberry Pi 2. Woe are us. Realizing that the Beaglebone has the same problem, namely that it’s running an Ubuntu distro compiled for ARMv7, I figured that someone would have solved this problem. And they have. They blogged about it at the link below. I’ve reproduced the applicable content here for posterity.

http://gencarelle.com/blog/2013/05/31/running-hamachi-on-a-beaglebone-black/

1. Update

apt-get update

2. Install packages (and package requirements).

apt-get install –fix-missing –no-install-recommends lsb lsb-core aptitude libc6-armel libc6-armel-cross linux-libc-dev-armel-cross

3. Make a directory to hold the downloaded packages.

 mkdir /root/packages

4. Change to the new package directory.

cd /root/packages

5. Download the armel libs.

aptitude download libstdc++6-armel-cross libgcc-4.7-dev-armel-cross  libgcc1-armel-cross libgomp1-armel-cross libc6-dev-armel-cross

6. There is no gcc-4.7-arm-linux-gnueabi-base package for this version of Ubuntu. Force Install the armel libs we have.

dpkg -i –force-all *.deb

7. Tell the system where to find the libs hamachi needs. Edit the /etc/ld.so.conf and add this path at the bottom.

/usr/arm-linux-gnueabi/lib

8. Update the library cache.

 ldconfig

9. Download the hamachi package.

wget https://secure.logmein.com/labs/logmein-hamachi_2.1.0.136-1_armel.deb

10. Install

dpkg –force-architecture -i logmein-hamachi_2.1.0.86-1_armel.deb

11. Enjoy!

C

HAT EEPROM Read/write, configuration for RPi B+, RPi 2

This article is more of a mental note than anything, and perhaps something others will find useful. I spent a couple hours with my strongest google-fu, some hardware, and better things to do than getting a memory chip programmed.

Introduction

The new Raspberry Pi B+ and it’s younger brother, the Raspberry Pi 2, have a breakout of the second i2c interface available on the Broadcom 2835 and 2836 chips. The interface is available on pins 28 and 29. While technically you could access it on previous versions, there weren’t jumper pins, and it was, well, harder. Still, even here the installation seemed unnecessarily complicated, if for no other reason than the information was spread across eighteen forum posts with a nearly broken forum board search feature.

Device Trees

The new RPi kernel uses what are referred to as Device Trees that load modules selectively at boot. This is in contrast to the previous versions, which used a blacklist to selectively disable devices. We won’t get into the pros and cons of this approach versus the other, but needless to say, when folks in userland upgraded their kernels, all of these things — SPI, I2C, etc, — stopped working. Big f**king oops for usability, guys.

Enable the I2C buses

Go to the file /boot/config.txt. At the end, find the lines (or add them yourself, that say:

dtparam=i2c_arm=on
dtparam=i2c_vc=on
dtparam=spi=on

Make sure they are uncommented. In order, these will enable i2c-1, i2c-0, and SPI. You have to be careful with i2c-0, as it can interfere with your GPU, specifically your camera. You have been warned.

Now, after the above, give it a good reboot. Now, when you give it an ls /dev,  you should get entries that look like:

i2c-0
i2c-1
spidev0.0
spidev0.1

You are now most of the way there! Now, one note before we move on: If you have used i2c devices before, you know that the convenient utility i2cdetect will typically list out all devices on the bus for you very nicely. Note that this is NOT the case for memory devices on i2c-0. If you take the device on i2c-1 that happily reads address 50 and put it on i2c-0, it will not respond. The script below, however, will tell you whether or not you have an EEPROM on your bus.

Reading/writing your EEPROM

Ok, well if your device doesn’t announce itself like other i2c devices, how do we read and write it? Good question. First, download the EEPROM utilities from the git repo: https://github.com/raspberrypi/hats/tree/master/eepromutils

Next — remember that in order to write to your EEPROM, you will need to make sure that the write-protect pin is either floating (defaults to pulled-down), or has been jumpered to ground. See circuit here: https://github.com/raspberrypi/hats/blob/master/eeprom-circuit.png

Finally, build yourself some binary data images by editing settings text file and using eepmake.c, and drop it onto your EEPROM using eepflash.sh, which you can also use to check your work. The docs on this stuff are good, so I won’t waste time repeating that info here.

Happy HATting !

C

P.S. If and when we figure out how you’re supposed to access the stuff in userland, we’ll let you know!!

 

Push Button RF Data Transmitter

Background

So a friend of mine who works in, let’s say, the sciences, approached me about a simple application, but one that a hardware company was charging an inordinate amount of money for. In the physical sciences equipment business, this is very common: overcharging for a very simple feature, because it’s convenient and it just works. The fact of the matter is that scientists simply don’t have the time to homebrew solutions, they don’t get accolades for making simple instruments work well and cheaply, and taking the time to do so takes them away from their real jobs — doing science. This is where consultants and outsourcing really make sense, even when at first it might seem a bit expensive. Regardless, this doesn’t mean that buying a receipt printer attached to a button to print out data from a device should cost $1000, or a dead-simple temperature controller in a box with a couple relays should cost $2k. but this is the twisted norm.

We are moving away from a time where a tiny handful of people were able to design things that once seemed very complicated, like a microcontroller IO device, to an era where these things are available for dirt cheap and are everywhere. The markets are therefore just waiting to be taken over or at least challenged and/or adjusted by the talents and creativity of the makers around us.

The Project

The device I was approached about was a laboratory instrument that has an RS232 interface. You can send it a couple characters and it sends back the data. What we envisioned was a single button: you press the button, it acquires data and sends it off to a sensor gateway, where the data is databased and stored for later processing, addition of metadata, etc. It’s a pretty general thing. To summarize:

basicmodel

Now the idea of a single button is pretty romantic, in a way. It is the absolute reduction of input to the simplest unit of data transfer: the binary digit. The ultimate in simplicity. As it turns out, it’s not a unique one, either. The Staples big red button has been around forever, and if you do some googling you can find a couple companies that make, essentially, big red networked buttons that do one thing. There are legion applications where you just need one thing to happen. Call an elevator, a taxi, turn on a light, disable an appliance … so many user interfaces can be created with one or three binary elements. Anyway, the imagination runs wild. But let’s get onto the simple nuts and bolts of how to make our little button here.

The Hardware

We’ll construct this unit exactly like our Enhanced Remote, which we wrote about previously. We’ll use the same enclosure, and just attach a button to the top, and run two cables out of one side. This will negate the IP rating of the cable entry, but make it so all of our cables come out of one side. This is a key feature for presentation and accessibility. As a result, however, we need to size up our cable entry to PG9, which really doesn’t fit on the side of the enclosure, despite the appearance. Next time we do this, we’ll get a single cable with enough conductors so we can use a smaller PG7 entry as on the standard remote.

Components for our RF button remote.
Components for our RF button remote.

What we SHOULD have done here was document the button insertion process and what the bare button looks like. It’s one of these guys at right.

Our button's product picture before we seal it up on the enclosure lid.
Our button’s product picture before we seal it up on the enclosure lid.

Strangely, the top bezel on this thing is really narrow, so you need to get the hole size just right. Let’s just say our set of bits didn’t prove up to getting it close enough. So we ended up using a grommet in the hole opening, which had the added benefit of keeping the top sealed. It also gives the button a little spring, and a totally secure feeling. To get it in there just right, we had to cut the grommet in half and use one half on each side. Not a very interesting story.

Wiring

Aside from the step putting this thing together we covered previously, we need to do our wiring to the outside and connect our button. From the outside, everything will be on one USB cable: 5V power, GND, and two data lines. The two data lines will be passed out on the second USB, and our data source can use them as they see fit. For an RS232 source, for example, we’ll use them as RX/TX, after level conversion if necessary. This will make using a Y USB cable possible in the future. Here’s a simple layout:

Simple wiring diagram of our button and RF sender unit. All the external connections go on a single USB bus.
Simple wiring diagram of our button and RF sender unit. All the external connections go on a single USB bus.

The button is pretty straightforward – two pins for the contact and two for the LED. One button contact will go to ground, with the other connected to the IO1, which we’ll configure as INPUT_PULLUP on the Arduino. Not coincidentally, this pin has a hardware interrupt (pin 3) on the Moteino/Arduino. When our button is pressed, the input will change from it’s normal state of 1 to a state of 0. We can set an interrupt on RISING or FALLING and we’ll be good to go.

For the button LED, we’ll need a current-limiting resistor. 330 ohms turns out to be about right, using the 5V input from our USB power input. We attach this to the positive LED terminal, and connect the negative terminal to IO1, which we’ll use as an open-drain output. The IO4 terminal is connected to the drain of the MOSFET footprint, into which we’ve soldered a 2N7000 NPN. IO4 on our mote is io7 internally, and corresponds to pin A2. Once we enable the output, it will be set each cycle, so we don’t /need/ a pulldown, but we’ll configure one anyway.

Construction

Now we need to physically get everything into the box. Not to complicated. First, we solder our button wire. I am personally so tired of soldering crappy stranded wire, such as that in ribbon cable, that I went with some solid core hookup wire. It’s not as pretty and tidy, and makes closing up the box a bit of a pain, but it’s much more robust and cuts down on the short/bad connection sleuthing endemic to stranded wire, especially when you are moving connections around a lot. It also makes screw terminal insertion (especially difficult with the space constraints here) a breeze. Totally worth it in this application.

Getting everything into a box. Pulling cable in and wiring our button.
Getting everything into a box. Pulling cable in and wiring our button.

Once we do some wire coiling and screw terminal insertions, we’re ready to close ‘er up (and then open it up again to program it).

All done! Time to play.
All done! Time to play.

Micro Programming

Programming this thing is pretty basic. You can find the sketch here. We’ll mention a couple things we did that were perhaps noteworthy. The rest has all been covered in our UniMote programming. We decided to slightly modify our interrupt programming. We have noticed that there are issues with running any serial print debug on a function run by an interrupt. This can be quite annoying, especially if you are calling a function that is designed to provide output, such as debug accompanied by our RF send functions, for example. To mitigate this, we used our interrupt to do nothing except set a loop routine to run:

pinMode (3,INPUT_PULLUP);
attachInterrupt (1,setDoButton,RISING);

...

void setDoButton() {
  runDoButton = 1;
}

Then, inside our loop:

if (runDoButton) {
  doButton();
}

The only requirement here is that the loop time is short enough that feedback is fast. Because we’re not really doing much, as long as we make sure our LOOPPERIOD is sufficiently short, we’re good to go.

Finally, sending our value is as simple as wrapping up our standard SendWithSerialNotify function with something that retrieves our data, and sends a specially formatted message. In this case, we’re using the identifiers ‘scalepin’ and ‘scalevalue’. This will allow our gateway to interpret the data accordingly. This is shown below, with dummy data for testing. You can see here that we manually turn the LED output on and off again when the function enters and exits, respectively.

void doButton() {
  if (millis() - lastbutton > debounce && iovalue[7] == 0){
    Serial.println("Button Pressed");
    iovalue[7]=1;
    
    // manually turn on output
    digitalWrite(16,1);
    
    float scalevalue = 9999.9999;
    sendScaleMessage(iopins[7], scalevalue);
    lastbutton = millis();
  }
  runDoButton = 0;
}
void sendScaleMessage(byte pin, float value) {
  int wholePart = value;
  long fractPart = (value - wholePart) * 10000;
  int sendlength = 35; 
  char sendstring[sendlength];
  sprintf(sendstring, "scalepin:%02d,scalevalue:%05d.%05d", pin, wholePart, fractPart);
  sendWithSerialNotify(GATEWAYID, sendstring, sendlength); 
  Serial.println("Waiting, hang on");
  delay(1000); //  This is for visual feedback
  Serial.println("Done");
  iovalue[7]=0;
  digitalWrite(16,0); // turn off light manually
}

Gateway Programming

On the gateway, all we need to do is include a handler for our special message with data fields of ‘scalepin’ and ‘scalevalue’. Pretty easy stuff. We’ll fancy it up later, but for now it’s just as simple as an additional line or two in our serialhandler.py:

elif 'scalevalue' in datadict:
            querylist.append('create table if not exists scalevalues (value float, time string)')
            querylist.append(pilib.makesqliteinsert('scalevalues',[datadict['scalevalue'], pilib.gettimestring()],['value','time']))
            pilib.sqlitemultquery(pilib.logdatabase, querylist)

A couple presses of the button later, and sure enough, we have a table with some values!

Button press dummy data, as viewed in phpliteadmin. Yay data!
Button press dummy data, as viewed in phpliteadmin. Yay data!

We’ll spend some time getting this into a proper page for export and addition of metadata shortly. Oh yeah, and getting real data!

Reference

The above makes use of the open source libraries available on github:

Explanation and installation here:

UniMote MEGA : Templated Control for Moteino MEGA (UniMote MEGA)

Alright, so we’re super into the modular controller idea (see here for original post on the 328P version), and really like this new MEGA unit offered by LowPowerLab, the Moteino MEGA. It’s got tons of room for our programs, routines, and who doesn’t want MOAR? Anyhow, we’ll have to redesign our memory map based on the scads of IO now available. Here is what our available io map looks like:

UniMote Pin Label Name Function
0 0 PB0
1 1 PB1
2 3 PD0
3 8 PD1
4 9 PD2
5 10 PD3
6 11 PD4
7 12 PD5
8 13 PD6
9 14 PD7
10 16 PC0
11 17 PC1
12 18 PC2
13 19 PC3
14 20 PC4
15 21 PC5
16 22 PC6
17 24 PA0
18 25 PA1
19 26 PA2
20 27 PA3
21 28 PA4
22 29 PA5
23 30 PA6
24 31 PA7

Here’s our EEPROM map:

Item Bytes EEPROM Beg EEPROM End
NodeID 1 0 0
NetworkID 1 1 1
GatewayID 1 2 2
Encryption Key 16 3 18
iomode 25 19 43
ioenabled 25 44 68
ioreadfreq 25 69 93
loopperiod 1 94 94
sleepmode 1 95 95
sleepdelay 1 96 96
chanenabled 8 97 104
chanmode 8 105 112
chanposfdbk 8 113 120
channegfdbk 8 121 128
chandeadband 8 129 136
chanpvindex 8 137 144
chansv 8 145 152

With all this space, we’ve also added a load of other features. We’ll detail those here shortly.

CuPID Remote : Wireless RF Temperature Controller

The Project

After our remote temperature sensor project, we wanted to work on another of the basic unit operations of a typical controls systems network: the temperature controller. Building the basic CuPID Remote, we wanted to incorporate a number of features:

  • Local indication of control variables
  • Local control of control parameters and current system status
  • RF transmission to a CuPID base unit
  • Flexible IO to allow addition of sensors and local control of relays
  • Encapsulation within a waterproof enclosure

Basically, we wanted a fully functional, waterproof industrial temperature controller, with the added features that it would communicate with RF to our CuPID controller, and then to the world over the internets.

For this project, we’re going to use a 5V usb power supply (although the regulator built in to our Moteino microcontroller could take less or more), and we’ll send out a few logic-level (3.3V) signals to control a solid-state relay. The idea, however, is that we can power it directly using the same power supply that runs something like a solenoid, if for example you were controlling the temperature on a jacketed fermenter by controlling the flow of glycol. In that case, we’d throw in a switching regulator to bring the input voltage down for the microcontroller, and put in a miniature relay or power MOSFET.  We’ll get to this shortly.

The Parts

For the project, we’ll use the following parts:

  • Moteino R4 with RFM69W Receiver
  • 4-digit 7-segment common-anode display, e.g. this
  • Encoder with button, e.g. this guy
  • Polycase WC22F waterproof enclosure
  • MAX 7221 8-digit LED Display Driver
  • PG Cable Glands, e.g. HEYCO 3207
  • Assorted connectors, ribbon cables, and a prototyping board. We won’t list these because they’ll change when we get our PCBs made

Construction

So the really unfortunate part about prototyping embedded electronics is trying to get a bunch of stuff into a small box without a PCB … as you shall see. At first layout, we wanted to shove everything above into our teensy box, along with a couple shift registers to drive a 12-bar LED graph, like we did before. We’ll end up putting this option into the PCB, but as you can see, the prospect of hand-soldering all of these components is a bit mind-numbing.

Layout of all components within our enclosure. We don't use all of these for the prototypes.
Layout of all components within our enclosure. We don’t use all of these for the prototypes.

So we pulled off the shift registers, the power supply, the relay, and soldered up the rest. These are all options for the future. The soldering was still pretty ugly. Here is the wiring diagram for what was left:

Pin layout for the prototype
Pin layout for the prototype

First, we create our holes, insert our cable glands and cables, and insert them into our screw terminals. From left to right, we have USB (5V, GND), Cat5 (outputs 1,2,3, 5V, and GND), and our 1Wire sensor (Data, 5V, GND).

Our cable entries. From left to right: USB, IO, and 1Wire temperature sensor.
Our cable entries. From left to right: USB, IO, and 1Wire temperature sensor.

Now we solder it all up. We add vertical headers to the Moteino board so we can still use an FTDI cable with the lid off. Here is everything all soldered up and inserted:

Assembly of our components into our little box.
Assembly of our components into our little box.

Alright, so let’s mount her and plug her in. Here’s what we get all plugged in and running:

Live action shot of our controller. If you look closely, you can see how we tried a number of adhesives we weren't terribly happy with, which left remnants. They don't affect the visibility once the LEDs are on.
Live action shot of our controller. If you look closely, you can see how we tried a number of adhesives we weren’t terribly happy with, which left remnants. They don’t affect the visibility once the LEDs are on.

One of the nice parts we mentioned previously is the mounting of the headers on the board so we can open it up and easily connect our FTDI cable:

A nice part about this design is that we can easily attach our FTDI connector cable to the headers once we open the top. In the future, we'll install an FTDI chip on-board and use the USB cable for data transfer.
A nice part about this design is that we can easily attach our FTDI connector cable to the headers once we open the top. In the future, we’ll install an FTDI chip on-board and use the USB cable for data transfer.

 

The Code Bits

 7-segment LCD

The tough part of the LCD is handled by a neat little chip, the MAX7221, which will handle up to eight digits, four of which we’re using here. You don’t, of course, need to have this chip, using shift registers, as we demonstrated with a bar segment display previously. If you have a common anode display, however, this chip is a great way to offload the processing and not worry about the timing of the remainder of the program. Here is the code. We use the LedControl library, which has built-in functions for numerical digits, and the remaining characters can be pieced together with binary addition of the digits and the set row command:

void handledisplay(){
  byte displaymode = 0;
  float newlcdvalue;
  
  if (menuy) {
    blinky = 1;
  }
  else {
    blinky = 0;
  }
  
  //  Serial.println(testvalue);
  if (blinky) {
    byte blinkinterval = (millis()/200) % 2;
    if (!blinkinterval) {
      displayon = 1;
    }
    else {
      displayon = 0;
    }
  }
  else {
    displayon = 1;
  }
  
  if (menux == 0){
    if (menuy == 0){
      if (tempmode == 'F'){
        newlcdvalue = CtoF(chanpv[0]);
      }
      else{
        newlcdvalue = chanpv[0];
      }
    }
    if (menuy == 1) {
//      Serial.println("I am in the menu");
      displaymode = 1;  // display characters
      lcdvalue = chartoint(tempmode);
    }
  }
  else if (menux == 1) {
    if (tempmode == 'F'){
        newlcdvalue = CtoF(chansv[0]);
      }
      else{
        newlcdvalue = chansv[0];
      }
  }
  
  if (displayon){
    if (newlcdvalue != lcdvalue) {
      if (displaymode == 0){
        lcdvalue = newlcdvalue;
        setdigits(newlcdvalue);
      }
      else if (displaymode == 1){
        setchars('-','-','-',tempmode);
      }
    }
  }
  else {
    lc.clearDisplay(0);
    lcdvalue = -9999; // ensure refresh
  }
}
void setchars(char char1, char char2, char char3, char char4){
  lc.clearDisplay(0);
  lc.setRow(0,0,chartoint(char1));
  lc.setRow(0,1,chartoint(char2));
  lc.setRow(0,2,chartoint(char3));
  lc.setRow(0,3,chartoint(char4));
}
byte chartoint(char mychar){
  byte charbyte = 62;
  switch (mychar) {
    case '-':
      charbyte = 0;
      break;    
    case 'F':
      charbyte = 71;
      break;
    case 'C':
      charbyte = 78;
      break;
    case 'V':
      charbyte = 62;
      break;
    case 'S':
      charbyte = 91;
      break;
    case 'P':
      charbyte = 104;
      break;
  }
  return charbyte;
}
void setdigits(float lcdvalue) {
   
  int dig1 = lcdvalue / 10;
  float leftovers = lcdvalue - (dig1 * 10);
  int dig2 = leftovers;
  leftovers -= dig2;
  int dig3 = leftovers * 10;
  leftovers -= dig3 / 10;
  int dig4 = leftovers;
    
  if (lcdvalue < 0) {
    
    // display value
    lc.clearDisplay(0);
    
    
    // this should work for the negative sign
    lc.setRow(0,0,1);
    lc.setDigit(0,1,dig1,false);
    lc.setDigit(0,2,dig2,true);
    lc.setDigit(0,3,dig3,false);
  }
  else {
    // display value
    lc.clearDisplay(0);
    lc.setDigit(0,0,dig1,false);  
    lc.setDigit(0,1,dig2,true);
    lc.setDigit(0,2,dig3,false);
    lc.setDigit(0,3,dig4,false);
  }
}

Eventually, we’ll wrap up all the digits for setRow, using our chartoint() function, so we can use the same subfunction for all set functions.  In the meantime, here are the binary values for the segments, using the notation A-H shown above in the wiring schematic:

LCD segment binary value
a 64
b 32
c 16
d 8
e 4
f 2
g 1
h 128
Encoder

Although the way the encoder works is a bit bizarre and archaic, the code is pretty simple. We attach an interrupt one of the pins so that the handler function is called whenever the encoder is activated. It looks a bit like this:

pinMode (encoder0PinA,INPUT);
  pinMode (encoder0PinB,INPUT);
  attachInterrupt(1, doEncoder, CHANGE);

void doEncoder() {
  int pinAval = digitalRead(encoder0PinA);
  int pinBval = digitalRead(encoder0PinB);
  byte direction;
  if ((pinAval == HIGH) && (lastpinAval == LOW)) {
    if (pinBval == HIGH) {
      direction = 0; // CCW
    } else {
      direction = 1; // CW
    }
    // PV menu
    if (menux == 0){
      if (menuy == 0){
        if (direction == 1) {
          menux = 1;
        }
      }
      else if (menuy == 1){
        if (tempmode == 'F'){
          tempmode = 'C';
        }
        else if (tempmode == 'C'){
          tempmode = 'F';
        }
      }
    } // menu top  level
    // SV menu
    else if (menux == 1){
      if (menuy == 0){
        if (direction == 0) {
          menux = 0;
        }
      }
      else if (menuy == 1) {
        if (direction == 1) {
          chansv[0] += 0.1;
        }
        else if (direction == 0) {
          chansv[0] -= 0.1;
        }
      }
    } // menu top  level
  } // 
  lastpinAval = pinAval;
}

You can see our simple menu structure handling here as well, a simple 2D array of states and handling.

The Web UI

Clips coming. Looks like this.

Testing

In production. Here’s a screenshot:

Temperature cycling of our Beer Freezer with a deadband of 2C.
Temperature cycling of our Beer Freezer with a deadband of 2C.

Next

Add segment display for output indication

We’d like to shove one of these in a box. We’ve already prepared one, but we’ll definitely need to put it on a PCB to make the connections. Stand by.

Put Moteino interface on power USB

We’ve already got USB going into our enclosure. No reason to require opening the enclosure lid, if we’ve already got data lines headed in. Trick is, we’ll have to put a permanent FTDI in place to do so. We’ll at least create pads on our PCB to populate if we want to. IF we have room.

Moteino / Arduino and 1Wire : Optimize your read for speed

Hello all,

I’ve been picking apart my code to do some optimization, and an obvious target was the read of 1Wire devices, the DS18B20 in particular. Currently, I use the OneWire library, without the DallasTemperature library, which I didn’t much care for.

What all 1Wire read routines have in common is that they issue the Convert command prior to reading a measured value. For the DS18B20, this is the Convert T command, 0x44. Depending on the resolution, the conversion takes from 75-750ms, per the datasheet.

There are three main issues I had with the implementation using OneWire as in the examples:

  • There was no provision for setting the resolution. While this is available in the DallasTemperature library, it just didn’t make sense to me to rewrite all of my lean code around this library to get this one feature.
  • Wait times are hardcoded. Per the datasheet, it is possible to check with the device for status after issuing the Convert T command to know exactly when the temperature conversion is complete. In most implementations, however, this wait time is hard-coded, and typically VERY generously. For example, in a read operation at 12 bits that actually takes <600ms, the hardcoded wait time is 1000ms. This is a 40% savings, ladies and gentlemen. Futhermore, most test code does not even adjust the delay time for different resolution. This is just insane. A ready that should take 60-75ms will implement a wait of 1000ms (?!). I mean, I understand if speed is not that important to you, but this is crazy.
  • Everything is done synchronously. Nothing else can happen while we are waiting for this conversion This is very inefficient, that is, in the case that your program needs to do anything else. This is  the real reason I dug into this. I wanted to read sensors, but be able to update a display in real-time. The moral of the story: if anything else in your program depends on timing, forget using the default 1Wire read code. So to easily work out the first two items, I created the example below in code box 1. Run it after substituting your 1Wire pin, and you’ll get something like this for reading 9, 10, 11, and 12 bit resolutions:
dsaddress:2838FF400500004E,
 Conversion took: 76 ms
 Raw Scratchpad Data:
 50 1 0 0 1F FF 10 10 21
 Temp (C): 21.00

dsaddress:2838FF400500004E,
 Conversion took: 150 ms
 Raw Scratchpad Data:
 50 1 0 0 3F FF 10 10 51
 Temp (C): 21.00

dsaddress:2838FF400500004E,
 Conversion took: 298 ms
 Raw Scratchpad Data:
 50 1 0 0 5F FF 10 10 C1
 Temp (C): 21.00

dsaddress:2838FF400500004E,
 Conversion took: 596 ms
 Raw Scratchpad Data:
 4F 1 0 0 7F FF 1 10 37
 Temp (C): 20.94

A HUGE improvement over stock wait times. With the accuracy of the DS18B20, it really doesn’t make much sense to use 12 bits, so 10 bits saves me loads in timing. I left in a bunch of original code that’s been commented out so you can see how it was done previously.

Now, we can also separate conversion commands and reading the data back. If you have multiple sensors, you actually want to use the Skip ROM and Convert T commands to tell all devices on the bus to convert simultaneously, but that’s another topic. In the meantime while conversion is taking place, we can do other stuff.

So we separate our read DS18B20 routine into find, set resolution, send conversion command, and finally read temperature. Between the last two, we just continually check in our loop to see that data is ready, and when it is, we read it. Pretty simple, but SUPER EFFECTIVE. You can see we lose a little due to overhead, but still, plenty fast, and we can do other stuff at the same time.

Enjoy!
C

Temp (C): 21.50
Elapsed time (ms): 99
Temp (C): 21.50
Elapsed time (ms): 170
Temp (C): 21.62
Elapsed time (ms): 321
Temp (C): 21.56
Elapsed time (ms): 618

Code box 1:

#include <OneWire.h>

#define LED 9
#define SERIAL_BAUD   115200

void setup(void) {
Serial.begin(SERIAL_BAUD);
}

void loop(void) {
for (int i=9;i<13;i++){
handleOWIO(6,i);
Serial.println();
}

delay(1000);
Blink(LED,3);
}

void handleOWIO(byte pin, byte resolution) {
int owpin = pin;

// Device identifier
byte dsaddr[8];
OneWire myds(owpin);
getfirstdsadd(myds,dsaddr);

Serial.print(F("dsaddress:"));
int j;
for (j=0;j<8;j++) {
if (dsaddr[j] < 16) {
Serial.print('0');
}
Serial.print(dsaddr[j], HEX);
}
// Data

Serial.println(getdstemp(myds, dsaddr, resolution));

} // run OW sequence

void getfirstdsadd(OneWire myds, byte firstadd[]){
byte i;
byte present = 0;
byte addr[8];
float celsius, fahrenheit;

int length = 8;

//Serial.print("Looking for 1-Wire devices...\n\r");
while(myds.search(addr)) {
//Serial.print("\n\rFound \'1-Wire\' device with address:\n\r");
for( i = 0; i < 8; i++) {
firstadd[i]=addr[i];
//Serial.print("0x");
if (addr[i] < 16) {
//Serial.print('0');
}
//Serial.print(addr[i], HEX);
if (i < 7) {
//Serial.print(", ");
}
}
if ( OneWire::crc8( addr, 7) != addr[7]) {
//Serial.print("CRC is not valid!\n");
return;
}
// the first ROM byte indicates which chip

//Serial.print("\n\raddress:");
//Serial.print(addr[0]);

return;
}
}

float getdstemp(OneWire myds, byte addr[8], byte resolution) {
byte present = 0;
int i;
byte data[12];
byte type_s;
float celsius;
float fahrenheit;

switch (addr[0]) {
case 0x10:
//Serial.println(F("  Chip = DS18S20"));  // or old DS1820
type_s = 1;
break;
case 0x28:
//Serial.println(F("  Chip = DS18B20"));
type_s = 0;
break;
case 0x22:
//Serial.println(F("  Chip = DS1822"));
type_s = 0;
break;
default:
Serial.println(F("Device is not a DS18x20 family device."));
}

// Get byte for desired resolution
byte resbyte = 0x1F;
if (resolution == 12){
resbyte = 0x7F;
}
else if (resolution == 11) {
resbyte = 0x5F;
}
else if (resolution == 10) {
resbyte = 0x3F;
}

// Set configuration
myds.reset();
myds.select(addr);
myds.write(0x4E);         // Write scratchpad
myds.write(0);            // TL
myds.write(0);            // TH
myds.write(resbyte);         // Configuration Register

myds.write(0x48);         // Copy Scratchpad

myds.reset();
myds.select(addr);

long starttime = millis();
myds.write(0x44,1);         // start conversion, with parasite power on at the end
while (!myds.read()) {
// do nothing
}
Serial.print("Conversion took: ");
Serial.print(millis() - starttime);
Serial.println(" ms");

//delay(1000);     // maybe 750ms is enough, maybe not
// we might do a ds.depower() here, but the reset will take care of it.

present = myds.reset();
myds.select(addr);
myds.write(0xBE);         // Read Scratchpad

//Serial.print("  Data = ");
//Serial.print(present,HEX);
Serial.println("Raw Scratchpad Data: ");
for ( i = 0; i < 9; i++) {           // we need 9 bytes
data[i] = myds.read();
Serial.print(data[i], HEX);
Serial.print(" ");
}
//Serial.print(" CRC=");
//Serial.print(OneWire::crc8(data, 8), HEX);
Serial.println();

// convert the data to actual temperature

unsigned int raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// count remain gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
} else {
byte cfg = (data[4] & 0x60);
if (cfg == 0x00) raw = raw << 3;  // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw << 2; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw << 1; // 11 bit res, 375 ms
// default is 12 bit resolution, 750 ms conversion time
}
}
celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;
Serial.print("Temp (C): ");
//Serial.println(celsius);
return celsius;
}

void Blink(byte PIN, int DELAY_MS)
{
pinMode(PIN, OUTPUT);
digitalWrite(PIN,HIGH);
delay(DELAY_MS);
digitalWrite(PIN,LOW);
}

Code box 2:

#include <OneWire.h>

#define LED 9
#define SERIAL_BAUD   115200

OneWire myds(6);
byte readstage;
byte resolution;
unsigned long starttime;
unsigned long elapsedtime;
byte dsaddr[8];

void setup(void) {
Serial.begin(SERIAL_BAUD);
readstage = 0;
resolution = 12;
}

void loop(void) {

if (readstage == 0){
getfirstdsadd(myds,dsaddr);
dssetresolution(myds,dsaddr,resolution);
starttime = millis();
dsconvertcommand(myds,dsaddr);
readstage++;
}
else {
if (myds.read()) {
Serial.println(dsreadtemp(myds,dsaddr, resolution));

Serial.print("Elapsed time (ms): ");
elapsedtime = millis() - starttime;
Serial.println(elapsedtime);
readstage=0;
if (resolution == 12){
resolution = 9;
}
else {
resolution ++;
}
}
}

Blink(LED,5);
}

void getfirstdsadd(OneWire myds, byte firstadd[]){
byte i;
byte present = 0;
byte addr[8];
float celsius, fahrenheit;

int length = 8;

//Serial.print("Looking for 1-Wire devices...\n\r");
while(myds.search(addr)) {
//Serial.print("\n\rFound \'1-Wire\' device with address:\n\r");
for( i = 0; i < 8; i++) {
firstadd[i]=addr[i];
//Serial.print("0x");
if (addr[i] < 16) {
//        Serial.print('0');
}
//      Serial.print(addr[i], HEX);
if (i < 7) {
//Serial.print(", ");
}
}
if ( OneWire::crc8( addr, 7) != addr[7]) {
Serial.print("CRC is not valid!\n");
return;
}
// the first ROM byte indicates which chip

//Serial.print("\n\raddress:");
//Serial.print(addr[0]);

return;
}
}

void dssetresolution(OneWire myds, byte addr[8], byte resolution) {

// Get byte for desired resolution
byte resbyte = 0x1F;
if (resolution == 12){
resbyte = 0x7F;
}
else if (resolution == 11) {
resbyte = 0x5F;
}
else if (resolution == 10) {
resbyte = 0x3F;
}

// Set configuration
myds.reset();
myds.select(addr);
myds.write(0x4E);         // Write scratchpad
myds.write(0);            // TL
myds.write(0);            // TH
myds.write(resbyte);         // Configuration Register

myds.write(0x48);         // Copy Scratchpad
}

void dsconvertcommand(OneWire myds, byte addr[8]){
myds.reset();
myds.select(addr);
myds.write(0x44,1);         // start conversion, with parasite power on at the end

}

float dsreadtemp(OneWire myds, byte addr[8], byte resolution) {
byte present = 0;
int i;
byte data[12];
byte type_s;
float celsius;
float fahrenheit;

switch (addr[0]) {
case 0x10:
//Serial.println(F("  Chip = DS18S20"));  // or old DS1820
type_s = 1;
break;
case 0x28:
//Serial.println(F("  Chip = DS18B20"));
type_s = 0;
break;
case 0x22:
//Serial.println(F("  Chip = DS1822"));
type_s = 0;
break;
default:
Serial.println(F("Device is not a DS18x20 family device."));
}

present = myds.reset();
myds.select(addr);
myds.write(0xBE);         // Read Scratchpad

//Serial.print("  Data = ");
//Serial.print(present,HEX);
//  Serial.println("Raw Scratchpad Data: ");
for ( i = 0; i < 9; i++) {           // we need 9 bytes
data[i] = myds.read();
//    Serial.print(data[i], HEX);
//    Serial.print(" ");
}
//Serial.print(" CRC=");
//Serial.print(OneWire::crc8(data, 8), HEX);
//  Serial.println();

// convert the data to actual temperature

unsigned int raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// count remain gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
} else {
byte cfg = (data[4] & 0x60);
if (cfg == 0x00) raw = raw << 3;  // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw << 2; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw << 1; // 11 bit res, 375 ms
// default is 12 bit resolution, 750 ms conversion time
}
}
celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;
Serial.print("Temp (C): ");
//Serial.println(celsius);
return celsius;
}

void Blink(byte PIN, int DELAY_MS)
{
pinMode(PIN, OUTPUT);
digitalWrite(PIN,HIGH);
delay(DELAY_MS);
digitalWrite(PIN,LOW);
}

LED Bar Graph with Arduino and shift registers

Intro

So, this article was supposed to be called ‘LED Bar Graph with MAX7221’. We’ve got a pile of these neat little 12-segment bar graphs over here that have red and green LEDs in each digit. As it turns out, however, we’ve got common anode, while the MAX7221/7219 only drives common cathode. This is a bummer, and probably why Adafruit discontinued the common anode and only carries the common cathode now. We’d just pick up some common cathode and be done with it, except, like I said, we’ve got a pile of them. And it seemed like a cool project to make it work with even simpler components than our MAX7221.

Multiplexing?

Connecting up a bunch of LEDs on a shift register really isn’t newsworthy, in fact we posted about it earlier. The key here is that, while we can control all LEDs on one anode individually (each anode of three controls four segments), we can’t turn one LED in one group and one in another group without turning a bunch of others on inadvertently. Looking at our datasheet, which is similar to this one, reveals that we have 14 pins total, three of which are duplicated. So we’ve got eight cathodes and three anodes.

Some basic math is illuminating: we have 24 individual LEDs, each with two states. That gives 2^24 possible state combinations. We only have 14 pins, 3 of which are duplicates, leaving 11. This is only 2^11 possible pin states. We obviously need some trickery. And that’s where LED multiplexing comes in. It works the same way as a row/column setup does: each LED is lit very quickly in succession. It tricks our eyes, and allows fewer pins for a fixed number of LEDs. Take a typical 7-segment plus dot digit, for example. If you have eight eight-segment digits (what you can put on a MAX7221), you have 8×8=64 segments. Individually wired, this would take 128 pins, or 64 with a common anode or cathode. But with only eight cathodes and eight anodes, one per digit and one per segment type for a total of 16, you can multiplex it and operate it at one fourth the pin count. You could do the same thing with two eight-bit shift registers, such as the 74HC595 we’re using here. In our case, we have eight total cathodes per group (four green, four red), and three anodes. We’ll use one register for the cathodes (as we did above), and use the other register for our three anodes. So on to it.

Shift Registers

Each shift register gives us eight bits (we’ll use 74HC595s), so we can get away with two of them and have five bits to spare. They’re about $0.10 apiece. A MAX7221 is something like a dollar, so it’s marginally cheaper.

Testing reveals that driving these guys from 5V seems to be just fine, so our wiring looks a bit like this:

Wiring schematic for two shift registers and a 12-segment bar display.

Notice that we’ve wired our output enable pin to ground, which will make it permanently active. This is fine, since we aren’t sharing our SPI lines with other devices.

We’ve hooked up our clock and data lines on outputs 8-10 on the ‘duino, and we borrowed and modified some code to allow us to set the color on each of the first four digits (eight LEDs):

 void setDisplay(int byte1, int byte2) {
  int numtoset1 = 255-byte1;
  int numtoset2 = byte2;
  digitalWrite(latchPin, LOW);
  
    // shift out the bits:
    shiftOut(dataPin, clockPin, MSBFIRST, numtoset2);
    shiftOut(dataPin, clockPin, MSBFIRST, numtoset1);  

    //take the latch pin high so the LEDs will light up:
    digitalWrite(latchPin, HIGH);
}

Note that we’ve added provision for two bytes, which we’ll need for our remaining digits (actually, for controlling the anodes, but we’ll get to that later). This means that our closest shift register will get the second byte we send, meaning for our one shift register testing, we’ll put our test value in byte1. To start, we have set up the first four bits for red, and the second four for green. Our cathodes are the outputs, so low turns them on, but we take the complement so we can use 1 for ‘on’. This yields the following values to turn on each LED:

segment color value
1 R 1
1 G 16
2 R 2
2 G 32
3 R 4
3 G 64
4 R 8
4 G 128

So to get yellow, we just apply both red and green. 255 is all on, 0 is all off. We don’t need to remember these, as we can just use the built-in bit() function, passing 0-3 for red and 4-7 for green. We wrap it up again in a function that allows us to set, using two arguments: digit and color. We’ll modify this later when we create control for all digits. Here is the wrapper function:

void setDigit(int digit, int colorint) {
  int byte2 = 0;
  int byte1 = 0;
  digit = digit %4;
  if (colorint > 0){
    if (colorint == 1){
      byte1 = bit(digit);
    }
    else if (colorint == 2){
      byte1 = bit(digit + 4);
    }
    else if (colorint == 3){
      byte1 = bit(digit) + bit(digit +4);
    }    
  }  
  setDisplay(byte1,byte2);
}

We set up a basic function to set the first digits:

void loop() {
  setDigit(0,0);
  setDigit(1,1);
  setDigit(2,2);
  setDigit(3,3);
}

This will set the digits one at a time. Persistence of vision dictates that we can’t tell the difference whether or not they are on simultaneously or in sequence (as long as the frequency is >25Hz (!)) … and we get what we expect:

Our segments, lit in series, appear to be lit simultaneously, the core principle of multiplexed LED illumination.
Our segments, lit in series, appear to be lit simultaneously, the core principle of multiplexed LED illumination.

Neither I nor the camera can tell that they aren’t all constantly on. Now, the astute reader should ask: “Why are you turning the digits on separately, instead of all together?” This is a great question, and really the point of this post. See Mutiplexing, above. The short answer is that we don’t have enough pins to set all states simultaneously and uniquely, so we have to shuffle through them.

Now on to the remaining digits. We’ll modify our setDigit() routine to tolerate digits greater than four, and set the proper anode. Note the use of the mod operator to bring all digit values into the range of 0-3 for our cathodes.

void setDigit(int digit, int colorint) {
  int byte2 = 0;
  int byte1 = 0;
  if (digit < 4){
    byte2 = 1;
  }
  else if (digit < 8){
    byte2 = 2;
  }
  else if (digit < 16) {
    byte2 = 4;
  }
  digit = digit %4;
  if (colorint > 0){
    if (colorint == 1){
      byte1 = bit(digit);
    }
    else if (colorint == 2){
      byte1 = bit(digit + 4);
    }
    else if (colorint == 3){
      byte1 = bit(digit) + bit(digit +4);
    }    
  }  
  setDisplay(byte1,byte2);
}

We’ll call it like so:

setDigit(0,0);
  setDigit(1,1);
  setDigit(2,2);
  setDigit(3,3);
  setDigit(4,0);
  setDigit(5,1);
  setDigit(6,2);
  setDigit(7,3);
  setDigit(8,0);
  setDigit(9,1);
  setDigit(10,2);
  setDigit(11,3);

This seems fine, but what we get are fluttering LEDs, as shown here:

Obviously, 8×8 matrices make eight calls work, so our timing is probably suspect, or the difference between eight and twelve calls breaks the camel’s back. We don’t feel like doing the math. We don’t need to troubleshoot that, however. Remember that we’re making eight calls for only three anodes, so we can further optimize. We can combine the twelve calls into three (there are only three anodes). We’ll just restructure our algorithm a bit.

First, we define a quick function to return a value from digit and color:

int getByteFromColorInt(int digit, int colorint){
  int returnvalue=0;
  if (colorint > 0){
    if (colorint == 1){
      returnvalue = bit(digit);
    }
    else if (colorint == 2){
      returnvalue = bit(digit + 4);
    }
    else if (colorint == 3){
      returnvalue = bit(digit) + bit(digit +4);
    }    
  }
  return returnvalue;
}

Then, we split up the calls into groups:

void setBarDigits(int barColors[]) {
  int i;
  int byte21 = 0;
  int byte22 = 0;
  int byte23 = 0;
  for (i=0;i<4;i++) {
    byte21 += getByteFromColorInt(i, barColors[i]);
  }
  Serial.print("byte21: ");
  Serial.println(byte21);
  setDisplay(byte21,1);
  for (i=4;i<8;i++){
    byte22 += getByteFromColorInt(i%4, barColors[i]);
  }
  Serial.print("byte22: ");
  Serial.println(byte22);
  setDisplay(byte22,2);
  for (i=8;i<12;i++){
    byte23 += getByteFromColorInt(i%4, barColors[i]);
  } 
  Serial.print("byte23: ");
  Serial.println(byte23);
  setDisplay(byte23,4);
}

And simply call it like so:

void loop() {
  int barColors[12]={0,1,0,1,0,1,0,1,0,1,0,1};
  setBarDigits(barColors);
}

And we have success:

 

Alternating LEDs show that our segment control of multiple anodes is workign as advertised.
Alternating LEDs show that our segment control of multiple anodes is working as advertised.

One thing to remember is that the setBarDigits() function is running three states in series, and ending on one. So if you throw in a delay after the call, or do something time-consuming between when it ends and when it’s called again, it’s going to remain in the last state. So doing this, for example:

 void loop() {
  int barColors[12]={0,1,0,1,0,1,0,1,0,1,0,1};
  setBarDigits(barColors);
  delay(1000);
}

Will give you this:

 

Which is probably not what you want. So you need to run it often. This is why separate chips dedicated to this like the MAX7219/7221 make sense: they do all of this elsewhere so you don’t have to worry about it. Anyway, so let’s have some fun with some animation. First, we modify our setBarDigits() function to accept a runtime argument. This way the function will run its loop to maintain an set of digits for the time we specify:

void setBarDigits(byte barColors[], int runtime) {
  int i;
  int byte21 = 0;
  int byte22 = 0;
  int byte23 = 0;
  long starttime = millis();
  boolean doRun = true;
  
  while (doRun){
    byte21 = 0;
    byte22 = 0;
    byte23 = 0;
    for (i=0;i<4;i++) {
      byte21 += getByteFromColorInt(i, barColors[i]);
    }
    Serial.print("byte21: ");
    Serial.println(byte21);
    setDisplay(byte21,1);
    for (i=4;i<8;i++){
      byte22 += getByteFromColorInt(i%4, barColors[i]);
    }
    Serial.print("byte22: ");
    Serial.println(byte22);
    setDisplay(byte22,2);
    for (i=8;i<12;i++){
      byte23 += getByteFromColorInt(i%4, barColors[i]);
    } 
    Serial.print("byte23: ");
    Serial.println(byte23);
    setDisplay(byte23,4);
    
    if (millis()-starttime < runtime) {
      doRun = true;
    }
    else {
      doRun = false;
    }
  }
}

Then we create an array of states we’d like to see and pass them one by one:

void loop() {
  byte barColors1[12]={1,0,0,0,1,0,0,0,1,0,0,0};
  byte barColors2[12]={0,1,0,0,0,1,0,0,0,1,0,0};
  byte barColors3[12]={0,0,1,0,0,0,1,0,0,0,1,0};
  byte barColors4[12]={0,0,0,1,0,0,0,1,0,0,0,1};
  byte barColors5[12]={2,0,0,0,2,0,0,0,2,0,0,0};
  byte barColors6[12]={0,2,0,0,0,2,0,0,0,2,0,0};
  byte barColors7[12]={0,0,2,0,0,0,2,0,0,0,2,0};
  byte barColors8[12]={0,0,0,2,0,0,0,2,0,0,0,2};
  byte barColors9[12]={3,0,0,0,3,0,0,0,3,0,0,0};
  byte barColors10[12]={0,3,0,0,0,3,0,0,0,3,0,0};
  byte barColors11[12]={0,0,3,0,0,0,3,0,0,0,3,0};
  byte barColors12[12]={0,0,0,3,0,0,0,3,0,0,0,3};
  
  byte* barColorArray[12];
  barColorArray[0] = barColors1;
  barColorArray[1] = barColors2;
  barColorArray[2] = barColors3;
  barColorArray[3] = barColors4;
  barColorArray[4] = barColors5;
  barColorArray[5] = barColors6;
  barColorArray[6] = barColors7;
  barColorArray[7] = barColors8;
  barColorArray[8] = barColors9;
  barColorArray[9] = barColors10;
  barColorArray[10] = barColors11;
  barColorArray[11] = barColors12;

  for (int i=0;i<12;i++){
    setBarDigits(barColorArray[i],200);
  }
}

And we get this charming display:

But we still need to verify that we can set digits independently. The above doesn’t really demonstrate that, as we could be tying all the anodes together. So let’s switch it up a bit:

Perfect!

CuPID Remote : Read, Database, Logging and UI

The Project

Building on our previous post, where we laid out the fundamentals for our Mote communication in a temperature Mote, and built up a modular Mote sketch that allows us to reprogram parameters via serial and radio, this time we’ll build up the processing back end that will get our data into our CuPID gateway and our User Interface – a web browser.

So we’ll set out to do the following:

  • Process IO data on gateway end into remote table
  • Process IO data into control and display algorithms alongside local data in the gateway

The Basics

We talked briefly about getting data into our CuPID from our gateway Moteino in another post, but we’ll rehash the basics here just to cover the bases.

We’ve got our gateway Moteino talking to our remotes, and passing on the data over serial to our Pi/CuPID, which is listening on the serial port /dev/ttyAMA0. It parses out the data between message demarcations and ends up with a nice json-formatted message, which it drops into a dictionary. We end up with something like this:

{'iovalue': '0', 'RX_RSSI': '-67', 'iomode': '01', 'iopin' : '04', 'nodeid': '2'}

This message comes from node 2, which is telling us that pin 4 is in mode 01 (digital output), and currently has a value of 0 (off). Cool. Let’s get it into tables, where it will be useful!

Tabling the Data

We are going to have a table of values brought in from the remotes, aptly called ‘remotes’, so we’ll throw this in there. Before we do so, however, let’s think about the structure of the table. We want enough fields to be present so that we can quickly identify whether a record should be inserted or replaced. To do this, we’ll set up a few auxiliary variables:

  • msgtype – this is metadata that will tell our scripts how to process
  • keyvalue – a unique value for the data to be reported. will vary with msgtype
  • keyvaluename – an identifier for the keyvalue, for handy use

Based on the keys present in the data, we’ll classify the message, and do a smart replace in the remote table.

So, for example, for two common message types:

runquery = False
        nodeid = datadict['nodeid']
        querylist = []
        if 'iovalue' in datadict:
            # iovalue type message
            try:
                msgtype = 'iovalue'
                keyvalue = datadict['iopin']
                keyvaluename = 'iopin'
            except:
                print('oops')
            else:
                runquery = True

        elif 'owdev' in datadict:
            # 1Wire message
            try:
                msgtype = 'owdev'
                keyvalue = datadict['owrom'][2:]
                keyvaluename = 'owrom'
            except:
                print('oops')
            else:
                runquery = True
        if runquery:
            deletequery = pilib.makedeletesinglevaluequery('remotes',{'conditionnames':['nodeid', 'keyvalue','keyvaluename'],'conditionvalues':[nodeid,keyvalue,keyvaluename]})
            insertquery = pilib.makesqliteinsert('remotes',  [nodeid, msgtype, keyvaluename, keyvalue, stringmessage, pilib.gettimestring()], ['nodeid', 'msgtype', 'keyvaluename', 'keyvalue', 'data', 'time'])
            querylist.append(deletequery)
            querylist.append(insertquery)
            pilib.sqlitemultquery(pilib.controldatabase, querylist)

So here, for each message type, we create a key with a value and a name. For io values, this is ‘iopin’ and then the number of the pin. This uniquely identifies a table entry. If an entry exists with the same nodeid, key name, and key value, we will replace it. We do this by constructing a query with our auxiliary function, makedeletesinglevaluequery, which takes a list of conditions and values and constructs a query. We then create an insert query with our data, which consists of the above, a data field that contains the raw message, and a timestamp. We can do most of the above with foreign key constraints on the database, but we keep it in our script for now. Running our serial monitor with this processing sequence, we get what we expect in our remotes table:

Our basic motes table, viewed here in phpliteadmin
Our basic motes table, viewed here in phpliteadmin

We’ll need to add in a hygiene function to remove old entries, but this will work for now. We’re now ready to do some postprocessing on our table, but first let’s create a basic page to display our mote table. Some copy pasta of existing pages and a little jQuery later, and we have this:

Our basic Mote table.
Our basic Mote table.

Great! Now let’s get it into our IO tables, so we can view, log and process it just like everything else.

Integrate the IO

To get our remote data into our IO tables, we’ll add the mote as an interface into our interfaces table. This way, we can selectively enable and disable processing of individual motes, and we can add options for poll period, how stale data needs to be before we ignore it, and other good stuff.  For now, we create a basic interface entry:

Our interfaces table entry for our Mote will allow selectively setting options and enable/disable. It will be processed just like our other interfaces.
Our interfaces table entry for our Mote will allow selectively setting options and enable/disable. It will be processed just like our other interfaces.

Alright, so we just need to add a case for our updateio script to process the entries. This takes a little py-fu, but it’s not too bad. We’ll need to process each message type separately, and we cheat on the name of each input for the moment, as we don’t feel like setting up metadata entries in our ioinfo table just yet. We are pleasantly greeted:

Our mote entries, all wrapped up and inserted into our IO table. We're now ready to process them as if they were local inputs!
Our mote entries, all wrapped up and inserted into our IO table. We’re now ready to process them as if they were local inputs!

We can now use our remote inputs as if they were local! Let’s take a view in our dataviewer. We scroll down to find our io automatically logging (default of 100 points), and select the onewire sensor. We’re done!

Our imported data in our dataviewer. We're now transparently reading remote data as if it were local.
Our imported data in our dataviewer. We’re now transparently reading remote data as if it were local.

Icing

Let’s revisit the idea of metadata for these channels. We hardcoded colloquial names for our mote inputs based on their unique input IDs, but it would be ideal if we again treat them like our other IO and give them metadata entries. This will also allow us to add other options, such as scaling. So we add a text column to our ioinfo table, and create entries for our remote devices. We can do this simply by editing the text in the IO table and pressing “update”:

Updating metadata for our remote inputs.

Next, we’d like to add a scaling factor. Our analog input being reported here, iopin 20, is hooked up to a voltage divider that will monitor battery input. So the value, 512, is 513 / 1024 * Vcc, where Vcc is 3.3V. We’re reading about 1.65V. Our voltage divider is a 470k resistor and a 1M resistor, with the 470k on the ground side. In other words, if we are reading 1.65V, the voltage at the high side is 1.65V * (1000 + 470)/470 = 5.2V. This is about right, within error on the resistors. We probably shouldn’t be using 5V power for our 1Wire sensor which will put data at 5V where Vcc is 3.3V, but oh well. Seems ok. So this gives us a total scaling factor of (1470/470)/1024*3.3 =0.0101 . We should technically add 1 to the input value, as it’s zero-indexed, but we’ll ignore that for now. Later, we’ll parse formulas in python, but leave that for another day.

So back to how we’ll make this happen. We’ll put a json-encoded option of ‘scale’ into the options field of the ioinfo metadata table. Then, when we process our io, we’ll see if this field exists, and scale it as we process it into the io table. A little more py-fu and this happens. We make the entry manually using phpliteadmin, since we don’t have an edit page set up for this options field yet:

Adding our options field in manually in phpliteadmin. We add scale to our battery monitor channel to scale it properly.
Adding our options field in manually in phpliteadmin. We add scale to our battery monitor channel to scale it properly.

Now let’s flip back to our inputs page. Just like magic, we’re all scaled! Now we have an accurate battery monitor that will log!

Our Battery monitor is updated and scaled, as we specified in our metadata table.
Our Battery monitor is updated and scaled, as we specified in our metadata table.

 Next time:

  • Programming our Motes to send channel status data to our gateway
  • Setting up our gateway to reprogram our Motes when they come online, using a table of set values, and

Reference

The above makes use of the open source libraries available on github here:

Explanation and installation here:

 

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:

 

Your CuPID as a PLC: Modbus Client LED Op Amp Monitoring

Our Demo circuit: An LED Driver

In this example, we’re going to set up a simple constant current source for an LED using an op amp and verify that we’re doing a good job of maintaining constant current as the voltage supply changes.

How it works

The really short version of how an op amp functions in most applications is simple: the op amp will vary voltage output to attempt to bring the two input terminals to the same voltage. Depending on the circuit configuration, this can result in inverting or non-inverting amplification, signal buffering, and all sorts of other neat things.

In the setup we’ll use, shown below, we set the input to the positive terminal using a couple of potentiometers, so that we can achieve the full range offered by the regulator, but still keep current low to avoid overheating it. The other op amp input terminal is connected above a resistor in line with our LED. This way, it directly measures current as a voltage, ILED*Rsense. The op amp controls the current by varying voltage on the gate of an n-channel mosfet. As a result, by tuning the input voltage on the op amp with our potentiometers, we set our target current. With a sense resistor of 10 ohms and a target current of 300mA, for example, we’d have a sense voltage of 3V. Hence, by setting our input voltage on the op amp to 3V, we get 300mA if the op amp does it’s job (this depends on a few other details).

The beauty of the op amp for this application is that it will maintain even very low current levels, which is very very hard to do by controlling only voltage. The diode’s IV curve is exponential, so dialing it in is very hard, and one reason constant current sources are used. Another good reason is that these characteristics change as the devices heat up. Because the light produced is directly related to current (the number of electrons reaching an LED’s junction is proportional to the number that recombine and produce light), the current is the correct quantity to control.

The circuit

We’ll use a voltage regulator with our pots to keep our op amp input voltage constant as we vary the supply voltage. We’re not showing it here, but our power supply to the op amp is +/-12V. We’ll get around to converting this to single supply at some point. We also throw in a current limiting resistor just in case we rail the op amp (maximum output) so we don’t nuke the LED.

Constant Current source for LED using an op amp

Putting it all together

Now let’s get this breadboarded up. We’ll use a few things we have laying around: a 2N7000 MOSFET, an LM341 op amp, a mic2937 3.3V LDO regulator, a Cree XPE blue LED, and a couple 5k pots. This means that if we have the pots set to get max input (R1=0, R2=5k), we’ll get 3.3V / 10ohm = 0.33A. This puts it comfortably in range of the LED’s spec. We’ll have a maximum supply voltage of 12V. With a drop on the LED of 3.5V at maximum current, we have about 8.5V to work with, ignoring the sense resistor and any FET on resistance. This means 28ohms, so 27 is close enough. Our voltage divider uses a 4.7K and a 22K resistor, so our scaling ratio for out Vs input is about 5.7. At about Vs=6.9V, our input reads within about 50mV of our voltmeter, which we can live with for now and calibrate later if we see fit.

We just lost a variable voltage power supply and the replacement is in the mail, so we’ll muddle through with a fixed 12V source power an adjustable switch-mode power supply. Here what we’re looking at. I didn’t clean up for you.

Our test setup, with the important bits labeled.
Our test setup, with the important bits labeled.

We started taking data, and then realized that the UE9 has the unfortunate limitation that the analog inputs are only +/-5V. We’ve got a T7 with a range of +/-10V, but we find the UE9 makes a more reliable MB TCP server. To get around the input voltage limitation, we’ll throw the inputs into a voltage divider. And, instead of having to remember the scaling, we’ll code it in as an option for the input.  All we need to do is add the ‘scale’ keyword to the options field in the mbtcp table. So, for example, for a 4:1 voltage divider, we’d simply add ‘scale:4’ to the options field and be set. This way we can measure up to about 20V on our 5V input. We go ahead and take care of that:

We set out scaling options to allow the use of a voltage divider with output of scaled values.
We set out scaling options to allow the use of a voltage divider with output of scaled values.

We jump over to our dataviewer, and we can see that our scaling has indeed been reflected in the read values (notice the jump from 5 to 20 on the axis where the scaling changed):

Out scaling change is clearly visible in our data viewer. The value jumps from 5 to 20 where we added in our factor of 4 scaling.
Out scaling change is clearly visible in our data viewer. The value jumps from 5 to 20 where we added in our factor of 4 scaling.

Alright, so on with it. We label our inputs and start with our LED supply voltage at about 7V and our op amp input set at 150mV , giving us about 15mA on the LED. Our opamp tracks nicely, matching voltage on the input.

Supply Voltage Tolerance

The first test is to vary the input voltage. The point of this setup is to compensate for Vs variation while keeping constant current, so this is the important test.  So we vary slowly up to the max, ensuring we retain the same sense voltage, and then come back down again:

Varying supply voltage and maintaining constant current.
Varying supply voltage and maintaining constant current.

We notice that at about 3.25V, the op amp output maxes out our analog voltage input. What this means is that the op amp is trying its hardest to compensate and match input voltages, and is having to increase the output voltage to do so. At some point, it will be unable to do so, and it will saturate or ‘rail’ at its maximum output value. Before we add a voltage divider and scale our input for this value, we can’t see how high it can get, but we expect it to get somewhere near its high rail voltage of ~12V. Now, taking a look at our circuit, we can see that a minimum supply voltage of 3.5V for this current makes sense. We’re putting 3.5V supply in at about 20mA. At this current, the forward voltage on the LED is about 2.75, leaving us 0.75V to work with. Our combined sense and current limiting resistance is about 37ohms. Dividing our 0.75V by 37 ohms gives us 20mA. It all makes sense!

Stability

The next important criterion is stability. At a fixed condition, how stable will the current remain, should we want to use the LED signal for measurement purposes? We peg the current at about 50mA at 12V, a pretty typical condition for our envisioned application. Things look quite nice:

Steady-state current output (shown here as voltage inputs to op amp). The condition here is roughly 50mA at 12V supply voltage.
Steady-state current output (shown here as voltage inputs to op amp). The condition here is roughly 50mA at 12V supply voltage. Very little noise, and systematic. Against the resolution of the ADC, this appears to be measurement error on our analog inputs.

Dumping the data and running some statistics, we come up with the following plot and data:

Statistics on our constant current stability test. Measurement is within the noise of the ADC, so our op amp is certainly doing its job. This comes out to something like 0.01% error. Not bad.

The takeaways are the following:

  • Standard error on all signals are within the resolution of the ADC. Within error, they are constant. This explains why the noise on the input signal and sense measurement are the same.
  • Error is low on all measurements: <0.01%. This is suitable for most measurement techniques.
  • Offset voltage is low, at 660uV. This corresponds well to the specified offset voltage of 1mV for the op amp

Summary

Our op amp did great, as did our modbus client. While we need to adapt our design to single-supply operation, the basics are here and appear to work for our current requirements (pun intended).

Join us next time, as we apply these principles to reading a photodiode, and eventually as we control and measure our photodiode for a detection tool!

Reference

The above makes use of the open source libraries available on github here:

Explanation and installation here: