def run(self): ser = Serial('/dev/ttyS1') ser.setDTR(True) wait_signals = (TIOCM_RNG | TIOCM_DSR | TIOCM_CD | TIOCM_CTS) log("Waiting for signal...") last_time = None while True: try: ioctl(ser.fd, TIOCMIWAIT, wait_signals) if ser.getCD(): # Take action now = time.time() if not last_time or now - last_time > 1: last_time = now continue else: log("CD seen within 1s") subprocess.call( ["shutdown", "-h", "now", "Front button pressed"]) last_time = None except (KeyboardInterrupt, Exception) as e: log("Stop requested") log(e) break ser.setDTR(False)
def run(self): ser = Serial('/dev/ttyS1') ser.setDTR(True) wait_signals = (TIOCM_RNG | TIOCM_DSR | TIOCM_CD | TIOCM_CTS) log( "Waiting for signal...") last_time = None while True: try: ioctl(ser.fd, TIOCMIWAIT, wait_signals) if ser.getCD(): # Take action now = time.time() if not last_time or now - last_time > 1: last_time = now continue else: log( "CD seen within 1s") subprocess.call(["shutdown", "-h", "now", "Front button pressed"]) last_time = None except (KeyboardInterrupt, Exception) as e: log("Stop requested" ) log(e) break ser.setDTR(False)
class CopyNES(object): ''' Interface to a CopyNES device ''' ########################################################################### ## ## Status operations ## ########################################################################### def __init__(self, data_device, control_device): ''' Connects to a USB CopyNES ''' self.data_channel = Serial(data_device, 115200, timeout = 5) self.control_channel = Serial(control_device, 115200, timeout = 5) # Empty the read buffer time.sleep(0.1) self.data_channel.flushInput() def disconnect(self): ''' Disconnects from the CopyNES device and frees up all resources ''' self.data_channel.close() self.control_channel.close() def power(self): ''' Returns True if the CopyNES is powered on ''' return not self.control_channel.getCD() def reset(self): ''' Resets the CopyNES CPU. In "play" mode, this runs the cartridge program In "copy" mode, the CopyNES will be waiting for commands ''' self.control_channel.setDTR(False) self.control_channel.setDTR(True) def play_mode(self): ''' Resets the CopyNES and puts it in "play" mode This disables the BIOS, so no I/O operations will be available until "copy" mode is enabled again ''' self.control_channel.setRTS(False) self.reset() def copy_mode(self): ''' Resets the CopyNES and puts it in "copy" mode ''' self.control_channel.setRTS(True) self.reset() ########################################################################### ## ## IO operations ## ########################################################################### def write(self, data): ''' Writes a sequence of bytes to the CopyNES ''' if isinstance(data, int): data = bytes((data,)) result = self.data_channel.write(data) self.data_channel.flush() return result def wait_for_data(self, timeout): ''' Wait until data is available for reading ''' while self.data_channel.inWaiting() == 0 and timeout > 0: time.sleep(0.1) timeout -= 0.1 # XXX Not very exact method of waiting return self.data_channel.inWaiting() > 0 def read(self, size = 1): ''' Reads a sequence of bytes transferred from the CopyNES ''' if size == 0: return result = self.data_channel.read(size) if len(result) != size: raise Exception("timeout") return result def read_int(self): ''' Reads a single byte transferred from the CopyNES ''' return int(self.read()[0]) def read_string(self): ''' Reads a zero terminated string ''' result = bytearray() byte = self.read() while ord(byte) != 0x00: result += byte byte = self.read() return result.decode() def flush(self): self.data_channel.flush() ########################################################################### ## ## BIOS operations ## ########################################################################### def version_string(self): ''' Returns the CopyNES firmware version string ''' self.write(0xa1) return self.read_string() def version(self): ''' Returns the CopyNES firmware version number ''' self.write(0xa2) return self.read_int() def __send_command(self, prolog, address, length, epilog): ''' Send a CopyNES command ''' address_lsb = (address & 0x00ff) address_msb = (address & 0xff00) >> 8 length_lsb = (length & 0x00ff) length_msb = (length & 0xff00) >> 8 if length_lsb != 0x00: raise Exception('Memory may only be read/written in $0100 byte chunks, you are trying with a $%04x byte chunk' % length) self.write(prolog) self.write(address_lsb) self.write(address_msb) self.write(length_msb) self.write(epilog) def read_cpu_memory(self, address, length): self.__send_command(0x3a, address, length, 0xa3) return self.read(length) def write_cpu_memory(self, address, data): self.__send_command(0x4b, address, len(data), 0xb4) return self.write(data) def execute_code(self, address): self.__send_command(0x7e, address, 0x00, 0xe7) ########################################################################### ## ## Convenience methods ## ########################################################################### def run_plugin(self, plugin): self.write_cpu_memory(0x0400, plugin.data) self.execute_code(0x0400) def download_rom(self, plugin, ines_mapper): ## ## Packet types sent by dumping plugins ## PACKET_EOD = 0 PACKET_PRG_ROM = 1 PACKET_CHR_ROM = 2 PACKET_WRAM = 3 PACKET_RESET = 4 self.run_plugin(plugin) prom = crom = wram = [] mirroring = self.read_int() ^ 0x01 (packet_type, data) = self.__read_packet() while packet_type != PACKET_EOD: if packet_type == PACKET_PRG_ROM: prom = data elif packet_type == PACKET_CHR_ROM: crom = data elif packet_type == PACKET_WRAM: wram = data elif packet_type == PACKET_RESET: self.reset() else: raise Exception('Unexpected packet type: %d' % packet_type) (packet_type, data) = self.__read_packet() logging.debug('Downloaded prg: %d bytes, chr: %d bytes, wram: %d bytes, mirroring: %d, mapper: %d', len(prom), len(crom), len(wram), mirroring, ines_mapper) return NESROM(prom, crom, ines_mapper, mirroring) def __read_packet(self): pages = self.read_int() | (self.read_int() << 8) length = pages << 8 packet_type = self.read_int() if packet_type == 0: return (packet_type, 0) data = self.read(length) return (packet_type, data) @staticmethod def default_data_device(): if platform.system() == 'Windows': return 'COM3' elif platform.system() == 'Darwin': serial_devices = glob.glob('/dev/tty.usbserial-*') return serial_devices[0] if len(serial_devices) >= 2 else '?' else: return '/dev/ttyUSB0' @staticmethod def default_control_device(): if platform.system() == 'Windows': return 'COM4' elif platform.system() == 'Darwin': serial_devices = glob.glob('/dev/tty.usbserial-*') return serial_devices[1] if len(serial_devices) >= 2 else '?' else: return '/dev/ttyUSB1'
class IridiumConnection(SerialConnection): ''' classdocs ''' def __init__(self, modem, port, baudrate, number, timeout=0.1): ''' Constructor ''' self._incoming_line_buffer = "" self.connection_type = "direct_iridium" self.modem = modem self.port = port self.baudrate = baudrate self.timeout = timeout self._serialport = Serial(port, baudrate, timeout=self.timeout) self._thread = Thread(target=self._listen) self._thread.setDaemon(True) self._thread.start() self.state = 'DISCONNECTED' self.modem = modem self.number = str(number) self.counter = 0 @property def is_connected(self): return self._serialport.getCD() @property def can_change_baudrate(self): return True def change_baudrate(self,baudrate): return self.baudrate def _listen(self): while True: if self._serialport.isOpen(): msg = self.readline() if not self._serialport.getCD(): # Not connected via Iridium # Processing I/O with Iridium dialer. self.process_io(msg) else: # We are connected, so pass through to NMEA self.modem._process_incoming_nmea(msg) self.modem._process_outgoing_nmea() else: # not connected sleep(0.5) # Wait half a second, try again. def process_io(self, msg): # This is called by the primary serial processing loop. # It will be called whenever we have a line of data, or periodically (based on a timeout) if msg is None: msg = "" if self.state == "DIALING": if "CONNECT" in msg: self.state = "CONNECTED" elif "NO CARRIER" in msg: self.state = "DISCONNECTED" elif "NO ANSWER" in msg: self.state = "DISCONNECTED" elif "BUSY" in msg: self.state = "DISCONNECTED" elif "ERROR" in msg: self.state = "DISCONNECTED" if self.counter > 600: #Counts are about 0.1s self.state = "DISCONNECTED" self.modem._daemon_log.info("$IRIDIUM,{0},Dialing Attempt Timed Out.".format(self.modem.name)) self.counter += 1 elif self.state == 'DISCONNECTED': self.counter = 0 self.do_dial() elif self.state == "CONNECTED": # In theory, we shouldn't be here, because if we are connected, traffic is passed through to the umodem module. # So, give us a 1 message margin of error (basically, ignore this input) and try dialing on the next timeout/message. self.state = "DISCONNECTED" if msg is not "": self.modem._daemon_log.info("$IRIDIUM,{0},{1}".format(self.modem.name, msg.strip())) self.modem._daemon_log.debug("$IRIDIUM,{0},Current State:{1}".format(self.modem.name, self.state)) def do_dial(self): # Toggle DTR self.modem._daemon_log.info("$IRIDIUM,{0},Dialing {1}".format(self.modem.name, self.number)) sleep(2) self._serialport.setDTR(False) sleep(0.1) self._serialport.setDTR(True) sleep(0.1) self._serialport.write("AT+CREG?\r\n") sleep(1) self._serialport.write("AT+CEER\r\n") sleep(1) self._serialport.write("AT+CSQ?\r\n") sleep(5) self._serialport.write("ATD{0}\r\n".format(self.number)) self.state = "DIALING" def close(self): self._serialport.setDTR(False) sleep(0.2) self._serialport.close() def wait_for_connect(self): while self.state != "CONNECTED": sleep(1)
class IridiumConnection(SerialConnection): ''' classdocs ''' def __init__(self, modem, port, baudrate, number, timeout=0.1): ''' Constructor ''' self._incoming_line_buffer = "" self.connection_type = "direct_iridium" self.modem = modem self.port = port self.baudrate = baudrate self.timeout = timeout self._serialport = Serial(port, baudrate, timeout=self.timeout) self._thread = Thread(target=self._listen) self._thread.setDaemon(True) self._thread.start() self.state = 'DISCONNECTED' self.modem = modem self.number = str(number) self.counter = 0 @property def is_connected(self): return self._serialport.getCD() @property def can_change_baudrate(self): return True def change_baudrate(self, baudrate): return self.baudrate def _listen(self): while True: if self._serialport.isOpen(): msg = self.readline() if not self._serialport.getCD(): # Not connected via Iridium # Processing I/O with Iridium dialer. self.process_io(msg) else: # We are connected, so pass through to NMEA self.modem._process_incoming_nmea(msg) self.modem._process_outgoing_nmea() else: # not connected sleep(0.5) # Wait half a second, try again. def process_io(self, msg): # This is called by the primary serial processing loop. # It will be called whenever we have a line of data, or periodically (based on a timeout) if msg is None: msg = "" if self.state == "DIALING": if "CONNECT" in msg: self.state = "CONNECTED" elif "NO CARRIER" in msg: self.state = "DISCONNECTED" elif "NO ANSWER" in msg: self.state = "DISCONNECTED" elif "BUSY" in msg: self.state = "DISCONNECTED" elif "ERROR" in msg: self.state = "DISCONNECTED" if self.counter > 600: #Counts are about 0.1s self.state = "DISCONNECTED" self.modem._daemon_log.info( "$IRIDIUM,{0},Dialing Attempt Timed Out.".format( self.modem.name)) self.counter += 1 elif self.state == 'DISCONNECTED': self.counter = 0 self.do_dial() elif self.state == "CONNECTED": # In theory, we shouldn't be here, because if we are connected, traffic is passed through to the umodem module. # So, give us a 1 message margin of error (basically, ignore this input) and try dialing on the next timeout/message. self.state = "DISCONNECTED" if msg is not "": self.modem._daemon_log.info("$IRIDIUM,{0},{1}".format( self.modem.name, msg.strip())) self.modem._daemon_log.debug("$IRIDIUM,{0},Current State:{1}".format( self.modem.name, self.state)) def do_dial(self): # Toggle DTR self.modem._daemon_log.info("$IRIDIUM,{0},Dialing {1}".format( self.modem.name, self.number)) sleep(2) self._serialport.setDTR(False) sleep(0.1) self._serialport.setDTR(True) sleep(0.1) self._serialport.write("AT+CREG?\r\n") sleep(1) self._serialport.write("AT+CEER\r\n") sleep(1) self._serialport.write("AT+CSQ?\r\n") sleep(5) self._serialport.write("ATD{0}\r\n".format(self.number)) self.state = "DIALING" def close(self): self._serialport.setDTR(False) sleep(0.2) self._serialport.close() def wait_for_connect(self): while self.state != "CONNECTED": sleep(1)
class IridiumCsdListener(object): def __init__(self, port, baudrate=9600, timeout=0.1, unified_log=None): self._incoming_line_buffer = "" self.connection_type = "iridium_csd_listener" self.port = port self.baudrate = baudrate self.timeout = timeout if unified_log is None: unified_log = UnifiedLog(log_path='.') self._daemon_log = unified_log.getLogger("iridium_csd_listener") self._nmea_in_log = unified_log.getLogger("nmea.from.iridium_csd") self._nmea_out_log = unified_log.getLogger("nmea.to.iridium_csd") self._daemon_log.setLevel(logging.INFO) self._nmea_in_log.setLevel(logging.INFO) self._nmea_out_log.setLevel(logging.INFO) self._serialport = Serial(port, baudrate, timeout=self.timeout) self.serial_tx_queue = Queue() self.nmea_listeners = [] self.iridium_buffer_length = 20 self._last_connected_status = self.is_connected self._thread = Thread(target=self._listen) self._thread.setDaemon(True) self._thread.start() self._daemon_log.info("Iridium Listener Started") self.iridium_init() @property def is_connected(self): connected = self._serialport.getCD() and self._serialport.isOpen() return connected @property def can_change_baudrate(self): return True def change_baudrate(self, baudrate): self.baudrate = baudrate self._serialport.baudrate = baudrate return baudrate def iridium_init(self): sleep(1) self._serialport.setDTR(False) self._daemon_log.info("Clear DTR") sleep(0.1) self._serialport.setDTR(True) self._daemon_log.info("Set DTR") sleep(0.1) self._serialport.write("AT+CREG?\r\n") self._daemon_log.info("> AT+CREG?") sleep(1) self._serialport.write("AT+CEER\r\n") self._daemon_log.info("> AT+CEER") sleep(1) self._serialport.write("AT+CSQ?\r\n") self._daemon_log.info("> AT+CSQ?") sleep(5) def close(self): self._serialport.setDTR(False) sleep(0.2) self._serialport.close() def _listen(self): while True: # Log connection changes if self.is_connected is not self._last_connected_status: self._daemon_log.info("Iridium Connected: {}".format( self.is_connected)) self._last_connected_status = self.is_connected if self._serialport.isOpen(): msg = self.readline() if msg is None: msg = "" # Figure out if this is an NMEA message or Iridium traffic, in the dumbest way possible. if len(msg) > 0: if msg[0] == '$': self._process_incoming_nmea(msg) else: self.process_iridium(msg) self._process_outgoing_nmea() else: # not connected sleep(0.5) # Wait half a second, try again. def process_iridium(self, msg): # This is called by the primary serial processing loop. # It will be called whenever we have a line of data, or periodically (based on a timeout) # Basically, all we do here is pick up the phone. if "RING" in msg: self._serialport.write("ATA\r\n") self._daemon_log.info("> ATA") # and log messages if msg is not "": self._daemon_log.info("< {}".format(msg.strip())) def _process_incoming_nmea(self, msg): self._nmea_in_log.info(msg.rstrip('\r\n')) msg = Message(msg) try: for func in self.nmea_listeners: func(msg) # Pass the message to any custom listeners. except Exception as e: self._daemon_log.warn("Error in NMEA listener: ") self._daemon_log.warn(repr(e)) def _process_outgoing_nmea(self): # Now, transmit anything we have in the outgoing queue. if self.is_connected: try: txstring = self.serial_tx_queue.get_nowait() self._serialport.write(txstring) self._nmea_out_log.info(txstring.rstrip('\r\n')) #If the queue is empty, then pass, otherwise log error except Empty: pass except: self._daemon_log.exception("NMEA Output Error") else: # If we aren't connected, remove old messages from the queue. overflow = self.serial_tx_queue.qsize( ) - self.iridium_buffer_length if overflow > 0: for i in range(overflow): txstring = self.serial_tx_queue.get_nowait() self._daemon_log.info("Dropped outgoing NMEA: {}".format( txstring.rstrip('\r\n'))) def write_nmea(self, msg): """Call with the message to send, as an NMEA message. Correct checksum will be computed.""" if type(msg) == str: # Automagically convert it into an NMEA message (or try, at least) msg = Message(msg) message = ",".join([str(p) for p in [msg['type']] + msg['params']]) chk = nmeaChecksum(message) message = "$" + message.lstrip('$').rstrip( '\r\n*') + "*" + chk + "\r\n" # Queue this message for transmit in the serial thread self._daemon_log.debug("Writing NMEA to output queue: %s" % (message.rstrip('\r\n'))) try: self.serial_tx_queue.put(message, block=False) # If queue full, then ignore except Full: self._daemon_log.debug("write_nmea: Serial TX Queue Full") def write_string(self, string): self._daemon_log.debug("Writing string to output queue: %s" % (string.rstrip('\r\n'))) try: self.serial_tx_queue.put(string, block=False) # If queue full, then ignore except Full: self._daemon_log.debug("write_string: Serial TX Queue Full") def readline(self): """Returns a \n terminated line from the modem. Only returns complete lines (or None on timeout)""" rl = self._serialport.readline() if rl == "": return None # Make sure we got a complete line. Serial.readline may return data on timeout. if rl[-1] != '\n': self._incoming_line_buffer += rl return None else: if self._incoming_line_buffer != "": rl = self._incoming_line_buffer + rl self._incoming_line_buffer = "" return rl def write(self, data): self._serialport.write(data)
# # swift.set_position(x=200, y=0, z=100) # time.sleep(5) # print(swift.get_polar()) # # # # # def test(ret): # # # print(ret) # # # # # # print(metal.send_cmd_sync('G0')) # # # metal.send_cmd_async('G0', callback=test) # # # # while True: # # time.sleep(1) import time from serial import Serial com = Serial("COM12", baudrate=115200) print('getCD:', com.getCD()) print('getCTS:', com.getCTS()) print('getDSR:', com.getDSR()) print('getRI:', com.getRI()) print('get_settings:', com.get_settings()) print('timeout:', com.timeout) while True: time.sleep(1)
class IridiumCsdListener(object): def __init__(self, port, baudrate=9600, timeout=0.1, unified_log=None): self._incoming_line_buffer = "" self.connection_type = "iridium_csd_listener" self.port = port self.baudrate = baudrate self.timeout = timeout if unified_log is None: unified_log = UnifiedLog(log_path='.') self._daemon_log = unified_log.getLogger("iridium_csd_listener") self._nmea_in_log = unified_log.getLogger("nmea.from.iridium_csd") self._nmea_out_log = unified_log.getLogger("nmea.to.iridium_csd") self._daemon_log.setLevel(logging.INFO) self._nmea_in_log.setLevel(logging.INFO) self._nmea_out_log.setLevel(logging.INFO) self._serialport = Serial(port, baudrate, timeout=self.timeout) self.serial_tx_queue = Queue() self.nmea_listeners = [] self.iridium_buffer_length = 20 self._last_connected_status = self.is_connected self._thread = Thread(target=self._listen) self._thread.setDaemon(True) self._thread.start() self._daemon_log.info("Iridium Listener Started") self.iridium_init() @property def is_connected(self): connected = self._serialport.getCD() and self._serialport.isOpen() return connected @property def can_change_baudrate(self): return True def change_baudrate(self,baudrate): self.baudrate = baudrate self._serialport.baudrate = baudrate return baudrate def iridium_init(self): sleep(1) self._serialport.setDTR(False) self._daemon_log.info("Clear DTR") sleep(0.1) self._serialport.setDTR(True) self._daemon_log.info("Set DTR") sleep(0.1) self._serialport.write("AT+CREG?\r\n") self._daemon_log.info("> AT+CREG?") sleep(1) self._serialport.write("AT+CEER\r\n") self._daemon_log.info("> AT+CEER") sleep(1) self._serialport.write("AT+CSQ?\r\n") self._daemon_log.info("> AT+CSQ?") sleep(5) def close(self): self._serialport.setDTR(False) sleep(0.2) self._serialport.close() def _listen(self): while True: # Log connection changes if self.is_connected is not self._last_connected_status: self._daemon_log.info("Iridium Connected: {}".format(self.is_connected)) self._last_connected_status = self.is_connected if self._serialport.isOpen(): msg = self.readline() if msg is None: msg = "" # Figure out if this is an NMEA message or Iridium traffic, in the dumbest way possible. if len(msg) > 0: if msg[0] == '$': self._process_incoming_nmea(msg) else: self.process_iridium(msg) self._process_outgoing_nmea() else: # not connected sleep(0.5) # Wait half a second, try again. def process_iridium(self, msg): # This is called by the primary serial processing loop. # It will be called whenever we have a line of data, or periodically (based on a timeout) # Basically, all we do here is pick up the phone. if "RING" in msg: self._serialport.write("ATA\r\n") self._daemon_log.info("> ATA") # and log messages if msg is not "": self._daemon_log.info("< {}".format(msg.strip())) def _process_incoming_nmea(self, msg): self._nmea_in_log.info(msg.rstrip('\r\n')) msg = Message(msg) try: for func in self.nmea_listeners: func(msg) # Pass the message to any custom listeners. except Exception as e: self._daemon_log.warn("Error in NMEA listener: ") self._daemon_log.warn(repr(e)) def _process_outgoing_nmea(self): # Now, transmit anything we have in the outgoing queue. if self.is_connected: try: txstring = self.serial_tx_queue.get_nowait() self._serialport.write(txstring) self._nmea_out_log.info(txstring.rstrip('\r\n')) #If the queue is empty, then pass, otherwise log error except Empty: pass except: self._daemon_log.exception("NMEA Output Error") else: # If we aren't connected, remove old messages from the queue. overflow = self.serial_tx_queue.qsize() - self.iridium_buffer_length if overflow > 0: for i in range(overflow): txstring = self.serial_tx_queue.get_nowait() self._daemon_log.info("Dropped outgoing NMEA: {}".format(txstring.rstrip('\r\n'))) def write_nmea(self, msg): """Call with the message to send, as an NMEA message. Correct checksum will be computed.""" if type(msg) == str: # Automagically convert it into an NMEA message (or try, at least) msg = Message(msg) message = ",".join([str(p) for p in [msg['type']] + msg['params']]) chk = nmeaChecksum(message) message = "$" + message.lstrip('$').rstrip('\r\n*') + "*" + chk + "\r\n" # Queue this message for transmit in the serial thread self._daemon_log.debug("Writing NMEA to output queue: %s" % (message.rstrip('\r\n'))) try: self.serial_tx_queue.put(message, block=False) # If queue full, then ignore except Full: self._daemon_log.debug("write_nmea: Serial TX Queue Full") def write_string(self, string): self._daemon_log.debug("Writing string to output queue: %s" % (string.rstrip('\r\n'))) try: self.serial_tx_queue.put(string, block=False) # If queue full, then ignore except Full: self._daemon_log.debug("write_string: Serial TX Queue Full") def readline(self): """Returns a \n terminated line from the modem. Only returns complete lines (or None on timeout)""" rl = self._serialport.readline() if rl == "": return None # Make sure we got a complete line. Serial.readline may return data on timeout. if rl[-1] != '\n': self._incoming_line_buffer += rl return None else: if self._incoming_line_buffer != "": rl = self._incoming_line_buffer + rl self._incoming_line_buffer = "" return rl def write(self, data): self._serialport.write(data)
class SerialButtonManager(object): def __init__(self, device, service): self.device = Serial(device) self.service_name = service @property def service_is_running(self): output = check_output( [ SERVICE_PATH, self.service_name, 'status', ] ) output = output.strip() logger.debug('Service status result: %s', output) if 'stop' in output: logger.debug('Service is not running.') return False elif 'start' in output: logger.debug('Service is running.') return True raise RuntimeError('Unable to get service status: "%s"' % output) @property def button_is_pressed(self): return self.device.getCD() def start_service(self): try: check_call( [ SERVICE_PATH, self.service_name, 'start', ] ) except CalledProcessError as e: logger.error( "Unable to start service: %s", e ) def stop_service(self): try: check_call( [ SERVICE_PATH, self.service_name, 'stop', ] ) except CalledProcessError as e: logger.error( "Unable to stop service: %s", e ) def _resolve_state_mismatch(self): running = self.service_is_running pressed = self.button_is_pressed if pressed and not running: logger.warning( "State mismatch detected; button was pressed, but " "service was not running; starting service." ) self.start_service() elif not pressed and running: logger.warning( "State mismatch detected; button was not pressed, but " "service was running; stopping service." ) self.stop_service() def run(self): self._resolve_state_mismatch() last_checked_state = time.time() last_state = self.button_is_pressed while True: pressed = self.button_is_pressed if pressed != last_state: last_checked_state = time.time() if pressed: logger.info( "Button press detected: starting service." ) self.start_service() else: logger.info( "Button unpress detected: starting service." ) self.stop_service() elif last_checked_state + STATE_CHECK_INTERVAL < time.time(): last_checked_state = time.time() self._resolve_state_mismatch() last_state = pressed time.sleep(0.5)