Power and Programming

The final part involves getting air into this guy. If you just want to poke around with inflating your new tentacle in the most lo-tech and lo-cost way possible, hunt down a cheap sphygmomanometer and cut the pump and air line off to use it as your air supply.

If you want precision control over your tentacle, it'll take a little more time and money. I use a cheap airbrush pump to provide the air and a solenoid valve assembly I found on eBay but any combo of air pump and solenoid valves should do. I use small diameter vinyl tubing and luer fittings to get everything together.

What you'll need to do is hook up the air coming from the compressor to three solenoids and run a tube from each of them to each of the tentacle's chambers. A bleed valve between the compressor and the solenoids lets air gradually leave the system.

Get yourself an Arduino and hook your solenoids to pin 3, 5, and 6. Load the code below on the Arduino.
Download: file
const int pin[] = {6,5,3};
const int pincount = sizeof(pin) / sizeof(*pin); // number of items in pinset

unsigned long pin_time[pincount] = {0}; // 0-255
unsigned long last_cycle_start = 0;
unsigned long cycle_length = 100; // in milliseconds

void check_serial();
void setup() {
  for (int i = 0; i < pincount; i++) {
    pinMode(pin[i], OUTPUT);
    digitalWrite(pin[i], HIGH);
  }
  Serial.begin(9600); // this gives us about 96 updates/sec, assuming 10 bytes/update, which should be more than enough
  last_cycle_start = millis();
  
}

void loop() {
  check_serial();
  // there is a latent bug here; every 50 days, there will be a short cycle. This probably doesn't matter, but here's where you'd fix it if it did.
  unsigned long cur_time = millis();
  unsigned long cycle_pos = cur_time - last_cycle_start;
  if (cycle_pos > cycle_length) { // this is where the bug is.
    last_cycle_start = cur_time;
    for (int i = 0; i < pincount; i++)
      if (pin_time[i] != 0) 
        digitalWrite(pin[i], HIGH);
  } else {
    // cycle_length can't be that long; in fact, it can't be longer than an int.
    int cv = cycle_pos * 256 / cycle_length;
    
    for (int i = 0; i < pincount; i++)
      digitalWrite(pin[i], (cv >= pin_time[i]) ? LOW : HIGH);
  }
  delay(10);
  //Serial.print('.');
}

void handle_command(char cmd, int reg, int value) {
  switch(cmd) {
    case 'c':
      // set cycle time.
      if (value <= 0) {
        Serial.println("!Cycle length negative");
        return;
      }
      cycle_length = value;
      break;
    case 'p':
      if (reg > pincount || reg < 1) {
        Serial.println("!Invalid pin no");
        return;
      } else if (value < 0 || value > 256) {
        Serial.println("!Pin duty cycle out of range");
        return;
      }
      pin_time[reg-1] = value;
      break;
    default:
      Serial.println("!Invalid command");
      return;
  }
  Serial.println("+OK");
}
      
      

void check_serial() {
  // protocol: lines containing command byte, ascii register number, '=', new value, then newline.
  // commands are:
  //  - c: cycle time, in ms. No register number applies.
  //  - p: set pin duty cycle. Register number is pin number (1-n). Value is from 0-256, where 256 is fully on.
  // response is either '!' followed by an error message, or '+OK', optionally followed by a response. No commands currently have a response.
  // Examples:
  //   c=1000  // set cycle time to 1s
  //   p1=0    // turn off pin 1
  //   p2=256  // turn on pin 2
  //   p3=128  // set pin 3 to half on
  
  const int
    ST_CMD = 0,
    ST_REGNO = 1,
    ST_VALUE = 2,
    ST_ERR = 3;
  static const char* errMsg = NULL;
  static int regno = 0;
  static int value = 0;
  
  static int state = 0;
  static char cmd = 0;
  
  // check if there's a serial byte waiting, and handle it.
  while (Serial.available() > 0) {
    int inByte = Serial.read();
    switch(state) {
      case ST_CMD:
        if (inByte == '\n' || inByte == '\0')  {
          // invalid command
          Serial.println("!No command found");
          break;
        }
        cmd = inByte;
        state = ST_REGNO;
        regno = 0;
        break;
      case ST_REGNO:
        if (inByte == '=') {
          state = ST_VALUE;
          value = 0;
          break;
        } else if (inByte >= '0' && inByte <= '9') {
          // no bounds checking. Regno can overflow
          regno = regno * 10 + inByte - '0';
        } else {
          state = ST_ERR;
          errMsg = "Expected digit or '=' in register no";
        }
        break;
      case ST_VALUE:
        if (inByte == '\r') {
          // windows box.
          break;
        } if (inByte == '\n') {
          state = ST_CMD;
          handle_command(cmd, regno, value);
          regno = 0;
          break;
        } else if (inByte >= '0' && inByte <= '9') {
          // no bounds checking. Regno can overflow
          value = value * 10 + inByte - '0';
        } else {
          state = ST_ERR;
          errMsg = "Expected digit or newline in value";
        }
        break;
      case ST_ERR:
        if (inByte == '\n') {
          Serial.print("!");
          Serial.println(errMsg);
          state = ST_CMD;
        }
        break;
    }
  }
}
Now, the Arduino is ready to PWM the solenoid valves (on pins 6, 5, and 3) in response to a signal over USB. The following Processing sketch will handle the interface.
Download: file
import controlP5.*;
import processing.serial.*;

