Traffic simulation using Simpy - python

I have been working on simpy to design a traffic simulation of a signalized intersection (three approaches). The attached code described the design of the cars driving on a single lane road and arrive at the intersection from one direction only. The traffic light switching between green and red intervals. my question is: how to modify the code to simulate more than one traffic signal with different arrival rate that work in sequence at the same intersection. My goal is to connect more than one road to the intersection and control the traffic movement by a traffic signal.
Thanks in advance.
# Section 1: Import from modules and define a utility class.
from collections import deque # double-ended queue
from numpy import random
import simpy
from simpy.util import start_delayed
class Struct(object):
"""
This simple class allows one to create an object whose attributes are
initialized via keyword argument/value pairs. One can update the attributes
as needed later.
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
# Section 2: Initializations.
random.seed([1, 2, 3])
# Total number of seconds to be simulated:
end_time= 200.0
# Cars cars arrive at the traffic light according to a Poisson process with an
# average rate of 0.2 per second:
arrival_rate= 0.2
t_interarrival_mean= 1.0 / arrival_rate
# Traffic light green and red durations:
t_green= 30.0; t_red= 40.0
# The time for a car at the head of the queue to depart (clear the intersection)
# is modeled as a triangular distribution with specified minimum, maximum, and
# mode.
t_depart_left= 1.6; t_depart_mode= 2.0; t_depart_right= 2.4
# Initially, no cars are waiting at the light:
queue= deque()
# Track number of cars:
arrival_count= departure_count= 0
Q_stats= Struct(count=0, cars_waiting=0)
W_stats= Struct(count=0, waiting_time=0.0)
# Section 3: Arrival event.
def arrival():
"""
This generator functions simulates the arrival of a car. Cars arrive
according to a Poisson process having rate `arrival_rate`. The times between
subsequent arrivals are i.i.d. exponential random variables with mean
t_interarrival_mean= 1.0 / arrival_rate
"""
global arrival_count, env, light, queue
while True:
arrival_count+= 1
if light == 'red' or len(queue):
# The light is red or there is a queue of cars. ==> The new car joins
# the queue. Append a tuple that contains the number of the car and
# the time at which it arrived:
queue.append((arrival_count, env.now))
print("Car #%d arrived and joined the queue at position %d at time "
"%.3f." % (arrival_count, len(queue), env.now))
else:
# The light is green and no cars are waiting. ==> The new car passes
# through the intersection immediately.
print("Car #%d arrived to a green light with no cars waiting at time "
"%.3f." % (arrival_count, env.now))
# Record waiting time statistics. (This car experienced zero waiting
# time, so we increment the count of cars, but the cumulative waiting
# time remains unchanged.
W_stats.count+= 1
# Schedule next arrival:
yield env.timeout( random.exponential(t_interarrival_mean))
# Section 4: Define event functions.
# Section 4.1: Departure event.
def departure():
"""
This generator function simulates the 'departure' of a car, i.e., a car that
previously entered the intersection clears the intersection. Once a car has
departed, we remove it from the queue, and we no longer track it in the
simulation.
"""
global env, queue
while True:
# The car that entered the intersection clears the intersection:
car_number, t_arrival= queue.popleft()
print("Car #%d departed at time %.3f, leaving %d cars in the queue."
% (car_number, env.now, len(queue)))
# Record waiting time statistics:
W_stats.count+= 1
W_stats.waiting_time+= env.now - t_arrival
# If the light is red or the queue is empty, do not schedule the next
# departure. `departure` is a generator, so the `return` statement
# terminates the iterator that the generator produces.
if light == 'red' or len(queue) == 0:
return
# Generate departure delay as a random draw from triangular distribution:
delay= random.triangular(left=t_depart_left, mode=t_depart_mode,
right=t_depart_right)
# Schedule next departure:
yield env.timeout(delay)
# Section 4.2: Light change-of-state event.
def light():
"""
This generator function simulates state changes of the traffic light. For
simplicity, the light is either green or red--there is no yellow state.
"""
global env, light
while True:
# Section 4.2.1: Change the light to green.
light= 'green'
print("\nThe light turned green at time %.3f." % env.now)
# If there are cars in the queue, schedule a departure event:
if len(queue):
# Generate departure delay as a random draw from triangular
# distribution:
delay= random.triangular(left=t_depart_left, mode=t_depart_mode,
right=t_depart_right)
start_delayed(env, departure(), delay=delay)
# Schedule event that will turn the light red:
yield env.timeout(t_green)
# Section 4.2.2: Change the light to red.
light= 'red'
print("\nThe light turned red at time %.3f." % env.now)
# Schedule event that will turn the light green:
yield env.timeout(t_red)
# Section 4.3: Schedule event that collects Q_stats.
def monitor():
"""
This generator function produces an interator that collects statistics on the
state of the queue at regular intervals. An alternative approach would be to
apply the PASTA property of the Poisson process ('Poisson Arrivals See Time
Averages') and sample the queue at instants immediately prior to arrivals.
"""
global env, Q_stats
while True:
Q_stats.count+= 1
Q_stats.cars_waiting+= len(queue)
yield env.timeout(1.0)
# Section 5: Schedule initial events and run the simulation. Note: The first
# change of the traffic light, first arrival of a car, and first statistical
# monitoring event are scheduled by invoking `env.process`. Subsequent changes
# will be scheduled by invoking the `timeout` method. With this scheme, there
# is only one event of each of these types scheduled at any time; this keeps the
# event queue short, which is good for both memory utilization and running time.
print("\nSimulation of Cars Arriving at Intersection Controlled by a Traffic "
"Light\n\n")
# Initialize environment:
env= simpy.Environment()
# Schedule first change of the traffic light:
env.process(light())
# Schedule first arrival of a car:
t_first_arrival= random.exponential(t_interarrival_mean)
start_delayed(env, arrival(), delay=t_first_arrival)
# Schedule first statistical monitoring event:
env.process(monitor())
# Let the simulation run for specified time:
env.run(until=end_time)
# Section 6: Report statistics.
print("\n\n *** Statistics ***\n\n")
print("Mean number of cars waiting: %.3f"
% (Q_stats.cars_waiting / float(Q_stats.count)))
print("Mean waiting time (seconds): %.3f"
% (W_stats.waiting_time / float(W_stats.count)))

You need a master light controller to manage all your lights. I used the subscribe / publish pattern where each lane subscribed to the light they are interested in with a callback function. the light uses these callback functions to publish when the light is on. I treat red and green light as separate lights. The master light controller rotates through all the lights, calling each light's subscribers' callback function when the light turns on.
This models a cross road, If you want to model a tee, just comment out any one lane.
"""
Simulation of a traffic intersection
Has four main lanes of traffic: North, South, East, and West
for each direction there is also a turn lane: North left.... ect
Assumes opposing lanes (such as North, and South)
will have the same light color
Light are manganed by a light controler
Lanes "subscribe" to the light color they are interested in
with a callback function
When the light color comes on, the light control calls the callback
function of the lanes that subscribed to that light color
When a lane has a green light, it sends cars through the intersection,
otherwise arriving cars accumulate in a queue
Programmer: Michael R. Gibbs
"""
import simpy
import random
class LightControl():
"""
Controls the color and timing of the lights
"""
def __init__(self, env):
self.env = env
# subscrition lists for each color and direction combo
self.red_ns_subs = []
self.red_ns_left_subs = []
self.red_ew_subs = []
self.red_ew_left_subs = []
self.green_ns_subs = []
self.green_ns_left_subs = []
self.green_ew_subs = []
self.green_ew_left_subs = []
# start the lights
self.env.process(self.run_lights())
def _braodcast(self, subscribers):
"""
utility to call the callback funcs for each subscriber
"""
for callback in subscribers:
callback()
def run_lights(self):
"""
controls the lights with a simple loop
note different light have different times
"""
while True:
print(f'{self.env.now} ns left is green')
self._braodcast(self.green_ns_left_subs)
yield self.env.timeout(60)
self._braodcast(self.red_ns_left_subs)
self._braodcast(self.green_ns_subs)
print(f'{self.env.now} ns is green')
yield self.env.timeout(180)
self._braodcast(self.red_ns_subs)
self._braodcast(self.green_ew_left_subs)
print(f'{self.env.now} ew left is green')
yield self.env.timeout(60)
self._braodcast(self.red_ew_left_subs)
self._braodcast(self.green_ew_subs)
print(f'{self.env.now} ew is green')
yield self.env.timeout(120)
self._braodcast(self.red_ew_subs)
class Lane():
"""
lane of traffic
accumulates cars,
waits for green light,
sends cars through interscetion, until red light
note if a car is already in the intersection when
the light truns red, it will finish its trip and
continue through
"""
def __init__(self, name, env):
self.name = name
self.env = env
self.lane_q = []
self.green = False
def add_car(self, car):
"""
car arrives at lane
"""
self.lane_q.append(car)
def move_cars(self):
"""
move cars through the intersection until light turns red
"""
while self.green and (len(self.lane_q) > 0):
car = self.lane_q.pop(0)
yield self.env.timeout(10)
print(f'{self.env.now} {self.name} sent car through')
def green_light(self):
"""
callback telling the lane that the light is green,
starts moving cars through the intersection
"""
self.green = True
self.env.process(self.move_cars())
def red_light(self):
"""
callback telling the lane the light is red
sets stop flag
"""
self.green = False
def gen_cars(env, lane, delay_func):
"""
generates cars arriving at a lane
"""
while True:
yield env.timeout(delay_func())
lane.add_car(object())
# init sim
env = simpy.Environment()
lights = LightControl(env)
# make lanes and subscibe to a lights with a callback
north =Lane('North lane', env)
lights.green_ns_subs.append(north.green_light)
lights.red_ns_subs.append(north.red_light)
north_left =Lane('North left lane', env)
lights.green_ns_left_subs.append(north_left.green_light)
lights.red_ns_left_subs.append(north_left.red_light)
south =Lane('South lane', env)
lights.green_ns_subs.append(south.green_light)
lights.red_ns_subs.append(south.red_light)
south_left =Lane('South left lane', env)
lights.green_ns_left_subs.append(south_left.green_light)
lights.red_ns_left_subs.append(south_left.red_light)
east =Lane('East lane', env)
lights.green_ew_subs.append(east.green_light)
lights.red_ew_subs.append(east.red_light)
east_left =Lane('East left lane', env)
lights.green_ew_left_subs.append(east_left.green_light)
lights.red_ew_left_subs.append(east_left.red_light)
west =Lane('West lane', env)
lights.green_ew_subs.append(west.green_light)
lights.red_ew_subs.append(west.red_light)
west_left =Lane('West left lane', env)
lights.green_ew_left_subs.append(west_left.green_light)
lights.red_ew_left_subs.append(west_left.red_light)
# generate cars for each lane
env.process(gen_cars(env, north, lambda : random.triangular(5,15,10)))
env.process(gen_cars(env, north_left, lambda : random.triangular(10,30,20)))
env.process(gen_cars(env, south, lambda : random.triangular(5,15,10)))
env.process(gen_cars(env, south_left, lambda : random.triangular(12,35,20)))
env.process(gen_cars(env, east, lambda : random.triangular(10,40,30)))
env.process(gen_cars(env, east_left, lambda : random.triangular(15,45,30)))
env.process(gen_cars(env, west, lambda : random.triangular(11,45,35)))
env.process(gen_cars(env, west_left, lambda : random.triangular(15,50,40)))
env.run(1200)

Related

Is there a way to print values every simulation tick (minute in my case) instead of every time an event happens?

I am creating a simulation of a 2 machine system where the processing speed is hundreds per second, 0.0012-0.0015 to be exact.
In my code I have a print statement telling me what each machine is doing. While running the simulation, I get the print statement for every instance, so about 100 per simulation minute (tick?). Is there a way to get it to only print at the whole tick and not every instance?
My code I have as an example is:
import simpy
# Machine 1
speed_1 = 0.0015 # Avg. processing time of Machine 1 in minutes
# Machine 2
speed_2 = 0.0013 # Processing time of Machine 2 in minutes
# Simulation time
time = 120 # Sim time in minutes
#-------------------------------------------------------------------------------------------
class Machine(object):
"""
A machine produces units at a fixed processing speed,
takes units from a store before and puts units into a store after.
"""
def __init__(self, env, name, in_q, out_q, speed):
self.env = env
self.name = name
self.in_q = in_q
self.out_q = out_q
self.speed = speed
# Start the producing process
self.process = env.process(self.produce())
def produce(self):
"""
Produce parts as long as the simulation runs.
"""
while True:
part = yield self.in_q.get()
# If want to see time {self.env.now:.2f}
print(f'{self.name} has got a part')
yield env.timeout(self.speed)
if len(self.out_q.items) < self.out_q.capacity:
print(f'{self.name} finished a part, next buffer has {len(self.out_q.items)} and capacity of {self.out_q.capacity}')
else:
print(f'{self.env.now:.2f} {self.name} output buffer full!!!')
yield self.out_q.put(part)
print(f'{self.name} pushed part to next buffer')
#-------------------------------------------------------------------------------------------
# Generating the arrival of parts in the entry buffer to be used by machine 1
def gen_arrivals(env, entry_buffer):
while True:
yield env.timeout(random.uniform(0,0.001))
# print(f'{env.now:.2f} part has arrived')
part = object() # Too lazy to make a real part class, also isn't necessary
yield entry_buffer.put(part)
#-------------------------------------------------------------------------------------------
# Create environment and start the setup process
env = simpy.Environment()
bufferStart = simpy.Store(env) # Buffer with unlimited capacity
buffer1 = simpy.Store(env, capacity = 900) # Buffer between machines with limited capacity
bufferEnd = simpy.Store(env) # Last buffer with unlimited capacity
# The machines __init__ starts the machine process so no env.process() is needed here
machine_1 = Machine(env, 'Machine 1', bufferStart, buffer1, speed_1)
machine_2 = Machine(env, 'Machine 2', buffer1, bufferEnd, speed_2)
env.process(gen_arrivals(env, bufferStart))
# Execute
env.run(until = time)
First off, a model 'tick' can be any unit you want, a year, a day, a second or even 0.0001 of a second. I like to have my steps be a integer to minimize computer float rounding.
I added a log process that prints the state of the machine at ever 0.01 ticks. To support the log, I added a state property to the machine that I update throughout processing. I also changed the rates and capacities to make a more interesting log
import simpy
import random
# Machine 1
speed_1 = 0.0015 # Avg. processing time of Machine 1 in minutes
# Machine 2
speed_2 = 0.0035 # Processing time of Machine 2 in minutes
# Simulation time
time = .1 # Sim time in minutes
#-------------------------------------------------------------------------------------------
class Machine(object):
"""
A machine produces units at a fixed processing speed,
takes units from a store before and puts units into a store after.
"""
def __init__(self, env, name, in_q, out_q, speed):
self.env = env
self.name = name
self.in_q = in_q
self.out_q = out_q
self.speed = speed
self.state = 'Started'
self.part_cnt = 0
# Start the producing process
self.process = env.process(self.produce())
# start the logging
env.process(self.log())
def produce(self):
"""
Produce parts as long as the simulation runs.
"""
while True:
self.state = 'Waiting for part'
part = yield self.in_q.get()
# If want to see time {self.env.now:.4f}
print(f'{self.name} has got a part')
self.state = 'Processing Part'
yield env.timeout(self.speed)
if len(self.out_q.items) < self.out_q.capacity:
print(f'{self.name} finished a part, next buffer has {len(self.out_q.items)} and capacity of {self.out_q.capacity}')
else:
print(f'{self.env.now:.4f} {self.name} output buffer full!!!')
self.state = 'Waiting to send part to next queue'
self.part_cnt += 1
yield self.out_q.put(part)
print(f'{self.name} pushed part to next buffer')
def log(self):
"""
logs the state of the machine every 0.01 ticks
"""
while True:
print(f'{self.env.now:.4f} Machine {self.name} has processed {self.part_cnt} parts and is in state {self.state}')
yield self.env.timeout(0.01)
#-------------------------------------------------------------------------------------------
# Generating the arrival of parts in the entry buffer to be used by machine 1
def gen_arrivals(env, entry_buffer):
while True:
yield env.timeout(random.uniform(0,0.001))
# print(f'{env.now:.2f} part has arrived')
part = object() # Too lazy to make a real part class, also isn't necessary
yield entry_buffer.put(part)
#-------------------------------------------------------------------------------------------
# Create environment and start the setup process
env = simpy.Environment()
bufferStart = simpy.Store(env) # Buffer with unlimited capacity
buffer1 = simpy.Store(env, capacity = 10) # Buffer between machines with limited capacity
bufferEnd = simpy.Store(env) # Last buffer with unlimited capacity
# The machines __init__ starts the machine process so no env.process() is needed here
machine_1 = Machine(env, 'Machine 1', bufferStart, buffer1, speed_1)
machine_2 = Machine(env, 'Machine 2', buffer1, bufferEnd, speed_2)
env.process(gen_arrivals(env, bufferStart))
# Execute
env.run(until = time)

How to write a method that checks if an agent is done with a task in Osbrain?

I have a question on how to write a proper function that checks if an agent is done with their task in Osbrain. I have three agents, the Transport agent, Node agent and the Coordinator agent. The coordinator agent's main task is to synchronize the actions of the other agents. The Coordinator agents bind to SYNC_PUB and the Node and the Transport agents SUB to the coordinator agent. My initial implementation hanged after the first timestep/iteration. Am I implementing the status_checker method wrongly?
from osbrain import run_nameserver, run_agent, Agent
import time
SYNCHRONIZER_CHANNEL_1 = 'coordinator1'
class TransportAgent(Agent):
def transportAgent_first_handler(self, message):
# time.sleep(2)
self.log_info(message)
self.send(SYNCHRONIZER_CHANNEL_1, 'is_done', handler='process_reply')
def process_reply(self, message):
yield 1
class NodeAgent(Agent):
def NodeAgent_first_handler(self, message):
time.sleep(2)
self.log_info(message)
self.send(SYNCHRONIZER_CHANNEL_1, 'is_done', handler='process_reply')
def process_reply(self, message):
yield 1
class SynchronizerCoordinatorAgent(Agent):
def on_init(self):
self.network_agent_addr = self.bind('SYNC_PUB', alias=SYNCHRONIZER_CHANNEL_1, handler='status_handler')
self.status_list = []
def first_synchronization(self, time_step, iteration):
self.send(SYNCHRONIZER_CHANNEL_1, message={'time_step': time_step, 'iteration': iteration},
topic='first_synchronization')
def status_handler(self, message):
yield 'I have added you to the status_list'
self.status_list.append(message)
def status_checker(self):
count = 0
while len(self.status_list) < 2:
count += 1
time.sleep(1)
return
self.status_list.clear()
def init_environment(self):
self.TransportAgent = run_agent('TransportAgent', base=TransportAgent)
self.NodeAgent = run_agent('NodeAgent', base=NodeAgent)
self.TransportAgent.connect(self.network_agent_addr, alias=SYNCHRONIZER_CHANNEL_1,
handler={'first_synchronization': TransportAgent.transportAgent_first_handler})
self.NodeAgent.connect(self.network_agent_addr, alias=SYNCHRONIZER_CHANNEL_1,
handler={'first_synchronization': NodeAgent.NodeAgent_first_handler})
if __name__ == '__main__':
ns = run_nameserver()
synchronizer_coordinator_agent = run_agent('Synchronizer_CoordinatorAgent',
base=SynchronizerCoordinatorAgent)
synchronizer_coordinator_agent.init_environment()
for iteration in range(1, 2):
for time_step in range(0, 90, 30):
synchronizer_coordinator_agent.first_synchronization(time_step=time_step, iteration=iteration)
synchronizer_coordinator_agent.status_checker()
time.sleep(1)
It prints this and then hangs
(NetworkAgent): {'time_step': 0, 'iteration': 1}
(RMOAgent): {'time_step': 0, 'iteration': 1}
Yeah, it seems your status_checker() method may be broken. I am guessing you want that method to block until 2 messages are in the status_list (one from the node agent and another one from the transport agent).
So you may be looking for something like:
def status_checker(self):
while True:
if len(self.status_list) == 2:
break
time.sleep(1)
self.status_list.clear()
However, when you call that method from the proxy:
synchronizer_coordinator_agent.status_checker()
The coordinator is blocking executing that call, so it will not be processing other incoming messages. A quick and dirty workaround is to use an unsafe call like this:
synchronizer_coordinator_agent.unsafe.status_checker()
The main issue I see here is the way you are handling that status checker from __main__. You should move your synchronization/steps into your coordinator. That means:
Call a .start_iterations() method on your coordinator from __main__, so that the coordinator executes the first step
Then make your coordinator reactive to incoming messages (once it receives 2 messages, it executes the next step)
The coordinator keeps executing steps until finished
From main, just monitor your coordinator agent periodically to see when it is done
Shut down your system with ns.shutdown()
Your main could look like this:
if __name__ == "__main__":
ns = run_nameserver()
coordinator = run_agent(...)
coordinator.init_environment()
coordinator.start_iterations()
while not coordinator.finished():
time.sleep(0.5)
ns.shutdown()
Not really related to this, but note that your current range(1, 2) will result in just one iteration (that may be intentional though). If you want 2 iterations you can use range(2).

Function not calling and how to wait for click in tkinter loop

I've hit a dead end at the moment - firstly when you click one of the dynamic buttons, the function call is made, and it does return the name of the button clicked. However the fEndDay function it's supposed to call as well doesn't appear to run.
EDIT: The day is now running. Just relaunched Liclipse and it started working. No explanation. However, the button wait issue remains.
I'm also a little stuck at the moment. In essence I want:
While current day < total days....
Run a daily event, updating the screen objects.
Take a choice via button click.
Increase the current day.
However, the day loop stops the screen from displaying (i.e. processing loop). I guess if there's code that pushes the object display up and sits in an infinate loop, which is broken by the button click, that would do. Other ideas? Current code below.
#!/usr/bin/python
# Reminder to self - lots to add. Include a function to reset the text content
# and populate with day number, score summary etc as a template. We can then
# add to it.
#################### IMPORT MODULES WE NEED ############################
import time # For sleep delays
from random import randint # for random numbers
from _ast import While # while loops
import tkinter as Tkinter # this one handles windows and buttons etc
from tkinter import * # skip the tkinter prefix (constants etc)
# import tkmessagebox # Python 2 alternative!
from tkinter import messagebox as tkMessageBox # Python 3
import sys # for quit when added ie sys.exit()
from functools import partial # So we can create lists of buttons & commands
import time # threading support - check events while waiting
import concurrent.futures # threading - think i'll be needing all this
################## CREATE A NEW CLASS (CONTAINER) ############################
class CrazyCoder (Tkinter.Tk):
# When the class is created, the function fInitialise is run, below.
def __init__(self,parent):
Tkinter.Tk.__init__(self,parent)
self.parent = parent
self.fInitialize() # State here any functions to run on creation
################################ FUNCTION ################################
# Set up variables etc...
def fInitialize(self):
########################### VARIABLES ###############################
# Overkill here probably but will revisit later! Listed / initialised
# here to simplify searching later on!
# Could pass most of theses but will keep it simple and update globally
# to start with
self.vDayNumber = 1 # What the current day is
self.vFinalDay = 10 # The last day of our game
self.vPlayerName = '' # To hold the players name
self.vGameName = '' # To hold the game name
self.vChoice = '' # To hold the user choices clicked
self.vRandom = 0 # To hold random numbers!
self.vGameplay = 0 # SCORES: Current gameplay score
self.vGraphics = 0 # SCORES: Current graphics score
self.vSound = 0 # SCORES: current sound score
self.vBugs = 0 # SCORES: current bug score'
self.vBackColor = 'grey' # The background colour of our app
self.vDynamic_Buttons = [] # To hold button objects each "screen"
self.vEntryBox = [] # To hold text entry box objects
self.vTextEntry = '' # Stores user text entry value temporarily
self.vAvailableButtons = [] # To hold a list of AVAILABLE buttons/event
########################## APP SETUP ################################
self.title('Crazy Coder') # set window title
self.geometry("500x500") # set screen size
self.configure(background=self.vBackColor) # set background colour
# Add a "Title box"
self.vTitle = Tkinter.Label(self,text='Crazy Coder')
self.vTitle.configure(background=self.vBackColor)
self.vTitle.pack(side=TOP,padx=10,pady=10)
# Add a picture box (gif supported)
self.vImage = PhotoImage(file="PUG.gif")
self.vPicture=Label(self,image=self.vImage)
self.vPicture.image=self.vImage
self.vPicture.pack()
# Add the main text box
self.vMessageText = '''
This is where your day number goes
Scores go here too
Event details go here too'''
self.vMessage = Tkinter.Label(self,text=self.vMessageText)
self.vMessage.configure(background=self.vBackColor)
self.vMessage.pack(side=TOP,padx=10,pady=10)
# While loop does not work - there is no concept of
# "display current buttons / screen and wait for click event"
#while self.vDayNumber <= self.vFinalDay:
self.vChoice = '' # Clear it ready to take a user choice each day
print('DEBUG: On Main screen, starting now')
self.vRandom = randint(1,100)
if self.vDayNumber == 1:
self.fWelcomeScreen() # Set up the welcome screen
elif self.vRandom >= 0:
self.fEvent1() # Kick off event 1
############################# FUNCTION #################################
# Sets the message on the main text box to whatever you put in brackets
def fSetText(self,TextMessage):
global vMessageText
self.vMessageText = TextMessage
self.vMessage['text'] = self.vMessageText # This updates the text box
############################# FUNCTION #################################
# Sets the image on the main picture box to whatever you put in brackets
def fSetImage(self,ImageName):
global vImage
self.vImage = PhotoImage(file=ImageName) # Example "PUG2.gif"
self.vPicture['image']=self.vImage # This updates the image box
############################# FUNCTION #################################
# Add a new Entry box to our screen. Supports multiple uses
def fAddEntryBox(self):
self.vNewBox = Entry(width=20)
self.vEntryBox.append(self.vNewBox)
self.vNewBox.pack(side=TOP,padx=10,pady=10)
self.vNewBox.focus()
############################# FUNCTION #################################
# Remove the Entry Boxes
def fDeleteEntryBoxes(self):
for each_box in self.vEntryBox:
each_box.destroy()
############################# FUNCTION #################################
# Read from the requested box number, cutting off the Enter at the end
def fReadEntryBox(self,BoxNumber): #BoxNumber 0 is the first box, 1 next
global vTextEntry
vTextEntry=self.vEntryBox[BoxNumber].get()
############################# FUNCTION #################################
# Handles the the day passing by
def fEndDay(self):
global vPlayerName, vDayNumber
self.vDayNumber = self.vDayNumber + 1
print("This print isn't running either!")
############################# FUNCTION #################################
# A simple step to take a choice from the user - used for button code below
def fMakeChoice(self,value):
global vChoice, vDayNumber
self.fEndDay()
self.vChoice = value
print('Just Clicked:',self.vChoice, "and it's day ", self.vDayNumber)
print('But fEndDay should have just run and increased it to 2!')
############################# FUNCTION #################################
# Add buttons to the screen, based on vAvailableButtons
def fAddButtons(self):
global vAvailableButtons # Shouldn't need this here but...
for each_button in self.vAvailableButtons:
# Important: the Lambda section takes the CURRENT value of the variable
# and stores is in the command string. Without it, all the buttons
# when clicked would do the same as the last button created! daft eh.
vNewButton = Tkinter.Button(self, text=each_button, command=lambda v=each_button: self.fMakeChoice(v))
self.vDynamic_Buttons.append(vNewButton)
vNewButton.pack(side= BOTTOM, padx = 10, pady = 10)
############################# FUNCTION #################################
# Clear the buttons out ie before drawing new ones
def fDeleteButtons(self):
for each_button in self.vDynamic_Buttons:
each_button.destroy()
############################# FUNCTION #################################
# Pop up message box
def fMessage(self,Message):
self.tkMessageBox.showinfo("News", Message)
#*********************************************************************#
#********************* EVENTS SECTION ***************************#
#*********************************************************************#
# We'll define a function here for each "daily event"
# The section will be responsible for:
# 1) Updating the screen image (if necessary)
# 2) Updating the text box content
# 3) Adding entry boxes and buttons as required
# 4) Ending the day (there will be one decision per day for now)
############################# EVENT #################################
# The welcome screen
def fWelcomeScreen(self):
global vAvailableButtons
self.vTextMessage = '''
Yawn. You wake up.
Whats your name?
'''
self.fSetText(self.vTextMessage)
time.sleep(1) # delays for 1 second
self.fAddEntryBox() # Add an entry box for a name
self.vAvailableButtons = ['Ok']
self.fAddButtons() # vChoice set on click, and day ended
############################# EVENT #################################
# A random event
def fEvent1(self):
global vAvailableButtons
self.vTextMessage = '''
This is an event. What will you do?
'''
self.fSetText(self.vTextMessage)
time.sleep(1) # delays for 1 second
self.vAvailableButtons = ['Ok']
self.fAddButtons() # vChoice set on click, and day ended
# Start the program
if __name__ == "__main__":
app = CrazyCoder(None)
app.mainloop()
The fact that you want to run some function once a day is no different than wanting an animation to run 30 frames per second. You effectively want an "animation" that runs 1 fpd (one frame per day).
The way to accomplish that is to create a function that should run once a day.
def do_once_a_day():
...
Next, create a function that will call that function once a day:
def call_daily(self):
self.do_once_a_day()
if self.vDayNumber <= self.vFinalDay:
root.after(1000*60*60*24, self.call_daily)
This will cause do_once_a_day to be called once every 24 hours. You can then do whatever you want inside that function.

How do I schedule a one-time script to run x minutes from now? (alternative for 'at')

(Background: I'd like to control a light source with a motion sensor. Light should turn off x minutes after last detected motion. The framework is in place, scheduling is what remains to be done.)
Currently, when motion is detected the light gets turned on and a job to turn it off in 'now + x minutes' is scheduled. Whenever motion is detected during the x minutes the job gets removed from the queue and a new one is set up, extending effectively the time the light stays on.
I tried the "at" command but job handling is quite clunky. Whenever a job is removed from the queue an email gets sent. I looked at the Python crontab module but it would need much additional programming (handling relative time, removing old cronjobs, etc.) and seems to be slower.
What are my alternatives (bash, python, perl)?
-- Edit: My python skills are at beginner level, here's what I put together:
#!/usr/bin/env python2.7
# based on http://raspi.tv/2013/how-to-use-interrupts-with-python-on-the-raspberry-pi-and-rpi-gpio-part-2
# more than 160 seconds without activity are required to re-trigger action
import time
from subprocess import call
import os
import RPi.GPIO as GPIO
PIR = 9 # data pin of PIR sensor (in)
LED = 7 # positive pin of LED (out)
timestamp = '/home/pi/events/motiontime' # file to store last motion detection time (in epoch)
SOUND = '/home/pi/events/sounds/Hello.wav' # reaction sound
# GPIO setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIR,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(LED,GPIO.OUT)
# function which gets called when motion is reported (sensor includes own delay-until-hot again
# and sensibility settings
def my_callback(channel):
now = time.time() # store current epoch time in variable 'now'
f = open(timestamp, "r")
then = float(f.readline()) # read last detection time from file
difference = now - then # calculate time that has passed
call(['/home/pi/bin/kitchenlights.sh', '-1']) # turn light on
call(['/home/pi/bin/lighttimer.sh']) # schedule at job to turn lights off
if difference > 160: # if more than 160 seconds without activity have passed then...
GPIO.output(LED, True) # turn on LED
if not os.path.isfile("/home/pi/events/muted"): # check if system is muted, else
call(['/usr/bin/mplayer', '-really-quiet', '-noconsolecontrols', SOUND]) # play sound
GPIO.output(LED, False) # turn of LED
f = open(timestamp, "w")
f.write(repr(now)) # update timestamp
f.close()
else: # when less than 160 seconds have passed do nothing and
f = open(timestamp, "w")
f.write(repr(now)) # update timestamp (thus increasing the interval of silence)
f.close()
GPIO.add_event_detect(PIR, GPIO.RISING,callback=my_callback,bouncetime=100) # add rising edge detection on a channel
while True:
time.sleep(0.2)
pass
Now that questions come in I think I could put a countdown in the while loop, right? How would that work?
I would approach this with the threading module. To do this, you'd set up the following thread class:
class CounterThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.count = 0
self.start()
def run(self):
while self.count < COUNTLIMIT:
time.sleep(0.1)
self.count += 0.1
#Call function to turn off light here
return
def newSig(self):
self.count = 0
This is a thread which everytime it recieves a new signal (the thread's newSig function is called), the counter restarts. If the COUNTLIMIT is reached (how long you want to wait in seconds), then you call the function to turn off the light.
Here's how you'd incorporate this into your code:
import threading
from subprocess import call
import os
import time
import RPi.GPIO as GPIO
PIR = 9 # data pin of PIR sensor (in)
LED = 7 # positive pin of LED (out)
SOUND = '/home/pi/events/sounds/Hello.wav' # reaction sound
COUNTLIMIT = 160
countThread = None
WATCHTIME = 600 #Run for 10 minutes
# GPIO setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIR,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(LED,GPIO.OUT)
#------------------------------------------------------------
class CounterThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.count = 0
self.start()
def run(self):
call(['/home/pi/bin/kitchenlights.sh', '-1']) # turn light on
while self.count < COUNTLIMIT:
time.sleep(0.1)
self.count += 0.1
call(['/home/pi/bin/kitchenlights.sh', '-0'])
threadKiller()
return
def newSig(self):
self.count = 0
#------------------------------------------------------------
def my_callback(channel):
'''function which gets called when motion is reported (sensor includes own delay-until-hot again and sensibility settings'''
global countThread
try:
countThread.newSig()
except:
countThread = CounterThread()
#------------------------------------------------------------
def threadKiller():
global countThread
countThread = None
#------------------------------------------------------------
def main():
GPIO.add_event_detect(PIR, GPIO.RISING,callback=my_callback,bouncetime=100) # add rising edge detection on a channel
t = 0
while t < WATCHTIME:
t += 0.1
time.sleep(0.1)
#------------------------------------------------------------
if __name__ == "__main__": main()
I don't have any way to test this, so please let me know if there is anything that breaks. Since you said you're new to Python I made a few formatting changes to make your code a bit prettier. These things are generally considered to be good form, but are optional. However, you need to be careful about indents, because as you have them in your question, your code should not run (it will throw an IndentError)
Hope this helps

Python threading for wheel encoders on a Robot

I'm writing the code for a robot which my college is entering into a competition. I'm currently trying to build some wheel encoders using reflectance sensors. I realised a while back that I would probably need to use threading to achieve this, seeing as the robot needs to monitor both the left and right encoders at the same time. The code below is what I have so far:
from __future__ import division
import threading
import time
from sr import *
R = Robot()
class Encoder(threading.Thread):
def __init__(self, motor, pin, div=16):
self.motor = motor
self.pin = pin
self.div = div
self.count = 0
threading.Thread.__init__(self)
def run(self):
while True:
wait_for(R.io[0].input[self.pin].query.d)
self.count += 1
def rotations(self, angle, start_speed=50):
seg = 360/self.div
startcount = self.count
current_dist = angle #Distance away from target
R.motors[self.motor].target = start_speed
while current_dist > 360:
newcount = self.count - startcount
current_dist = angle - newcount*seg
R.motors[self.motor].target = 50
while abs(current_dist) > seg/2:
newcount = self.count - startcount
current_dist = angle - newcount*seg
current_speed = start_speed * current_dist / 360
if current_speed < 5:
R.motors[self.motor].target = 5
else:
R.motors[self.motor].target = current_speed
R.motors[self.motor].target = 0
WheelLeft = Encoder(0,0)
WheelLeft.start()
WheelRight = Encoder(1,3)
WheelRight.start()
WheelRight.rotations(720)
WheelLeft.rotations(720)
The sr module is provided by Southampton University, who are running the competition. It allows us to interact with the robot's hardware.
Now, the threads which get created seem to allow the two reflectance sensors to be monitored separately. This bit of code: R.io[0].input[self.pin].query.d works out whether the value coming from the reflectance sensor has changed. The 'rotations' method turns the wheel through a certain angle by constantly checking how many degrees the wheel has already turned through, and slowing it down as it reaches the end. I would like both wheels to start turning when I run the program, and then slow down and stop when they have gone through 2 rotations. Currently though, when I run the program, one wheel starts turning and slows down and stops, followed by the other wheel. It seems to me like the 'rotations' method is not running in a thread, like the 'run' method is. Is it only the code under the 'run' method that runs in a thread, or is it the whole class?
If it helps, I've been following this tutorial: http://www.devshed.com/c/a/Python/Basic-Threading-in-Python/1/
Also, I would like to know why it is possible to start a thread only with Encoder(0,0).start(). Why do you not have to create an object using the class (e.g. Thread = Encoder(0,0).start() for a new thread to be created?
Sorry if the terminoligy I've used isn't up to scratch, as you can probably tell I'm quite new to threading, and programming in general.
Encoder(0,0).start() is a call to the method to start the thread. In turn, this method calls your run implementation, which doesn't use the rotations method. If you want to do so, then you have to call it in the while loop of run.
With Thread = Encoder(0,0).start() you store the value retrieved from that call (which is None), but to get it you need to start the new thread first anyway.
The run method is the thread of execution.
If you want something else to happen in that thread, you have to call it from Encoder.run().
Oh, and Encoder(0,0).start() does create an object. Just because you didn't bind that object to a local variable doesn't mean it doesn't exist. If it didn't exist you couldn't call its start method.
You have to be very careful about its lifetime though, without a local variable keeping it alive.
You can extend from SR's Poll class so that it can be used in a wait_for:
import poll
class Encoder(poll.Poll):
def __init__(self, motor, pin, div=16):
self.motor = motor
self.pin = pin
self.div = div
self.count = 0
self.target_reached = False
# kick off a thread to count the encoder ticks
self.counter_thread = threading.Thread(target=self._update_count)
self.counter_thread.start()
def _update_count(self):
while True:
wait_for(R.io[0].input[self.pin].query.d)
self.count += 1
def rotations(self, angle, start_speed=50):
if not self.target_reached:
raise Exception("Last motion still in progress!")
self.target_reached = False
# kick off a thread to control the speed
self.angle_thread = threading.Thread(
target=self._update_speeds,
args=(angle, start_speed)
)
self.angle_thread.start()
def _update_speeds(self, angle, start_speed):
# control the motor speed as before
...
# let things know we're done
self.target_reached = True
# implement poll methods
def eval(self):
return (self.target_reached, None)
Which then lets you do:
wheelLeft = Encoder(0,0)
wheelRight = Encoder(1,3)
wheelRight.rotations(720)
wheelLeft.rotations(720)
wait_for(wheelRight & wheelLeft)
Note that an encoder isn't itself a thread - it's a "has a" relationship, not an "is a" relationship

Categories