Aruino + Servo + Ikea Dekad = Freemometer

After some group success with the Continuous Integration build lights, I wanted an "extreme feedback"  way to graphically show our production traffic either through transaction metrics or through server utilization. People understand gauges and alarms so I wanted to incorporate both of those features into some type of device. The end result is a USB controlled dial gauge with real alarm bells and a single red/green LED status light. The total build cost about $53. It contains a text command console that accepts ASCII strings over the COM port.  The command console extends control over the LED, the servo motor and the bell motor to the connected host. 

Hardware Design

I know how to do the basic electronics but nice packaging has always been a problem. Ikea came to the rescue here with the $7.00 Dekad alarm clock with a metal case and alarm bells with an external hammer. The case is pretty good size was originally made for their wind up clock. It is well made and supports dis-assembly and re-assembly without damage. The space between the face and glass has plenty of room for servo arms and LEDs. Three screws hold the back on. The two legs and two bell posts hold the face-plate into the case. The whole clock mechanism and face comes out as a single piece. The clock face is paper stuck to the plate with double sided tape. The paper is 90mm in diameter and the clock face is 80mm in diameter. I was able to draw a new face plate in Microsoft Visio using the Metric page size.

You can the remove the clock hands once the face-plate is out of the case. The back of the face plate acts as a mounting board for the components. The clock mechanism is connected to the faceplate with two screws and can be removed. The clock unit fastens two the face-plate with two screws. I removed all of the back of the clock mechanism except for the sizes required for the mounting screws. This left enough room to hot glue the servo into the clock box. I then mounted the clock box back on the face using the two mounting screws.

This shows an OSEPP Arduino Nano, the dial servo mounted in the mechanism box, a simple transistor switch to run the bell and the original bell motor. The Nano was chosen because it runs on 5V, because it is available at my local Microcenter and because it fits in the AA battery compartment. The Nano has a USB connector for communication and power. The blue servo is the 9g servo that comes with one of the Arduino kits. A Red/Green two wire LED in the face is mounted in a hole I made with a leather punch. The LED is available at Microcenter in the case mod section (NTE30112).

An Arduino Nano can directly drive the LED and the hobby servo motor.  It requires a transistor driver to run the bell motor because the motor draws more power than an Arduino pin can provide. I used the motor circuit that comes in the inventor's kit manual. The only change here is that I needed more current for this motor so I removed the current limiting resistor on the base. The motor comes with a small capacitor that we retain.  You can see in the picture above.
The transistor circuit was built on a small Radio Shack perf-board There are really only 4 signals involved, three on the input side and 3 on the output side with power and ground being common to both.  I was able to lay out the components so that the 4 unique signals run parallel to each other. I could have also used the Sparkfun Opto-isolator breakout board. for a low solder count solution. The board has two connectors:
  • Inputs: Power, Ground and the signal from pin 7. 
  • Outputs: Power , n/c,n/c and the transistor side of the motor. 
I was going to make this a Bluetooth device using the Arduino Mini along with Sparkfun Bluesmurf.  I instead reduced the cost by using the Arduino Nano.  The Arduino Nano and a right angle USB adapter just fit in the old AA battery box. I cut milled a semi-circle in the battery door so it can be shut eve while in use.

There is a lot of space left in this clock with such a small CPU board and alarm bell driver.

Command Processor

Programs can communicate the with the Freemometer command processor over the Virtual COM port associated with Arduino. The command processor accepts ASCII commands making it easy to test the unit with the Arduino IDE console window or through some other serial port program like putty on Windows PCs.  The device communicates at 19200bps.  It supports the following commands
  • h: help
  • ?: help
  • bell ring <0..9>: Run one ringer patterns (0:off, 1:continuous)
  • bell off: sents the ringer to pattern 0, off
  • led red <0..9>: turn on red LED using the pattern (0:off, 1:continuous)
  • led green <0..9>: turn on green LED using the pattern (0:off, 1:continuous)
  • led off: turn off LED
  • servo set <0..100>: move servo maps to maximum range

Sample Gauge Faces

