#!/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 import globals ##### # Constants ##### # Display Hardware Configuration TIME_BRIGHT = 0x0a 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 # General Constants DEBUG = True # Debugging switch BEEP = 15 # Beep interval CALIBRATION_OFFSET = 0.003 # Compensate for program overhead in master loop ##### # 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) ) # Beep at the user at fixed intervals def beep(): print("Beep!") # 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]) def show_temp(display_temp, temp): 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() # 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) get_temps.start() sleep(1) # Wait a bit for the 1st temp measurement to complete # 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)) update_time.start() update_temp = Thread(name="Temp", target=show_temp, args=(display_temp, current_temp)) update_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)))