' {$STAMP BS1}
'------------------------------------------------------
' "Trainsaver" Digital Train Controller
' By Vern Graner Jul 18, 2005
' Last Update    Apr 25, 2006 (V1.0t)
' Any questions to vern@txis.com
'------------------------------------------------------
' Designed for the Prop-1/BS1 to control an electric
' train set and operate train sound effects
'------------------------------------------------------
  ' Hardware setup:
  ' P0 - Train sound 1 K3- Red (Crossing Bell)
  ' P1 - Train Sound 2 K4 -Black (Clickety-Clack)
  ' P2 - Train Sound 3 K2- Yellow (Train Whistle)
  ' P3 - IR Detector Motion detector (LOW=MOTION)
  ' P4 - LED Indicator (w/P4 LOW for lit)
  ' P5 - Train Motor (via ULN2803 bridged)
  ' P6 - Trigger Button (with jumper for pull-down)
  ' P7 - LapSense (IR Phototransistor with jumper for pull-down)

'------------------------------------------------------
' Create "human readble" names
'------------------------------------------------------
   SYMBOL Bell         = 0
   SYMBOL Click        = 1
   SYMBOL Whistle      = 2
   SYMBOL RoomEmpty    = PIN3
   SYMBOL LED          = 4
   SYMBOL TRAIN        = 5
   SYMBOL Btn          = 6
   SYMBOL Btn1         = PIN6
   SYMBOL TrainPresent = PIN7
   SYMBOL True         = 1
   SYMBOL False        = 0

'------------------------------------------------------
' Define Variables
'------------------------------------------------------
   SYMBOL IRSense         = BIT0 ' Flag for IR motion sense
   SYMBOL TrainRunning    = BIT1 ' Flag for train run state
   SYMBOL Rest            = BIT2 ' Flag for "Sleep" when no motion detected
'  SYMBOL Empty           = BIT3 ' Value can be 0 or 1
   SYMBOL I               = B1   ' value can be 0 to 254 used for loop couter
   SYMBOL btnWrk          = B2   ' value can be 0 to 254 used by BUTTON command
   SYMBOL Laps            = B3   ' Value can be 0 to 254
   SYMBOL TrainIdleTime   = B4   ' Value can be 0 to 254
   SYMBOL RoomIdleTime    = B5   ' Value can be 0 to 254
'  SYMBOL empty           = B6   ' Value can be 0 to 254
'  SYMBOL empty           = B7   ' Value can be 0 to 254
   SYMBOL LoopTicks       = W4   ' Uses B8/B9 value from 0 to 65535

'------------------------------------------------------
' Initialize Train System Variables
'------------------------------------------------------
   HIGH LED                      ' Turn off the LED in the button
   SYMBOL TrainIdleTarget = 10   ' Minutes of idle before a lap is added
                                 '  for the train to run (values 0-254)
   SYMBOL RoomIdleTarget  = 15   ' Minutes without motion before the train
                                 '  is put to sleep (values 0-254)
   SYMBOL MaskTime        = 1000 ' How many MS to wait b4 we check for the
                                 '  end of the train after beam-break.
   SYMBOL Credit          = 5    ' Number of laps added by a button press
                                 '  (or coin drop)
   SYMBOL LapLimit        = 9    '  Set the maximum number of laps that may
                                 '   be added
   SYMBOL TicksPerMinute  = 4000 ' 4000 ~number of ticks that pass in a minute
                                 '  Note that if you alter the loop length you
                                 '  will have to recalibrate.
   SYMBOL TunnelDelay     = 6000 ' Delay from end of train till stop execution
                                 ' (allow train to traverse tunnel)

'--------------------
' Calibration/Setup |
'-------------------------------------------------------------------------
' IR Beam Calibration and Walktest for PIR sensor Hold button down on
' powerup, then release the button within 3 seconds after powerup to
' invoke the "calibrate/Test" mode. LED will idicate motion detected.
' Press the button again to align the IR beam. LED will indicate beam
' presence. Note: All this code may be commented and/or moved to a stand
' alone program if on-site calibration is not needed or if additional
' code space is required for updtes/improvements.
'-------------------------------------------------------------------------
IF BTN1 = False THEN NoTest      ' No button, so no testing invoked
PAUSE 2000                       ' Wait a bit for the person to let go!

