Andy's Geeky Clock

I thought Jon Chandler’s Clock for Geeks was a great project and his idea to make a clock with two dials in a VU meter type setup was brilliant.  This is my take on that idea.

MeterClock

 

The Design

This is supposed to be a geeky clock.  I reckon the following should be features of any truly geeky product:

  • It should have a USB port;
  • It should have at least one blue LED;
  • It should be overly complicated and have more settings and configuration options than are strictly necessary;
  • My wife should probably think it's a little bit pointless.

With the above in mind, here are the features of my clock.

Front Panel

FrontPanel

+12H and ALM Indicators: These show AM/PM and Alarm On/Off respectively.  In keeping with the old-fashioned meter look I didn’t want to have LEDs sticking out of the front panel.  Instead, each indicator is made from a small brass tube with an RGB LED behind it.  When the LED is off there’s just a black disk with a brass surround on the panel, when it’s lit it looks like the tube is filled with colour.  There are 16 different brightness settings for each of the red, green and blue LEDs, giving over 4,000 possible colours.

H and M Dials: Hopefully these are self-explanatory!  The time shown in the photo is 7:20PM.  The pointers are made from a 1mm diameter rod which I found in a hobby shop.  I’m not exactly sure what it’s made of but I think it might be some sort of carbon rod used in RC aircraft and such.

Light Sensor: This is in the centre of the black stripe at the bottom of the face and is used to control when the front panel lamps are turned on and off.  The light sensor sits inside a small plastic tunnel which blocks the light from the lamps, meaning it senses only the ambient lighting level.  The sensor used is an LLS05-A which I got from Futurlec.

Front Panel Illumination: This is provided by four 6v tungsten filament lamps.  The lamps have 16 brightness settings and can be set to operate in one of six modes:

  • Always On
  • Always Off
  • Timed - The lamps turn on at the Lamp On Time and go off again at the Lamp Off Time.
  • Level - The lamps turn on and off according to the ambient lighting level.  There are separate Lamp On Level and Lamp Off Level thresholds.  The light level must drop below the Lamp On Level threshold for at least 60 seconds for the lamps to turn on.  Once the lamps are on, the light level must rise above the Lamp Off Level threshold for 60 seconds before the lamps to turn off.
  • Timed and Level - As per Level Mode but the lamps only turn on if the light level is low enough AND the current time is between the Lamp On Time and Lamp Off Time.
  • With USB -  The lamps turn on when a USB connection is detected, and are off otherwise.  I tend to have them in this mode as it means the clock “wakes-up” whenever my PC is on and then goes dark again when it’s turned off.

Buttons

Most of the clock’s settings are adjusted from a PC via the USB port.  However, three buttons on top of the clock perform the following functions:

Button 0 (left)

  • Press: Toggles Alarm on and off.  When alarm is sounding, press to silence alarm.
  • Hold: Enters Alarm Set Mode.  The pointers move to show the alarm time which can then be adjusted using Buttons 1 and 2.

Button 1 (middle)

In Alarm Set Mode

  • Press: Advances alarm time by one hour.
  • Hold: Button auto-repeats and continuously advances alarm time hours.

Button 2 (right)

In Normal Mode

  • Press: Turns on front panel lamps for 3 seconds.
  • Hold: Front panel lamps will light for as long as button is held down.

In Alarm Set Mode

  • Press: Advances alarm time by one minute.
  • Hold: Button auto-repeats and continuously advances alarm time minutes.

BacklightingButtons4

The buttons are backlit with RGB LEDs which, same as the front panel indicators, have 16 different brightness settings for each of the red, green and blue LEDs.  The backlighting LEDs can be set to operate in one of three modes:

  • Always On
  • Always Off
  • As Lamps - The button backlight LEDs will be on when the front panel lamps are on and off when they are off.

 

Rear Panel

mt_ignore

As well as the Reset button, the rear panel houses the sockets for the 6V DC input, the USB connection and the ICSP connection.

USB Port 

All of the clock settings (LED settings, alarm time, lamp on / off thresholds, mode settings etc.) are adjusted from a PC connected via USB.  The USB connection is also used to synchronise the clock time to the PC clock which keeps it nice and accurate.


The Build

Case

This is the first time I’ve ever tried making something like this and I’m no expert.  I’m not saying this is the best way to it - this is the method I used and it worked okay for me.  I’ve added a few “Lessons Learned” points which detail things I would have done differently if I was doing it again.

The case was made by laminating 0.75mm layers of wood around a Former.  As for the choice of wood, the shop I went to had sheets of balsa wood and sheets of something else.  I tried the balsa but found it didn’t take tight curves as well as the “something else”.  Not sure what the mystery wood is but I think it might be spruce.

Former

FormerThe process starts with a plastic Former which has the same external dimensions as the interior of the case.  I made mine by building bulkheads out of 3mm acrylic which I then wrapped a few layers of thin plastic sheet around.  Note that when you come to laminate the wood around the Former you need to clamp the wood quite tightly so the Former has to be sturdy enough to take the pressure.

Lesson Learned: Time spent on the Former will pay off later.  I didn’t take as much care with mine as I should have and it ended up with a few imperfections which not only made it harder to work with but also carried through to the final case.

Laminate

LaminatingAt its tightest, the curve in the case has a 4mm radius.  In order to get the wood to curve that tight I had to soak it in water for at least 24 hours.  I then strapped the wet wood around the Former and let it dry completely – usually for another 12 to 24 hours.  The dry (and now curved) piece was then taken off, glued-up, and strapped back into place until the glue dried.  This process is repeated for each layer.

Lesson Learned: I used a bandage to strap the wood in place whilst it dried.  This worked okay around the curves but didn’t work very well on the straight sides of the case.  The wood tended to bow away from the Former and really I should have used some planks of wood and clamps to hold it tightly in place along the sides.

For strength, it’s best if the laminate is arranged so that alternate layers have the wood-grain running perpendicular to each other.  For this case I didn’t want lots of thin stripes showing in the cross section at the front so, rather then doing every other layer, I put two layers with the grain going one way and then two layers with the grain going the other way and so on.  There are 6 layers for the main body of the case and a further 4 layers in a thin strip at the front to produce the “lip” which holds the face in place – 10 layers in total.

As you can see from the photo, each layer of wood is larger than the Former and the edges are far from neat and uniform!  I found it was easier to do that and then trim it back when everything was finished than to try and get it all to line up from the start.

Finishing

CaseOffFormerOnce everything is dry, the wooden case can be trimmed to the right size and sanded to finish.  This photo shows the case removed from the Former and with the additional “lip” at the front added.

The final finish was to give it a few coats of matte polyurethane varnish with a light sanding between coats.

Servos

ServoIn exactly the same way as Jon’s clock, the hour and minute pointers are driven by RC style servos.  As the clock is pretty small I had to use micro- rather than standard-sized servos.  The servos can swing through about 175° whilst the pointers only need to cover a little over 90°.  I therefore decided to gear-down the servos’ output which brought a number of benefits:

- Any error in the servo’s position is reduced by the amount of the gear ratio.  i.e. if the output is geared-down 2:1 then an error of 1° in the servo translates to an error of only 0.5° at the pointer.  However, this improved accuracy does have to be offset against any “play” in the gear-train so the benefit may not always be realised.

