|
#!/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 : |