From 61401a1ab7898ab28479bba14b81a59444d950b7 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Fri, 12 Aug 2022 11:17:09 +0200
Subject: [PATCH] Develop kort-client

---
 99-evolis.rules                             |   1 +
 kort_client/api.py                          |   6 +
 kort_client/card_detectors.py               | 102 +++++++++++++
 kort_client/run.py                          |  32 +++-
 kort_client/usb_barcode_scanner/__init__.py |   0
 kort_client/usb_barcode_scanner/scanner.py  | 158 ++++++++++++++++++++
 match.sh                                    |   9 ++
 pyproject.toml                              |   1 +
 8 files changed, 308 insertions(+), 1 deletion(-)
 create mode 100644 99-evolis.rules
 create mode 100644 kort_client/card_detectors.py
 create mode 100644 kort_client/usb_barcode_scanner/__init__.py
 create mode 100755 kort_client/usb_barcode_scanner/scanner.py
 create mode 100755 match.sh

diff --git a/99-evolis.rules b/99-evolis.rules
new file mode 100644
index 0000000..d5b2524
--- /dev/null
+++ b/99-evolis.rules
@@ -0,0 +1 @@
+SUBSYSTEM=="usb", ATTRS{idVendor}=="0f49", ATTRS{idProduct}=="0b00", MODE="0664", GROUP="dialout"
diff --git a/kort_client/api.py b/kort_client/api.py
index 0a1300f..6cdda86 100644
--- a/kort_client/api.py
+++ b/kort_client/api.py
@@ -24,6 +24,7 @@ class KortAPI:
         "printer": "app/kort/api/v1/printers/",
         "next_job": "app/kort/api/v1/printers/{}/jobs/next/",
         "job_status": "app/kort/api/v1/jobs/{}/status/",
+        "chip_number": "app/kort/api/v1/jobs/{}/chip_number/",
     }
 
     def __init__(self):
@@ -54,6 +55,11 @@ class KortAPI:
         r = self._do_request("PUT", "job_status", [job_id], data=data)
         return r
 
+    def set_chip_number(self, job_id: int, chip_number: str) -> dict[str, Any]:
+        data = {"chip_number": chip_number}
+        r = self._do_request("PUT", "chip_number", [job_id], data=data)
+        return r
+
     def set_printer_status(
         self, printer_id: int, status: str, status_text: str = None
     ) -> dict[str, Any]:
diff --git a/kort_client/card_detectors.py b/kort_client/card_detectors.py
new file mode 100644
index 0000000..6a08bee
--- /dev/null
+++ b/kort_client/card_detectors.py
@@ -0,0 +1,102 @@
+import os
+import subprocess  # noqa
+
+from .usb_barcode_scanner.scanner import BarcodeReader
+
+
+class CardDetector:
+    id_ = None
+
+    def read_id(self):
+        raise NotImplementedError()
+
+    def abort(self):
+        pass
+
+    def clear(self):
+        pass
+
+    @classmethod
+    def get_id(cls):
+        if not cls.id_:
+            raise NotImplementedError()
+        return cls.id_
+
+    @classmethod
+    def get_card_detectors(cls):
+        card_detectors = cls.__subclasses__()
+        return {d.get_id(): d() for d in card_detectors}
+
+
+class EvolisCardDetector(CardDetector):
+    """This is a card detector for Evolis card printers like Evolis Primacy.
+
+    To use it, you have to get the evocom tool from the Evolis customer support.
+    Then place it at /opt/evocom/evocom.
+    """
+
+    id_ = "evolis_evocom"
+    EVOCOM_PATH = "/opt/evocom/evocom"
+
+    def get_detector(self):
+        from kort_client.run import KortClientException
+
+        files = []
+        for __, __, files in os.walk("/dev/"):
+            files = files
+            break
+        files = {f for f in files if f.startswith("hidraw")}
+        possible_interfaces = []
+        for hid_interface in list(files):
+            path_to_read = f"/sys/class/hidraw/{hid_interface}/device/uevent"
+            with open(path_to_read) as f:
+                content = f.read()
+                if "rfid" in content.lower():
+                    possible_interfaces.append(hid_interface)
+
+        if len(possible_interfaces) != 1:
+            raise KortClientException("No or more than one  reader found")
+
+        return os.path.join("/dev", possible_interfaces[0])
+
+    def get_printer(self):
+        from kort_client.run import KortClientException
+
+        files = []
+        for __, __, files in os.walk("/dev/usb/"):
+            files += files
+
+        files = {f for f in files if f.startswith("lp")}
+
+        if len(files) != 1:
+            raise KortClientException("No or more than one printer found")
+
+        suffix = list(files)[0]
+        return f"/dev/usb/{suffix}"
+
+    def read_id(self):
+        from kort_client.run import KortClientException
+
+        reader = BarcodeReader(self.get_detector())
+        # Transport the card to the printer
+        try:
+            r = subprocess.run(  # noqa
+                [self.EVOCOM_PATH, "-p", self.get_printer(), "Sis"]
+            )
+        except (subprocess.CalledProcessError, OSError) as e:
+            raise KortClientException(e)
+
+        # Read the card
+        chip_number = reader.read_barcode()
+
+        try:
+            r.check_returncode()
+        except (subprocess.CalledProcessError, OSError) as e:
+            raise KortClientException(e)
+        return chip_number
+
+    def clear(self):
+        subprocess.run([self.EVOCOM_PATH, "-p", self.get_printer(), "Ser"])  # noqa
+
+    def abort(self):
+        self.clear()
diff --git a/kort_client/run.py b/kort_client/run.py
index 970ba4d..2a4ec40 100644
--- a/kort_client/run.py
+++ b/kort_client/run.py
@@ -7,6 +7,7 @@ import cups
 import requests
 
 from kort_client.api import KortAPIException