- I could position the pointers closer to the edges of the case because I didn’t have to worry about leaving enough clearance to fit the servo in the same place as where the pointer’s axle was.

- Damage Control!  During coding and testing there were a few occasions when the servos swung wildly from one end of their travel to the other.  If the output hadn’t been geared-down then the pointers would’ve smacked into the sides of the case and probably broken off.

Lesson Learned: When it comes to servos it seems there can be quite a difference in quality between brands.  The first ones I was using (pictured above) had pretty poor accuracy which caused me quite a few headaches.  During testing, one of them totally failed and mechanically locked-up meaning it had to be binned.  I had much better results with a TowerPro SG50 which is what is now installed in the clock.  TowerPro aren’t a high-end brand but I’m pretty pleased with the performance.

Face

FaceThe face was drawn in DesignCAD, printed onto matt photo-paper and mounted onto a 3mm piece of acrylic.  The white plastic surround hides the wooden inside of the case and holds the front “glass”, which is actually a 1mm piece of acrylic.  The holes along the bottom are for the leads from the lamps and light sensor which are eventually covered-up by the black stripe on the bottom of the glass.  The above photo also gives a good view of the +12H and ALM indicators before the RGB LEDs were put behind them.

Buttons

To cut out the holes for the buttons I made a hole-saw by roughing up one end of piece of  5mm diameter brass tubing with a needle file to create some crude “teeth”.  I used a hole-saw rather than a drill so that I could save the pieces of wood which came out of the centre of the holes and use them as the button tops – this meant the wood-grain on the buttons matched the surrounding case.

Nothing very special about the switches themselves - three regular pushbuttons mounted on stripboard with RGB LEDs between them.  I used a double-decker board because there wasn’t enough height clearance to mount the LEDs onto the same board as the switches.  Once the board was built, I put a plastic enclosure around the LEDs to direct as much of the light as possible upwards and also to diffuse and blend the red, green, and blue LEDs into one smooth colour.Switches

Circuit

CircuitThe clock runs on a PIC18F2550.  I chose this because it had USB support, enough IO pins and (most importantly) I had one.  A 20MHz crystal is used and the PIC’s PLL pre- and post-scalers set to give a 32MHz clock.

A piezo buzzer is used for the alarm.  The PIC drives the buzzer directly but it can’t handle all the LEDs and lamps so I used a couple of ULN2803A drivers.  The ULN2803 is an 8 channel version of the ULN2003 which Graham’s article does a great job of explaining.  Each channel of the ULN2803A can handle 500mA continuous – more than enough for the LEDs but the front panel lamps are rated at 200mA so I had to parallel two channels to drive all four of them.

Power comes from a 6V DC wall-wart capable of providing up to 3A.  The 6V supply is used to power the servos, LEDs and lamps directly.  An LM2940 low-dropout regulator provides the 5V supply for the PIC.  This particular regulator was chosen because I had one – anything with a dropout voltage of less than 1V would have done.

The board was assembled onto stripboard using my favoured “stick-piece-of-paper-with-layout-to-board-and-then-populate” method.

Board

The photo below shows the rear panel, circuit board, switches, servos and face assembled and ready to be fixed inside the case.  The silver box on the back of the face houses the RGB LEDs used for the +12H and ALM indicators.  The box is lined with aluminium foil to prevent light from the LEDs leaking out around the edges of the face.InsideComplete


 

Software

Controlling the Servos

 

I used my Servo.bas module to handle controlling the servos.  The module is interrupt-driven so the main code must be written in such a way that it can handle being interrupted at any time.  For best results the module needs to use the high-priority interrupt.

Graham has already written a great description of how servos work so I won’t repeat it here.  Using the Servo.bas module, you move a servo by setting the variable ServoPosition(x) to the pulse length you want in uS, where x is the servo number from 0 to 7.

In this project only two servos are connected so I set the module option as such:

#option Servo_NumberOfServos = 2

To improve readability I also aliased the ServoPosition variables so they had meaningful names:

Dim ServoHours As Servo.ServoPosition(0) '}Aliases to Servo Module servo
Dim ServoMinutes As Servo.ServoPosition(1) '}position variables

The pulse lengths required for each pointer position on the dials are held in two constant arrays “Hours” and “Minutes”.  The Hours array has 48 values – one for every 15 minutes from 00:00 to 11:45.  The Minutes array has 60 values – one for each minute.  During testing I was changing these arrays quite a bit so I found it easier to keep them in a separate module – “ServoLookup.bas”

Module ServoLookup
 
Public Const Hours(48) As Word = (708, 735, 768, 801, 830, 861, 892, 920, 952,
986, 1018, 1050, 1081, 1108, 1135, 1166, 1196, 1224, 1258, 1290, 1316, 1349,
1382, 1414, 1452, 1486, 1518, 1553, 1583, 1612, 1644, 1676, 1704, 1731, 1760,
1792, 1823, 1850, 1880, 1918, 1961, 2000, 2032, 2067, 2102, 2133, 2167, 2207)
 
Public Const Minutes(60) As Word = (2150, 2124, 2089, 2054, 2029, 2011, 1981, 
1957, 1919, 1890, 1873, 1845, 1817, 1788, 1760, 1732, 1703, 1674, 1644, 1615, 
1586, 1557, 1528, 1500, 1471, 1442, 1417, 1392, 1366, 1341, 1316, 1289, 1262, 
1235, 1208, 1181, 1156, 1132, 1107, 1083, 1058, 1034, 1009, 985, 960, 936, 912, 
888, 865, 841, 817, 789, 766, 747, 726, 700, 676, 655, 630, 611)

 

USB

I used the Swordfish USBHID.bas module to handle the USB comms.  The EasyHID plugin was used to generate the necessary HID descriptor module.

To maintain the USB connection with the PC, it needs to be serviced every 1mS or so.  By default, the USBHID.bas module will handle this using interrupts.  As noted above, the Servo.bas module uses the high-priority interrupt so that leaves the low-priority interrupt for the USB.  I tested the code with this dual-interrupt set-up and didn’t experience any problems.  However, in the end I decided that less interrupts = less chance of things going wrong.  I therefore disabled the USB interrupt servicing by setting the module option:

#option USB_SERVICE = False
Disabling interrupt servicing means that that the main program must call HID.Service every 1mS.

USB RAM

The 18F2550 has 256 bytes of USB dual port RAM which can be used by the USBHID.bas module for sending and receiving data.  I used this area to store almost all the system variables – i.e. current time, alarm time, lamp brightness, etc. etc.  Using this area has the advantage that no processing has to be done on the incoming USB stream – simply reading in the USB report overwrites the variables with the new clock settings from the PC.  This also has the disadvantage that no processing is done on the incoming USB stream and simply reading in the USB report overwrites the variables with whatever was read in!  If the PC starts sending garbage then the clock will most likely lose its tiny mind.  So far, I’ve not had any problems but time will tell how reliable this method is – I probably wouldn’t use it for anything mission-critical but it seems okay for a desktop clock!

In order to use the USB RAM and still have meaningful variable names, I declared a structure for the format of the data:

Structure tTime
    Hours As Byte
    Minutes As Byte
    Seconds As Byte
End Structure
 
