feat: add barcode-websocket-server, webextension-tabfocus
This commit is contained in:
commit
70f52b63e4
6 changed files with 233 additions and 0 deletions
16
LICENSE
Normal file
16
LICENSE
Normal 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
15
README.md
Normal 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
167
barcode-websocket-server
Executable 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()
|
27
webextension-tabfocus/manifest.json
Normal file
27
webextension-tabfocus/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
webextension-tabfocus/tabfocus_background.js
Normal file
4
webextension-tabfocus/tabfocus_background.js
Normal 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});
|
||||||
|
});
|
4
webextension-tabfocus/tabfocus_content.js
Normal file
4
webextension-tabfocus/tabfocus_content.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
function tabfocus() {
|
||||||
|
browser.runtime.sendMessage({focus: 1});
|
||||||
|
}
|
||||||
|
exportFunction(tabfocus, window, { defineAs: "extension_tabfocus" });
|
Loading…
Reference in a new issue