WalkTest:                        ' Light the LED when motion detected.
  IF RoomEmpty = True THEN NoLED1
    LOW LED
  NoLED1:
  HIGH LED
  IF BTN1 = True THEN DoneWalk   ' Exit the test on button press
  GOTO WalkTest
DoneWalk:

PAUSE 2000                       ' Wait for the person to let go!

IRTest:                          ' Light the LED when IR beam is aligned
 IF TrainPresent = True THEN NoLED2
    LOW LED
  NoLED2:
  HIGH LED
  IF BTN1 = True THEN DoneIR     ' Exit the test on button press
  GOTO IRTest
DoneIR:

PAUSE 2000                       ' Wait for the person to let go!
                                 ' (so we don't add laps)
NoTest:

'------------------------------------------------------
' Main Program Begins Here
'------------------------------------------------------
   MAIN:
     GOSUB CheckButton    'See if the button has been pressed
     GOSUB CheckMotion    'See if there has been motion detected
     GOSUB CheckIR        'See if the train is blocking the beam
     GOSUB CheckLaps      'See if the train should stop/start
     GOSUB Counters       'Increment all activity counters
   GOTO MAIN

'------------------------------------------------------
'Subroutines-------------------------------------------
'------------------------------------------------------

'--------------------
'Check for Button   |
'------------------------------------------------------
' Routine to see if the button has been pressed. If so
' add the # of laps indicated by the "credit" value.
'------------------------------------------------------
CheckButton:
  'BUTTON Pin, DownState, Delay, Rate, Workspace, TargetState, Address
   BUTTON Btn,         1,   254,  150,    btnWrk,           0, NoPress

AddLaps:
'    Code here is executed ONCE for EACH button press
'     DEBUG "Button pressed",CR
     LOW LED
     Laps = Laps + Credit 'Number of laps added by button press/credit
     PAUSE 50
     HIGH LED
  NoPress:                'Comes here if button is NOT pressed
  RETURN

'--------------------
' Check PIR motion  |
'------------------------------------------------------
' Routine to see if motion has been detected in the room
' If no motion for the amount of minutes specified by
' RoomIdleTarget value, the set the "rest" bit to TRUE
' So the train will cease "self-adding" laps
'------------------------------------------------------
CheckMotion:

  IF RoomIdleTime < RoomIdleTarget THEN NoRest ' Has room been empty long enough
                                               ' to stop the show?
     Rest = TRUE          ' YES -Put the system "to sleep" till we see motion again.
     TrainIdleTime = 0    '      Hold the idle time to zero so no laps will auto-add
  NoRest:                 ' NO - Don't put the system to sleep

  IF RoomEmpty = TRUE THEN DontWakeUp 'Is motion detected?
    Rest = FALSE          ' YES- tell the train system to wake up!
    RoomIdleTime = 0      '      Reset the room idle time to zero
  DontWakeUp:             ' NO- Don't reset the counter

  RETURN

'--------------------
' Check the IR Beam |
'-------------------------------------------------------------------------
' Routine to see if the train is present (has blocked the IR LED). If so,
' decrement the lap counter. This section also plays sounds to indicate
' function. If the train will be passing through, the "click" sound is
' played. The Mask time variable is used To keep the space between rail
' cars from decrementing multiple laps. This value should reflect the
' amount of time it takes for the all the cars to pass the sensor.
' Note: If the sensor is placed so that the couplers between cars continue
' to block the beam, the mask is not necessary.
'-------------------------------------------------------------------------
CheckIR:

  IF TrainPresent = True THEN IRBlocked  ' Is the IR beam blocked?
    RETURN                               ' NO- Train not present
  IRBlocked:                             ' YES- The IR is blocked

  FOR I =1 TO 100                        ' Crock pot detector :)
   IF TrainPresent = FALSE THEN CheckIR: ' IR sensor must stay in state for
                                         ' for 100 iterations in order to prove "valid"
  NEXT I

  PAUSE MaskTime                        ' Ignore the sensor while other train cars pass
                                        ' Note: May not be necessary if sensor is located
                                        ' at the level of the train car couplers
  CheckAgain:
  IF TrainPresent = False THEN IRUnBlocked 'Wait for the train to pass the sensor
    GOTO CheckAgain
  IRUnBlocked:

