Compare View

switch
from
...
to
 

Diff

Showing 2 changed files Inline Diff

ssa/alarm/alarmist.py View file @ 7b0ebee
""" 1 1 """
alarmist.py 2 2 alarmist.py
3 3
Periodically checks on SOARS state and processes alarms 4 4 Periodically checks on SOARS state and processes alarms
""" 5 5 """
6 6
import configparser 7 7 import configparser
import collections 8 8 import collections
import influxdb 9 9 import influxdb
import sys 10 10 import sys
import threading 11 11 import threading
import ssa.alarm.lightstack as lightstack 12 12 import ssa.alarm.lightstack as lightstack
13 13
DEBUG = False 14 14 DEBUG = False
15 15
# bearing coolant is measured by gauge A 16 16 # bearing coolant is measured by gauge A
BEARING_COOLANT_QUERY = """ 17 17 BEARING_COOLANT_QUERY = """
SELECT last(flow_rate) as flow_rate 18 18 SELECT last(flow_rate) as flow_rate
FROM coolant_flow_rate 19 19 FROM coolant_flow_rate
WHERE meter_id='A' 20 20 WHERE meter_id='A'
""" 21 21 """
# we have to infer if the paddle is moving based on the motor torque 22 22 # we have to infer if the paddle is moving based on the motor torque
PADDLE_MOVING_QUERY = """ 23 23 PADDLE_MOVING_QUERY = """
SELECT last(value) as value FROM Torque GROUP BY * 24 24 SELECT last(value) as value FROM Torque GROUP BY *
""" 25 25 """
# figure out the hottest bearing 26 26 # figure out the hottest bearing
MAX_BEARING_TEMPERATURE_QUERY = """ 27 27 MAX_BEARING_TEMPERATURE_QUERY = """
SELECT max(temperature) as max_temperature FROM ( 28 28 SELECT max(temperature) as max_temperature FROM (
SELECT last(temperature) as temperature 29 29 SELECT last(temperature) as temperature
FROM paddle_bearings 30 30 FROM paddle_bearings
WHERE location != 'Ambient' 31 31 WHERE location != 'Ambient'
GROUP BY location 32 32 GROUP BY location
) 33 33 )
""" 34 34 """
# grab ambient temperature 35 35 # grab ambient temperature
AMBIENT_TEMPERATURE_QUERY = """ 36 36 AMBIENT_TEMPERATURE_QUERY = """
SELECT last(temperature) as temperature 37 37 SELECT last(temperature) as temperature
FROM paddle_bearings 38 38 FROM paddle_bearings
WHERE location = 'Ambient' 39 39 WHERE location = 'Ambient'
""" 40 40 """
41 41
# annoying, but we need to map the measurement name and value key from influxdb 42 42 # annoying, but we need to map the measurement name and value key from influxdb
# onto a key in the soars state object 43 43 # onto a key in the soars state object
MeasurementInfo = collections.namedtuple("MeasurementInfo", "name value_key state_key") 44 44 MeasurementInfo = collections.namedtuple("MeasurementInfo", "name value_key state_key")
45 45
46 46
def dprint(str): 47 47 def dprint(str):
if DEBUG: 48 48 if DEBUG:
print(str) 49 49 print(str)
50 50
51 51
class Alarmist: 52 52 class Alarmist:
def __init__(self): 53 53 def __init__(self):
self.config = configparser.ConfigParser() 54 54 self.config = configparser.ConfigParser()
self.config.read("alarmist_config.ini") 55 55 self.config.read("alarmist_config.ini")
self.state_update_event = threading.Event() 56 56 self.state_update_event = threading.Event()
self.soars_state = ThreadsafeState(self.state_update_event) 57 57 self.soars_state = ThreadsafeState(self.state_update_event)
self.lights = lightstack.LightStack() 58 58 self.lights = lightstack.LightStack()
self.threads = list() 59 59 self.threads = list()
self.threads.append( 60 60 self.threads.append(
PeriodicInfluxdbPoller( 61 61 PeriodicInfluxdbPoller(
interval=2, 62 62 interval=2,
soars_state=self.soars_state, 63 63 soars_state=self.soars_state,
query=BEARING_COOLANT_QUERY, 64 64 query=BEARING_COOLANT_QUERY,
measurment_info=MeasurementInfo( 65 65 measurment_info=MeasurementInfo(
"coolant_flow_rate", "flow_rate", "coolant_flow_rate" 66 66 "coolant_flow_rate", "flow_rate", "coolant_flow_rate"
), 67 67 ),
db_config=self.config["influxdb"], 68 68 db_config=self.config["influxdb"],
) 69 69 )
) 70 70 )
self.threads.append( 71 71 self.threads.append(
PeriodicInfluxdbPoller( 72 72 PeriodicInfluxdbPoller(
interval=2, 73 73 interval=2,
soars_state=self.soars_state, 74 74 soars_state=self.soars_state,
query=PADDLE_MOVING_QUERY, 75 75 query=PADDLE_MOVING_QUERY,
measurment_info=MeasurementInfo("Torque", "value", "motor_torque"), 76 76 measurment_info=MeasurementInfo("Torque", "value", "motor_torque"),
db_config=self.config["influxdb"], 77 77 db_config=self.config["influxdb"],
) 78 78 )
) 79 79 )
self.threads.append( 80 80 self.threads.append(
PeriodicInfluxdbPoller( 81 81 PeriodicInfluxdbPoller(
interval=5, 82 82 interval=5,
soars_state=self.soars_state, 83 83 soars_state=self.soars_state,
query=MAX_BEARING_TEMPERATURE_QUERY, 84 84 query=MAX_BEARING_TEMPERATURE_QUERY,
measurment_info=MeasurementInfo( 85 85 measurment_info=MeasurementInfo(
"paddle_bearings", "max_temperature", "max_temperature" 86 86 "paddle_bearings", "max_temperature", "max_temperature"
), 87 87 ),
db_config=self.config["influxdb"], 88 88 db_config=self.config["influxdb"],
) 89 89 )
) 90 90 )
self.threads.append( 91 91 self.threads.append(
PeriodicInfluxdbPoller( 92 92 PeriodicInfluxdbPoller(
interval=5, 93 93 interval=5,
soars_state=self.soars_state, 94 94 soars_state=self.soars_state,
query=AMBIENT_TEMPERATURE_QUERY, 95 95 query=AMBIENT_TEMPERATURE_QUERY,
measurment_info=MeasurementInfo( 96 96 measurment_info=MeasurementInfo(
"paddle_bearings", "temperature", "ambient_temperature" 97 97 "paddle_bearings", "temperature", "ambient_temperature"
), 98 98 ),
db_config=self.config["influxdb"], 99 99 db_config=self.config["influxdb"],
) 100 100 )
) 101 101 )
self.event_watcher_thread = threading.Thread(target=self.check_state) 102 102 self.event_watcher_thread = threading.Thread(target=self.check_state)
self.event_watcher_thread_cancel_flag = threading.Event() 103 103 self.event_watcher_thread_cancel_flag = threading.Event()
104 104
def run(self): 105 105 def run(self):
# start polling threads 106 106 # start polling threads
for thread in self.threads: 107 107 for thread in self.threads:
thread.start() 108 108 thread.start()
# start update event watcher 109 109 # start update event watcher
self.event_watcher_thread.start() 110 110 self.event_watcher_thread.start()
111 111
def cleanup(self): 112 112 def cleanup(self):
# cancel polling loops 113 113 # cancel polling loops
for thread in self.threads: 114 114 for thread in self.threads:
thread.cancel() 115 115 thread.cancel()
# cancel the event watcher thread 116 116 # cancel the event watcher thread
self.event_watcher_thread_cancel_flag.set() 117 117 self.event_watcher_thread_cancel_flag.set()
self.state_update_event.set() 118 118 self.state_update_event.set()
# wait for threads to stop 119 119 # wait for threads to stop
self.join(timeout=1.0) 120 120 self.join(timeout=1.0)
# finally, call cleanup on lights 121 121 # finally, call cleanup on lights
self.lights.cleanup() 122 122 self.lights.cleanup()
123 123
def join(self, timeout=None): 124 124 def join(self, timeout=None):
for thread in self.threads: 125 125 for thread in self.threads:
thread.join(timeout=timeout) 126 126 thread.join(timeout=timeout)
self.event_watcher_thread.join(timeout=timeout) 127 127 self.event_watcher_thread.join(timeout=timeout)
128 128
def check_state(self): 129 129 def check_state(self):
# TODO - create an influxdb client here and log alarm states 130 130 # TODO - create an influxdb client here and log alarm states
131 131
# this method gets run in its own thread, so loop indefinitely, 132 132 # this method gets run in its own thread, so loop indefinitely,
# waking up whenever the state_update_event flag is set 133 133 # waking up whenever the state_update_event flag is set
while self.state_update_event.wait(timeout=None): 134 134 while self.state_update_event.wait(timeout=None):
# exit thread loop if the cancel flag is set 135 135 # exit thread loop if the cancel flag is set
if self.event_watcher_thread_cancel_flag.is_set(): 136 136 if self.event_watcher_thread_cancel_flag.is_set():
break 137 137 break
# 138 138 #
# update state values that depend on db values 139 139 # update state values that depend on db values
# 140 140 #
# check if paddle is running 141 141 # check if paddle is running
(torque, _) = self.soars_state.get( 142 142 (torque, _) = self.soars_state.get(
"motor_torque", default=(0.0, None), lookback=0 143 143 "motor_torque", default=(0.0, None), lookback=0
) 144 144 )
(previous_torque, _) = self.soars_state.get( 145 145 (previous_torque, _) = self.soars_state.get(
"motor_torque", default=(0.0, None), lookback=1 146 146 "motor_torque", default=(0.0, None), lookback=1
) 147 147 )
(previouser_torque, _) = self.soars_state.get( 148 148 (previouser_torque, _) = self.soars_state.get(
"motor_torque", default=(0.0, None), lookback=2 149 149 "motor_torque", default=(0.0, None), lookback=2
) 150 150 )
paddle_on = torque > float(self.config["thresholds"]["on_torque"]) 151 151 paddle_on = torque > float(self.config["thresholds"]["on_torque"])
# Check three last points to see if paddle is moving 152 152 # Check three last points to see if paddle is moving
paddle_moving = paddle_on and ( 153 153 paddle_moving = paddle_on and (
(torque > float(self.config["thresholds"]["moving_torque"])) 154 154 (torque > float(self.config["thresholds"]["moving_torque"]))
or (torque != previous_torque or previous_torque != previouser_torque) 155 155 or (torque != previous_torque or previous_torque != previouser_torque)
) 156 156 )
# check if coolant is flowing 157 157 # check if coolant is flowing
(coolant_flow_rate, _) = self.soars_state.get( 158 158 (coolant_flow_rate, _) = self.soars_state.get(
"coolant_flow_rate", (0.0, None) 159 159 "coolant_flow_rate", (0.0, None)
) 160 160 )
coolant_on = coolant_flow_rate > float(self.config["thresholds"]["coolant"]) 161 161 coolant_on = coolant_flow_rate > float(self.config["thresholds"]["coolant"])
162 162
(ambient, _) = self.soars_state.get("ambient_temperature", (-1, None)) 163 163 (ambient, _) = self.soars_state.get("ambient_temperature", (-1, None))
(max_bearing, _) = self.soars_state.get("max_temperature", (-1, None)) 164 164 (max_bearing, _) = self.soars_state.get("max_temperature", (-1, None))
ambient_delta = float( 165 165 ambient_delta = float(
self.config["thresholds"]["ambient_temperature_delta"] 166 166 self.config["thresholds"]["ambient_temperature_delta"]
) 167 167 )
bearing_temperature_warning = False 168 168 bearing_temperature_warning = False
if ambient > 0 and max_bearing > 0: 169 169 if ambient > 0 and max_bearing > 0:
if max_bearing > ambient + ambient_delta: 170 170 if max_bearing > ambient + ambient_delta:
bearing_temperature_warning = True 171 171 bearing_temperature_warning = True
172 172
dprint( 173 173 dprint(
f"State: paddle_on: {paddle_on}, paddle_moving: {paddle_moving}, coolant_on: {coolant_on}" 174 174 f"State: paddle_on: {paddle_on}, paddle_moving: {paddle_moving}, coolant_on: {coolant_on}"
) 175 175 )
dprint(f"Ambient temperature: {ambient}C Hottest bearing: {max_bearing}C") 176 176 dprint(f"Ambient temperature: {ambient}C Hottest bearing: {max_bearing}C")
177 177
# 178 178 #
# run checks to throw alarms 179 179 # run checks to throw alarms
# 180 180 #
warning = False 181 181 warning = False
alarm = False 182 182 alarm = False
# is paddle running? (solid white, no problems) 183 183 # is paddle running? (solid white, no problems)
if paddle_on: 184 184 if paddle_on:
self.lights.on(lightstack.Channel.White) 185 185 self.lights.on(lightstack.Channel.White)
else: 186 186 else:
self.lights.off(lightstack.Channel.White) 187 187 self.lights.off(lightstack.Channel.White)
# is paddle moving? (solid green, no problems) 188 188 # is paddle moving? (solid green, no problems)
if paddle_moving: 189 189 if paddle_moving:
self.lights.on(lightstack.Channel.Green) 190 190 self.lights.on(lightstack.Channel.Green)
else: 191 191 else:
self.lights.off(lightstack.Channel.Green) 192 192 self.lights.off(lightstack.Channel.Green)
# is water running? (solid blue, no problems) 193 193 # is water running? (solid blue, no problems)
if coolant_on: 194 194 if coolant_on:
self.lights.on(lightstack.Channel.Blue) 195 195 self.lights.on(lightstack.Channel.Blue)
else: 196 196 else:
self.lights.off(lightstack.Channel.Blue) 197 197 self.lights.off(lightstack.Channel.Blue)
# is paddle moving without water? (alarm state and flash blue) 198 198 # is paddle moving without water? (alarm state and flash blue)
if paddle_moving and not coolant_on: 199 199 if paddle_moving and not coolant_on:
self.lights.start_blink(0.5, lightstack.Channel.Blue) 200 200 self.lights.start_blink(0.5, lightstack.Channel.Blue)
alarm = True 201 201 alarm = True
else: 202 202 else:
self.lights.stop_blink(lightstack.Channel.Blue) 203 203 self.lights.stop_blink(lightstack.Channel.Blue)
# is the hottest bearing above ambient? (warning) 204 204 # is the hottest bearing above ambient? (warning)
if bearing_temperature_warning: 205 205 if bearing_temperature_warning:
warning = True 206 206 warning = True
# TODO - piston temps > 100F? (flash yellow w/ buzzer) 207 207 # TODO - piston temps > 100F? (flash yellow w/ buzzer)
208 208
# did any warnings get thrown? (turn on yellow flashing) 209 209 # did any warnings get thrown? (turn on yellow flashing)
if warning: 210 210 if warning:
self.lights.start_blink(0.5, lightstack.Channel.Yellow) 211 211 self.lights.start_blink(0.5, lightstack.Channel.Yellow)
else: 212 212 else:
self.lights.stop_blink(lightstack.Channel.Yellow) 213 213 self.lights.stop_blink(lightstack.Channel.Yellow)
214 214
# 215 215 #
# did any alarm want to turn on the buzzer? 216 216 # did any alarm want to turn on the buzzer?
# 217 217 #
if alarm: 218 218 if alarm:
self.lights.start_blink(0.3, lightstack.Channel.Red) 219 219 self.lights.start_blink(0.3, lightstack.Channel.Red)
self.lights.start_blink(0.3, lightstack.Channel.Buzzer) 220 220 self.lights.start_blink(0.3, lightstack.Channel.Buzzer)
else: 221 221 else:
self.lights.stop_blink(lightstack.Channel.Red) 222 222 self.lights.stop_blink(lightstack.Channel.Red)
223 self.lights.off(lightstack.Channel.Red)
self.lights.stop_blink(lightstack.Channel.Buzzer) 223 224 self.lights.stop_blink(lightstack.Channel.Buzzer)
225 self.lights.off(lightstack.Channel.Buzzer)
224 226
# clear event flag 225 227 # clear event flag
self.state_update_event.clear() 226 228 self.state_update_event.clear()
227 229
228 230
class RingBuffer: 229 231 class RingBuffer:
def __init__(self, size): 230 232 def __init__(self, size):
self.size = size 231 233 self.size = size
self.data = [None] * size 232 234 self.data = [None] * size
self.head = -1 233 235 self.head = -1
234 236
def __getitem__(self, index=0): 235 237 def __getitem__(self, index=0):
if index >= self.size: 236 238 if index >= self.size:
raise Exception(f"Index out of bounds (index: {index}, size: {self.size})") 237 239 raise Exception(f"Index out of bounds (index: {index}, size: {self.size})")
internal_index = (self.head - index) % self.size 238 240 internal_index = (self.head - index) % self.size
return self.data[internal_index] 239 241 return self.data[internal_index]
240 242
def get(self, index=0, default=None): 241 243 def get(self, index=0, default=None):
val = self[index] 242 244 val = self[index]
return val if val is not None else default 243 245 return val if val is not None else default
244 246
def push(self, item): 245 247 def push(self, item):
self.head = (self.head + 1) % self.size 246 248 self.head = (self.head + 1) % self.size
self.data[self.head] = item 247 249 self.data[self.head] = item
248 250
249 251
# global state object protected by a lock 250 252 # global state object protected by a lock
# only update values if the new timestamp differs from the most recent item's timestamp 251 253 # only update values if the new timestamp differs from the most recent item's timestamp
# store history of past [lookback] items 252 254 # store history of past [lookback] items
class ThreadsafeState: 253 255 class ThreadsafeState:
def __init__(self, update_event, lookback=5): 254 256 def __init__(self, update_event, lookback=5):
self.lookback = lookback 255 257 self.lookback = lookback
self.state = dict() 256 258 self.state = dict()
self.lock = threading.RLock() 257 259 self.lock = threading.RLock()
self.update_event = update_event 258 260 self.update_event = update_event
259 261
def set(self, key, value, time): 260 262 def set(self, key, value, time):
with self.lock: 261 263 with self.lock:
if self.state.get(key, None) is None: 262 264 if self.state.get(key, None) is None:
self._init_entry(key) 263 265 self._init_entry(key)
history = self.state.get(key) 264 266 history = self.state.get(key)
item = history.get() 265 267 item = history.get()
if item is None: 266 268 if item is None:
history.push((value, time)) 267 269 history.push((value, time))
self.update_event.set() 268 270 self.update_event.set()
else: 269 271 else:
(_, previous_time) = item 270 272 (_, previous_time) = item
if time != previous_time: 271 273 if time != previous_time:
history.push((value, time)) 272 274 history.push((value, time))
self.update_event.set() 273 275 self.update_event.set()
dprint(f"ThreadsafeState: set: {key}: {value} ({time})") 274 276 dprint(f"ThreadsafeState: set: {key}: {value} ({time})")
275 277
def _init_entry(self, key): 276 278 def _init_entry(self, key):
with self.lock: 277 279 with self.lock:
self.state[key] = RingBuffer(self.lookback) 278 280 self.state[key] = RingBuffer(self.lookback)
279 281
def get(self, key, default=None, lookback=0): 280 282 def get(self, key, default=None, lookback=0):
with self.lock: 281 283 with self.lock:
history = self.state.get(key, None) 282 284 history = self.state.get(key, None)
if history is None: 283 285 if history is None:
return default 284 286 return default
else: 285 287 else:
return history.get(index=lookback, default=default) 286 288 return history.get(index=lookback, default=default)
287 289
288 290
# extend the Timer thread object to run repeatedly. Subclass this. 289 291 # extend the Timer thread object to run repeatedly. Subclass this.
class RepeatedTimer(threading.Timer): 290 292 class RepeatedTimer(threading.Timer):
def __init__(self, interval): 291 293 def __init__(self, interval):
# Timer constructer expects a callback function, but we don't care 292 294 # Timer constructer expects a callback function, but we don't care
# about that, so just pass None 293 295 # about that, so just pass None
super(RepeatedTimer, self).__init__(interval, None) 294 296 super(RepeatedTimer, self).__init__(interval, None)
295 297
# override Timer's run method to loop on our injest_data function 296 298 # override Timer's run method to loop on our injest_data function
# wait will block while loop for timeout interval 297 299 # wait will block while loop for timeout interval
# calling timer.cancel() sets the finish flag and exits the loop 298 300 # calling timer.cancel() sets the finish flag and exits the loop
def run(self): 299 301 def run(self):
while not self.finished.wait(timeout=self.interval): 300 302 while not self.finished.wait(timeout=self.interval):
self.do_work() 301 303 self.do_work()
302 304
def do_work(self): 303 305 def do_work(self):
raise Exception( 304 306 raise Exception(
"RepeatedTimer baseclass do_work() called. Override this method in a subclass" 305 307 "RepeatedTimer baseclass do_work() called. Override this method in a subclass"
) 306 308 )
307 309
308 310
# makes a [query] to influxdb every [interval] 309 311 # makes a [query] to influxdb every [interval]
# influxdb returns a resultset object: 310 312 # influxdb returns a resultset object:
# https://influxdb-python.readthedocs.io/en/latest/resultset.html#resultset 311 313 # https://influxdb-python.readthedocs.io/en/latest/resultset.html#resultset
class PeriodicInfluxdbPoller(RepeatedTimer): 312 314 class PeriodicInfluxdbPoller(RepeatedTimer):
def __init__(self, interval, soars_state, query, measurment_info, db_config): 313 315 def __init__(self, interval, soars_state, query, measurment_info, db_config):
ssa/alarm/lightstack.py View file @ 7b0ebee
""" 1 1 """
lightstack.py 2 2 lightstack.py
Control program for buzzer and 5-light stack. 3 3 Control program for buzzer and 5-light stack.
4 4
LTA 5 5 LTA
""" 6 6 """
import threading 7 7 import threading
import functools 8 8 import functools
import os 9 9 import os
10 10
TEST_MODE = True if os.environ.get("TEST_GPIO") == "True" else False 11 11 TEST_MODE = True if os.environ.get("TEST_GPIO") == "True" else False
12 12
if TEST_MODE == True: 13 13 if TEST_MODE == True:
import fakeGPIO as GPIO 14 14 import fakeGPIO as GPIO
else: 15 15 else:
import RPi.GPIO as GPIO 16 16 import RPi.GPIO as GPIO
17 17
18 18
class Channel: 19 19 class Channel:
# mappings for channel name (e.g. "Buzzer", "Red", "Green") to pin 20 20 # mappings for channel name (e.g. "Buzzer", "Red", "Green") to pin
# number using the BCM number (e.g. GPIO 17, as opposed to BOARD 21 21 # number using the BCM number (e.g. GPIO 17, as opposed to BOARD
# header numbering) 22 22 # header numbering)
Buzzer = 17 23 23 Buzzer = 17
White = 27 24 24 White = 27
Blue = 22 25 25 Blue = 22
Green = 23 26 26 Green = 23
Yellow = 24 27 27 Yellow = 24
Red = 25 28 28 Red = 25
29 29
30 30
# decorator function 31 31 # decorator function
# A lot of complication just so that we can decorate 32 32 # A lot of complication just so that we can decorate
# other class methods with @_with_lock so that it is 33 33 # other class methods with @_with_lock so that it is
# guarded by self.lock 34 34 # guarded by self.lock
def _with_lock(func): 35 35 def _with_lock(func):
@functools.wraps(func) 36 36 @functools.wraps(func)
def wrapper(self, *args): 37 37 def wrapper(self, *args):
with self.lock: 38 38 with self.lock:
func(self, *args) 39 39 func(self, *args)
40 40
return wrapper 41 41 return wrapper
42 42
43 43
class LightStack: 44 44 class LightStack:
ON = GPIO.HIGH 45 45 ON = GPIO.HIGH
OFF = GPIO.LOW 46 46 OFF = GPIO.LOW
47 47
def __init__(self): 48 48 def __init__(self):
# use BCM pin numbering 49 49 # use BCM pin numbering
GPIO.setmode(GPIO.BCM) 50 50 GPIO.setmode(GPIO.BCM)
# I guess make this threadsafe 51 51 # I guess make this threadsafe
self.lock = threading.RLock() 52 52 self.lock = threading.RLock()
# dicts to store state and threads 53 53 # dicts to store state and threads
self.channel_states = dict() 54 54 self.channel_states = dict()
self.channel_threads = dict() 55 55 self.channel_threads = dict()
# initialize states 56 56 # initialize states
for channel in LightStack.enumerate_channels(): 57 57 for channel in LightStack.enumerate_channels():
print(f"setup channel {channel}") 58 58 print(f"setup channel {channel}")
GPIO.setup(channel, GPIO.OUT) 59 59 GPIO.setup(channel, GPIO.OUT)
GPIO.output(channel, GPIO.LOW) 60 60 GPIO.output(channel, GPIO.LOW)
self.channel_states[channel] = LightStack.OFF 61 61 self.channel_states[channel] = LightStack.OFF
print("LighStack initialized") 62 62 print("LighStack initialized")
63 63
@_with_lock 64 64 @_with_lock
def on(self, channel): 65 65 def on(self, channel):
GPIO.output(channel, LightStack.ON) 66 66 GPIO.output(channel, LightStack.ON)
self.channel_states[channel] = LightStack.ON 67 67 self.channel_states[channel] = LightStack.ON
68 68
@_with_lock 69 69 @_with_lock
def off(self, channel): 70 70 def off(self, channel):
GPIO.output(channel, LightStack.OFF) 71 71 GPIO.output(channel, LightStack.OFF)
self.channel_states[channel] = LightStack.OFF 72 72 self.channel_states[channel] = LightStack.OFF
73 73
@_with_lock 74 74 @_with_lock
def toggle(self, channel): 75 75 def toggle(self, channel):
if self.channel_states[channel] == LightStack.OFF: 76 76 if self.channel_states[channel] == LightStack.OFF:
self.on(channel) 77 77 self.on(channel)
else: 78 78 else:
self.off(channel) 79 79 self.off(channel)
80 80
@_with_lock 81 81 @_with_lock
def start_blink(self, interval, channel): 82 82 def start_blink(self, interval, channel):
if self.channel_threads.get(channel, None) is None: 83 83 if self.channel_threads.get(channel, None) is None:
new_thread = _Blinker(interval, self, channel) 84 84 new_thread = _Blinker(interval, self, channel)
self.channel_threads[channel] = new_thread 85 85 self.channel_threads[channel] = new_thread
new_thread.start() 86 86 new_thread.start()
87 87
@_with_lock 88 88 @_with_lock
def stop_blink(self, channel): 89 89 def stop_blink(self, channel):
thread = self.channel_threads.get(channel, None) 90 90 thread = self.channel_threads.get(channel, None)
if thread is not None: 91 91 if thread is not None:
thread.cancel() 92 92 thread.cancel()
thread.join() 93 93 thread.join()
self.channel_threads[channel] = None 94 94 self.channel_threads[channel] = None
self.off(channel) 95 95 self.off(channel)
96 96
def get(self, channel): 97 97 def get(self, channel):
return self.channel_states.get(channel, None) 98 98 return self.channel_states.get(channel, None)
99 99
@staticmethod 100 100 @staticmethod
def enumerate_channels(): 101 101 def enumerate_channels():
for channel_name in dir(Channel): 102 102 for channel_name in dir(Channel):
if channel_name[0] != "_": 103 103 if channel_name[0] != "_":
yield getattr(Channel, channel_name) 104 104 yield getattr(Channel, channel_name)
105 105