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