class CircuitProbe: # Number of available GPIO pins NUM_GPIO = 15 # All of the availble GPIO pins reserved_pins = { "power": 3, "clock": 5 } available_pins = [7, 8, 10, 11, 12, 13, 15, 16, 18, 19, 21, 22, 23, 24, 27] # Controls default debug state default_debug_state = True def __init__(self, inputs, outputs, states = 0, enable_powercycle = True, propogation_time = .01, debug = None): """ If debug is None, it will default to the default debug state of the CircuitProbe class. To enable or disable debug mode Initialize CircuitProbe with debug = True of debug = False. """ if debug is None: debug = CircuitProbe.default_debug_state # Enable or disable debug mode self.set_debug(debug) # Set the pin numbering mode GPIO.setmode(GPIO.BOARD) # Setup the power and clock pins for i in CircuitProbe.reserved_pins: pin = CircuitProbe.reserved_pins[i] if self.__debug: print("Setting pin {} as {}".format(pin, i)) GPIO.setup(pin, GPIO.OUT, initial=GPIO.HIGH) # Setup the base state self.reset(inputs = inputs, outputs = outputs, states = states, enable_powercycle = enable_powercycle, propogation_time = propogation_time) def get_type_for_column(self, col): """ Returns the type string for the column. """ row_type = "Input" if col >= self.__inputs: row_type = "State" if col >= self.__inputs + self.__states: row_type = "Output" if col >= self.__inputs + self.__states + self.__outputs: row_type = "Next State" return row_type def get_title_for_column(self, col): """ Returns the type string for the column. """ row_var = chr(ord('A') + col) if col >= self.__inputs: row_var = col - self.__inputs if col >= self.__inputs + self.__states: row_var = col - self.__inputs - self.__states if col >= self.__inputs + self.__states + self.__outputs: row_var = col - self.__inputs - self.__states - self.__outputs return row_var def get_channel_for_column(self, col): """ Returns the GPIO channel number for the column. """ return CircuitProbe.available_pins[col] def get_num_inputs(self): return self.__inputs def get_num_outputs(self): return self.__outputs def get_num_states(self): return self.__states def set_debug(self, debug): """ Enables or disables circuit probe debug mode which prints additional output. """ self.__debug = debug GPIO.setwarnings(debug) def __valid_num_io(self, inputs, outputs, states): """ Checks that the number of inputs/outputs/states is valid for the number of GPIO pins available. >>> CP = CircuitProbe(1, 1) >>> CP.reset(4, 3, 1) >>> CP.reset(4, 3, 2) >>> CP.reset(5, 3, 1) >>> CP.reset(4, 4, 1) >>> CP.reset(4, 3, 0) """ return (CircuitProbe.NUM_GPIO > (inputs + states) and inputs > 0 and outputs > 0 and states >= 0) def reset(self, inputs = None, outputs = None, states = None, enable_powercycle = True, propogation_time = 0.01, clock_time = 0.01): """ Resets the CircuitProbe to its base unanalysed state. You can use this to change the number of inputs/outputs/states, if not specified the number of inputs/outputs/states is not changed. """ if inputs is not None and outputs is not None and states is not None: if not self.__valid_num_io(inputs, outputs, states): raise Exception("Invalid number of inputs/outputs/states specified for CircuitProbe: {}, {}, {}".format(inputs, outputs, states)) # Set the inputs/outputs/states self.__inputs = inputs self.__outputs = outputs self.__states = states # Keep track of the largest input given the number of available inputs self.__max_input = (2**inputs) - 1 ## Setup GPIO pins # Configure inputs for i in CircuitProbe.available_pins[:self.__inputs]: if self.__debug: print("Setting channel {} as circuit input.".format(i)) GPIO.setup(i, GPIO.OUT, initial=GPIO.LOW) # Configure states for i in CircuitProbe.available_pins[self.__inputs:(self.__inputs + self.__states)]: if self.__debug: print("Setting channel {} as circuit state.".format(i)) GPIO.setup(i, GPIO.IN) # Configure outputs for i in CircuitProbe.available_pins[(self.__inputs + self.__states):(self.__inputs + self.__states + self.__outputs)]: if self.__debug: print("Setting channel {} as circuit output".format(i)) GPIO.setup(i, GPIO.IN) # Initialize the matrix for holding values self.matrix = Matrix(0, self.__inputs + self.__outputs + 2*self.__states) self.analyzed_outputs = [] # Some internal state stuff self.__powercycle_enabled = enable_powercycle self.__circuit_propogation_time = propogation_time self.__circuit_clock_time = clock_time def powercycle(self): """ Turns the circuit off, sleeps then turns back on. Then sleeps for the circuit timeout time before returning. """ if not self.__powercycle_enabled: if self.__debug: print("Powercycling not enabled.") return if self.__debug: print("Powercycling circuit.") # Power cycle pin is the first pin GPIO.output(CircuitProbe.reserved_pins["power"], GPIO.LOW) time.sleep(self.__circuit_propogation_time) GPIO.output(CircuitProbe.reserved_pins["power"], GPIO.HIGH) time.sleep(self.__circuit_propogation_time) def power_on(self): """ Turns the circuit power on and waits the propogation time. """ if self.__debug: print("Circuit power on.") GPIO.output(CircuitProbe.reserved_pins["power"], GPIO.HIGH) time.sleep(self.__circuit_propogation_time) def power_off(self): """ Turns the circuit power off and waits the propogation time. """ if self.__debug: print("Circuit power off.") GPIO.output(CircuitProbe.reserved_pins["power"], GPIO.LOW) time.sleep(self.__circuit_propogation_time) def get_matrix(self): return self.matrix def get_current_state(self): """ Checks the state probes to determine what state the circuit is currently in and returns a state list """ state_list = [] for i in range(self.__states): state_list.append(bool(GPIO.input(CircuitProbe.available_pins[self.__inputs + i]))) print("Output on: {} is {}".format(CircuitProbe.available_pins[self.__inputs + i], state_list[-1])) return state_list def get_current_output(self): """ Checks the current state of all circuit outputs """ output_list = [] for i in range(self.__outputs): output_list.append(GPIO.input(CircuitProbe.available_pins[self.__inputs + self.__states + i])) if self.__debug: print("Pin {} output: {}".format(CircuitProbe.available_pins[self.__inputs + self.__states + i], output_list[-1])) return output_list def set_inputs(self, val): """ Sets the ouputs to the binary representation of val """ for i in range(0, self.__inputs): GPIO.output(CircuitProbe.available_pins[i], val>>(self.__inputs-i-1) & 1) def pulse_clock(self): """ Generates one clock cycle on the output and waits for one propogation_time. """ GPIO.output(CircuitProbe.reserved_pins["clock"], GPIO.HIGH) time.sleep(self.__circuit_clock_time) GPIO.output(CircuitProbe.reserved_pins["clock"], GPIO.LOW) time.sleep(self.__circuit_clock_time) def test_and_record(self, current_state, test_val): """ Applies a test input to the circuit and record what the output of the circuit becomes and which state it goes to. Returns the new state. Order of operations: Set inputs -> Measure output -> Pulse Clock -> Record Next State """ # Probe the next circuit inputs for this state self.set_inputs(test_val) # Wait a propogation time time.sleep(self.__circuit_propogation_time) # Record the state we're in current_state_bin = self.get_binary_state_representation(current_state) # Record the input/output of the circuit self.matrix.insert_row() self.matrix.bin_set_row(-1,test_val, self.__inputs) self.matrix.bin_set_row(-1,current_state_bin, self.__states, start_offset = self.__inputs) self.matrix.bin_set_row(-1,self.get_current_output(), self.__outputs, start_offset = self.__inputs + self.__states) # Clock the circuit self.pulse_clock() # Record the state we went to # Check which state the circuit went to current_state = self.get_numerical_state_representation(self.get_current_state()) # Record the state in the matrix self.matrix.bin_set_row(-1,current_state, self.__states, start_offset = self.__inputs + self.__states + self.__outputs) return current_state def get_ordered_output_matrix(self): """ Returns a "sorted" copy of the output matrix. Sort is done by circuit inputs and states only. Outputs are not sorted """ # Build "indices for each row" row_index = lambda r: (self.get_numerical_state_representation(r[:self.__inputs]) + (self.get_numerical_state_representation(r[self.__inputs:self.__inputs + self.__states]) * (2**self.__states))) self.matrix.sort(key=row_index) return self.matrix def get_binary_state_representation(self, num): """ Takes an input number and returns an array of bits to represent that number. The length of the representation is always the number of states for the circuit probe. >>> cp = CircuitProbe(3, 3, 0) >>> cp.get_binary_state_representation(0) [] >>> cp.get_binary_state_representation(1) [] >>> cp.get_binary_state_representation(2) [] >>> cp = CircuitProbe(3, 3, 2) >>> cp.get_binary_state_representation(0) [0, 0] >>> cp.get_binary_state_representation(1) [0, 1] >>> cp.get_binary_state_representation(2) [1, 0] >>> cp.get_binary_state_representation(3) [1, 1] """ rep = [] index = 0 shifted = num while index < self.__states: rep.append(shifted & 1) # Advance the shift index += 1 shifted >>= 1 return rep[::-1] def get_numerical_state_representation(self, num): """ Takes an input list of bytes and returns it as a number >>> cp = CircuitProbe(3, 3, 0) >>> cp.get_numerical_state_representation([0, 0]) 0 >>> cp.get_numerical_state_representation([0, 1]) 1 >>> cp.get_numerical_state_representation([1, 0]) 2 >>> cp.get_numerical_state_representation([1, 1]) 3 >>> cp.get_numerical_state_representation([]) 0 >>> cp.get_numerical_state_representation([0]) 0 >>> cp.get_numerical_state_representation([1]) 1 >>> cp.get_numerical_state_representation([0, 0, 0]) 0 """ index = 0 shifted = 0 for i in num[::-1]: shifted |= (i<<index) index += 1 return shifted def get_closest_untested_state(self, untested, current_state, base_state, edges): """ Finds the closest untested or partially untested state from the current one. If one is found and is reachable, returns the path to the state as a list of inputs needed to get there. If there are no reachable partially untested states returns None. Performs a BFS for the closest state. """ # Current cost # Check that there are known connected states to this one queue = { current_state: 0, base_state: 1 } # List of previous nodes previous = { 0: None } while queue: # Get the item with min cost in the queue (currentElement, currentCost) = min(queue.items(), key=lambda item: item[1]) # Remove that element from the queue queue.pop(currentElement) # Check if the current element is within the untested set if currentElement in untested: # Walk backwards in history until we get to the source node path = [ currentElement ] while path[-1] != current_state: # None is the shortcut for powercycling the circuit if path[-1] is None: break # Use path.append for speed and reverse later path.append( previous[ path[-1] ] ) # The path is now backwards, reverse and return path.reverse() return path # and append to the queue with history if currentElement in edges: for child in edges[currentElement]: if child in previous.keys(): continue previous[child] = currentElement # The cost of each step is onex queue[child] = currentCost + 1 return None def probe(self): """ Probes the circuit. Returns a list of unreachable states. """ # Can't do anything if all outputs have been analyzed if len(self.analyzed_outputs) >= self.__outputs: if self.__debug: print("All outputs have been analyzed") return tested = {} # Power cycle the circuit and check its base state self.power_on() self.powercycle() last_state = base_state = self.get_numerical_state_representation(self.get_current_state()) if self.__debug: print("Circuit base state: {}".format(base_state)) # Edges contains the path from each state to each state that can be reached from that state # Destinations are contained in a tuple: (destination, input) Where input is the required input # from the source state to get to the destination edges = {} # Start by applying inputs sequentially to whatever state the circuit is currently in. # This should reduce path lengths generated by the pathfinding while True: # Check the next test value if last_state in tested: # Check if this state has received all possible inputs if tested[last_state] < self.__max_input: tested[last_state] += 1 else: # It has received all inputs. Exit the while loop break else: tested[last_state] = 0 if self.__debug: print("Testing state {} with input {}".format(last_state, tested[last_state])) # Apply the input value t_last_state = last_state last_state = self.test_and_record(last_state, tested[last_state]) # Add this to the edges if we've moved between states if t_last_state != last_state: if t_last_state not in edges: edges[t_last_state] = [ (base_state, None) ] # Each state can go to the base state by resetting edges[t_last_state].append((last_state, tested[t_last_state])) if self.__debug: print("Edges are now: ") print(edges) if self.__debug: print("Test stage 1 finished.") # Start by building a list of states/inputs that require testing untested = {} for i in range(self.__states): # Get a binary representation of the state i_state = self.get_binary_state_representation(i) # Check if this state has untested inputs if i not in tested or tested[i] != self.__max_input: if i not in untested: untested[i] = [] # State has untested inputs if i not in tested: untested[i] = 0 else: untested[i] = tested[i] # for j in range(tested[i] + 1, self.__max_input + 1): # untested[i].append(j) if self.__debug: print("Untested states for part 2: {}".format(untested)) # If there are no untested states, we can stop here! if len(untested) == 0: self.power_off() return [] # Return empty list for no unreachable states # Now attempt to get to each of these test cases while untested: # Check if state has untested inputs if last_state in untested: # The current state has untested inputs, try the next one t_last_state = last_state last_state = self.test_and_record(last_state, untested[last_state]) # Add this to the edges if we've moved between states if t_last_state != last_state: if t_last_state not in edges: edges[t_last_state] = [ (base_state, None) ] # Each state can go to the base state by resetting edges[t_last_state].append((last_state, tested[last_state])) # Check if this was the last untested input for the state and remove if yes if untested[t_last_state] == self.__max_input: untested.pop(t_last_state) else: untested[t_last_state] += 1 else: # This state has no untested inputs, pathfind to the closest state with untested inputs path_to_closest = self.get_closest_untested_state(untested, last_state, base_state, edges) print("Closest state: {}".format(path_to_closest)) # None indicates there is no path to any state we need to check if path_to_closest is None: break # The force is strong here... This path we should follow... # follow_path(path_to_closest) for i in path_to_closest: # For each direction, apply the needed input and check where we are if i is None: # None indicates a powercycle is needed self.powercycle() else: # Get the input thats going to go from this state to the next required_input = [e[1] for e in edges if e[0] == i][0] print("Required Input: {}".format(required_input)) # Set the inputs and pulse the clock to go! self.set_inputs(required_input) self.pulse_clock() last_state = self.get_numerical_state_representation(self.get_current_state()) if last_state in untested: # If the current state has untested stuff, break free! print("State found!") breaks # If we made it this far, then we most likely introduced duplicate rows in the matrix. self.matrix.remove_duplicate_rows() self.power_off() return untested