Serial myPort; 
String p1 = "p1=0\n";
String p2 = "p2=0\n";
String p3 = "p3=0\n";

ControlP5 cp5;
int myColor = color(0,0,0);

int p1slider = 0;
int p2slider = 0;
int p3slider = 0;
int Frequency = 0;
Slider abc;

void setup() {
  size(450,400);
  noStroke();
  cp5 = new ControlP5(this);

myPort = new Serial(this, Serial.list()[0], 9600);
     
  // add a vertical slider
  cp5.addSlider("p1slider")
     .setPosition(100,50)
     .setSize(200,20)
     .setRange(0,256)
     .setValue(0)
     .setDecimalPrecision(0)
     ;
     
  cp5.addSlider("p2slider")
     .setPosition(100,100)
     .setSize(200,20)
     .setRange(0,256)
     .setValue(0)
     .setDecimalPrecision(0)
     ;

  cp5.addSlider("p3slider")
     .setPosition(100,150)
     .setSize(200,20)
     .setRange(0,256)
     .setValue(0)
     .setDecimalPrecision(0)
     ;
     
  cp5.addSlider("Frequency")
     .setPosition(100,200)
     .setSize(200,20)
     .setRange(10,250)
     .setValue(50)
     .setDecimalPrecision(0)
     ;
     
  cp5.addButton("off1")
     .setValue(0)
     .setPosition(315,50)
     .setSize(30,20)
     ;
 
   cp5.addButton("off2")
     .setValue(0)
     .setPosition(315,100)
     .setSize(30,20)
     ;

   cp5.addButton("off3")
     .setValue(0)
     .setPosition(315,150)
     .setSize(30,20)
     ;

   cp5.addButton("AllOff")
     .setValue(0)
     .setPosition(315,200)
     .setSize(30,20)
     ;

   cp5.addButton("Dia1")
     .setValue(0)
     .setPosition(100,250)
     .setSize(30,20)
     ;

   cp5.addButton("Dia2")
     .setValue(0)
     .setPosition(150,250)
     .setSize(30,20)
     ;

   cp5.addButton("Dia3")
     .setValue(0)
     .setPosition(200,250)
     .setSize(30,20)
     ;

  // reposition the Label for controller 'slider'
  cp5.getController("p1slider").getValueLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  cp5.getController("p1slider").getCaptionLabel().align(ControlP5.RIGHT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  cp5.getController("p2slider").getValueLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  cp5.getController("p2slider").getCaptionLabel().align(ControlP5.RIGHT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  cp5.getController("p3slider").getValueLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  cp5.getController("p3slider").getCaptionLabel().align(ControlP5.RIGHT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  cp5.getController("Frequency").getValueLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
  cp5.getController("Frequency").getCaptionLabel().align(ControlP5.RIGHT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
}

void draw() {
  
  fill(150);
  rect(0,0,width,height);
  
  //cp5.getController("p1slider").setValue(0);
  
  //add diagonal buttons, off buttons, all off button
  
  String p1 = "p1=" + p1slider + "\n";
  String p2 = "p2=" + p2slider + "\n";
  String p3 = "p3=" + p3slider + "\n";
  String freq = "c=" + Frequency + "\n";
    
  if(cp5.getController("off1").isMousePressed() == true ){
  p1slider = 0;
  cp5.getController("p1slider").setValue(0);
  }
  
  if(cp5.getController("off2").isMousePressed() == true ){
  p2slider = 0;
  cp5.getController("p2slider").setValue(0);
  }
  
  if(cp5.getController("off3").isMousePressed() == true ){
  p3slider = 0;
  cp5.getController("p3slider").setValue(0);
  }
  
  if(cp5.getController("AllOff").isMousePressed() == true ){
  p1slider = 0;
  p2slider = 0;
  p3slider = 0;
  cp5.getController("p1slider").setValue(0);
  cp5.getController("p2slider").setValue(0);
  cp5.getController("p3slider").setValue(0);
  }
  
  if(cp5.getController("Dia1").isMousePressed() == true ){
  p1slider = 256;
  p2slider = 256;
  p3slider = 0;
  cp5.getController("p1slider").setValue(256);
  cp5.getController("p2slider").setValue(256);
  cp5.getController("p3slider").setValue(0);
  }
  
  if(cp5.getController("Dia2").isMousePressed() == true ){
  p1slider = 256;
  p2slider = 0;
  p3slider =256;
  cp5.getController("p1slider").setValue(256);
  cp5.getController("p2slider").setValue(0);
  cp5.getController("p3slider").setValue(256);
  }
  
  if(cp5.getController("Dia3").isMousePressed() == true ){
  p1slider = 0;
  p2slider = 256;
  p3slider = 256;
  cp5.getController("p1slider").setValue(0);
  cp5.getController("p2slider").setValue(256);
  cp5.getController("p3slider").setValue(256);
  }
    
  myPort.write(p1);
  myPort.write(p2);
  myPort.write(p3);
  myPort.write(freq);
  
}
This thread contains the above code and some experiments for getting the UI and control system together. It might help you get a clearer idea of what's going on if you find yourself stuck.
This guide was first published on Sep 28, 2013. It was last updated on Sep 28, 2013. This page (Power and Programming) was last updated on Sep 20, 2019.