Card swipe demo
Introduction
This demo is designed to give an example of how to take a picture of an event, a security card being swiped, and sending the image across the network to a server for storage/display.
This process is accomplished in 3 parts:
- The card being swipe triggers the card reader to send serial traffic to the connected XBee RS-232 serial adapter, which forwards this message to the Gateway
- The Gateway receives the event from the XBee RS-232 serial adapter, and captures the current image buffer from the attached USB camera. The image is given a footer, and sent to the server.
- The server receives the bundled image and footer, extracts the footer, and saves the image for other applications.
Requirements
- RS-232 enabled card reader. The card reader must be programmed that when a card is swiped it outputs serial data.
- XBee RS-232 serial adapter. The adapter must be programmed with compatible serial settings with the card reader, and be set to transparent mode, forwarding all serial traffic to the coordinator of the Mesh network.
- ConnectPort X8 or ConnectPort X4 with available USB port.
- Watchport/V2 or later USB Camera, or other Gateway compatible camera.
- PC/Laptop with Python 2.4.3 interpreter or later installed. This will act as a server to the Gateway.
- Access to the command line of the Gateway
- Read/Write access to the PC/Laptop
Setting up the XBee RS-232 serial adapter
Setup the serial adapter to associate with the Gateway's PAN. If unsure of how to do so, consult the documentation for the serial adapter. Other settings to configure would be the serial settings (Baud, Data bits, Parity, Stop Bits, Flow Control). These settings must be compatible with your card reader. Make note of the 64 bit MAC address of the radio installed in the serial adapter.
Connect the serial adapter to the card reader's rs 232 port.
Setting up the gateway
The Gateway must be associated with the XBee RS-232 serial adapter, as well as be able to make a TCP connection to the server. To verify association with the XBee RS-232 serial adapter, perform a discovery on the mesh network.
Edit device_config.py to match your environment. There are 3 settings to change:
- address_list - A list containing strings of the 64 bit MAC addresses of associated adapters that will trigger the Gateway to take a picture and send it to the server. Set this to the 64 bit MAC address of the serial adapter that you noted in a earlier section.
- server_ip - The IP address the server is located at.
- server_port - The port of the server to connect to.
Once configured, upload device_config.py and card_listener.py to the Gateway. Do not start the application yet.
Setting up the server
Edit server_config.py to match your environment. There are 3 settings to change:
log_path - The server will store all images received under this base folder. The folder must exist already on your system.
listen_count - The number of simultaneous clients to allow uploading of pictures.
port - The port which to listen for clients on. This should match device_config.server_port setting.
Starting up the demo
With the server_config.py configured, now start up the card_server.py script. Text should appear indicating that the server is ready to accept client connections. If this is not the case, review the generated log file for any errors that occurred.
Next, startup the card_listener.py script on the Gateway. This should also indicate that it is ready.
Finally, swipe a card!
How it worked
There are two parts to look at code wise, card_listener.py and card_server.py.
card_listener.py
Declarations
from socket import * from select import * import device_config import camera image_count = 0 image_queue = {} zig_sock = socket(AF_ZIGBEE, SOCK_DGRAM, ZBS_PROT_TRANSPORT) zig_sock.bind(("", 0xe8, 0, 0)) zig_sock.settimeout(0) image_sock = socket(AF_INET, SOCK_STREAM) image_sock.connect((device_config.server_ip, device_config.server_port)) quit_sock = socket(AF_INET, SOCK_STREAM) quit_sock.bind(("", 31000)) quit_sock.listen(1)
The first thing the script does is import the necessary files that are used. 'device_config' is the device_config.py we uploaded to the Gateway with the script, which contains the environment information our script needs. 'camera' is a built in module that gives access to the attached USB camera. See the 'Camera Module' page for more details.
The script then defines a number of variables:
- image_count - An indicator on how many images we've successfully processed.
- image_queue - A dictionary object that maps image data to a 64 bit MAC address. This is used to queue up image data being sent to the server, in the cases that not all data could be sent in each pass (which is common in this application).
- zig_sock - The zigbee socket descriptor.
- image_sock - The image server to send the image data to. The IP and port are defined in device_config.py.
- quit_sock - A socket designed so when it is accessed, the application quits.
Functions
def remake_connection(): print "Setting up connection to server again" global image_sock try: image_sock.close() image_sock = socket(AF_INET, SOCK_STREAM) image_sock.connect((device_config.server_ip, device_config.server_port)) except: return False else: return True
The above is a function that would reconnect the image_sock if possible. In the advent it does not reconnect, it returns False. This is used to reconnect in case of TCP error.
Main loop: select statement
while 1: rl, wl, el = select([zig_sock, quit_sock], [image_sock], [])
We listen on the zigbee socket, quit socket and image_socket for changes.
Main loop: Zigbee reading
if zig_sock in rl: try: data, addr = zig_sock.recvfrom(255) except Exception, e: print "Error with zigbee socket: ", e print "Received data from address: ", addr if addr[0] in device_config.address_list: print "Received packet from a target in device_config.address_list" (image, timestamp) = camera.get_image() if image is not None: if not image_queue.has_key(addr[0]): image_queue[addr[0]] = [] image_queue[addr[0]].append(image+addr[0]) image_count += 1
In the event that the zigbee socket is in the read list 'rl', we receive data from a specific address. If the address's 64 bit MAC address is listed in the device_config.address_list attribute, we consider this a trigger to capture a picture from the camera, and queue it up to be sent to the server.
For every image capture we do, we append that to the image_queue keyed off the 64 bit MAC address. Each image appended is also appended with a footer, which is also the 64 bit MAC address. This is used by the server to indicate when a image is completed or not, and where to store the image.
Main loop: image sock writing
for key in image_queue: if image_sock in wl and len(image_queue[key]) != 0: print "Sending image: ", image_count data = image_queue[key][0] try: sent_bytes = image_sock.send(data) except Exception, e: print "Error with image socket" if not remake_connection(): raise e continue else: data = data[sent_bytes:] if len(data) == 0: print "Image sent succesfully!" image_queue[key].pop(0) else: image_queue = data
If the image socket is writable, we cycle through the pending images and send the pending data to the server. If we encounter a socket error, we attempt to reconnect the connection and move on.
Note The socket error handling here is simplistic for demo purposes.
Main loop: quit socket reading
if quit_sock in rl: raise KeyboardInterrupt("Quit socket activated, stopping app")
If the quit socket has activity, raise a KeyboardInterrupt exception, stopping the application.
Limitations
There are a few limitations with the above code. The first and foremost is it does not demonstrate parsing of the received data to verify that it is an event that warrants sending a image to the server. Secondly, it does not contain a mechanism to slow down the images being produced. A possible solution would be to add a timestamp to the code that would indicate the last time a image was taken for that client, and would not take another till the N time has elapsed.
Finally, because the camera module is agnostic about the current camera settings, it does not consider the network usage involved. A user may set the camera for maximum resolution and quality, resulting in very large images being produced and sent to the server, sometimes rapidly. It may be prudent to have the script to set the camera settings on startup to some known safe values using the digicli module, and sample the resulting image sizes for future comparison. Afterwards, if the size of the new image exceeds that limit by a large margin, reapply the known safe settings to camera.
card_server.py
Declarations
from socket import * from select import * import os import sys import datetime import server_config import time import logging import re log = get_logger() client_list = [] client_file_dict = {} client_data_dict = {}
We import the necessary libraries. Note that we import the server_config to act as our environment information. Also we make use of the logging module, which will be used to produce non-volatile logs.
- log - The instance of the logging module, this is used throughout the application, hence why it is on the lowest scope.
- client_list - list of connected client sockets
- client_file_dict - dictionary lookup between client sockets and image storage directories.
- client_data_dict - dictionary lookup between client sockets and the incoming data received on the socket.
Functions
def get_short_time(): t = datetime.datetime(1900, 1, 1).now() return ("%02d-%02d-%02d-%04d" %(t.hour, t.minute, t.second, t.microsecond)) def get_long_time(): t = datetime.datetime(1900, 1, 1).now() return ("%s-%02d-%02d" %(t.year, t.month, t.day)) def get_timestamp(): return get_long_time() + '-' + get_short_time() def make_client_dir(dir_name): if not os.path.exists(dir_name): os.mkdir(dir_name) def get_file_name(s): s = s.lower() s = s.strip() for sym in [' ', "'", '"', '\\', '/', ':', '*', '|', '<', '>', '?', ';', '!', '(', ')']: s = s.replace(sym, '_') while s.replace('__', '_') != s: s = s.replace('__', '_') if s[0] == '_': s = s.lstrip('_') if s[-1] == '_': s = s.rstrip('_') return s
Here we have some timestamping functions, which will be used to generate the filename of the images. We include 4 digits of the microseconds because it is possible we receive multiple images from a source in one second, and we would want to make sure that the file name is unique, but still sorts correctly.
Other functions are creating the directory, and sanitizing a file name.
def get_logger(file_name=sys.argv[0]): log = logging.Logger(file_name) log.setLevel(logging.DEBUG) log_format = logging.Formatter("%(levelname)-8s %(asctime)s.%(msecs)03d %(filename)s:%(lineno)d %(message)s", "%H:%M:%S") stream_handler = logging.StreamHandler() stream_handler.setFormatter(log_format) log.addHandler(stream_handler) t = datetime.datetime(1900, 1, 1).now() t = t.__str__() t = t.split(".")[0] t = t.replace(":", "-") t = t.replace(" ", "-") file_name = os.path.split(file_name)[1] file_name = "%s-%s.log" %(file_name, t) file_handler = logging.FileHandler(file_name) file_handler.setFormatter(log_format) log.addHandler(file_handler) return log def cleanUp(conn): global client_list global client_file_dict global client_data_dict client_list.remove(conn) del client_file_dict[conn] del client_data_dict[conn]
The get logger function creates a useful logging instance for us. Both a stream handler and a file handler, so any messages logged will both be displayed to the console and saved to a file.
The cleanUp function is used to remove information referencing a client connection. We clear it out of the client_list, and both client dictionaries.
def find_footer(data): sub_patt = "[a-f0-9][a-f0-9]" pattern = "\[" + ((sub_patt + ":") *7) + sub_patt + "\][!]" reg = re.compile(pattern, re.IGNORECASE) search = reg.search(data) if search is None: return None else: return search.span() def process_and_save_image(dir_name, addr, image): log.info("Processing a image into directory: %s" %dir_name) timestamp = get_timestamp() make_client_dir(dir_name) os.chdir(dir_name) sub_dir = get_file_name(addr) make_client_dir(sub_dir) os.chdir(sub_dir) fh = open(timestamp + '.jpg', 'wb') fh.write(image) fh.close() os.chdir(server_config.log_path)
The find_footer function attempts to find the footer inside the stream of data. The pattern is designed to match something like '[00:13:a2:00:40:0a:12:c8]!'. If it does match the data, it returns the start and end index of inside the data stream. Otherwise it will return None.
The process_and_save_image function takes care of storing an image. It creates the directories using make_client_dir calls, and changes to them. It then opens a file handle in binary mode, and writes the image to a file, and returns to the starting directory.
Mainloop: declarations
def main(): log.info("Running Main") listen_list = [] log.info("Creating directory") make_client_dir(server_config.log_path) os.chdir(server_config.log_path) log.info("Creating socket") listen_sock = socket(AF_INET, SOCK_STREAM) listen_sock.bind(("", server_config.port)) listen_sock.listen(server_config.listen_count) listen_sock.settimeout(0) listen_list.append(listen_sock)
We not enter the main loop of the application. Notably the main is inside a function, rather then on the lower level namespace. This is because we may want to have another program import this one as a library, and run main as a thread.
We setup the initial directory and change to it. We then setup out listening socket and prepare it for the upcoming select statement.
Main loop: select + read on listen socket
log.info("Main loop") while 1: rl, wl, el = select(listen_list + client_list, [], []) if listen_sock in rl: client_sock, addr = listen_sock.accept() log.info("Accepting client from:") log.info(addr) ##Setting up the directory for the client dir_name = get_file_name(addr[0]) make_client_dir(dir_name) ##Setting up the dictionaries for future lookup client_file_dict[client_sock] = dir_name client_data_dict[client_sock] = "" client_list.append(client_sock)
We start a never ending loop, and select on the listen_list and client_list.
If activity on the listen_sock, we accept the connection and setup the dictionaries for the client. We put the new client socket into the client list so next select, we will include it.
Mainloop: activity on the client
for client_sock in client_list: if client_sock in rl: try: data = client_sock.recv(8192) except: cleanUp(client_sock) else: if len(data) == 0: cleanUp(client_sock) continue log.info("Receiving data from client") client_data_dict[client_sock] += data log.info("Received %d bytes" %len(data)) indexes = find_footer(client_data_dict[client_sock]) if indexes is not None: ## we have a complete image log.info("Found completed image") data = client_data_dict[client_sock] #retrieve all data image = data[:indexes[0]] #extract only the image addr = data[indexes[0]:indexes[1]] #extract the footer client_data_dict[client_sock] = data[indexes[1]:] #put back date minus image and footer dir_name = client_file_dict[client_sock] #Retrieve directory name process_and_save_image(dir_name, addr, image) #send info to be processed
If there is activity on the client socket, we perform a receive on it. If the receive is 0, we clean up the socket and move on. Otherwise we append the data to the client's queue.
We then attempt to find the footer, and if it was found it indicates that the image is complete and ready to be stored. We use the returned indexes to chop up the data into the image, address and remaining data. We return the remaining data back into the queue, and feed the address and image along with the directory name to process_and_save_image function.
Final snippet
if __name__ == '__main__': main()
If the file was run directly, call main.
Notes
The card_server.py script was purely designed to act as a saving mechanism for the images. Parts of it were designed so it can be run stand alone or as part of a greater application. If it is run as a part of a larger application, run it in a separate thread to avoid blocking on select call.
Limitations
Better directory handling is possible. Condensing some of the helper functions would clean up the source a bit.
We do not track much about the creation of images, and it may be beneficial to do so. Not just for statistics, but for change in behavior.
We do not have a mechanism to talk back to the clients, possibly to change camera settings or to allow/disallow certain addresses from producing images.