Thursday, January 10, 2013

Build your own Morrow Project CBR Kit





Assuming you want to make a prop vastly similar to this one, this post assembles all the parts, links and diagrams I can provide.



Physical Layer:

This prop consists of a shell with knobs and lights fitted on it, and electronics inside to make the box light up and make noise.


THE SHELL:

The top or "faceplate" was modeled in 3d and printed at Shapeways:



That links to my Shapeways store, where you can purchase a print from them, or download the model for free.

The bottom or "case" was sculpted and cast.  The mold is horrible but I still have the original sculpt and if there was interest I could do a run.  Otherwise, there is a 3d model:
















That also links to my Shapeways store, and I must hasten to add I have not tried to print that particular model.  You can take your chances, or download the mesh free from there and adapt it as a guide or even make a pepakura out of it.

If you do chose to go with the printed option, you will need to fill the battery panel in the back.  The print has a cut-out sized to fit a AA battery pack such as this one from Sparkfun Electronics.

The circuitry was designed around a 9v power supply, though, so it would probably be smarter to fill the hole with sheet styrene.



PANEL ITEMS:

1) Old Soviet Stock ILC1-9/8 vacuum fluorescent display.
2) Dialight indicator bulbs
2) miniature 6-turn, 1-deck rotary switches.
1) small button
1) chrome LED bezel.

The VFD can be found on eBay for about $15 a pair. 

I purchased the rotary switches at Allelectronics, (they no longer have them), but very similar switches can be found at Digikey.

The LED bezel is very common and standard.  I got mine at Radio Shack.

The Dialights I found at Al Lasher's electronics.  I believe they are a standard size but I don't have a part number for them.  There was a bulb holder which I omitted; for this project I suspended an LED in a hot-glue plug, and epoxied the barrel of the lamp holder to the CBR Kit panel.


The knobs, on the other hand, were also a printed item.  They can be found here and here:











For both knobs, I bought the cheapest 1/8" shaft knobs at Allelectronics and broke them apart to get at the pre-threaded brass insert.  Then I drilled out an access hole in the printed knobs for the hex key, and superglued the insert inside.


So, obviously, building the case will take a couple of steps.  There's clean-up of 3d prints, fitting the parts, building/finding knobs and other hardware, making some cuts and trims so all the parts fit together, then painting.

The boxes were sprayed with Krylon primer, then Tamiya Olive Drab.



DATA PLATE:


This is the actual artwork.  Print it out at approx 1 cm x 4 cm on waterslide decal paper, and attach the decal to a thin sheet of tin or aluminium.


(There are also a couple of binding posts on the bottom of the box for hooking up an M42 external alarm horn or similar.  I bought a pair of small screw terminals that looked like they fit at, again, Radio Shack, but I have no idea of the parts number.)




Electrical Layer:


The above circuit diagram was drawn after-the-fact and may not be completely accurate.  First, though, a parts list:

1) ATMEGA328p AVR (aka the same chip that's in many Arduinos and compatibles)
1) Supertex HV5812 (a 20-bit high-power shift register/latching driver)
1) RECOM RY9245 (9v to 24v DC-DC converter)
1) 7805 in TO5 package (the venerable 5v voltage regulator chip)
1) TIP120 in TO5 package (power Darlington)
3) Superbright LED's (5mm, 20ma approx, two reds and one amber)
1) 16MHz ceramic resonator
1) 8 ohm speaker
2) 1 - 10 uf electrolytic capacitors (for balancing the 7805...people use a lot of different values here)
8) 220 ohm resistors (for LED ballast, and for making a resistor ladder)
1) 10 ohm resistor (pick one low enough so the VFD works but high enough you don't see the filament glowing)
1) 1 meg resistor (or therabouts; used to make an R/C circuit as part of the capacitance sensor).



But let's split this up in more manageable chunks.

The first chunk is a standard 7805-based 5v regulator, that turns our battery into something the other electronics will want to live with.  These are dead simple and there are sketches for them all over the web.  I just built one with my niece and I had HER look up the circuit diagram.

I always include a small LED (with ballast resistor, natch) in my power supplies, just so I can tell everything is working.


The next chunk is an Arduino compatible.  I could have done this with an Arduino Mini or Nano but chose to make a minimal Arduino.  The only required part is the external crystal; either a crystal with two capacitors, or a ceramic resonator (the capacitors are included inside).

You need to run power and ground (pins 7-8), tie AREF and AVcc to supply (actually, should really put a low-pass filter on the AVcc pin instead), and ground pin 22 as well.

Most people put a tiny ceramic cap right on the power pins to smooth out any power fluctuations.  Many Arduinos and compatibles also include an LED (and ballast resistor) on digital I/O 13 -- if nothing else, that will confirm your Arduino is properly installed by running the default "blink" sketch.

In the case of my Morrow Project CBR, much of the programming was done installed.  Thus, the last and most complicated part of wiring the Arduino compatible; including the 6-pin programming header.  For that, you need to check the circuit diagram and run wires from RST, CLK, MOSI, MISO, Vcc and Ground to the correct places on the ICSP Header.

At that point, your compatible can then either be programmed via an FTDI cable or with something like the Adafruit ISP I use.



I'll pass briefly over the LED and speaker section.  The LED's are plugged into I/O ports that support PWM.  You can do software PWM but it is more messy.  The speaker was originally on a PWM port as well but the sound library I'm using now can be used on any I/O pin.  The speaker is driven from a power Darlingon in lieu of a proper amp; a better design would be an LM386-based power amp.  The Arduino can handle a small speaker on its own but it isn't very loud and it can cause nasty with back EMF.



