commit 70f52b63e4e9303dea0632c136d22e5b7a31de33 Author: s3lph Date: Wed Nov 27 21:13:00 2024 +0100 feat: add barcode-websocket-server, webextension-tabfocus diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f97c93a --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Copyright 2024 s3lph + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b0c817 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# barcode-utils + +Collection of tools for our beverage POS kiosk system with barcode scanner: + +## barcode-websocket-server + +Service that binds to the USBHID barcode scanner and provides scan events via a websocket to web clients. + +Can also wake the screen via ydotool. + +## webextension-tabfocus + +Web Extension that injects a `window.extension_tabfocus()` function into browser tabs' Javscript context. + +When called, this function will cause the browser to switch to the calling tab. diff --git a/barcode-websocket-server b/barcode-websocket-server new file mode 100755 index 0000000..96ccd57 --- /dev/null +++ b/barcode-websocket-server @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +import argparse +import select +import os +import socket +import struct +import sys +import time +import webbrowser +import queue +from threading import Thread, Event +from websockets.sync.server import serve + +import hid + + +# struct input_event data to move the mouse +# the timeval header uses native types, the event data uses platform-independent types +# struct timeval { tv_sec = 0, tv_usec = 0 } +evhdr = struct.pack('@LL', 0, 0) +# struct input_event { time = evhdr, type = EV_REL(2), code = REL_X(0), value = 1/-1 } +moveleft = evhdr + struct.pack('=HHi', 2, 0, -1) +moveright = evhdr + struct.pack('=HHi', 2, 0, 1) +# struct input_event { time = evhdr, type = EV_SYN(0), code = 0, value = 0 } +evsyn = evhdr + struct.pack('=HHi', 0, 0, 0) + + +class BarcodeServer: + + def __init__(self, ns): + self.vid = int(ns.vid, 16) + self.pid = int(ns.pid, 16) + self.url = ns.url + self.aim = ns.aim + self.chars = ns.chars + self.q = queue.Queue() + self.e = Event() + self.shutdown = Event() + + self.sock = None + if ns.ydotool_socket: + self.ydotool_socket = ns.ydotool_socket + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + + self.wshost = ns.websocket_host + self.wsport = ns.websocket_port + + def start(self): + self.t_ws = Thread(target=self.serve, args=[]) + self.t_ws.start() + self.t_bc = Thread(target=self.loop, args=[]) + self.t_bc.start() + try: + self.t_ws.join() + self.t_bc.join() + except KeyboardInterrupt: + print('Shutting down...') + self.shutdown.set() + self.t_ws.join() + self.t_bc.join() + + def ws_handler(self, websocket): + a = websocket.remote_address + if self.e.is_set(): + return + self.e.set() + print(f'websocket connected: {a}') + try: + while not self.shutdown.is_set(): + try: + code = self.q.get(timeout=1) + print(f'sending code {code} via ws {a}') + try: + websocket.send(code) + self.q.task_done() + except: + self.q.put(code) + finally: + websocket.close() + break + except queue.Empty: + websocket.ping() + finally: + self.e.clear() + print(f'websocket {a} disconnected') + + def serve(self): + with serve(self.ws_handler, self.wshost, self.wsport) as server: + server.serve_forever() + + def loop(self): + while not self.shutdown.is_set(): + try: + h = hid.device() + h.open(self.vid, self.pid) + print(f'Device manufacturer: {h.get_manufacturer_string()}') + print(f'Product: {h.get_product_string()}') + print(f'Serial Number: {h.get_serial_number_string()}') + while not self.shutdown.is_set(): + # Line from Honeywell scanner consists of: + # - 0x02 (magic?) + # - 1 byte payload length (without AIM, so 13 for EAN-13) + # - 3 bytes AIM, e.g. "]E0" for EAN-13 + # - payload bytes + # - Rest is filled with zeros + hidmsg = h.read(255, 1000) + try: + flag = hidmsg.index(ord(']')) + except ValueError: + continue + if len(hidmsg) < flag+3: + continue + try: + end = hidmsg[flag:].index(0) + end += flag + except ValueError: + continue + aim = bytes(hidmsg[flag:flag+3]).decode() + data = bytes(hidmsg[flag+3:end]).decode().strip() + print(aim, hidmsg[flag+3:end], data) + if not aim.startswith(self.aim): + print('aim mismatch') + continue + if any(filter(lambda c: c not in self.chars, data)): + print('chars mismatch') + continue + url = self.url.format(data) + if self.sock: + try: + print(f'Moving mouse via ydotoold') + self.sock.sendto(moveleft, self.ydotool_socket) + self.sock.sendto(evsyn, self.ydotool_socket) + self.sock.sendto(moveright, self.ydotool_socket) + self.sock.sendto(evsyn, self.ydotool_socket) + except Exception as e: + print(e) + if not self.e.is_set(): + with self.q.mutex: + print('clearing queue') + self.q.queue.clear() + print(f'Opening URL {url}') + webbrowser.open(url, new=2) + self.q.put(data) + print(f'enqued {data}') + except Exception as e: + print(e) + time.sleep(5) + + +def main(): + ap = argparse.ArgumentParser(sys.argv[0], description='Opens a website when a barcode is scanned with an USB-HID barcode scanner.') + ap.add_argument('--url', '-u', type=str, help='The URL to open. "{}" is replaced by the barcode data. Default: http://localhost/?ean={}', default='http://localhost/?ean={}') + ap.add_argument('--vid', '-v', type=str, help='The USB vendor ID. Default: 0c2e', default='0c2e') + ap.add_argument('--pid', '-p', type=str, help='The USB product ID. Default: 0b07', default='0b07') + ap.add_argument('--aim', '-A', type=str, help='AIM filter prefix, e.g. "]E0" for EAN13, "]E" for all EAN variants, or "]" for all barcodes. Default: "]E"', default=']E') + ap.add_argument('--chars', '-C', type=str, help='Permitted characters. Defaults to "0123456789"', default='0123456789') + ap.add_argument('--ydotool-socket', '-Y', type=str, help='If provided, the ydotool socket to wake the screen.') + ap.add_argument('--websocket-host', '-w', type=str, help='Websocket host to listen on. Defaults to "localhost"', default='localhost') + ap.add_argument('--websocket-port', '-W', type=int, help='Websocket port to listen on. Defaults to 47808', default=47808) + ns = ap.parse_args() + + BarcodeServer(ns).start() + + +if __name__ == '__main__': + main() diff --git a/webextension-tabfocus/manifest.json b/webextension-tabfocus/manifest.json new file mode 100644 index 0000000..86da08a --- /dev/null +++ b/webextension-tabfocus/manifest.json @@ -0,0 +1,27 @@ +{ + "manifest_version": 2, + "name": "tabfocus", + "version": "0.1", + "description": "Allow tabs to request focus", + "permissions": [ + "tabs", + "" + ], + "content_scripts": [ + { + "matches": [""], + "js": ["tabfocus_content.js"] + } + ], + "background": { + "scripts": ["tabfocus_background.js"], + "persistent": false, + "type": "module" + }, + "applications": { + "gecko": { + "id": "tabfocus@ccc-basel.ch", + "strict_min_version": "1.0" + } + } +} diff --git a/webextension-tabfocus/tabfocus_background.js b/webextension-tabfocus/tabfocus_background.js new file mode 100644 index 0000000..6202814 --- /dev/null +++ b/webextension-tabfocus/tabfocus_background.js @@ -0,0 +1,4 @@ +browser.runtime.onMessage.addListener((request, sender, responsewi) => { + browser.tabs.update(sender.tab.id, {active: true}); + browser.windows.update(sender.tab.windowId, {focused: true}); +}); diff --git a/webextension-tabfocus/tabfocus_content.js b/webextension-tabfocus/tabfocus_content.js new file mode 100644 index 0000000..5da83a4 --- /dev/null +++ b/webextension-tabfocus/tabfocus_content.js @@ -0,0 +1,4 @@ +function tabfocus() { + browser.runtime.sendMessage({focus: 1}); +} +exportFunction(tabfocus, window, { defineAs: "extension_tabfocus" });