Here are some sample gauge faces drawn with Microsoft Visio.  The file is in the GitHub repository on a metric scale with US paper sizes.

 
I'm using the DangerMeter face for a project status gauge with this software on github.  Now if I could figure out how to query my TFS backlog...


Source Code
This code uses MsTimer2 to support long term activities like bell timeouts and servo motor motion without having to resort to the delay() function. An MsTimer2 event method moves the servo towards its intended destination by spreading the motion over small steps over some longer period of time. It also tracks the bell pattern and LED flash pattern against the current time, advancing the bell and/or the LED through the pattern as needed.  The bell and LED support simple sequences so you can create a variety of patterns to meet different needs or notify of different types of events. There are 10 patterns with  0 as silent and pattern 1 as continuous.

The firmware is available on GitHub and in source form below.


/**
 * download the MsTimer2 library and put it in your arduino workspace's libraries directory.
 * The workspace is where all your arduino projects are.  You may have to create the libraries directory.
 * 
 * Written by Joe Freeman joe+arduino@freemansoft.com
 * http://joe.blog.freemansoft.com/2012/07/aruino-servo-ikea-dekad-freemometer.html
 * 
 * This is the command firmware fro the Freemometer USB controlled gauge, light and alarm built
 * on top of the Ikea Dekad retro clock with external hammer bells. Details are on my blog
 * 
 */

#include <Servo.h> 
#include <MsTimer2.h>

// these two may have to be changed if you swap the connector on the LED
// assumes NTE30112 2-wire bi-color LED that can be purchased at Microcenter in casemod section
// would also work with 3-wire LED
const int PIN_LED_GREEN =  12;    // bias green LED connected to digital pin 
const int PIN_LED_RED =  13;    // bias red LED connected to digital pin 
// servo control pin
const int PIN_BELL = 7;
// tune to your servo.  Mine was 138 degrees with this offset from center.
const int SERVO_START_9 = 38;
const int SERVO_START_10 = 38;
const int SERVO_END_9 = 176;
const int SERVO_END_10 = 176;
const int SERVO_FULL_RANGE = 100;

// Servo library uses Timer1 which means no PWM on pins 9 & 10.
// might as well configure both for servos then.
// Timer 0 used for millis() and micros() and PWM
// Timer 2 available for PWM 8 bit timer or MsTimer2
Servo servo10;
Servo servo9;
// current position
volatile int servoPosition9 = 0;
volatile int servoPosition10 = 0;
// where we want to be
volatile int servoTarget9 = 0;
volatile int servoTarget10 = 0;

// adjust these two for noise and speed
// how many steps per interrupt
int servoSpeedPerStep = 4;  // in degrees
// time between steps
int servoStepInterval = 30; // in milliseconds 

// 
volatile int bellPattern = 0;             // active bell pattern
volatile int  bellCurrentState = 0;        // Current Position in the state array
volatile unsigned long bellLastChangeTime; // the 'time' of the last state transition - saves the millis() value

// support led patterns using same templates as bells
volatile int ledActivePin = PIN_LED_GREEN;
volatile int ledPattern = 0;
volatile int ledCurrentState = 0;
volatile unsigned long ledLastChangeTime;

/*================================================================================
 *
 * bell pattern buffer
 * programming pattern lifted from http://arduino.cc/playground/Code/MultiBlink
 *
 *================================================================================*/

typedef struct
{
  boolean  isRinging;          // digital value for this state to be active (Ring/Silent)
  unsigned long activeTime;    // time to stay active in this state stay in milliseconds 
} stateDefinition;

// the number of pattern steps in every bell pattern 
const int MAX_RING_STATES = 4;
typedef struct
{
  stateDefinition state[MAX_RING_STATES];    // can pick other numbers of slots
} ringerTemplate;

