This is a tale of heroism, of overcoming obstacles and hardships. This is a tale of ingenuity, of originality and thinking outside the box. This is a tale… of how I was too lazy to go and look if someone was already playing table tennis in the game room.
Hiking Across the Office is a Drag
Taboola’s Israeli office in Tel Aviv, houses about 350 people, spread over five large floors. The game room, however is smack dab in the middle of them.
In smaller companies, if you wanted to know if the game room was available, all you had to do was to look slightly over your monitor and you would have your answer.
Here, it takes 60 seconds and 110 steps, including one flight of stairs, to get from my workstation all the way to the game room – believe me, I counted. Unfortunately, due to the unbending laws of physics, it takes exactly the same time to go back, frustrated that it was already being used.
And so, after one too many of these time-wasting, context-switching, hope-shattering trips, I started asking myself one simple question – what if we could just ask if someone was already using the game room?
Weapon(s) of Choice – Slack Bot, PIR Sensor and a Raspberry Pi
Salvation came in the form of a Passive Infrared (PIR) motion sensor connected to a Raspberry Pi. The sensor data is transmitted to the Raspberry Pi, which in turn saves the data to a local DB. The Slack bot user then queries the DB, to find out if actual movement has been detected in the room recently.
The entire system looks like this:
I Sense a Disturbance in the Room
A PIR sensor is just a fancy name for your run-of-the-mill motion detector, the ones you usually see mounted on walls of houses. Basically, the way it works is you have a sensor that measures infrared light, radiating from objects in its field of view. The term passive in this instance refers to the fact that PIR devices do not generate or radiate energy for detection purposes, they work entirely by detecting infrared radiation, emitted by or reflected from objects (find more information about PIR sensors here).
Easy as Pi
Raspberry Pi is a series of small single-board computers designed to promote the teaching of basic computer science in schools and in developing countries. It achieves this goal by being highly versatile, easy to setup and most important – very, VERY affordable, starting at only $5 (!) for the most basic model and $35 for the latest model.
It has a lot of cool features, but maybe the most important one is the General Purpose Input/Output (GPIO) connector. These pins are a physical interface between the Pi and the outside world. This is what is going to be used in order to connect the motion sensor output to the Raspberry Pi.
A Tale of Two Scripts
Raspberry Pi can run all sorts of images on its MicroSDHC card, but the most prevalent one is its own flavor of Unix – Raspbian, which is what I installed to run the Python interpreter and the various dependencies and tools required for this project.
The solution was divided into two separate Python scripts, which run on startup – the motion detection and the slackbot script.
A local MySQL database is used to store information about the activity in the room, meaning a list of all previous sessions and a current active one, should one exist.
This is to allow for persistency and other possible future features, such as to determine when is the optimal time to go and play, or how long someone has already been using the room.
The motion detection script listens for movement (or lack thereof) and updates the database accordingly – opens a new game session if one is not already opened, extends active sessions and closes sessions after a certain period of non-movement.
The slackbot script handles Slack integration and queries the database to provide information about the room’s status, by checking if there are currently any active open sessions.
Halt! Who Goes There?
I used an HC-SR501 motion sensor, which you can get online for $1. It only has three pins that you need to connect to the Raspberry Pi – GND (Ground), VCC (5V) and OUT (Data) – follow the diagram above to connect these (see the pin layout here).
The motion detection script communicates with the GPIO channels by listening on the data pin – marked as OBSTACLE_PIN in the code. This is done utilizing the RPi.GPIO package, which allows you to access the various pins on the board and get their current state (see documentation here).
Once the motion detection script starts running it goes in an endless loop, which is divided to one second intervals, and in each iteration the sensor input is read. Querying the sensor gives a binary response – a movement was detected or not.
Since the motion detector samples motion every second, it needs to be able to determine that an actual movement occurred not only in a particular second, but several times during a short period of time, to avoid false positives, i.e. detecting movement when there was none, and false negatives, i.e. not detecting movement when it should have.
So, in every iteration a “detection frame” is examined, which is basically a time window in which the number of seconds a movement was detected is counted. The number of seconds with movement, out of the total number of seconds in the time window, is compared to a predefined threshold.
If the number exceeds the threshold, then a movement was detected in the room. This allows control of the sensitivity of the detection, in addition to being able to adjust it physically on the sensor itself.
The following code demonstrates this logic:
# The physical pin number on the board connected to the DATA output pin of the motion sensor OBSTACLE_PIN = 7 # The maximum time in seconds allowed for a session to be inactive MAX_SESSION_TIME_IN_SECONDS = 60 # The time window in seconds we are looking for consecutive movement DETECTION_FRAME_LENGTH_IN_SECONDS = 10 # The threshold that determines if a movement was detected DETECTION_INCIDENTS_THRESHOLD = 0.4 def setup(): print_log('initializing sensor') GPIO.setmode(GPIO.BOARD) GPIO.setup(OBSTACLE_PIN, GPIO.IN) print_log('initializing db') time.sleep(10) # preparing tables, cleaning existing open sessions setup_database() def is_motion_detected(): return GPIO.input(OBSTACLE_PIN) def loop(): iteration_counter = 0 detection_counter = 0.0 print_log('started monitoring') # start monitoring for movement while True: # if the detection window is over if (iteration_counter >= DETECTION_FRAME_LENGTH_IN_SECONDS): # if we saw enough movement during that window # i.e. seconds with movement out of total window time is larger than the threshold if (detection_counter / iteration_counter >= DETECTION_INCIDENTS_THRESHOLD): handle_motion_detected() else: handle_no_motion(DETECTION_FRAME_LENGTH_IN_SECONDS, MAX_SESSION_TIME_IN_SECONDS) # reset counters for next detection window iteration_counter = 0 detection_counter = 0.0 # if a movement is detected in this particular second if (is_motion_detected()): detection_counter += 1 print_log('motion ' + str(detection_counter) + ' detected') iteration_counter += 1 time.sleep(1)
Slacking Off
Below is a code snippet from the slackbot script, outlining the interaction with the aptly named PingPongBot – you will need to add a new bot user to your Slack team (see this link for more information), after which you will receive a token to be used in your client:
# these can be obtained when creating a new slack bot user # see instructions on how to do that here - https://my.slack.com/services/new/bot SLACK_BOT_TOKEN = '<your slack bot token here>' SLACK_BOT_NAME = '<your slack bot name here>' # aux method to get the slack bot id, required for authentication def get_slack_bot_id(slack_client, bot_name): if __name__ == "__main__": api_call = slack_client.api_call('users.list') if api_call.get('ok'): users = api_call.get('members') for user in users: if 'name' in user and user.get('name') == bot_name: print("Bot ID for '" + user['name'] + "' is " + user.get('id')) return user.get('id') else: print('could not find bot user with the name ' + bot_name) return None # set up connection to Slack client slack_client = SlackClient(SLACK_BOT_TOKEN) BOT_ID = get_slack_bot_id(slack_client, SLACK_BOT_NAME) AT_BOT = '<@' + BOT_ID + '>' # the actual command to look for when addressing the bot in chat EXAMPLE_COMMAND = 'free' # the actual logic when receiving a message in the chat def handle_command(command, channel): response = 'Hello! I am the ping pong bot! Ask me if the table is free by typing - \"free?\"' # parse the chat message and check if it contains the desired command if EXAMPLE_COMMAND in command.lower(): if is_open_session_exists(): response = 'Sorry, someone is playing right now... Try again later!' else: response = 'Free to play! enjoy :)' # return the response in the chat according to the result from the database slack_client.api_call('chat.postMessage', channel=channel, text=response, as_user=True) # aux method to receieve messages from the chat def parse_slack_output(slack_rtm_output): output_list = slack_rtm_output if output_list and len(output_list) > 0: for output in output_list: if output and 'text' in output and AT_BOT in output['text']: return output['text'].split(AT_BOT)[1].strip().lower(), output['channel'] return None, None if __name__ == "__main__": READ_WEBSOCKET_DELAY = 1 # establishing connection to bot if slack_client.rtm_connect(): print(SLACK_BOT_NAME + ' connected and running') # start listening for messages while True: command, channel = parse_slack_output(slack_client.rtm_read()) if command and channel: handle_command(command, channel) time.sleep(READ_WEBSOCKET_DELAY) else: print('connection failed, invalid Slack token or bot ID?')
You can find the full project code on my github page.
I Love It When a Plan Comes Together
This is what the sensor looks like taped to the floating ceiling of our game room (the Raspberry Pi is hidden from view):
And finally, this is how it looks like when you ask the PingPongBot if the game room is free:
Not Only for the Office
Hopefully by now you realize how simple and easy it is to implement an entire motion detection system using accessible and affordable parts.
There are many other sensors that you can incorporate in your next project, turning your office/home into a smart one. For example, a temperature and humidity sensor can be used as input to control your AC, or a rain detection module to let you know when to turn off your sprinklers or close the electric windows.
Well, writing this post really made me parched. Hmm… I wish there was a way to know if there are beers left in the kitchen…