+from kort_client.card_detectors import CardDetector
 
 
 class KortClientException(Exception):
@@ -22,6 +23,9 @@ class PrintClient:
         self.api = api
         self.printer_id = None
         self.conn = cups.Connection()
+        self.card_detectors = CardDetector.get_card_detectors()
+        self.card_detector = None
+        self.card_detector_in_process = False
 
     def run(self):
         try:
@@ -35,6 +39,8 @@ class PrintClient:
                         "An error occured, try again in five seconds: {}".format(e),
                         fg="red",
                     )
+                    if self.card_detector_in_process and self.card_detector:
+                        self.card_detector.abort()
                 else:
                     printer_status = "online"
                     status_text = ""
@@ -56,6 +62,8 @@ class PrintClient:
                 self.api.set_printer_status(
                     self.printer_id, "offline", "Printer client was stopped by user."
                 )
+            if self.card_detector and self.card_detector_in_process:
+                self.card_detector.abort()
             raise
 
     def _validate_printer(self):
@@ -77,6 +85,13 @@ class PrintClient:
 
         self._validate_printer()
 
+        self.card_detector = self.card_detectors.get(
+            self.printer_config["card_detector"], None
+        )
+        self.card_detector_in_process = False
+        if self.card_detector:
+            self.card_detector.clear()
+
         next_job = self.api.get_next_job(self.printer_id)
 
         print(next_job)
@@ -84,9 +99,24 @@ class PrintClient:
             job_id = next_job["id"]
             click.secho("Got new print job {}".format(next_job), fg="green")
 
+            self.api.set_job_status(
+                job_id,
+                "in_progress",
+            )
+
             if not self.printer_config.get("generate_number_on_server"):
                 # Now do something to set the number, but irrelevant for this example
-                pass
+                click.secho("Generate number on server disabled", fg="yellow")
+                if self.card_detector:
+                    self.card_detector_in_process = True
+                    chip_number = self.card_detector.read_id()
+                    if chip_number:
+                        next_job = self.api.set_chip_number(job_id, str(chip_number))
+                        time.sleep(1)
+                    else:
+                        raise KortClientException("No valid chip number read")
+                else:
+                    raise KortClientException("No card detector configured")
 
             if next_job["card"]["chip_number"]:
                 # Download PDF file