The fun part is the VFD.  The basic idea of a vacuum fluorescent display is a heated filament releases free electrons.  Those are sucked towards the high-voltage anode by way of the grid, which disperses them.  The surface of the anode is covered in phosphorescent material which lights up when the electrons hit.  That means you have to connect both anode and grid to a high-voltage supply, and the filament (cathode) to a low-voltage supply, in order for it to light.

What is around these days is largely old stock from the glory days of the Soviet Union, although there are still modern ones manufactured, especially in China.  You will need to look up the correct voltages for the model you are using.

Technically, the filament needs to be a current-regulated low-voltage AC source.  Practically speaking, in many small VFD's you can get away with putting a resistor in circuit with your power supply.  Especially if you chose to go off the regulated 5v end, like I did.

The anode voltage is 30-60 volts (don't exceed 40 for more than a fraction of a second at a time, though).  Again, you can light them with as little as 9v, although they won't be very bright.  So for testing purposes, it is enough to attach ground, a resistor to filament, then brush multiple leads with a 9v battery until you get a light.

In the case of the Medkit, this is all I did; I found the right anodes for "0" and I tied the first four grids to 9v as well, making it display "0000."  That way, I could have something on the display without having to put all that other electronics inside the box.

Assuming you want more, however, you need to switch that voltage.  Like many display devices, the VFD is a multiplexed device.  In the case of the 7-segment ILC1-9/8, the seven segments (and decimal point) form one side of the matrix, and the eight digits form the other.  To display actual words, you need to write one letter at a time very quickly, over and over at a rate so high the human eye barely detects a flicker.

The simplest tool for this is a dedicated driver chip.  The Supertex HV518/5812 are designed specifically for use with VFDs and similar devices (like Nixie tubes).  The HV5812 is a latching shift register.  What this means is, you shift in a value, and until you command it to unlatch, it will hold those selected pins high for as long as you need.

This ties up a mere four I/O pins on the Arduino; clock, data, latch, and blanking.  Since the software was custom, and I wasn't using any of the built-in serial output functionality, any arbitrary four I/O pins could be used.

Now, this will work on 9v.  It will work even better on a pair of 9v batteries tied together (the HV input of the HV5812 is quite independent of the supply voltage to it or any other part of the circuit.  Tie all the grounds together, however!)

However, that takes up space.  And is still not that bright, when you are multiplexing (because each digit is only on for a fraction of the total time).  So a better solution is a high-voltage power supply.  I chose to try out, and go with, a RECOM DC-DC converter; a pre-packaged solution.  A better answer is building a boost converter with an inductor, fast switching diode, and using the Arduino itself as a pulse source.

Read the tech entry on Lady Ada's Ice Tube clock for details on how to accomplish this.

Anyhow.  The RECOM and the HV5812 were both available at Mouser.


Of course you still have to solder it.  I chose to make my main circuit on a piece of strip board:


The LEDs were prepped with their ballasts, and the resistor ladder for the rotary switch was soldered directly to the back of the switch:


Then the main circuit board and display were hot-glued to the faceplate and a wire harness built in place to the various elements:


This meant the only wires that had to be tucked into the case every time it was opened were those going to the speaker, battery, and injector.




Software Layer:


My software is garbage.  I know it is.  I'd really suggest you write your own.  Possibly study mine to see how I got the thing to work, but even then...!

The speaker sounds and the capacitance sensor were both Arduino library functions; you can find them easily at the Playground. 


VFD CODE:

My hand-rolled VFD code was split into basically four functions.  The primary function, as seen below, parses the current contents of the character string "wordBuffer" and writes it to the display.


/////////////////VFD DISPLAY WRITE TO DISPLAY

void write_word()                                      // this function writes a word (from "wordBuffer", which
{                                                     // as an indexed array of char with a null leading
  for (incWord = 1; incWord < 9; incWord ++)          // character.
  {
    digitalWrite(hvBlanking, HIGH);
    charBuffer = wordBuffer[incWord-1];
    look_up(charBuffer);
    for (incSegment = 1; incSegment < 10; incSegment ++)
    {
      if (bitRead(bBuffer, (incSegment - 1)))
      {
        digitalWrite(hvData, HIGH);
      }
      else
      {
        digitalWrite(hvData, LOW);
      }
      shift_one(1);
    }
    digitalWrite(hvData, LOW);
    shift_one(3);

    for (incDigit = 1; incDigit < 9; incDigit ++)
    {
      if (incDigit == incWord)
      {
        digitalWrite(hvData, HIGH);
      }
      else
      {
        digitalWrite(hvData, LOW);
      }
      shift_one(1);
    }
    digitalWrite(hvBlanking, LOW);      // the way the blanking works, display is blanked while one digit is
    delayMicroseconds(500);            // read in.  the delay here is the time the latched digit is held on.
  }                                     // if I used direct pin write the duty cycle would be higher.
}

 The above function is called via "write_word()" and calls in turn "look_up(charBuffer)";

byte look_up(char thisChar)                   //this function uses ASCII value of character as index to lookup
{                                            //table and returns binary byte of segments to be lit
  //  lookupBuffer = lookupTable[thisChar - 48]; 
  bBuffer = lookupTable[thisChar - 48];
}
Which in turn performs its magic via a look-up table way up in the constants and assigns:

const byte lookupTable[50] = {
  B10110111, B00000110, B01110011, B01010111, B11000110, B11010101, B11110100,
  B00000111, B11110111, B11000111,
  B00001000, B01000000, B01110000, B01010000, B01010100, B01100011, B00000000,
  B11100111, B11110100, B10110001, B01110110, B11110001, B11100001, B11010111, B11100110,
  B10100000, B00110110, B11110010, B10110000, B01100101, B10100111, B10110111, B11100011, B11000111,
  B10100001, B11010101, B11110000, B10110110, B00110100, B00110101, B11100110, B11010110, B01110011};
// punctuation for : ; < + > ? @ are . - < = > ? space


The lookup table returns a binary truth table for each character look_up() wants to look up.  That's what converts the original ASCII in "wordBuffer" into information about which segment needs to be lit.

write_word() performs this operation eight times, one time for each character place in the wordBuffer string, and for each letter then clocks a 20-bit word into the HV5812.  After each character, it blanks the HV5812 for a short interval to return the display to dark and thus make the distinction between characters clear (otherwise you get a lot of bleed).

My space character is "@" (as I am only using the capital letters and numbers out of the ASCII table, and the punctuation characters occur between those.  The code expects a full 8-character string, thus, to display "Sarin" you would write "wordBuffer = "SARIN@@@";"

write_word() isn't like a PWM function.  You have to keep calling it at frequent intervals for as long as you want something to be displayed (and if you stop calling it on anything other than a blank wordBuffer -- aka "@@@@@@@@" -- you will have a garbage character glowing way too brightly on the far right.)



OTHER CODE:

And with all that said, here's the whole sorry mess (I realized while writing this post I'd accidentally deleted all my copies of the code while making room for uploading new mixdowns for a client.  Fortunately, Dropbox allows you to recover old files!)

You might note the only thing that happens in the main program loop is reading the selector switch (read_rotary() does an analog read off the resistor ladder, and then calls a function for whichever state the box should then be in.  When the switch is left alone, it basically just turns each state function into a main() with a button check.

Unfortunately that didn't work for the simulated alarm sequence, so that is a pearls-on-a-string set of functions each which pass to the next function in line.  And each function calls special_rotary() instead of read_rotary(), which returns TRUE only if you've moved out of the alarm setting, and otherwise doesn't return all the way back to the base of the current-state loop.

Like I said: messy.
#include <CapacitiveSensor.h>

#define hvBlanking 8
#define hvClock 7
#define hvStrobe 6
#define hvData 5

#define lightRad 9
#define lightChem 1
#define speaker 11
#define rotary 5 //aka pin 28
#define lightInjectable 10
#define button 4
#define injector 2
#define lightInjector 0

int incWord = 0;
int incDigit = 0;
int incSegment = 0;
int shifting = 0;
int incMaster = 0;
int rotaryRead = 0;
int rotaryState = 0;
long incMasterClock = 0;
long incElapsed = 0;
int incTimes = 0;
int oldRotaryState = 0;
int injectable = 0;
int incInjectableLight = 0;
int clicks = 0;
int simFlag = 0;
int index = 0;
int silent = 0;
int pressed = 0;
int threat = 1;
int rads = 0;
int totalRads[9] = {
  0, 0, 0, 0, 0, 0, 0, 0, 0};
int tempRads = 0;
int tempRads2 = 0;
int tempRads3 = 0;
int tempRads4 = 0;
int tempRads5 = 0;
int tempRads6 = 0;
int radRate = 34;
int radLight = 255;
int doses = 6;
int numIndex = 0;

int incBuffer = 0;
String nibbleBuffer = "@@@@";
String wordBuffer = "@@@@@@@@";
String wordBuffer2 = "00000000";
char charBuffer;
byte bBuffer = B11101111;

const byte lookupTable[50] = {
  B10110111, B00000110, B01110011, B01010111, B11000110, B11010101, B11110100,
  B00000111, B11110111, B11000111,
  B00001000, B01000000, B01110000, B01010000, B01010100, B01100011, B00000000,
  B11100111, B11110100, B10110001, B01110110, B11110001, B11100001, B11010111, B11100110,
  B10100000, B00110110, B11110010, B10110000, B01100101, B10100111, B10110111, B11100011, B11000111,
  B10100001, B11010101, B11110000, B10110110, B00110100, B00110101, B11100110, B11010110, B01110011};
// punctuation for : ; < + > ? @ are . - < = > ? space

CapacitiveSensor   cs_3_4 = CapacitiveSensor(3,4);

////////////////////////////

void setup()
{
  pinMode(hvBlanking, OUTPUT);
  pinMode(hvClock, OUTPUT);
  pinMode(hvStrobe, OUTPUT);
  pinMode(hvData, OUTPUT);

  pinMode(speaker, OUTPUT);
  pinMode(lightInjectable, OUTPUT);
  pinMode(lightRad, OUTPUT);
  pinMode(lightChem, OUTPUT);
  pinMode(lightInjector, OUTPUT);
  pinMode(injector, OUTPUT);

  digitalWrite(lightInjectable, HIGH);
  digitalWrite(lightRad, HIGH);
  digitalWrite(lightChem, HIGH);
  digitalWrite(lightInjector, HIGH);
  digitalWrite(injector, HIGH);

  digitalWrite(hvBlanking, LOW);
  digitalWrite(hvClock, LOW);
  digitalWrite(hvStrobe, LOW);
  digitalWrite(hvData, LOW);

}



///////////////////

void loop()
{
  read_rotary();
}



////////////////SELECTOR POSITION "OFF"

void off() 
{
  incMasterClock ++;
  if (incMasterClock == 29940)
  {
    analogWrite(lightInjectable, 240);  
  }
  if (incMasterClock > 30000)
  {
    digitalWrite(lightInjectable, HIGH);
    incMasterClock = 0;
  }
}



////////////SELECTOR POSITION "TEST"

void test()
{
  write_word();
  read_button();

  if (pressed == 1 && clicks == 0) // detect button and use master for blink or goto elapsed time
  {
    clicks = 1;
    pressed = 0;
  }
  if (clicks == 1)
  {
    incMasterClock ++;
    incElapsed = 0;
  }  
  else
  {
    incMasterClock = 0;
    incElapsed ++;
  }

  if (incMasterClock == 1 || incMasterClock == 31) //double blink
  {
    digitalWrite(lightChem, LOW);
    digitalWrite(lightRad, LOW);
    analogWrite(lightInjectable, 250);
    tone(speaker, 400, 15);
  }
  if (incMasterClock == 2 || incMasterClock == 32)
  {
    digitalWrite(lightInjectable, HIGH);
  }
  if (incMasterClock == 6 || incMasterClock == 36)
  {
    digitalWrite(lightChem, HIGH);
    digitalWrite(lightRad, HIGH);
  }
  if (incMasterClock == 100)
  {
    incMasterClock = 0;
    clicks = 0;
  }

  if (incElapsed < 600)
  {  
    wordBuffer = "TEST@@@@";  //display this up until self test begins
  }
  else
  {
    switch(incElapsed/70)   //prelim display to self test
    {
    case 8:
      wordBuffer = "@@@@@@@@";
      break;
    case 10:
      wordBuffer = "SLF@TEST";
      break;
    case 11:
      wordBuffer = "@@@@@@@@";
      break;
    case 12:
      wordBuffer = "SLF@TEST";
      break;
    case 13:
      wordBuffer = "@@@@@@@@";
      break;
    case 14:
      wordBuffer = "SLF@TEST";
      break;
    case 15:
      wordBuffer = "@@@@@@@@";
      break;
    }
  }
  if (incElapsed < 8000)
  {
    if (incElapsed > 1200 && incElapsed < 6000 && (incElapsed % 4) == 0) //make up numbers at ap 8x second
    {
      create_random(4);
      switch(incElapsed/400)  // change the header every few seconds
      {
      case 3:
        nibbleBuffer = "GEI@";
        break;
      case 4:
        nibbleBuffer = "CHE@";
        break;
      case 5:
        nibbleBuffer = "PU@@" ;
        break;
      case 6:
        nibbleBuffer = "RGX@";
        break;
      case 7:
        nibbleBuffer = "DDT@";
        break;
      case 8:
        nibbleBuffer = "INJ@";
        break;
      case 9:
        nibbleBuffer = "NAN@";
        break;
      case 10:
        nibbleBuffer = "LHC@";
        break;
      default:
        nibbleBuffer = "SYS@";
      }
      concenate();
    }
  }
  if (incElapsed > 6000)
  {
    incElapsed = 60000;
    wordBuffer = "TST@GOOD"; //display this forever
  }
}


///////SELECTOR POSITION "ON" OR "SILENT"

void armed()
{
  incMasterClock ++;
  if (incMasterClock < 600)  //displays selector position, then after interval blinks it slowly
  {
    if (silent == 1)
    {
      wordBuffer = "SILENT@@";
    }
    else
    {
      wordBuffer = "ON@@@@@@";
    }
  }
  else
  {
    wordBuffer = "@@@@@@@@";
  }

  if (incMasterClock > 800)
  {
    incMasterClock = 400;
  }
  if (injectable == 1)
  {
    light_injectable_blink(24);
  }
  else
  {
    digitalWrite(lightInjectable, HIGH);
  }


  read_button();
  if (pressed == 1 && clicks == 0)   //detects short or long test button press
  {
    clicks = 1;
    pressed = 0;
    randomSeed(incMasterClock);
  }
  if (clicks == 1)
  {
    incElapsed ++;
  }
  if (incElapsed > 120)
  {
    if (pressed == 1)
    {
      simFlag = 2;
    }
    else
    {
      simFlag = 1;
    }
    clicks = 0;
    incElapsed = 0;
    sim_start();
  }
  write_word();
}


////////////////SELECTOR POSITION INJECT/TREAT

void inject()
{
  injectable = 0;
  if (doses > 0)
  {
    analogWrite(lightInjectable, 250);
  }
  else
  {
    light_injectable_blink(40);
  }
  if (incTimes < 300)
  {
    wordBuffer = "INJECT@@";
    incTimes ++;
  }
  if ((incTimes == 300))
  {
    switch (doses)
    {
    case 6:
      wordBuffer = "6@DOSES@";
      break;
    case 5:
      wordBuffer = "5@DOSES@";
      break;
    case 4:
      wordBuffer = "4@DOSES@";
      break;
    case 3:
      wordBuffer = "3@DOSES@";
      break;
    case 2:
      wordBuffer = "2@DOSES@";
      break;  
    case 1:
      wordBuffer = "1@DOSE@@";
      break;
    case 0:
      wordBuffer = "REFILL@@";
      break;

    }
  }
  write_word();


  if (incTimes == 300)
  {
    read_button();
    long total1 =  cs_3_4.capacitiveSensor(30);
    if (total1 > 120  || pressed == 1)
    {
      total1 = 0;
      pressed = 0;
      if (doses > 0)
      {


        digitalWrite(lightInjector, LOW);
        wordBuffer = "INJCTING";
        doses --;

        for (incMasterClock = 0; incMasterClock < 300; incMasterClock ++)
        {
          write_word();

          if (incMasterClock > 0 && incMasterClock < 100)
          {
            light_injectable_blink(6);
          }
          if (incMasterClock == 40)
          {
            digitalWrite(injector, LOW);
          }
          if (incMasterClock == 80)
          {
            wordBuffer = "@@@@@@@@";
            digitalWrite(injector, HIGH);
            digitalWrite(lightInjector, HIGH);
            digitalWrite(lightInjectable, HIGH);
          }

        }
      }
      else
      {
        wordBuffer = "ERROR@@@";
        digitalWrite(injector, HIGH);
        digitalWrite(lightInjector, HIGH);
        digitalWrite(lightInjectable, HIGH);
        tone(speaker, 40, 600);
        for (incMasterClock = 0; incMasterClock < 200; incMasterClock ++)
        {
          write_word();
        }
      }

    }
    else
    {
      //      analogWrite(lightInjectable, 255);
      digitalWrite(lightInjector, HIGH);
      digitalWrite(injector, HIGH);
    }

  }
}



//////////////display routine at beginning of simulated threat (used for both chem and rad)

void sim_start()
{
  for (incMasterClock = 0; incMasterClock < 800; incMasterClock ++)
  {
    special_rotary();
    if (clicks > 50)
    {
      clicks = 0;
      return;
    }

    if (incMasterClock == 0 || incMasterClock == 200 || incMasterClock == 400)
    {
      if (simFlag == 1)
      {
        digitalWrite(lightChem, LOW);
        wordBuffer = "BIOCHEM@";
      }
      else
      {
        digitalWrite(lightRad, LOW);
        wordBuffer = "NUCLEAR@";
      }
    }
    if (incMasterClock == 2 || incMasterClock == 202 || incMasterClock == 402)
    {
      digitalWrite(lightChem, HIGH); //turn off lights quick so they only blink
      digitalWrite(lightRad, HIGH);
    }
    if (incMasterClock == 100 || incMasterClock == 300 || incMasterClock == 500)
    {
      wordBuffer = "SIMULATD";
    }
    write_word();
  }

  if (silent == 1)
  {
    wordBuffer = "SILENT@@";
  }
  else
  {
    wordBuffer = "ON@@@@@@";
  }

  for (incMasterClock = 0; incMasterClock < (random(8, 14) * 100); incMasterClock ++)
  {
    write_word();   //holds in simulated ready status for random interval
    special_rotary();
    if (clicks > 50)   //debouncing switch when turned past "silent" to end sim
    {
      clicks = 0;
      return;
    }
  }

  incMasterClock = 0;
  incElapsed = 0;
  threat = random(1, 20);  // clean-up and branch
  if (simFlag == 1)
  {
    sim_chem();
  }
  else
  {
    sim_rad();
  }
}


////////////////CORE ROUTINE FOR SIMULATED RADIOLOGICAL ATTACK

void sim_rad()  // rad written!
{
  noTone(speaker);
  radRate = 36;


  //////////first phase of rad; type, reading, and warning alternate

  for (incTimes = 1; incTimes < 4; incTimes ++)
  {
    for (incElapsed = 6; incElapsed < 18; incElapsed ++) ///rough number of display events
    {
      special_rotary();  //moved test to higher loop
      if (clicks > 50)
      {
        clicks = 0;
        return;
      }
      if (incElapsed % 6 == 0)
      {
        switch (threat)
        {
        case 1:
          wordBuffer = "ALPHA@@@";
          break;
        case 2:
          wordBuffer = "BETA@RAY";
          break;
        case 3:
          wordBuffer = "GAMMA@@@";
          break;
        case 4:
          wordBuffer = "NEUTRON@";
          break;
        case 5:
          wordBuffer = "ALDO@RAY";
          break;
        case 6:
          wordBuffer = "CAESM137";
          break;
        case 7:
          wordBuffer = "COBALT60";
          break; 
        case 8:
          wordBuffer = "AMERICIM";
          break;  
        case 9:
          wordBuffer = "CALIFRNM";
          break;
        case 10:
          wordBuffer = "IRIDM192";
          break;         
        case 11:
          wordBuffer = "PLUTONIM";
          break;   
        case 12:
          wordBuffer = "STRONTIM";
          break;   
        case 13:
          wordBuffer = "RADIUM@@";
          break;
        case 14:
          wordBuffer = "RADON@@@";
          break;     
        case 15:
          wordBuffer = "POLONIUM";
          break;      
        case 16:
          wordBuffer = "GAMMA@@@";
          break;      
        case 17:
          wordBuffer = "NEUTRON@";
          break;         
        case 18:
          wordBuffer = "ALPHA@@@";
          break;
        case 19:
          wordBuffer = "BETA@@@@";
          break;
        case 20:
          wordBuffer = "CAROLINM";
          break;        
        }
      }
      if (((incElapsed % 6 > 0 ) && (incElapsed % 6 < 3)) || incElapsed % 6 > 3) //random on twice a cycle
      {
        switch (incTimes) // larger leading digits over time
        {
        case 1:
          wordBuffer = "@@@@1678";
          break;
        case 2:
          wordBuffer = "@@@45678";
          break;
        case 3:
          wordBuffer = "@1045678";
          break;
        default:
          wordBuffer = "@@@@@@@@";
        }
      }
      if (incElapsed % 6 == 3)
      {
        switch (incTimes)
        {
        case 1:
          wordBuffer = "CAUTION@";
          radRate = 37;
          break;

        case 2:
          wordBuffer = "HAZARD@@";
          radRate = 40;
          break;

        case 3:
          wordBuffer = "EXIT@NOW";
          radRate = 47;
          break;

        default:
          wordBuffer = "WHOOPS@@";
        }
      }

      for (incMasterClock = 0; incMasterClock < 80; incMasterClock ++)  //innermost loop
      {
        if ((((incElapsed % 6 > 0 ) && (incElapsed % 6 < 3)) || incElapsed % 6 > 3) && incMasterClock % 10 == 0)
        {
          create_random(2);
     /*   
          if (incTimes == 1)
          {
            create_random(1); // this is done inside the loop so number changes during number display phase
          }                      // the master clock modulo sets the change rate
          if (incTimes == 3  || incTimes == 2)
          {
            create_random(2);
          }
          */
        }
        write_word(); 
        rads = random(1, radRate);
        radLight = 255 - (rads * 2);
        if (radLight > 190)
        {
          radLight = 255;
        }
        if (radLight < 1)
          radLight = 0;
        analogWrite(lightRad, radLight);
        if(rads > 33 && silent == 0)  //added flag for silencing
        {
          tone(speaker, 10);
          delayMicroseconds(6);
          noTone(speaker);
          delayMicroseconds(4);
        }
      }
    }
  }
  digitalWrite(lightRad, LOW);



 
  //////////the "ring the alarm" final phase
  noTone(speaker);
  delay(3);
//  digitalWrite(lightRad, LOW);
  incTimes = 0;
  incElapsed = 0;
  numIndex = 0;
  incMasterClock = 0;

          for (numIndex = 4; numIndex > 0; numIndex --)
          {
           totalRads[numIndex] = 0;
          }

  for (incTimes = 1; incTimes < 16; incTimes ++)  // number of "whoops" alarm makes
  {
    for (incElapsed = 1; incElapsed < 150; incElapsed ++)
    {
      if (incElapsed > 35)
      {

       if (incElapsed % 4 == 0)
       {
       increment_random((incTimes/4) + 1);
       }
 
          wordBuffer = "@@@@@@@@";

          for (numIndex = 7; numIndex > 0; numIndex --)
          {
//           totalRads[numIndex] = 7;
            wordBuffer.setCharAt(numIndex, char(totalRads[numIndex] + 48));
          }

          //         number_to_buffer;


      }
      else
      {
        wordBuffer = "TOTAL@RD";
      }
      write_word();
      special_rotary();
      if (clicks > 50)
      {
        clicks = 0;
        return;
      }
      if (silent == 0)
      {
        if (incElapsed > 10)
        {
          if (incElapsed % 2 == 0)  //alternates tones one per loop
          {
            tone(speaker, 9000 - (incElapsed * 20));
//noTone(speaker);
          }
          else
          {
            tone(speaker, 6000 - (incElapsed * 20));
          }
//          delay(1);
        }
        else
        {
          noTone(speaker);
        }
      }
      else
      {
        noTone(speaker);
//        delay(1);
      }
    }
  }

  noTone(speaker);
  incElapsed = 0;   //clean-up before passing back to armed()
  incMasterClock = 0;
  incTimes = 0;
  clicks = 0;
  simFlag = 0;
  pressed = 0;
  digitalWrite(lightRad, HIGH);

}



////////////CORE ROUTINE FOR SIMULATED CHEMICAL OR BIOLOGICAL ATTACK

void sim_chem()
{

  for (incTimes = 1; incTimes < 10; incTimes ++)  // the number of discrete "threat is building" displays
  {
    for (incMasterClock = 0; incMasterClock < 400; incMasterClock ++)
    {
      special_rotary();
      if (clicks > 50)
      {
        clicks = 0;
        return;
      }
      incElapsed ++;
      if(random(1, (1400/incTimes)) == 1 || (incMasterClock == 0 && incTimes == 1))
      {
        digitalWrite(lightChem, LOW);  //allows light to "tick" on at random intervals plus first
        if (silent == 0)
        {
          tone(speaker, 600, 6);
        }
        else
        {
          noTone(speaker);
        }
        incElapsed = 0;
      }
      if (incElapsed == 4)
      {
        digitalWrite(lightChem, HIGH);  //using imcElapsed this time to blink light.  consistency?  bah!
      }
      if (incMasterClock < 200)
      {
        switch(threat)
        {
        case 1:
          wordBuffer = "SARIN@@@";
          break;
        case 2:
          wordBuffer = "CHOLERA@";
          break;
        case 3:
          wordBuffer = "TULARMIA";
          break;
        case 4:
          wordBuffer = "TYPHUS@@";
          break;
        case 5:
          wordBuffer = "SMALLPOX";
          break;
        case 6:
          wordBuffer = "NOVICHOK";
          break;
        case 7:
          wordBuffer = "BUFOTOXN";
          break;
        case 8:
          wordBuffer = "RICIN@@@";
          break;
        case 9:
          wordBuffer = "EBOLA@@@";
          break;
        case 10:
          wordBuffer = "BOLUNIN@";
          break;
        case 11:
          wordBuffer = "MARBURG@";
          break;
        case 12:
          wordBuffer = "ARSINE@@";
          break;
        case 13:
          wordBuffer = "PHOSGENE";
          break;        
        case 14:
          wordBuffer = "CHLORINE";
          break;        
        case 15:
          wordBuffer = "BROMINE@";
          break;        
        case 16:
          wordBuffer = "CYANIDE@";
          break;
        case 17:
          wordBuffer = "ANTHRAX@";
          break;
        case 18:
          wordBuffer = "SPECTROX";
          break;
        case 19:
          wordBuffer = "C@DEIMOS";
          break;
        case 20:
          wordBuffer = "IOCANE@@";
          break;       
        }
      }
      else
      {
        switch(incTimes)  // alternates display of threat with threat level
        {
        case 1:
          wordBuffer = "PPM@@:02";  //could add a call to random_number here to flicker last digits.
          break;
        case 2:
          wordBuffer = "PPM@@:10";
          break;
        case 3: 
          wordBuffer = "PPM@0:09";
          break;
        case 4:
          wordBuffer = "PPM@0:35";
          break;
        case 5:
          wordBuffer = "PPM@1:30";
          break;
        case 6:
          wordBuffer = "PPM@21:2";
          break;
        case 7:
          wordBuffer = "PPM@50:0";
          break;
        case 8:
          wordBuffer = "PPM@97:7";
          break;
        case 9:
          wordBuffer = "PPM@@225";
          break;
        }
      }
      write_word();
    }
  }

  ////////////////audible alarm phase

  digitalWrite(lightChem, LOW);
  switch(threat) // reloads name for permanent display during audible alarm
  {
  case 1:
    wordBuffer = "SARIN@@@";
    break;
  case 2:
    wordBuffer = "CHOLERA@";
    break;
  case 3:
    wordBuffer = "TULARMIA";
    break;
  case 4:
    wordBuffer = "TYPHUS@@";
    break;
  case 5:
    wordBuffer = "SMALLPOX";
    break;
  case 6:
    wordBuffer = "NOVICHOK";
    break;
  case 7:
    wordBuffer = "BUFOTOXN";
    break;
  case 8:
    wordBuffer = "RICIN@@@";
    break;
  case 9:
    wordBuffer = "EBOLA@@@";
    break;
  case 10:
    wordBuffer = "BOLUNIN@";
    break;
  case 11:
    wordBuffer = "MARBURG@";
    break;
  case 12:
    wordBuffer = "ARSINE@@";
    break;
  case 13:
    wordBuffer = "PHOSGENE";
    break;        
  case 14:
    wordBuffer = "CHLORINE";
    break;        
  case 15:
    wordBuffer = "BROMINE@";
    break;        
  case 16:
    wordBuffer = "CYANIDE@";
    break;
  case 17:
    wordBuffer = "ANTHRAX@";
    break;
  case 18:
    wordBuffer = "SPECTROX";
    break;
  case 19:
    wordBuffer = "C@DEIMOS";
    break;
  case 20:
    wordBuffer = "IOCANE@@";
    break;       
  }
  if (threat < 14)  //semi-arbitrary limitation of antidote compatibility
  {
    injectable = 1;
  }
  else
  {
    injectable = 0;
  }

  for (incTimes = 0; incTimes < 12; incTimes ++)  // number of "whoops" alarm makes
  {
    for (incElapsed = 0; incElapsed < 140; incElapsed ++)
    {
      light_injectable_blink(24);
      write_word();
      special_rotary();
      if (clicks > 50)
      {
        clicks = 0;
        return;
      }
      if (silent == 0)
      {
        tone(speaker, 200 + (incElapsed * 3));
      }
      else
      {
        noTone(speaker);
      }
    }
    noTone(speaker);
  }
  incElapsed = 0;   //clean-up before passing back to armed()
  incMasterClock = 0;
  clicks = 0;
  simFlag = 0;
  pressed = 0;
  digitalWrite(lightChem, HIGH);
  digitalWrite(lightInjectable, HIGH);
}


////////////////////TESTS THE ROTARY SWITCH FOR SILENT/NOT SILENT/NEITHER -- GO HOME

int special_rotary()
{
  rotaryRead = analogRead(rotary);
  if (rotaryRead > 400 && rotaryRead < 600)
  {
    rotaryState = 2;
    silent = 0;
    clicks = 0;
  }
  if (rotaryRead > 100 && rotaryRead < 400)
  {
    rotaryState = 1;
    silent = 1;
    clicks = 0;
  }
  if (rotaryRead > 600 || rotaryRead < 100)
  {
    clicks ++;
  }
}

//////////////////

int read_button()
{
  if (analogRead(button) < 400)
  {
    pressed = 1;
    delayMicroseconds(20);
  }
  else
  {
    pressed = 0;
  }
}


////////

void light_injectable_blink(int howFast)
{
  //  if (injectable == 1)
  //  {

  incInjectableLight ++;
  if (incInjectableLight > howFast/2)
  {
    analogWrite(lightInjectable, 250);
  }
  if (incInjectableLight > howFast)
  {
    incInjectableLight = 0;
    digitalWrite(lightInjectable, HIGH);
  }
  //  }
}



///////////////////////////Reads selector, clears variables when changing to new setting

int read_rotary()        //would it be better to name states?
{
  oldRotaryState = rotaryState;
  rotaryRead = analogRead(rotary);
  if (rotaryRead < 100)
  {
    rotaryState = 0;
  }
  if (rotaryRead > 100 && rotaryRead < 400)
  {
    rotaryState = 1;
  }
  if (rotaryRead > 400 && rotaryRead < 600)
  {
    rotaryState = 2;
  }
  if (rotaryRead > 600 && rotaryRead < 900)
  {
    rotaryState = 3;
  }
  if (rotaryRead > 900)
  {
    rotaryState = 4;
  }

  if (rotaryState != oldRotaryState)
  {
    flush_all();
  }
  switch(rotaryState)
  {
  case 4:
    off();
    break;
  case 3:
    test();
    break;
  case 2:
    silent = 0;
    armed();
    break;
  case 1:
    silent = 1;
    armed();
    break;
  case 0:
    inject();
    break;
  }

}



//////////////clear all variables and flags

int flush_all()
{
  incMasterClock = 0;
  incElapsed = 0;
  incTimes = 0;
  pressed = 0;
  clicks = 0;
  simFlag = 0;

  noTone(speaker);
  digitalWrite(lightChem, HIGH);
  digitalWrite(lightRad, HIGH);
  digitalWrite(lightInjectable, HIGH);
  wordBuffer = "@@@@@@@@";
  write_word();
}


/////////////////////VFD DISPLAY CHARACTER SETUP

byte look_up(char thisChar)                   //this function uses ASCII value of character as index to lookup
{                                            //table and returns binary byte of segments to be lit
  //  lookupBuffer = lookupTable[thisChar - 48];
  bBuffer = lookupTable[thisChar - 48];
}


/////////////////VFD DISPLAY WRITE TO DISPLAY

void write_word()                                      // this function writes a word (from "wordBuffer", which
{                                                     // as an indexed array of char with a null leading
  for (incWord = 1; incWord < 9; incWord ++)          // character.
  {
    digitalWrite(hvBlanking, HIGH);
    charBuffer = wordBuffer[incWord-1];
    look_up(charBuffer);
    for (incSegment = 1; incSegment < 10; incSegment ++)
    {
      if (bitRead(bBuffer, (incSegment - 1)))
      {
        digitalWrite(hvData, HIGH);
      }
      else
      {
        digitalWrite(hvData, LOW);
      }
      shift_one(1);
    }
    digitalWrite(hvData, LOW);
    shift_one(3);

    for (incDigit = 1; incDigit < 9; incDigit ++)
    {
      if (incDigit == incWord)
      {
        digitalWrite(hvData, HIGH);
      }
      else
      {
        digitalWrite(hvData, LOW);
      }
      shift_one(1);
    }
    digitalWrite(hvBlanking, LOW);      // the way the blanking works, display is blanked while one digit is
    delayMicroseconds(500);            // read in.  the delay here is the time the latched digit is held on.
  }                                     // if I used direct pin write the duty cycle would be higher.
}

void shift_one(int howMuch)                //this function shifts and latches one bit into the hv5812
{
  for (shifting = 0; shifting < howMuch; shifting ++)
  {
    digitalWrite(hvClock, HIGH);
    digitalWrite(hvClock, LOW);
    digitalWrite(hvStrobe, HIGH);
    digitalWrite(hvStrobe, LOW);
  }
}



////////////////REPLACE CHAR WITH NUMBERS IN THE DISPLAY BUFFER FROM LEFT TO RIGHT

String create_random(int length)
{
  for (index = 8; index > (7 - length); index --)
  {
    wordBuffer.setCharAt(index, char(random(48, 57)));
  }
}

////attempts to code routine that increments randomly

int increment_random(int howMuch)
  //  for (index = 7; index > 0; index --)
  //  {
  //    if (index > (7 - howMuch))
  for (index = 7; index > (7 - howMuch); index --)
  {
    totalRads[index] = totalRads[index] + random(1, 9);
    if (totalRads[index] > 9)
    {
      totalRads[index] = totalRads[index] - 10;
      tempRads = totalRads[index - 1] + 1;

      if (tempRads > 9)
      {
        tempRads = tempRads - 10;
        tempRads2 = totalRads[index - 2] + 1;

        if (tempRads2 > 9)
        {
          tempRads2 = tempRads2 - 10;
          tempRads3 = totalRads[index - 3] + 1;
          if (tempRads3 > 9)
          {
            tempRads3 = tempRads3 - 10;
            tempRads4 = totalRads[index - 4] +1;
            if (tempRads4 > 9)
            {
              tempRads4 = tempRads4 - 10;
              tempRads5 = totalRads[index - 5] + 1;
              if (tempRads5 > 9)
              {
                tempRads5 = tempRads = 10;
                tempRads6 = totalRads[index - 6] + 1;
                if (tempRads6 > 9)
                {
                  tempRads6 = tempRads6 - 10;
                }
                totalRads[index - 6] = tempRads6;
              }
              totalRads[index - 5] = tempRads5;
            }
            totalRads[index - 4] = tempRads4;
          }
          totalRads[index - 3] = tempRads3;
        }
        totalRads[index - 2] = tempRads2;
      }
      totalRads[index - 1] = tempRads;
    }

  }
}

String number_to_buffer()
{
  for (numIndex = 7; numIndex > 0; numIndex --)
  {
    wordBuffer.setCharAt(numIndex, char(totalRads[numIndex] + 48));
  }
  return wordBuffer;
}




/////////////PLUNK A NEW FOUR RIGHT-MOST CHARACTERS INTO DISPLAY BUFFER

void concenate()
{
  for (index = 0; index < 4; index ++)
  {
    wordBuffer.setCharAt(index, nibbleBuffer.charAt(index));
  } 
}

2 comments:

  1. Ok, this is crazy awesome.

    Would you consider making a pair on commission?

    ReplyDelete
  2. There's been a surprising number of hits on this old post recently (and I have no idea why).

    If people are interested, I could be talked into a run. With some modifications for easier manufacturability. Off the top of my head, I'd go 3d printing, OSHpark board, and swap out the dial-lights for something more easily sourced; raw costs on the order of a hundred bucks each and with my set-up costs I'd have to ask $175 or so assembled. Plus of course I'd make all files freely available, including the 3d meshes for home printing.

    If this is so, contact me at the RPF -- I use the handle "nomuse" there. If I see activity I'll start an interest thread there and we'll see what comes of that.

    ReplyDelete