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:

Requirements

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:

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:

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.

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.

Source

Card_swipe_demo_source.zip