const int NUM_BELL_PATTERNS = 10;
ringerTemplate ringPatterns[] =
{
    //  state0 state1 state2 state3 
  { /* no variable before stateDefinition*/ {{false, 10000}, {false, 10000}, {false, 10000}, {false, 10000}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{true,  10000},  {true, 10000},  {true, 10000},  {true, 10000}}   /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{true , 300}, {false, 300}, {false, 300}, {false, 300}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{false, 300}, {true , 300}, {true , 300}, {true , 300}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{true , 200}, {false, 100}, {true , 200}, {false, 800}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{false, 200}, {true , 100}, {false, 200}, {true , 800}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{true , 300}, {false, 400}, {true , 150}, {false, 400}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{false, 300}, {true , 400}, {false, 150}, {true , 400}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{true , 100}, {false, 100}, {true , 100}, {false, 800}}  /* no variable after stateDefinition*/ },
  { /* no variable before stateDefinition*/ {{false, 100}, {true , 100}, {false, 100}, {true , 800}}  /* no variable after stateDefinition*/ },
};

/*================================================================================
 *
 * Start the real code
 *
 *================================================================================*/
// The setup() method runs once, when the sketch starts
void setup()   {                
  // initialize the digital pin as an output:
  Serial.begin(19200);
  pinMode(PIN_BELL,OUTPUT);
  bell_silence();
  bellPattern = 0;             // active bell pattern
  bellCurrentState = 0;        // Current Position in the state array
  bellLastChangeTime = millis();
  pinMode(PIN_LED_GREEN, OUTPUT);
  pinMode(PIN_LED_RED, OUTPUT);
  led_off();
  // stat the servo code
  servo9.attach(9);  // attaches the servo on pin 9 to the servo object 
  servo10.attach(10);  // attaches the servo on pin 9 to the servo object 
  // put them in the star tposition
  servoPosition9 = SERVO_START_9;
  servoPosition10 = SERVO_START_10;
  // will be on start so set end to be same as start
  servoTarget9 = servoPosition9;
  servoTarget10 = servoPosition10;
  // actually position the servos to their initial position - without delay
  servo9.write(servoPosition9);
  servo10.write(servoPosition10);
  // register function for timer handler at 15ms
  MsTimer2::set(servoStepInterval, process_servos_and_bell);
  MsTimer2::start();

  delay(200);
  servo_10_demo(1);
  //bell_ring_pattern(3);
  

}

// main loop that runs as long as Arduino has power
void loop()                     
{
  const int READ_BUFFER_SIZE = 20;
  char readBuffer[READ_BUFFER_SIZE];
  int readCount = 0;
  char newCharacter = '\0';
  while((readCount < READ_BUFFER_SIZE) && newCharacter !='\r'){
    if (Serial.available()){
      newCharacter = Serial.read();
      if (newCharacter != '\r'){
        readBuffer[readCount] = newCharacter;
        readCount++;
      }
    }
  }
  if (newCharacter == '\r'){
    readBuffer[readCount] = '\0';
    // got a command so parse it
    //    Serial.print("received ");
    //    Serial.print(readCount);
    //    Serial.print(" characters, command: ");
    //    Serial.println(readBuffer);
    process_command(readBuffer,readCount);
  } 
  else {
    // too many characters so start over
  }
}

/*=============================================
 * make stuff happen
 *=============================================*/

// this should move to the interrupt handler like the servo code
void bell_ring(){
  digitalWrite(PIN_BELL,HIGH);          // ring bell
}

// convenience for command
void bell_silence(){
  digitalWrite(PIN_BELL,LOW);          // silence bell
}

// 0 is the all off pattern.  1 is the all on pattern
void bell_ring_pattern(int patternNumber){
    bellLastChangeTime = millis();
    bellCurrentState=0;
    bellPattern = patternNumber;
    check_bell();
}

// 0 is the all off pattern.  1 is the all on pattern
void led_pattern(int pin, int patternNumber){
    ledLastChangeTime = millis();
    ledCurrentState=0;
    ledActivePin = pin;
    ledPattern = patternNumber;
    check_led();
}

// remember we have a two pin red/green led with no ground so we change color by flipping pins
void led_off(){
  digitalWrite(PIN_LED_GREEN, LOW);    // set the LED on
  digitalWrite(PIN_LED_RED, LOW);       // set the LED off
}