Structure tUSBData                      'Used to map variables onto USB RAM:
 
    Time As tTime                       'Current Time (hours, minutes, seconds)
 
    Alarm As tTime                      'Alarm Time (hours, minutes, seconds)
    AlarmSet As Bit                     '=1 if alarm is set
    AlarmOn As Bit                      '=1 if alarm is sounding
 
    Mode As Byte                        'Which mode clock is operating in
 
    …etc
    …etc
 
End Structure
I then declared a variable of type tUSBData:
Dim Data As tUSBData Absolute BufferRAM

This makes Swordfish overlay the structure tUSBData onto the USB RAM location.  The data can then be accssed like any other variable:

'Set alarm time to 12:30:00 and turn on alarm
Data.Alarm.Hours = 12
Data.Alarm.Minutes = 30
Data.Alarm.Seconds = 00
Data.AlarmSet = 1

The Swordfish Help file has a lot of useful information about using the USB RAM.

PCApp

PC Application

On the PC side, Graham’s iHID came to the rescue once again.  I used his code to put together a simple PC app which allows me to sync the clock to the PC’s clock and monitor and adjust all the settings – it’s not pretty but it’s functional.  In progress is a small app which will be scheduled to run once a day and which will sync the clock automatically, keeping it always at the right time.

The PC app really doesn't have to do anything clever - every 100mS the clock sends a USB report containing the values of all of the system variables.  If any settings need to be changed the PC sends a report back to the clock with the new values.

 

Main Program Loop

MainProgramSmall_copyThe flowchart shows the high-level tasks carried out in the main program loop (click to see the full picture).  All the tasks the are divided according to how frequently they need to occur – every 1mS, 10mS, 100mS or 1000mS.

Timer2 is set-up to rollover every 1mS.  For most of the time the PIC sits in a loop waiting for the rollover to occur.  When it does, the PIC carries out the tasks which need to happen every 1mS, namely:

- Service the USB connection;

- Update the software PWM signal being fed to the LEDs and lamps;

- Increase the event counters for 10mS, 100mS and 1000mS events.

Once the 10mS event counter equals 10, 10mS have passed.  The PIC checks the state of the buttons and, if any have been pressed, carries out the appropriate action.  Checking the buttons every 10mS may seem excessive but sampling them at that rate is necessary for the software de-bouncing and button auto-repeat routines to work well (see “Buttons” below).

Every 100mS all the outputs are updated – servo positions, lamps, LEDs and buzzer.  This is also when the USB report is sent to the PC and any data from the PC is read.

Every 1000mS the time is updated and checked against the alarm time to see if the alarm should sound.  The output from the light sensor is also read and compared against the on / off thresholds.

Lighting PWM

LightPWMFlowchartThe brightness of the front panel lamps and LEDs are controlled by a software PWM signal output from the PIC.  The subroutine UpdateLightPWM generates the signals and is called every 1mS.  This flowchart shows what happens inside UpdateLightPWM but simplified for just one LED.

With 16 brightness levels and the routine called every 1mS, the frequency of the PWM output is just over 60Hz which means there is no noticeable flicker.

Check Buttons

CheckButtonFlowchartThe subroutine CheckButtons is responsible for checking the state of the buttons and doing software debouncing.  The routine sets or clears two flags for each button - “Pressed” and “Held”.  “Pressed” is set when a button is pressed for longer than the Debounce_Delay period and then released.  “Held” is set if a button is held down for longer than the Hold_Delay period.  The flowchart below shows the process for one button.

 

Full Code Listing

Complete Program
{ ******************************************************************************** *  Name    : Meter Clock                                                       * *  Author  : AndyO                                                             * *  Notice  : Copyright (c) 2010 AndyO                                          * *          : All Rights Reserved                                               * *  Date    : 02 December 2010                                                  * *  Version : 1.0                                                               * *  Notes   : Servo module uses Timer1, Timer2 used for timekeeping and USB     * *          : service.                                                          * *          :                                                                   * *          : Use 20MHz crystal                                                 * *          :                                                                   * *          : Servos connected to PortB.0 and PortB.1                           * *          :                                                                   * ******************************************************************************** }
 
'===============================================================================
'Device, Clock and Config directives
'-------------------------------------------------------------------------------
Device = 18F2550
Clock = 32
 
Config                                  
   PLLDIV = 5,                          '}
   CPUDIV = OSC2_PLL3,                  '}Set oscillator options to give 32MHz
   USBDIV = 2,                          '}internal clock from 20MHz crystal 
   FOSC = HSPLL_HS,                     '}
 
   VREGEN = On                          'Enable internal 3.3V regulator for USB
 
 
'===============================================================================
'Module Options & Includes
'-------------------------------------------------------------------------------
 
'---Options---------------------------------------------------------------------
#option Debug_Mode = False              'TRUE turns on debugging parts of code
 
#option USB_DESCRIPTOR = "MeterClockUSBDesc.bas"   '}Descriptor generated
                                                   '}by EasyHID
 
#option USB_SERVICE = False             'USB not automatically serviced - call
                                        'HID.Service every 1mS
 
#option Servo_NumberOfServos = 2        'Servo module drives PortB.0 & PortB.1
#option Servo_Priority = ipHigh         'Servo module on high priority interrupt
 
'---Includes--------------------------------------------------------------------
Include "usbhid.bas"
Include "Servo.bas"
Include "ServoLookup.bas"
 
 
'===============================================================================
'Variable and Constant Declarations
'-------------------------------------------------------------------------------
 
'---Constants-------------------------------------------------------------------
Const ipHigh = 2                '}Interrupt priority constants
Const ipLow = 1                 '}
 
Const DebounceDelay = 2         'Time in 10mS increments button must be down for
Const SwitchHoldDelay = 35      'Time in 10mS increments button must be held for
Const SwitchRepeatDelay = 10    'Time in 10mS increments between auto-repeats
 
Const LampOnOffDelay = 60       'Number of seconds light level must be below /
                                'above the lamp on / off threshold levels for
                                'the front panel lamp to turn on / off
 
Const LampFlashTime = 3         'Number of seconds front panel lamp is on for
                                'when button 2 is pushed
 
Const MaxAlarmSoundTime = 600   'Maximum number of seconds alarm can sound for
 
Const Mode_Normal = 0                   '}           
Const Mode_AlarmSet = 1                 '}Clock Mode constants
Const Mode_LightTest = 2                '}
 
Const SwitchLEDMode_AlwaysOn = 0        '}
Const SwitchLEDMode_AlwaysOff = 1       '}Switch LED Mode constants
Const SwitchLEDMode_AsLamps = 2         '}
 
Const LampMode_AlwaysOn = 0             '}
Const LampMode_AlwaysOff = 1            '}
Const LampMode_Timed = 2                '}Front panel lamp Mode constants
Const LampMode_Level = 3                '}
Const LampMode_TimedAndLevel = 4        '}
Const LampMode_WithUSB = 5              '}
 
 
'---Structures------------------------------------------------------------------
Structure tTime
    Hours As Byte
    Minutes As Byte
    Seconds As Byte
End Structure
 
Structure tRGBLed
    Red As Byte
    Green As Byte
    Blue As Byte
End Structure
 
