feat: add barcode-websocket-server, webextension-tabfocus

This commit is contained in:
s3lph 2024-11-27 21:13:00 +01:00
commit 70f52b63e4
Signed by: s3lph
GPG key ID: 0AA29A52FB33CFB5
6 changed files with 233 additions and 0 deletions

16
LICENSE Normal file
View file

@ -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.

15
README.md Normal file
View file

@ -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.

167
barcode-websocket-server Executable file
View file

@ -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()

View file

@ -0,0 +1,27 @@
{
"manifest_version": 2,
"name": "tabfocus",
"version": "0.1",
"description": "Allow tabs to request focus",
"permissions": [
"tabs",
"<all_urls>"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"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"
}
}
}

View file

@ -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});
});

View file

@ -0,0 +1,4 @@
function tabfocus() {
browser.runtime.sendMessage({focus: 1});
}
exportFunction(tabfocus, window, { defineAs: "extension_tabfocus" });