'  DEBUG "IR Unblocked",CR
'  DEBUG "Blink # of laps remaining (",#laps,")",CR

  IF LAPS = 1 THEN NoBLink  ' No laps remain if we're stopping the train, so no blink.

    HIGH CLICK              ' Play click/clack sound when just passing through
    PAUSE 100
    LOW CLICK

    FOR I = 2 TO LAPS       ' Blink the LED with the remaining number of laps
      LOW LED
      PAUSE 75
      HIGH LED
      PAUSE 250
    NEXT

  NoBLink:
  IF LAPS=0 THEN NoDecrement 'Don't decrement if we are already at zero
    Laps=Laps-1              'Decremet one lap from the counter
  NoDecrement:
  RETURN

'--------------------
'Check Laps Routine |
'-------------------------------------------------------------------------
' Routine to see if its time to stop/start the train
' If the Train Idle time exceeds the Train Idle Target time, a lap will
' be added to the lap counter unless the "rest" switch has been thrown
' due to no activity in the viewing area. The start and stop of the train
' engine is "eased" by using PWM values here. These values may need to
' to be "tweaked" to allow smooth acceleration/deceleration of your
' particular engine. This section will also play a sound effect if the
' train is starting (whistle) or if the train is stopping (crossing bell
' sound).
'-------------------------------------------------------------------------
CheckLaps:

   IF TrainIdleTime < TrainIdleTarget OR REST=1 THEN NoLap1
      LAPS=LAPS+1       ' Add laps if we have been without a lap for TrainIdleTarget time
      TrainIdleTime = 0 ' Once a lap is added, reset the idle timer
    NoLap1:

  IF LAPS = 0 THEN StopTrain

  StartTrain:
    IF TrainRunning = True THEN AlreadyStarted
'        DEBUG "PWM Start (Whistle)",CR

         HIGH Whistle        ' Play the Whistle
         PAUSE 100           ' Have to pause long enough for the module to reliably
         LOW Whistle         ' get a "play sound" signal

       ' PWM   Pin, Duty, Duration

        FOR I = 3 TO 10      ' Ease the motor on using PWM
          PWM Train, I, 100
        NEXT

        HIGH Train           ' Train full on here
        PAUSE 5500           ' wait for train to reach bridge
                             ' added for PokeJos train only!!
                             ' NOTE: May need to be removed as button ignored all this time!

        HIGH CLICK           ' play click/clack sound
        PAUSE 100
        LOW CLICK

        TrainRunning=TRUE ' Set the flag to indicate the train is running
    AlreadyStarted:
    RETURN

  StopTrain:
    IF TrainRunning = FALSE THEN AlreadyStopped
'        DEBUG "PWM Stop (Bell)",CR
        HIGH Bell            ' Play the bell
        PAUSE 100            ' Have to pause long enough for the module to reliably
        LOW Bell             '  get a "play sound" signal

        PAUSE TunnelDelay    ' Allow the train to get to the start of the tunnel b4 stopping

        FOR I = 10 TO 3 STEP -1 ' Ease the motor on using PWM
          PWM Train, I, 100
        NEXT

        LOW Train          ' Train Full Stop here
        TrainIdleTime = 0  ' Reset the idle counter to zero
        TrainRunning=FALSE ' Set the flag to indicate the train is stopped
    AlreadyStopped:        ' Gets here if we have been circling in this loop.
    RETURN

'-------------------------
'Check/Incremet Counters |
'-------------------------------------------------------------------------
'Routine to increment the counters for idle and motion. This section
' increments "ticks" until it reachs the number that indicate approximately
' one minute of elapsed time. Once a minute has been reached the RoomIdleTime
' and TrainIdle variables are incremented. Note: The number of ticks per
' minute is estimated and may have to be altered if the code is changed
' dramaticly.
'-------------------------------------------------------------------------
Counters:
      LoopTicks=LoopTicks + 1
      IF LoopTicks < TicksPerMinute THEN NoCascade1 ' don't add one minute to the counter
'        DEBUG "Train Idle=",#TrainIdle," Room Idle=",#RoomIdle," Laps=",#laps," Rest=",#rest,CR
        LoopTicks = 0                        ' Resent the Loopstick so we can start counting again
        TrainIdleTime = TrainIdleTime + 1    ' add to the train idle time counter
        RoomIdleTime  = RoomIdleTime  + 1    ' add to the motion sensor idle time
        RoomIdleTime  = RoomIdleTime MAX 250 ' when room is idle, make sure we don't "rollover" the counter
      NoCascade1:
     Laps = Laps MAX LapLimit                'Limit the Maximum laps that may be added
RETURN