Structure tUSBData                      'Used to map variables onto USB RAM:
 
    Time As tTime                       'Current Time (hours, minutes, seconds)
 
    Alarm As tTime                      'Alarm Time (hours, minutes, seconds)
    AlarmSet As Bit                     '=1 if alarm is set
    AlarmOn As Bit                      '=1 if alarm is sounding
 
    Mode As Byte                        'Which mode clock is operating in
 
    SwitchLed As tRGBLed                'RGB brightness values for Switch LEDs
    SwitchLedOn As Bit                  '=1 if Switch LEDs are on
    SwitchLedMode As Byte               'Which mode Switch LEDs are operating in
 
    PMLed As tRGBLed                    'RGB brightness values for AM/PM LED            
    PMLedOn As Bit                      '=1 if AM/PM LED is on
 
    AlarmLed As tRGBLed                 'RGB brightness values for Alarm LED         
    AlarmLedOn As Bit                   '=1 if Alarm LED is on
 
    LampBrightness As Byte              'Brightness value for front panel lamp
    LampMode As Byte                    'Which mode lamp is operating in
    LampOnThreshold As Byte             'Lamp On light level
    LampOffThreshold As Byte            'Lamp Off light level
    LampOnTime As tTime                 'Lamp On time (Hours, Minutes, Seconds)
    LampOffTime As tTime                'Lamp Off time (Hours, Minutes, Seconds)
    LampOn As Bit                       '=1 if Lamp is on
 
    LightLevel As Byte                  'Reading from light sensor
 
End Structure
 
 
'---Variables-------------------------------------------------------------------
Dim Data As tUSBData Absolute BufferRAM 'Overlay above tUSBData structure onto
                                        'the USB RAM buffer
 
Dim LedPWMCounter As Byte               'Used for LED and Lamp PWM
 
Dim Switch0DebounceCount As Byte        '}
Dim Switch1DebounceCount As Byte        '}Counts whether button is pressed for
Dim Switch2DebounceCount As Byte        '}the debounce delay period
 
Dim Switch0DownCount As Byte            '}
Dim Switch1DownCount As Byte            '}Counts whether button is pushed down
Dim Switch2DownCount As Byte            '}for the hold delay period
 
Dim Switch0Pressed As Boolean           '}
Dim Switch1Pressed As Boolean           '}=TRUE when a switch has been pressed
Dim Switch2Pressed As Boolean           '}
 
Dim Switch0Held As Boolean              '}
Dim Switch1Held As Boolean              '}=TRUE when a switch is being held down
Dim Switch2Held As Boolean              '}
 
Dim Switch1RepeatCount As Byte          '}Counts whether switch has been held
Dim Switch2RepeatCount As Byte          '}down for the auto-repeat delay period
 
Dim LampOnLevelCount As Byte            '}Counts how long light level is below
Dim LampOffLevelCount As Byte           '}or above the lamp on & off thresholds
 
Dim LampOverride As Boolean             '=TRUE when Switch 2 turns on lamp
Dim LampFlashCount As Byte              'Counter for momentarily lighting lamp
 
Dim Buzzer100mSToggle As Bit            'Used to toggle alarm buzzer every 100mS
Dim Buzzer1SecToggle As Bit             'Used to toggle alarm buzzer every 1 Sec
 
Dim AlarmSoundCount As Word             'Number seconds alarm has been sounding
 
Dim Timer2On As T2CON.Booleans(2)               '}
Dim Timer2InterruptFlag As PIR1.Booleans(1)     '}Aliases to Timer 2 registers
Dim Timer2InterruptEnable As PIE1.Booleans(1)   '}
 
Dim StartADConversion As ADCON0.Booleans(1)     'Alias to ADC Go/Done bit.
                                                'Set TRUE to start conversion,
                                                '= FALSE when conversion done
 
Dim Tick As Timer2InterruptFlag                 '=TRUE when Timer2 rolls over
 
Dim mS10Count As Byte                           '}Period counters for 10mS,
Dim ms100Count As Byte                          '}100mS and 1 Second events
Dim mS1000Count As Word                         '}
 
Dim ServoHours As Servo.ServoPosition(0)        '}Aliases to Servo Module servo
Dim ServoMinutes As Servo.ServoPosition(1)      '}position variables
 
 
'---Input / Output Aliases------------------------------------------------------
#if Debug_Mode = True Then
    Dim DebugPin0 As PORTB.7                    '}PGC & PGD pins used for 
    Dim DebugPin1 As PORTB.6                    '}debugging
#endif
 
Dim LightSensor As PORTA.0                      'Analog input from light sensor
 
Dim SwitchLedRedPin As PORTA.1                  '}
Dim SwitchLedGreenPin As PORTA.2                '}RGB LEDs illuminating switches
Dim SwitchLedBluePin As PORTA.3                 '}
 
Dim LampPin As PORTA.4                          'Front-panel illumination lamps
 
Dim BuzzerPin As PORTA.5                        'Alarm buzzer
 
Dim PMLedRedPin As PORTB.5                      '}
Dim PMLedGreenPin As PORTB.3                    '}AM/PM RGB LED
Dim PMLedBluePin As PORTB.4                     '}
 
Dim AlarmLedRedPin As PORTB.2                   '}
Dim AlarmLedGreenPin As PORTC.6                 '}Alarm RGB LED
Dim AlarmLedBluePin As PORTC.7                  '}
 
Dim Switch0 As PORTC.0                          '}
Dim Switch1 As PORTC.1                          '}Switches
Dim Switch2 As PORTC.2                          '}
 
 
'===============================================================================
'Subs and Functions
'-------------------------------------------------------------------------------
 
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Name:      UpdateTime()   
'Purpose:   Updates current time by 1 second - advancing seconds, minutes and
'           hours as necesary
'
'Notes:     Inline sub 
'           Called every 1 Second  
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Inline Sub UpdateTime()
 
    Inc(Data.Time.Seconds)
 
    If Data.Time.Seconds = 60 Then
 
        Data.Time.Seconds = 0
        Inc(Data.Time.Minutes)
 
        If Data.Time.Minutes = 60 Then
 
            Data.Time.Minutes = 0
            Inc(Data.Time.Hours)
 
            If Data.Time.Hours = 24 Then
 
                Data.Time.Hours = 0
 
            EndIf
 
        EndIf
 
    EndIf    
 
End Sub
 
 
 
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Name:      UpdateLightPWM()   
'Purpose:   Updates LED and Lamp PWM output according to brightness values
'
'Notes:     Inline sub
'           Called every 1mS   
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Inline Sub UpdateLightPWM()
 
