Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Demonstration of using a cheap chinese RFID reader from a background application without messing up foreground ones
# Use `udevadm info -a -n /dev/input/whatever` to look up ATTRS{name}
SUBSYSTEM=="input", ATTRS{name}=="HID 04d9:1400", MODE="0666"
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""A simple proof-of-concept to exclusively grab my cheap Chinese RFID reader
using python-evdev and then log entry and exit events for any RFID tag held up
to it.
"""
from __future__ import (absolute_import, division, print_function,
with_statement, unicode_literals)
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__appname__ = "Simple RFID Timeclock"
__version__ = "0.0pre0"
__license__ = "MIT"
# ATTRS{name} value from `udevadm info -a -n /dev/input/eventXX`
RFID_DEVICE_NAME = 'HID 04d9:1400'
# A bunch of unisex names used to demonstrate recognizing the RFID IDs without
# revealing them to the user
PLACEHOLDER_NAMES = [
'Alex',
'Ali',
'Ash',
'Casey',
'Corey',
'Dakota',
'Dana',
'Drew',
'Harley',
'Indiana',
'Jamie',
'Jean',
'Jordan',
'Kelly',
'Max',
'Pat',
'Peyton',
'Regan',
'Riley',
'Rowan',
'Sam',
'Shannon',
'Sidney',
'Taylor',
'Terry',
]
import logging, os, random, time
from math import sin, pi
import evdev
from evdev.ecodes import EV_KEY, ecodes
log = logging.getLogger(__name__)
def make_tone(freq, sample_num):
"""By: PM 2Ring from http://forums.xkcd.com/viewtopic.php?t=49360"""
samples = [chr(127 + int(127 * sin(i * 2 * pi * freq / sample_num)))
for i in range(sample_num)]
# Support both Python 2 and Python 3
if isinstance(samples[0], bytes):
return b''.join(samples)
else:
return ''.join(samples).encode('latin1')
def get_device():
"""Find and return an evdev.InputDevice for the RFID reader"""
for dev_path in evdev.list_devices():
device = evdev.InputDevice(dev_path)
if device.name != RFID_DEVICE_NAME:
continue # Skip unwanted devices
# Ignore the vestigial mouse/mediakeys descriptor exposed by the device
if ecodes['KEY_0'] in device.capabilities(absinfo=False)[EV_KEY]:
return device
# No device found
return None
class Timeclock(object):
"""Proof-of-concept timeclock implementation"""
def __init__(self, rfid_dev):
self.accumulator = []
self.rfid_dev = rfid_dev
self.last_seen = {}
self.id_mappings = {}
self.status = {}
self.unused_names = PLACEHOLDER_NAMES[:]
random.shuffle(self.unused_names)
# Bare minimum example to play sound
if os.path.exists('/dev/dsp'):
self.dsp = open('/dev/dsp', 'wb')
def play_tones(self, freqs):
"""If /dev/dsp was found, play the provided sequence of tones"""
if self.dsp:
samples = b""
for freq in freqs:
samples += make_tone(freq, 8000 // 8)
self.dsp.write(samples)
self.dsp.flush()
def handle_punch(self, ev_time, tok_id):
"""Handle one checkin/checkout event"""
# Debounce
last_seen = self.last_seen.get(tok_id, 0)
self.last_seen[tok_id] = ev_time
if ev_time - last_seen < 3:
return
if len(tok_id) != 8:
log.error("Invalid token ID (len != 8)")
return
# Provision a placeholder name if necessary
if tok_id in self.id_mappings:
name = self.id_mappings[tok_id]
else:
name = self.id_mappings[tok_id] = self.unused_names.pop()
# Toggle the stored state
self.status.setdefault(tok_id, False)
self.status[tok_id] = not self.status[tok_id]
# Display placeholder for actual functionality
print("{}: Status of {} changed to {}".format(
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ev_time)),
name,
'Present' if self.status[tok_id] else 'Absent'))
# Provide a cue in case someone gets confused about their status
self.play_tones((220, 440) if self.status[tok_id] else (440, 220))
def handle_event(self, event):
"""Handle one python-evdev event"""
if event.type != EV_KEY:
return # Ignore non-key events
event_c = evdev.categorize(event)
if event_c.keystate != event_c.key_down:
return # Ignore key release events
keyname = event_c.keycode[4:]
if keyname == 'SLASH':
self.handle_punch(int(event.sec), ''.join(self.accumulator))
if keyname in ('SEMICOLON', 'SLASH'):
self.accumulator = []
elif len(keyname) == 1 and keyname in '1234567890':
self.accumulator.append(keyname)
def loop(self):
"""Mainloop for the timeclock"""
for event in self.rfid_dev.read_loop():
self.handle_event(event)
def main():
"""The main entry point, compatible with setuptools entry points."""
# If we're running on Python 2, take responsibility for preventing
# output from causing UnicodeEncodeErrors. (Done here so it should only
# happen when not being imported by some other program.)
import sys
if sys.version_info.major < 3:
reload(sys)
sys.setdefaultencoding('utf-8') # pylint: disable=no-member
from argparse import ArgumentParser, RawDescriptionHelpFormatter
parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0])
parser.add_argument('--version', action='version',
version="%%(prog)s v%s" % __version__)
parser.add_argument('-v', '--verbose', action="count",
default=2, help="Increase the verbosity. Use twice for extra effect.")
parser.add_argument('-q', '--quiet', action="count",
default=0, help="Decrease the verbosity. Use twice for extra effect.")
# Reminder: %(default)s can be used in help strings.
args = parser.parse_args()
# Set up clean logging to stderr
log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]
args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1)
args.verbose = max(args.verbose, 0)
logging.basicConfig(level=log_levels[args.verbose],
format='%(levelname)s: %(message)s')
# Grab the device exclusively so it doesn't inject keyboard events
dev = get_device()
dev.grab()
clk = Timeclock(dev)
clk.loop()
if __name__ == '__main__':
main()
# vim: set sw=4 sts=4 expandtab :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment