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.

Leave a Reply

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