Inc(LedPWMCounter)                              'Increment the PWM counter
 
    If LedPWMCounter.Bits(4) = 1 Then           'When PWM counter reaches 16:   
 
        LedPWMCounter = 0                           'Reset counter
 
        SwitchLedRedPin = Data.SwitchLedOn          '}
        SwitchLedGreenPin = Data.SwitchLedOn        '}    
        SwitchLedBluePin = Data.SwitchLedOn         '}Set the output pin High
                                                    '}for each LED or lamp which
        PMLedRedPin = Data.PMLedOn                  '}is set to be On
        PMLedGreenPin = Data.PMLedOn                '}
        PMLedBluePin = Data.PMLedOn                 '}
                                                    '}
        AlarmLedRedPin = Data.AlarmLedOn            '}
        AlarmLedGreenPin = Data.AlarmLedOn          '}
        AlarmLedBluePin = Data.AlarmLedOn           '} 
                                                    '}
        LampPin = Data.LampOn                       '}
 
    EndIf
 
 
    '---Switch RGB LEDs:
    If LedPWMCounter = Data.SwitchLed.Red Then  '}
        SwitchLedRedPin = 0                     '}
    EndIf                                       '}When PWM counter = Brightness
                                                '}setting for the LED, set
    If LedPWMCounter = Data.SwitchLed.Green Then'}output pin Low for that LED
        SwitchLedGreenPin = 0                   '}
    EndIf                                       '}
                                                '}
    If LedPWMCounter = Data.SwitchLed.Blue Then '}
        SwitchLedBluePin = 0                    '}
    EndIf                                       '}
 
 
    '---AM/PM RGB LEDs:
    If LedPWMCounter = Data.PMLed.Red Then      '}
        PMLedRedPin = 0                         '}
    EndIf                                       '}When PWM counter = Brightness
                                                '}setting for the LED, set
    If LedPWMCounter = Data.PMLed.Green Then    '}output pin Low for that LED
        PMLedGreenPin = 0                       '}
    EndIf                                       '}
                                                '}
    If LedPWMCounter = Data.PMLed.Blue Then     '}
        PMLedBluePin = 0                        '}
    EndIf                                       '}
 
 
    '---Alarm RGB LEDs:
    If LedPWMCounter = Data.AlarmLed.Red Then   '}
        AlarmLedRedPin = 0                      '}
    EndIf                                       '}When PWM counter = Brightness
                                                '}setting for the LED, set
    If LedPWMCounter = Data.AlarmLed.Green Then '}output pin Low for that LED
        AlarmLedGreenPin = 0                    '}
    EndIf                                       '}
                                                '}
    If LedPWMCounter = Data.AlarmLed.Blue Then  '}
        AlarmLedBluePin = 0                     '}
    EndIf                                       '}
 
 
    '---Front Panel Lamp:
    If LedPWMCounter = Data.LampBrightness Then '}When PWM counter = Brightness
        LampPin = 0                             '}setting, set output pin Low
    EndIf                                       '}
 
End Sub
 
 
 
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Name:      CheckButtons()   
'Purpose:   Checks buttons, debounces, and sets flags indicating if pressed or
'           held
'
'Notes:     Inline sub.
'           Each button has two flags - "Pressed" and "Held".  Pressed is set
'           when the button is pushed once and released.  Held is set when the
'           button is pressed and remains down for longer than "SwitchHoldDelay"
'
'           ---Routine for each switch:-----------------------------------------
'
'           If pin reads High and it hasn't been down for the Debounce Delay,
'           increase the Debounce Count;
'
'           If pin reads High and Debounce Count = Debounce Delay then it is a
'           valid press so increase the Switch Down Count.  If the switch has
'           been down for Hold Delay then set the Held flag;
'
'           If pin goes Low after Debounce Count = Debounce Delay but before
'           Hold Delay is reached, set switch's "Pressed" flag and reset all
'           counters;
'
'           If pin goes Low before Debounce Count = Debounce Delay then reset
'           all counters.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Inline Sub CheckButtons()
 
    '---Switch 0----------------------------------------------------------------
    If Switch0 = 1 And Switch0DebounceCount < DebounceDelay Then
 
        Inc(Switch0DebounceCount)
 
    ElseIf Switch0 = 1 And Switch0DebounceCount = DebounceDelay And
           Switch0Held = False Then
 
        Inc(Switch0DownCount)
 
        If Switch0DownCount = SwitchHoldDelay Then
 
            Switch0Held = True
 
        EndIf
 
    ElseIf Switch0 = 0 And Switch0DebounceCount = DebounceDelay And
           Switch0Held = False Then
 
        Switch0Pressed = True
        Switch0DownCount = 0
        Switch0DebounceCount = 0        
 
    ElseIf Switch0 = 0 Then
 
        Switch0Pressed = False
        Switch0Held = False
        Switch0DownCount = 0
        Switch0DebounceCount = 0
 
    EndIf    
 
 
 
    '---Switch1-----------------------------------------------------------------
    If Switch1 = 1 And Switch1DebounceCount < DebounceDelay Then
 
        Inc(Switch1DebounceCount)
 
    ElseIf Switch1 = 1 And Switch1DebounceCount = DebounceDelay And
           Switch1Held = False Then
 
        Inc(Switch1DownCount)
 
        If Switch1DownCount = SwitchHoldDelay Then
 
            Switch1Held = True
 
        EndIf
 
    ElseIf Switch1 = 0 And Switch1DebounceCount = DebounceDelay And
           Switch1Held = False Then
 
        Switch1Pressed = True
        Switch1DownCount = 0
        Switch1DebounceCount = 0        
 
    ElseIf Switch1 = 0 Then
 
        Switch1Pressed = False
        Switch1Held = False
        Switch1DownCount = 0
        Switch1DebounceCount = 0
 
    EndIf            
 
 
 
    '---Switch2-----------------------------------------------------------------
    If Switch2 = 1 And Switch2DebounceCount < DebounceDelay Then
 
        Inc(Switch2DebounceCount)
 
    ElseIf Switch2 = 1 And Switch2DebounceCount = DebounceDelay And
           Switch2Held = False Then
 
        Inc(Switch2DownCount)
 
        If Switch2DownCount = SwitchHoldDelay Then
 
            Switch2Held = True
 
        EndIf
 
    ElseIf Switch2 = 0 And Switch2DebounceCount = DebounceDelay And
           Switch2Held = False Then
 
        Switch2Pressed = True
        Switch2DownCount = 0
        Switch2DebounceCount = 0        
 
    ElseIf Switch2 = 0 Then
 
        Switch2Pressed = False
        Switch2Held = False
        Switch2DownCount = 0
        Switch2DebounceCount = 0
 
    EndIf
 
End Sub
 
 
 
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Name:      ActionButtons()   
'Purpose:   Carries out appropriate action, depending on Mode, for any buttons 
'           which are held down or have been pressed
'
'Notes:     Inline sub
'           Called every 10mS
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Inline Sub ActionButtons()
 
    '---Switch 1 Behaviour------------------------------------------------------
    '
    'In Normal Mode or Alarm_Set Mode: 
    '
    '   "Pressed" - if alarm is sounding, silence alarm.
    '             - if alarm is not sounding, toggle Alarm On / Off.
    '
    '      "Held" - enter Alarm-Set Mode
    '---------------------------------------------------------------------------
 
    If Data.Mode <> Mode_LightTest Then
 
        If Switch0Pressed = True Then                            
 
            If Data.AlarmOn = 1 Then                  
 
                Data.AlarmOn = 0                        
 
            Else                                           
 
                Data.AlarmSet = Not(Data.AlarmSet)     
 
            EndIf
 
        EndIf
 
 
        If Switch0Held = True Then 
 
            Data.Mode = Mode_AlarmSet                 
 
        Else
 
            Data.Mode = Mode_Normal
 
        EndIf
 
    EndIf
 
 
 
    '---Switch 1 Behaviour------------------------------------------------------
    '
    'In Alarm-Set Mode: 
    '
    '   "Pressed" - advances Alarm-Time Hours by 1
    '
    '      "Held" - auto-repeats, advancing Alarm-Time Hours by 1 each time
    '---------------------------------------------------------------------------
 
    If Data.Mode = Mode_AlarmSet Then
 
        If Switch1Pressed = True Then
 
            Data.Alarm.Hours = Data.Alarm.Hours + 1
 
            If Data.Alarm.Hours = 24 Then
 
                Data.Alarm.Hours = 0
 
            EndIf
 
        EndIf
 
 
        If Switch1Held = True Then
 
            Inc(Switch1RepeatCount)
 
            If Switch1RepeatCount = SwitchRepeatDelay Then              
 
                Switch1RepeatCount = 0
                Data.Alarm.Hours = Data.Alarm.Hours + 1
 
                If Data.Alarm.Hours = 24 Then
 
                    Data.Alarm.Hours = 0
 
                EndIf
 
            EndIf
 
        Else
 
            Switch1RepeatCount = 0
 
        EndIf
 
    EndIf
 
 
 
    '---Switch 2 Behaviour------------------------------------------------------
    '
    'In Normal Mode:
    '
    '   "Pressed" - turns on Front Panel Lamp for "LampFlashTime" seconds
    '
    '      "Held" - turns on Front Panel Lamp for duration button is held
    '
    '
    'In Alarm-Set Mode: 
    '
    '   "Pressed" - advances Alarm-Time Minutes by 1
    '
    '      "Held" - auto-repeats, advancing Alarm-Time Minutes by 1 each time
    '---------------------------------------------------------------------------
 
    Select Data.Mode
 
        Case Mode_Normal
 
            If Switch2Pressed = True Then
 
                LampFlashCount = LampFlashTime
                LampOverride = True
 
            ElseIf Switch2Held = True Then
 
                LampOverride = True
 
            EndIf
 
 
        Case Mode_AlarmSet
 
            If Switch2Pressed = True Then
 
                Data.Alarm.Minutes = Data.Alarm.Minutes + 1
 
                If Data.Alarm.Minutes = 60 Then
 
                    Data.Alarm.Minutes = 0
 
                EndIf
 
 
            ElseIf Switch2Held = True Then
 
                Inc(Switch2RepeatCount)
 
                If Switch2RepeatCount = SwitchRepeatDelay Then            
 
                    Switch2RepeatCount = 0
                    Data.Alarm.Minutes = Data.Alarm.Minutes + 1
 
                    If Data.Alarm.Minutes = 60 Then
 
                        Data.Alarm.Minutes = 0
 
                    EndIf
 
                EndIf
 
            Else
 
                Switch2RepeatCount = 0
 
            EndIf
 
     EndSelect
 
End Sub
 
 
 
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Name:      UpdateDisplay()   
'Purpose:   Updates servo positions, LEDs and front panel lamp depending on mode
'
'Notes:     Inline sub
'           Called every 100mS   
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Inline Sub UpdateDisplay()
 
    '---Servos------------------------------------------------------------------
    '
    'Servo position values for each pointer position are stored in two constant
    'arrays in ServoLookup.bas module:
    '
    'ServoLookup.Minutes has 60 values (0-59 mins)
    'ServoLookup.Hours has 48 values (0-11 hrs, 4 x 15min slots for each hour)
    '---------------------------------------------------------------------------
 
    If Data.Mode = Mode_Normal Or Data.Mode = Mode_LightTest Then
 
        If Data.Time.Hours > 11 Then
 
            ServoHours = ServoLookup.Hours(((Data.Time.Hours - 12) * 4) + (Data.Time.Minutes / 15))
 
        Else
 
            ServoHours = ServoLookup.Hours((Data.Time.Hours * 4) + (Data.Time.Minutes / 15))
 
        EndIf
 
        ServoMinutes = ServoLookup.Minutes(Data.Time.Minutes)
 
 
    ElseIf Data.Mode = Mode_AlarmSet Then
 
        If Data.Alarm.Hours > 11 Then
 
            ServoHours = ServoLookup.Hours(((Data.Alarm.Hours - 12) * 4) + (Data.Alarm.Minutes / 15))
 
        Else
 
            ServoHours = ServoLookup.Hours((Data.Alarm.Hours * 4) + (Data.Alarm.Minutes / 15))
 
        EndIf
 
        ServoMinutes = ServoLookup.Minutes(Data.Alarm.Minutes)
 
    EndIf
 
 
 
    '---Front Panel Lamp--------------------------------------------------------
    '
    'Turn on or off front panel lamp depending on Lamp Mode:
    '
    'LampMode_Always On:     ON
    '
    'LampMode_AlwaysOff:     OFF
    '
    'LampMode_Timed:         If time is after "LampOnTime" - ON
    '                        If time is after "LampOffTime" - OFF
    '
    '
    'LampMode_Level:         If light level has been above "LampOnThreshold" for
    '                        longer than "LampOnOffDelay" - ON
    '
    '                        If light level has been below "LampOffThreshold"
    '                        for longer than "LampOnOffDelay" - OFF
    '
    '
    'LampMode_TimedAndLevel: If light level has been above "LampOnThreshold" for
    '                        longer than "LampOnOffDelay" AND time is after
    '                        "LampOnTime" - ON
    '
    '                        If time is after "LampOffTime" - OFF
    '
    '
    'LampMode_WithUSB:       If USB connected - ON
    '                        If USB not connected - OFF
    '
    '
    'If Switch 2 has been pressed to turn on lamp then "LampOverride" = TRUE
    '---------------------------------------------------------------------------
 
    If Data.Mode = Mode_LightTest Then
 
        Data.LampOn = 1
 
    Else
 
        Select Data.LampMode
 
            Case LampMode_AlwaysOn
 
                Data.LampOn = 1
 
 
            Case LampMode_AlwaysOff
 
                Data.LampOn = 0
 
 
            Case LampMode_Timed
 
                If Data.Time.Hours >= Data.LampOnTime.Hours And
                   Data.Time.Minutes >= Data.LampOnTime.Minutes And
                   Data.Time.Seconds >= Data.LampOnTime.Seconds Then
 
                    Data.LampOn = 1
 
                EndIf
 
                If Data.Time.Hours >= Data.LampOffTime.Hours And
                   Data.Time.Minutes >= Data.LampOffTime.Minutes And
                   Data.Time.Seconds >= Data.LampOffTime.Seconds Then
 
                    Data.LampOn = 0
 
                EndIf                                                            
 
 
            Case LampMode_Level
 
                If LampOnLevelCount = LampOnOffDelay Then
 
                    Data.LampOn = 1
 
                ElseIf LampOffLevelCount = LampOnOffDelay Then
 
                    Data.LampOn = 0
 
                EndIf
 
 
            Case LampMode_TimedAndLevel            
 
                If Data.Time.Hours >= Data.LampOnTime.Hours And
                   Data.Time.Minutes >= Data.LampOnTime.Minutes And
                   Data.Time.Seconds >= Data.LampOnTime.Seconds Then
 
                    If LampOnLevelCount = LampOnOffDelay Then
 
                        Data.LampOn = 1
 
                    ElseIf LampOffLevelCount = LampOnOffDelay Then
 
                        Data.LampOn = 0
 
                    EndIf
                EndIf
 
                If Data.Time.Hours >= Data.LampOffTime.Hours And
                   Data.Time.Minutes >= Data.LampOffTime.Minutes And
                   Data.Time.Seconds >= Data.LampOffTime.Seconds Then
 
                    Data.LampOn = 0
 
                EndIf
 
 
            Case LampMode_WithUSB
 
                If HID.Attached Then
 
                    Data.LampOn = 1
 
                Else
 
                    Data.LampOn = 0
 
                EndIf                                                            
 
        EndSelect
 
        If LampOverride = True Then
 
            Data.LampOn = 1
 
        EndIf        
 
    EndIf
 
 
 
    '---Switch LED--------------------------------------------------------------
    '
    'Turn on or off switch backlight LED depending on SwitchLED Mode:
    '
    'SwitchLEDMode_Always On: ON
    '
    'SwitchLEDMode_AlwaysOff: OFF
    '
    'SwitchLEDMode_AsLamps:   Same state as front panel lamp
    '---------------------------------------------------------------------------
 
    If Data.Mode = Mode_LightTest Then
 
         Data.SwitchLedOn = 1
 
    Else
 
        Select Data.SwitchLedMode
 
            Case SwitchLEDMode_AlwaysOn
 
                Data.SwitchLedOn = 1
 
            Case SwitchLEDMode_AlwaysOff
 
                Data.SwitchLedOn = 0
 
            Case SwitchLEDMode_AsLamps
 
                Data.SwitchLedOn = Data.LampOn
 
            EndSelect
 
    EndIf
 
 
 
    '---Alarm LED---------------------------------------------------------------
    '
    'LED ON when Alarm is set, OFF otherwise
    '---------------------------------------------------------------------------  
 
    If Data.Mode = Mode_LightTest Then
 
        Data.AlarmLedOn = 1    
 
    Else
 
        Data.AlarmLedOn = Data.AlarmSet
 
    EndIf
 
 
 
    '---PM LED------------------------------------------------------------------
    '
    'In Normal mode, LED ON if Time is after 1159hrs, OFF otherwise
    '
    'In Alarm Set mode, LED ON if Alarm Time is after 1159hrs, OFF otherwise
    '---------------------------------------------------------------------------
 
    Select Data.Mode
 
        Case Mode_LightTest
 
            Data.PMLedOn = 1
 
        Case Mode_Normal
 
            If Data.Time.Hours > 11 Then
 
                Data.PMLedOn = 1        
 
            Else
 
                Data.PMLedOn = 0
 
            EndIf
 
        Case Mode_AlarmSet
 
            If Data.Alarm.Hours > 11 Then
 
                Data.PMLedOn = 1
 
            Else
 
                Data.PMLedOn = 0
 
            EndIf   
 
    EndSelect
 
