-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathmaster-clock.py
More file actions
executable file
·272 lines (250 loc) · 13.5 KB
/
master-clock.py
File metadata and controls
executable file
·272 lines (250 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#!/usr/bin/env python
#External settings
import settings
#External modules
if settings.piMode: import RPi.GPIO as GPIO
import os #includes path, listdir
import sys
import logging
import time #includes asctime, localtime
import subprocess #for starting it without hogging the shell
import threading #for advancing clock and meter simultaneously
from subprocess import call #synchronous
from subprocess import Popen #asynchronous
from datetime import datetime
from datetime import timedelta
#External modules, separately installed
import daemon #via sudo apt-get install python-daemon
#help on this from:
#http://www.gavinj.net/2012/06/building-python-daemon-process.html
#https://www.python.org/dev/peps/pep-3143/
class MasterClock():
def __init__(self):
self.dcLast = 0
self.slaveTime = datetime.now()
#This variable will keep (volatile) track of the slave-displayed time.
def convertValueToDC(self,valNew):
dcNew = 0
#Which calibration range does valNew fall into?
for i in range(0, len(settings.meterCal)):
#If it falls in this one, or in/past the last one:
if(valNew < settings.meterCal[i][0] or i == len(settings.meterCal)-1):
valMax = settings.meterCal[i][0]
valMin = 0
dcMax = settings.meterCal[i][1]
dcMin = 0
#if valNew < first calibration point (indicated by i==0), lower bound is assumed calibration point of (0,0)
if i > 0: #if valNew > first calibration point, set lower bound to previous calibration point
valMin = settings.meterCal[i-1][0]
dcMin = settings.meterCal[i-1][1]
#Map new value. Not sure if I've reduced this as far as it can go mathwise.
#print ("i="+str(i)+", valNew="+str(valNew)+", valMax="+str(valMax)+", valMin="+str(valMin)+", dcMax="+str(dcMax)+", dcMin="+str(dcMin))
return float(dcMax-dcMin)*(float(valNew-valMin)/(valMax-valMin)) + dcMin
#end found calibration range
#end for each calibration range
#end def convertValueToDC
def updateMeter(self,valNew):
#self.logger.debug('updateMeter to '+str(valNew))
#We will probably set it to valNew, but may want to set status instead. TODO
#if(no network connection): self.setMeter(10)
#elif(bad ntp): self.setMeter(20)
#ntpq -c rv | grep "reftime" with result e.g.
#reftime=dabcecde.167297c4 Sat, Apr 16 2016 11:54:54.087,
#reftime=00000000.00000000 Sat, Apr 16 2016 11:54:54.087,
#self.setMeter(30) during active slave setting is taken care of in syncSlave()
#else:
self.setMeter(valNew)
#end def updateMeter
def setMeter(self,valNew):
if settings.meterPin != False:
#self.pwm must already have been started
dcNew = self.convertValueToDC(valNew) #find new dc
if dcNew > 100: dcNew = 100 #apply range limits
if dcNew < 0: dcNew = 0
#set meter, using ballistics if dcChg is great enough
dcChg = dcNew-self.dcLast
if settings.piMode:
if(abs(dcChg) > settings.meterChg): #apply ballistics
#easing out equations by Robert Penner - gizma.com/easing
for t in range(1, settings.meterStp+1):
#quadratic t^2
t /= float(settings.meterStp)
nowDC = float(-dcChg) * t * (t-2) + self.dcLast
self.pwm.ChangeDutyCycle( nowDC )
if(t<settings.meterStp):
time.sleep(settings.meterLag)
else: #just go to there
self.pwm.ChangeDutyCycle(dcNew)
#end pi mode
#self.logger.debug('Set meter to val '+str(valNew)+': from dc '+str(self.dcLast)+' '+str(dcChg)+' to '+str(dcNew))
self.dcLast = dcNew
#end def setMeter
#Slave clock control
def getStoredSlaveTime(self):
masterTime = datetime.now()
#Normalize to the slave clock's interval
masterTime = masterTime.replace(second=masterTime.second-(masterTime.second % settings.slaveInterval), microsecond=0)
self.slaveTime = masterTime
if os.path.exists(settings.slavePath):
with open(settings.slavePath, 'r') as f:
#https://docs.python.org/2/tutorial/inputoutput.html
savedTime = f.read().split(':') #expecting format h:m:s
#shouldn't be necessary to close
if(len(savedTime)==3): #validation close enough
self.slaveTime = self.slaveTime.replace(hour=int(savedTime[0]),minute=int(savedTime[1]),second=int(savedTime[2])-(int(savedTime[2])%settings.slaveInterval))
self.logger.debug('Read from file: '+str(self.slaveTime.hour)+':'+str(self.slaveTime.minute)+':'+str(self.slaveTime.second))
else: self.logger.warn('Bad slave time file. Assumed: '+str(self.slaveTime.hour)+':'+str(self.slaveTime.minute)+':'+str(self.slaveTime.second))
else: self.logger.warn('No slave time file. Assumed: '+str(self.slaveTime.hour)+':'+str(self.slaveTime.minute)+':'+str(self.slaveTime.second))
#self.slaveTime now gives (presumably) time displayed on slave, always in 24h format
#If needed, apply offsets to put self.slaveTime within adjusting range of masterTime:
#masterTime-(slaveHrs-slaveHold) < self.slaveTime <= masterTime+slaveHold
#We consider slaveHrs because, if slave is 12h, we may assume 3:00 = 15:00 and vice versa
#if slave is now AHEAD by more than slaveHold, set it back a day
if (self.slaveTime-masterTime).total_seconds() > settings.slaveHold*3600:
self.slaveTime = self.slaveTime - timedelta(days=1)
#if slave is now BEHIND by more than slaveHrs-slaveHold, set it forward slaveHrs
if (self.slaveTime-masterTime).total_seconds() <= settings.slaveHrs*-3600 + settings.slaveHold*3600:
self.slaveTime = self.slaveTime + timedelta(hours=settings.slaveHrs)
#end getStoredSlaveTime
def setStoredSlaveTime(self):
try:
self.logger.debug('Writing to file: '+str(self.slaveTime.hour)+':'+str(self.slaveTime.minute)+':'+str(self.slaveTime.second))
with open(settings.slavePath, 'w') as f:
#w truncates existing file http://stackoverflow.com/a/2967249
f.seek(0)
#Don't worry about leading zeroes, they'll be parsed to ints at read anyway
f.write(str(self.slaveTime.hour)+':'+str(self.slaveTime.minute)+':'+str(self.slaveTime.second))
f.truncate()
#close
except:
self.logger.warn('Could not write slave time to file.')
#end setStoredSlaveTime
def impulseSlave(self,write=True):
self.slaveTime = self.slaveTime + timedelta(seconds=settings.slaveInterval)
if settings.piMode:
if settings.slaveBipolar:
#if interval is one second, assume we are driving seconds; else minutes
polarity = self.slaveTime.second % 2 if settings.slaveInterval==1 else self.slaveTime.minute % 2
if polarity:
GPIO.output(settings.slavePinOdd, GPIO.HIGH)
else:
GPIO.output(settings.slavePinEven, GPIO.HIGH)
else:
GPIO.output(settings.slavePin, GPIO.HIGH)
time.sleep(settings.slaveImpulse)
if settings.slaveBipolar:
GPIO.output(settings.slavePinOdd, GPIO.LOW)
GPIO.output(settings.slavePinEven, GPIO.LOW)
else:
GPIO.output(settings.slavePin, GPIO.LOW)
#end pi mode
self.logger.debug('Advance clock to '+str(self.slaveTime.hour)+':'+str(self.slaveTime.minute)+':'+str(self.slaveTime.second))
if write and settings.slaveWriteRealTime:
self.setStoredSlaveTime() #store in case of power failure.
def syncSlave(self):
#Check if slave is in sync, and if not, to wait or advance
#Run this synchronously, so it will interrupt normal time display
diff = (self.slaveTime-datetime.now()).total_seconds()
self.logger.info('syncSlave: diff is: '+str(diff))
if(diff > 0-settings.slaveInterval and diff <= 0): return
#lucyyyyy you got some adjustin' to do
self.setMeter(30)
while (self.slaveTime-datetime.now()).total_seconds() > -1:
#the -1 is to avoid advancing the clock as soon as it catches up
time.sleep(1)
#self.logger.debug('Waiting... diff is '+str((self.slaveTime-datetime.now()).total_seconds()))
while (self.slaveTime-datetime.now()).total_seconds() <= 0-settings.slaveInterval:
self.impulseSlave(False) #don't write to file for each advance
#self.logger.debug('Advancing... diff is '+str((self.slaveTime-datetime.now()).total_seconds()))
time.sleep(settings.slaveRecover)
self.logger.info('syncSlave: sync complete.')
if settings.slaveWriteRealTime:
self.setStoredSlaveTime()
#end syncSlave
def run(self):
#Let's go!
self.logger = logging.getLogger("DaemonLog")
if(settings.logDebug):
self.logger.setLevel(logging.DEBUG)
else:
self.logger.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
handler = logging.FileHandler(settings.logPath)
handler.setFormatter(formatter)
self.logger.addHandler(handler)
if settings.piMode:
GPIO.setmode(GPIO.BCM)
if settings.slaveBipolar:
GPIO.setup(settings.slavePinOdd, GPIO.OUT)
GPIO.setup(settings.slavePinEven, GPIO.OUT)
else:
GPIO.setup(settings.slavePin, GPIO.OUT)
if settings.meterPin != False:
GPIO.setup(settings.meterPin, GPIO.OUT)
self.pwm = GPIO.PWM(settings.meterPin, 50)
self.pwm.start(0)
#end pi mode
try:
self.logger.info("Master clock start. ********************")
self.getStoredSlaveTime() #just once per run
self.syncSlave()
lastMinute = -1
lastTick = -1
while 1:
#important to snapshot current time, so test and assignment use same time value
nowTime = datetime.now()
nowTick = nowTime.second*1000000 + nowTime.microsecond
#As soon as the minute changes, or we exceed the tick duration, time for a tick
#It will always fire on first run because of the minute mismatch
if nowTime.minute != lastMinute or nowTick > lastTick + settings.meterSec*1000000:
#Use the "clean" tick value, rather than the slightly late sample time, to avoid drift
nowTick = nowTick - (nowTick % (settings.meterSec*1000000))
#self.logger.debug('start of tick '+str(float(nowTick)/1000000))
#slave clock and meter adjustments are started as threads so they can be simultaneous
#thrMeter is always done; thrSlave is only done when conditions warrant
thrMeter = threading.Thread(target=self.updateMeter, args=(float(nowTick)/1000000,))
thrMeter.start() #always do this one
thrSlave = threading.Thread(target=self.impulseSlave)
if nowTime.second % settings.slaveInterval == 0:
#self.logger.debug('time to advance');
thrSlave.start()
#self.impulseSlave()
if nowTime.minute == 0 and nowTime.minute != lastMinute and lastMinute != -1:
#At the top of the hour, call syncSlave in case of DST changes
#self.logger.debug('time to sync');
if thrSlave.ident != None: thrSlave.join() #wait until the last impulse is done
thrMeter.join() #wait until the meter update is done
self.syncSlave() #call synchronously to interrupt main time display loop
#update last values
lastMinute = nowTime.minute
lastTick = nowTick
#don't proceed with the loop until all going threads have been handled
if thrSlave.ident != None: thrSlave.join()
thrMeter.join()
#self.logger.debug('end of tick');
#end tick
time.sleep(0.05)
#end while
except:
self.logger.exception('')
finally:
self.logger.info('Master clock stop. ....................')
self.setStoredSlaveTime()
if settings.piMode:
if settings.meterPin != False:
if self.dcLast > 20: #kill the meter softly
self.setMeter(0)
self.pwm.stop()
if settings.slaveBipolar:
GPIO.setup(settings.slavePinOdd, GPIO.OUT)
GPIO.setup(settings.slavePinEven, GPIO.OUT)
else:
GPIO.output(settings.slavePin, GPIO.LOW)
GPIO.cleanup()
#end pi mode
#end try/except/finally
#end def run
#end class MasterClock
masterClock = MasterClock()
with daemon.DaemonContext():
masterClock.run()