diff --git a/kort_client/usb_barcode_scanner/__init__.py b/kort_client/usb_barcode_scanner/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/kort_client/usb_barcode_scanner/scanner.py b/kort_client/usb_barcode_scanner/scanner.py
new file mode 100755
index 0000000..4c01be7
--- /dev/null
+++ b/kort_client/usb_barcode_scanner/scanner.py
@@ -0,0 +1,158 @@
+#!/usr/bin/python
+
+# Inspired by https://www.piddlerintheroot.com/barcode-scanner/
+# https://www.raspberrypi.org/forums/viewtopic.php?f=45&t=55100
+# from 'brechmos' - thank-you!
+
+CHARMAP_LOWERCASE = {
+    4: "a",
+    5: "b",
+    6: "c",
+    7: "d",
+    8: "e",
+    9: "f",
+    10: "g",
+    11: "h",
+    12: "i",
+    13: "j",
+    14: "k",
+    15: "l",
+    16: "m",
+    17: "n",
+    18: "o",
+    19: "p",
+    20: "q",
+    21: "r",
+    22: "s",
+    23: "t",
+    24: "u",
+    25: "v",
+    26: "w",
+    27: "x",
+    28: "y",
+    29: "z",
+    30: "1",
+    31: "2",
+    32: "3",
+    33: "4",
+    34: "5",
+    35: "6",
+    36: "7",
+    37: "8",
+    38: "9",
+    39: "0",
+    44: " ",
+    45: "-",
+    46: "=",
+    47: "[",
+    48: "]",
+    49: "\\",
+    51: ";",
+    52: "'",
+    53: "~",
+    54: ",",
+    55: ".",
+    56: "/",
+}
+CHARMAP_UPPERCASE = {
+    4: "A",
+    5: "B",
+    6: "C",
+    7: "D",
+    8: "E",
+    9: "F",
+    10: "G",
+    11: "H",
+    12: "I",
+    13: "J",
+    14: "K",
+    15: "L",
+    16: "M",
+    17: "N",
+    18: "O",
+    19: "P",
+    20: "Q",
+    21: "R",
+    22: "S",
+    23: "T",
+    24: "U",
+    25: "V",
+    26: "W",
+    27: "X",
+    28: "Y",
+    29: "Z",
+    30: "!",
+    31: "@",
+    32: "#",
+    33: "$",
+    34: "%",
+    35: "^",
+    36: "&",
+    37: "*",
+    38: "(",
+    39: ")",
+    44: " ",
+    45: "_",
+    46: "+",
+    47: "{",
+    48: "}",
+    49: "|",
+    51: ":",
+    52: '"',
+    53: "~",
+    54: "<",
+    55: ">",
+    56: "?",
+}
+CR_CHAR = 40
+SHIFT_CHAR = 2
+ERROR_CHARACTER = "?"
+
+
+class BarcodeReader:
+    def __init__(self, device_path="/dev/hidraw0"):
+        self.device_path = device_path
+        self.f = open(self.device_path, "rb")
+
+    def read_barcode(self):
+        barcode_string_output = ""
+        # barcode can have a 'shift' character; this switches the character set
+        # from the lower to upper case variant for the next character only.
+        CHARMAP = CHARMAP_LOWERCASE
+        while True:
+            # step through returned character codes, ignore zeroes
+            for char_code in [element for element in self.f.read(8) if element > 0]:
+                if char_code == CR_CHAR:
+                    # all barcodes end with a carriage return
+                    self.f.close()
+                    return barcode_string_output
+                if char_code == SHIFT_CHAR:
+                    # use uppercase character set next time
+                    CHARMAP = CHARMAP_UPPERCASE
+                else:
+                    # if the charcode isn't recognized, use ?
+                    barcode_string_output += CHARMAP.get(char_code, ERROR_CHARACTER)
+                    # reset to lowercase character map
+                    CHARMAP = CHARMAP_LOWERCASE
+
+
+def barcode_reader(dev="/dev/hidraw0"):
+    barcode_string_output = ""
+    # barcode can have a 'shift' character; this switches the character set
+    # from the lower to upper case variant for the next character only.
+    CHARMAP = CHARMAP_LOWERCASE
+    with open(dev, "rb") as fp:
+        while True:
+            # step through returned character codes, ignore zeroes
+            for char_code in [element for element in fp.read(8) if element > 0]:
+                if char_code == CR_CHAR:
+                    # all barcodes end with a carriage return
+                    return barcode_string_output
+                if char_code == SHIFT_CHAR:
+                    # use uppercase character set next time
+                    CHARMAP = CHARMAP_UPPERCASE
+                else:
+                    # if the charcode isn't recognized, use ?
+                    barcode_string_output += CHARMAP.get(char_code, ERROR_CHARACTER)
+                    # reset to lowercase character map
+                    CHARMAP = CHARMAP_LOWERCASE
diff --git a/match.sh b/match.sh
new file mode 100755
index 0000000..db5591a
--- /dev/null
+++ b/match.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+FILES=/dev/hidraw*
+for f in $FILES
+do
+  FILE=${f##*/}
+  DEVICE="$(cat /sys/class/hidraw/${FILE}/device/uevent | grep HID_NAME | cut -d '=' -f2)"
+  printf "%s \t %s\n" $FILE "$DEVICE"
+done
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 873dab7..ddf75d0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,7 @@ usersettings = "^1.1.5"
 requests = "^2.27.1"
 requests-oauthlib = "^1.3.1"
 pycups = "^2.0.1"
+usb-barcode-scanner-julz = "^0.2"
 
 [tool.poetry.dev-dependencies]
 safety = "^1.8.5"
-- 
GitLab