End Sub
 
 
 
 
'===============================================================================
'Main Program
'-------------------------------------------------------------------------------
 
'---Set-up Input / Output for all pins
#if Debug_Mode = True Then              
    Output(DebugPin0)                   
    Output(DebugPin1)                     
#endif                                    
 
Output(SwitchLedRedPin)                   
Output(SwitchLedGreenPin)                 
Output(SwitchLedBluePin)                
 
Output(PMLedRedPin)                     
Output(PMLedGreenPin)                   
Output(PMLedBluePin)                    
 
Output(AlarmLedRedPin)                  
Output(AlarmLedGreenPin)                
Output(AlarmLedBluePin)                 
 
Output(LampPin)                         
 
Output(BuzzerPin)                       
 
Input(Switch0)                          
Input(Switch1)                          
Input(Switch2)                          
 
Input(LightSensor)                      
 
 
'---Set all outputs low
PORTA = %00000000                       
PORTB = %00000000                       
PORTC = %00000000                       
 
 
'---Set-up A/D module:
ADCON1 = %00001110                      '}Vref set to Vdd and Vss;
                                        '}AN0 is analogue input
 
                                        '}Result left justified;
ADCON2 = %00000010                      '}Aquisition time set to manual;
                                        '}Conversion clock set to Fosc/32.
 
ADCON0 = %00000001                      'Channel AD0 selected, Module turned on
 
 
'---Set-up Timer2 to give 1mS roll-over:
T2CON = %01111001                       'Pre 1:4, Post 1:16
 
PR2 = 124                               '}125 = 1mS period (take off 1 cycle for
                                        '}timer reload)
 
Timer2InterruptFlag = False
Timer2InterruptEnable = False    
 
 
'---Initialise all variables
mS10Count = 0
ms100Count = 0
mS1000Count = 0 
 
LedPWMCounter = 0
 
Switch0DebounceCount = 0
Switch1DebounceCount = 0
Switch2DebounceCount = 0
 
Switch0DownCount = 0
Switch1DownCount = 0
Switch2DownCount = 0
 
Switch1RepeatCount = 0
Switch2RepeatCount = 0
 
Switch0Pressed = False
Switch1Pressed = False
Switch2Pressed = False
 
Switch0Held = False
Switch1Held = False
Switch2Held = False
 
Buzzer100mSToggle = 0
Buzzer1SecToggle = 0
 
AlarmSoundCount = 0
 
LampOnLevelCount = 0
LampOffLevelCount = 0
 
LampOverride = False
LampFlashCount = 0
 
Clear(HID.Buffer)
 
'---Load default values for settings
Data.SwitchLed.Red = 4
Data.SwitchLed.Green = 4
Data.SwitchLed.Blue = 12
Data.SwitchLedMode = SwitchLEDMode_AlwaysOn 
 
Data.PMLed.Red = 0
Data.PMLed.Green = 12
Data.PMLed.Blue = 8
 
Data.AlarmLed.Red = 9
Data.AlarmLed.Green = 0
Data.AlarmLed.Blue = 3
 
Data.LampBrightness = 7
Data.LampMode = LampMode_WithUSB
Data.LampOnThreshold = 70
Data.LampOffThreshold = 150
 
'---Set servos to their upper limit
ServoHours = ServoLookup.Hours(Bound(ServoLookup.Hours))
ServoMinutes = ServoLookup.Minutes(Bound(ServoLookup.Minutes))
 
Servo.On                                'Start servicing servos
DelayMS(1000)                           'Delay allows servos to move to limit
UpdateDisplay                           'Set servos to correct position
 
StartADConversion = True                'Get first A/D reading
 
Timer2On = True                         'Turn on Timer2
 
 
'===Main Program While-Wend Loop Begins Here====================================
 
While True
 
    Repeat                                      '}Wait for 1mS to occur by
    Until Tick = True                           '}checking Timer2 interrupt flag
 
    Tick = False                                'Reset interrupt flag
 
    #if Debug_Mode = True Then
        High(DebugPin0)
    #endif    
 
'---1mS - Everything in this section is done every 1mS--------------------------
 
    Inc(mS10Count)                              '}
    Inc(ms100Count)                             '}Increase all period counters
    Inc(mS1000Count)                            '}
 
    HID.Service                                 'Service USB connection
 
    UpdateLightPWM                              
 
 
 
'---10mS - Everything in this section is done every 10mS------------------------
 
    If mS10Count = 10 Then
 
        #if Debug_Mode = True Then
            High(DebugPin1)
        #endif
 
        mS10Count = 0                           
 
        CheckButtons
 
        ActionButtons   
 
        #if Debug_Mode = True Then
            Low(DebugPin1)
        #endif
 
    EndIf
 
 
 
