Newer
Older
devtimer / devtimer.py
#!/usr/bin/env python3
# devtimer.py - Temperature Controlled Photographic Darkroom Timer
# Targeted for RaspberryPi
# Copyright (c) 2018 TundraWare Inc.
# Permission Hereby Granted For Unrestricted Personal Or Commercial Use

#####
# Imports
#####

# General Imports

from threading import Thread
from time import time, sleep

# Hardware Support

from ds18b20 import *
from tm1637 import *
from wiringpi import wiringPiSetupGpio, pinMode, digitalRead, GPIO

import globals


#####
# Constants
#####

# GPIO Ports And Other Hardware Constants

TIME_BRIGHT = 0x0a    # Displays
TIME_CLK = 21
TIME_DIO = 20

TEMP_BRIGHT = 0x0a
TEMP_CLK = 12
TEMP_DIO = 16

DIMMING = 1   # How much to dim display in film mode

FOOTSW = 6      # GPIO port to read footswitch state
RUNNING = False # Whether or not to run the timer

# General Constants

DEBUG = True                  # Debugging switch

BEEP = 15                     # Beep interval
CALIBRATION_OFFSET = 0.003    # Compensate for program overhead in master loop
MINUS = 16                    # Index of minus segment table lookup


#####
# Lookup Table For Compensating Factors
#####

'''
  There are 3 tables in the list below.  In order:

      Realtime - never actually used, just there as a placeholder
      Paper
      Film

  Each contains entires for multiplicative corrections from 60F to 80F.

  The profile global above selects which of these tuples to index into
  - using the normalized temp global above as the index.  We don't
  want to use a dictionary here (with profile as the key) because of
  the overhead that incurs.  Straight tuple indexing should be much
  quicker.

  WARNING: It takes about 250ms to update the display on a Pi Zero.
           So, if the "virtual second" falls at or below this, the
           code will be attempting to do updates faster than the
           display can handle. So ... the total compensation cannot
           reduce the virtual second to less than about 0.300 to be on
           the safe side.
'''

compensate = (
              (1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000),
              (1.724, 1.611, 1.505, 1.406, 1.313, 1.227, 1.146, 1.070, 1.000, 0.934, 0.873, 0.815, 0.762, 0.711, 0.665, 0.621, 0.580, 0.542, 0.506, 0.473, 0.442),
              (1.445, 1.380, 1.318, 1.259, 1.202, 1.148, 1.096, 1.047, 1.000, 0.955, 0.912, 0.871, 0.832, 0.795, 0.759, 0.725, 0.692, 0.661, 0.631, 0.603, 0.576)
             )

#####
# Supporting Subroutines
#####

# Beep at the user at fixed intervals

def beep():
    print("Beep!")

# Check to see if footswitch got pressed

def monitor_footsw():

    while True:
        if not digitalRead(FOOTSW):  # Button push pulls down
            Thread(name="ReadFootSW", target=read_footsw).start()
        sleep(.5)

def read_footsw():
    global RUNNING

    sleep(0.10)
    if not digitalRead(FOOTSW):
        RUNNING = not RUNNING
        print(RUNNING)


# Update the display with elapsed time

def show_elapsed(display_time, elapsed):

    min = elapsed // 60
    sec = elapsed % 60
    d0 = display_time.digit_to_segment[min // 10]
    d1 = display_time.digit_to_segment[min % 10]
    d2 = display_time.digit_to_segment[sec // 10]
    d3 = display_time.digit_to_segment[sec % 10]
    display_time.set_segments([d0, 0x80 + d1, d2, d3])

# Display current temperature
# Negative temps are 2 digits, positive temps are 3 digits

def show_temp(display_temp, temp):

    if temp < 0:    # The leading digit is a minus sign
        hun = display_time.digit_to_segment[MINUS]
        temp = abs(temp)
    else:
        hun = display_time.digit_to_segment[temp // 100]

    ten = display_time.digit_to_segment[(temp % 100) // 10]
    one = display_time.digit_to_segment[(temp % 100) % 10]
    F = display_time.digit_to_segment[15]
    display_temp.set_segments([hun, ten, one, F])


#####
# Program entry point
#####

'''
  We start a perpetual thread to read the current temperature
  and update the relevant global variable.

  Notice that the actual updating of the display gets run on its own
  thread as well.  That's because - on a Pi Zero, at least - it takes
  over 250ms to do this.  We don't want that time added to our timing
  loop, so we send it off on a parallel thread, and initiate timing
  for the next round in this thread.
'''

if __name__ == "__main__":

    # Setup the hardware

    wiringPiSetupGpio()
    pinMode(FOOTSW, GPIO.INPUT)

    # Dim displays in film mode

    DIM_BY=0
    if globals.CURRENT_PROFILE == globals.FILM:
        DIM_BY = 2

    display_time = TM1637(TIME_CLK, TIME_DIO, TIME_BRIGHT-DIM_BY)
    display_temp = TM1637(TEMP_CLK, TEMP_DIO, TEMP_BRIGHT-DIM_BY)

    # Start measuring temperature

    get_temps = Thread(name="Temperatures", target=monitor_temps).start()
    sleep(1)     # Wait a bit for the 1st temp measurement to complete

    # Start monitoring for footswitch presses

    Thread(name="MonitorFootSW", target=monitor_footsw).start()

    # Start timing, using the selected profile and measured temperature

    elapsed_time = 0
    compensation_factor = 1

    while True:

        # Beep periodically

        if not elapsed_time % BEEP:
            beep()

        if DEBUG:
            last = time()

        # Get last known temp

        current_temp = globals.CURRENT_TEMP

        # Update the displays on separate threads

        update_time = Thread(name="Timer", target=show_elapsed, args=(display_time, elapsed_time)).start()

        update_temp = Thread(name="Temp", target=show_temp, args=(display_temp, current_temp)).start()

        # For temperatures in-range, look up the compensating factor

        if (globals.CURRENT_PROFILE == globals.REALTIME):
            compensation_factor = 1   # Realtime requires no compensation

        elif globals.TEMP_LOW <= globals.CURRENT_TEMP <= globals.TEMP_HIGH:
            compensation_factor = compensate[globals.CURRENT_PROFILE][current_temp-globals.TEMP_LOW]

        # Temperature is out of range for our correction table
        # This implictly uses the last known compensation factor so we can keep running

        else:
            print("Display Out-Of-Range Message Here")

        sleep(compensation_factor - CALIBRATION_OFFSET)
        elapsed_time += 1
        elapsed_time %= 6000

        if DEBUG:
           print("Current Temp: %s Factor: %s Inter-update Time: %s" % (current_temp, compensation_factor, str(time()-last)))