Created
November 2, 2017 14:48
-
-
Save ssokolow/8d7c6297eba0ebb6d8f4bd883aff593a to your computer and use it in GitHub Desktop.
Demonstration of using a cheap chinese RFID reader from a background application without messing up foreground ones
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Use `udevadm info -a -n /dev/input/whatever` to look up ATTRS{name} | |
SUBSYSTEM=="input", ATTRS{name}=="HID 04d9:1400", MODE="0666" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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