'---100mS - Everything in this section is done every 100mS----------------------
 
    If ms100Count = 100 Then
 
        ms100Count = 0
 
        UpdateDisplay
 
        'When alarm is sounding, buzzer makes 5 beeps per second and is on for
        'one second, off for one second, on for one, off for one etc.
        'Buzzer on/off is controlled by two bits - Buzzer100msToggle and
        'Buzzer1SecToggle.  
        BuzzerPin = Data.AlarmOn And Buzzer100mSToggle And Buzzer1SecToggle
 
        Buzzer100mSToggle = Not(Buzzer100mSToggle)
 
 
        If HID.Attached Then                    '}
                                                '}If USB connected, send Data
            HID.WriteArray(HID.Buffer32)      '}
 
            If HID.DataAvailable Then               '}
                                                    '}If USB data available from
                HID.ReadArray(HID.Buffer32)       '}the PC, read it in
                                                    '}
            EndIf                                   '}
 
        EndIf
 
    EndIf
 
 
 
'---1000mS - Everything in this section is done every 1000mS--------------------
 
    If mS1000Count = 1000 Then
 
        mS1000Count = 0
 
        UpdateTime
 
        Buzzer1SecToggle = Not(Buzzer1SecToggle)
 
 
        If Data.Time.Hours = Data.Alarm.Hours And       '}
           Data.Time.Minutes = Data.Alarm.Minutes And   '}
           Data.Time.Seconds = Data.Alarm.Seconds And   '}If Alarm Time reached
           Data.AlarmSet = 1 Then                       '}and alarm is set,
                                                        '}sound alarm
            Data.AlarmOn = 1                            '}
            Buzzer1SecToggle = 1                        '}
            Buzzer100mSToggle = 1                       '}
 
            AlarmSoundCount = 0
 
        EndIf
 
 
        If Data.AlarmOn = 1 Then                '}Keep track of how long alarm
                                                '}has been sounding
            Inc(AlarmSoundCount)                '}
 
            If AlarmSoundCount = MaxAlarmSoundTime Then '}If alarm has been 
                                                        '}sounding for max time,
                Data.AlarmOn = 0                        '}turn it off
                                                        '}
            EndIf                                       '}
 
        EndIf
 
                                                'Get light sensor reading:
        Data.LightLevel = ADRESH                '}Get result of A/D conversion
        StartADConversion = True                '}and start next conversion
 
 
        If Data.LightLevel <= Data.LampOnThreshold Then      '}If light level
                                                             '}is below ON
            LampOffLevelCount = 0                            '}threshold,
            Inc(LampOnLevelCount)                            '}increase the ON 
            If LampOnLevelCount > LampOnOffDelay Then        '}counter
                LampOnLevelCount = LampOnOffDelay            '}
            EndIf                                            '}
 
        ElseIf Data.LightLevel >= Data.LampOffThreshold Then '}If light level
                                                             '}is above OFF
            LampOnLevelCount = 0                             '}threshold,
            Inc(LampOffLevelCount)                           '}increase the OFF
            If LampOffLevelCount > LampOnOffDelay Then       '}counter
                LampOffLevelCount = LampOnOffDelay           '}
            EndIf                                            '}
 
        Else                                                 '}Light level is
                                                             '}not above or
            LampOnLevelCount = 0                             '}below ON / OFF
            LampOffLevelCount = 0                            '}thresholds, reset
                                                             '}both counters
        EndIf                                                '}
 
 
        If LampFlashCount > 0 Then          '}If switch 2 was pressed to flash
                                            '}the front panel lamp then decrease
            Dec(LampFlashCount)             '}the Flash counter.  
 
            If LampFlashCount = 0 Then          '}
                                                '}When the Flash counter reaches
                LampOverride = False            '}zero, clear the Lamp Override
                                                '}flag
            EndIf                               '}
 
        EndIf                           
 
    EndIf
 
    #if Debug_Mode = True Then
        Low(DebugPin0)
    #endif
 
Wend

Congratulations for reading this far!  In case you're wondering, whilst she's too nice to say so to my face, I suspect my wife does think it's a little bit pointless...


Posted: 3 years 6 months ago by bitfogav #5450
bitfogav's Avatar
Very professional job! Im quite interested how your USB communicates with your PC application, something ive always wanted to do
Posted: 3 years 6 months ago by andyo #5451
andyo's Avatar
I had USB comms in the "too hard" pile for a long time - it was only when Graham published his iHID: digital-diy.com/.../... program (with explanation) on the forum that I decided to take the plunge. That, and Jon's top tip: digital-diy.com/.../... about a cap on the Vusb pin made it all make sense! The Swordfish USBHID library takes care of the heavy-lifting on the PIC side.

Happy to share the source for my PC app but to be honest it's just a different front-end to Graham's iHID code - let me know if you want to see it though.

Perhaps there's scope for an article demonstrating a "hello world" type application using USB comms between a PIC and a PC?
Posted: 3 years 6 months ago by bitfogav #5452
bitfogav's Avatar
I would be greatful to see your source code of your PC app, it looks quite a tool if im honest for your clock design.


Im definitely going to look into USB on my next project, like you I used to think USB seems "too hard"! but I actually have some 18F4550 and a 18F2550 kicking about..
Posted: 3 years 6 months ago by andyo #5453
andyo's Avatar
No worries - source code here: http://digital-diy.com/Download-documen ... ource.html

Give us a shout if you need anything clarified or explained.
Posted: 3 years 6 months ago by Anonymous #5454
Anonymous's Avatar
I'm floored by how good this looks, from the inside-out. From the well thought out design, real subtle mix of technology with old materials, and it even has a USB port! You should consider using this as a prototype for mass-production!

One could even put 8-segement displays behind the front to display different data (not just Hours (h) and Minutes (m), but possibly bandwidth usage, email notifications, etc.

Love it!
Posted: 3 years 6 months ago by Anonymous #5455
Anonymous's Avatar
Absolutely brilliant craftsmanship. Kudos to you sir. You have not only built a beautiful timepiece, you have presented the building of it with equal elegance. Quite inspiring I must say. The ICSP DIN connector slayed me. I say again, absolutely brilliant.
Posted: 3 years 6 months ago by andyo #5456
andyo's Avatar
Thanks for the positive comments everyone - glad you like the project.
Posted: 3 years 6 months ago by Anonymous #5457
Anonymous's Avatar
As a woodworker (among other geekity) I can tell you that springback in wood bending is normal. I don't think you would need clamps for the initial (non-construction) bending--just what you did. Use the clamps during the glue-up to take care of the springback.

And, by the way, the pattern or template you used to bend the veneer is just called a "form."

Kudos for mixing woodworking with electronics and code writing.

LRod
Posted: 3 years 6 months ago by andyo #5458
andyo's Avatar
Thanks LRod - useful feedback. Are you able to identify the mystery wood? Out of curiosity, I'd really like to know what it is!

And thanks for providing the correct terminology for the template - when I said "I'm no expert", that might have been an understatement Could be worse - in my first draft I was calling it a 'mold' but I knew that wasn't right!!
Posted: 3 years 6 months ago by Jon Chandler #5459
Jon Chandler's Avatar
One advantage to this design if you didn't gear the servos down is that the linearity of most servos is better over a 90 degree range than a larger range. I too found quite a difference in servo linearity between the more costly ones and the cheap ones. In the first clock, I just brute-forced a few small corrections to make the pointer be on the proper side of the quarter-hour increments. For the second, with a "noname" servo, I had to resort to a lookup table to get decent results across the range.

Forum Activity

Member Access