void led_green(){
  digitalWrite(PIN_LED_GREEN, HIGH);    // set the LED on
  digitalWrite(PIN_LED_RED, LOW);       // set the LED off
}

void led_red(){
  digitalWrite(PIN_LED_GREEN, LOW);     // set the LED off
  digitalWrite(PIN_LED_RED, HIGH);  // set the LED on
}

void servo_set(int numericParameter){
  servo_set_10(numericParameter);
}

// we just set the target because the intterupt handler actually moves the servo
void servo_set_10(int numericParameter){
  // do bounds checking?
  if (numericParameter <= 0){
    servoTarget10 = SERVO_START_10;
  } 
  else if (numericParameter >= SERVO_FULL_RANGE){
    servoTarget10 = SERVO_END_10;
  } 
  else {
    // calculate percentage of full range
    int range = SERVO_END_10 - SERVO_START_10;
    long multiplicand = range * numericParameter;
    int scaledValue = multiplicand / SERVO_FULL_RANGE;
    int offsetValue = SERVO_START_10 + scaledValue;
    servoTarget10 = offsetValue;
  }
}

/*=============================================
 * interrupt handler section
 *=============================================*/

// Interrupt handler that smoothly moves the servo
void process_servos_and_bell(){
  move_servo(servo10, "S10", servoPosition10, servoTarget10, SERVO_START_10, SERVO_END_10);
  move_servo(servo9,  "S9", servoPosition9, servoTarget9, SERVO_START_9, SERVO_END_9);
  check_bell();
  check_led();
}

// checks the bell pattern
void check_bell(){
  if (bellPattern >= 0){ // quick range check
    if (millis() >= bellLastChangeTime + ringPatterns[bellPattern].state[bellCurrentState].activeTime){
      // calculate next state with rollover/repeat
      bellCurrentState = (bellCurrentState+1) % MAX_RING_STATES;
      bellLastChangeTime = millis();
    }
    // will this cause slight hickups in the bell if it's already ringing or already silent
    if (ringPatterns[bellPattern].state[bellCurrentState].isRinging){
      bell_ring();
    } else {
      bell_silence();
    }
  }
}

void check_led(){
  if (bellPattern >= 0){ // quick range check
    if (millis() >= ledLastChangeTime + ringPatterns[ledPattern].state[ledCurrentState].activeTime){
      // calculate next state with rollover/repeat
      ledCurrentState = (ledCurrentState+1) % MAX_RING_STATES;
      ledLastChangeTime = millis();
    }
    // will this cause slight flicker if already showing led?
    if (ringPatterns[ledPattern].state[ledCurrentState].isRinging){
      if (ledActivePin == PIN_LED_GREEN){
        led_green();
      } else if (ledActivePin == PIN_LED_RED){
        led_red();
      } else {
        led_off(); // don't know what to do so just turn off
      }
    } else {
      led_off();
    }
  }
}

// supports servo movement for multile servos even though prototype only has one
void move_servo(Servo servo, String label, volatile int &currentPosition, volatile int &targetPosition ,int servoMinimum, int servoMaximum){
  // only move the servo or process if we're not yet at the target
  if (currentPosition != targetPosition){
    if (currentPosition < targetPosition){
      currentPosition+=servoSpeedPerStep;
      if (currentPosition>targetPosition){
        currentPosition = targetPosition;
      }
      if (currentPosition>servoMaximum){
        currentPosition = servoMaximum;
      }
    }
    if (currentPosition > targetPosition){
      currentPosition-=servoSpeedPerStep;
      if (currentPosition<targetPosition ){
        currentPosition = targetPosition;
      }
      if (currentPosition<servoMinimum){
        currentPosition=servoMinimum;
      }
    }
    servo.write(currentPosition);
    //    Serial.print(label);
    //    Serial.print(":");
    //    Serial.println(currentPosition);
  }
}

/*=============================================
 * demo code
 *=============================================*/

// runs a short demo on servo 10
void servo_10_demo(int numberOfDemoCycles){
  for (int i = 0 ; i < numberOfDemoCycles; i++){
    led_green();
    servo_set(SERVO_FULL_RANGE);
    delay(1000);        // wait
    led_off();
    bell_ring();
    delay(50);
    bell_silence();
    delay(1000);                  // wait

    led_red();
    servo_set(0);
    delay(1000);                  // wait
    led_off();
    bell_ring();
    delay(50);
    bell_silence();
    delay(1000);                  // wait    
  }
}

/*================================================================================
 *
 * command handler area 
 *
 *================================================================================*/

// first look for commands without parameters then with parametes 
boolean  process_command(char *readBuffer,int readCount){
  // use string tokenizer to simplify parameters -- could be faster by only running if needed
  char *command;
  char *command2;
  char *parameter;
  char *parsePointer;
  //  First strtok iteration
  command = strtok_r(readBuffer," ",&parsePointer);
  command2 = strtok_r(NULL," ",&parsePointer);
  parameter = strtok_r(NULL," ",&parsePointer); // optional parameter

    boolean processedCommand = false;
  if (readCount == 1){
    help();
  } 
  else if (strcmp(command,"led") == 0){
    if (strcmp(command2,"green")==0){
      if (parameter!= NULL){
        int numericParameter = atoi(parameter);
        if (numericParameter < NUM_BELL_PATTERNS && numericParameter >= 0){
          led_pattern(PIN_LED_GREEN, numericParameter);
          processedCommand = true;
        }
      }      
    } 
    else if (strcmp(command2,"red")==0){
      if (parameter!= NULL){
        int numericParameter = atoi(parameter);
        if (numericParameter < NUM_BELL_PATTERNS && numericParameter >= 0){
          led_pattern(PIN_LED_RED, numericParameter);
          processedCommand = true;
        }
      }      
    } 
    else if (strcmp(command2,"off")==0){
      led_pattern(PIN_LED_GREEN, 0);  // pattern 0 is always off
      processedCommand = true;
    }
  } 
  else if (strcmp(command,"bell") == 0){
    if (strcmp(command2,"off")==0){
      bell_ring_pattern(0);
      processedCommand = true;
    } 
    else if (strcmp(command2,"ring")==0){
      if (parameter!= NULL){
        int numericParameter = atoi(parameter);
        if (numericParameter < NUM_BELL_PATTERNS && numericParameter >= 0){
          bell_ring_pattern(numericParameter);
          processedCommand = true;
        }
      }      
    }
  } 
  else if (strcmp(command,"servo")==0){
    if (strcmp(command2,"set") == 0){
      if (parameter != NULL){
        int numericParameter = atoi(parameter);
        if (numericParameter < 0){ 
          numericParameter = 0; 
        }
        else if (numericParameter > SERVO_FULL_RANGE) { 
          numericParameter = SERVO_FULL_RANGE; 
        }
        servo_set(numericParameter);
        processedCommand = true;
      }
    }
  } 
  else {
    // completely unrecognized
  }
  if (!processedCommand){
    Serial.print(command);
    Serial.println(" not recognized");
  }
  return processedCommand;
}

void help(){
  Serial.println("h: help");
  Serial.println("?: help");
  Serial.println("bell ring <0..9>: Run one ringer patterns (0:off, 1:continuous)");
  Serial.println("bell off: sents the ringer to pattern 0, off");
  Serial.println("led red <0..9>: turn on red LED using the pattern (0:off, 1:continuous)");
  Serial.println("led green <0..9>: turn on green LED using the pattern (0:off, 1:continuous)");
  Serial.println("led off: turn off LED");
  Serial.print  ("servo set <0..");Serial.print(SERVO_FULL_RANGE);Serial.println(">: move servo maps to maximum range");
  Serial.flush();
}


// end of file

Comments

  1. This comment has been removed by a blog administrator.

    ReplyDelete

Post a Comment

Popular posts from this blog

Understanding your WSL2 RAM and swap - Changing the default 50%-25%

Installing the RNDIS driver on Windows 11 to use USB Raspberry Pi as network attached

DNS for Azure Point to Site (P2S) VPN - getting the internal IPs