odoo-driver 0.9.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. odoo_driver/__init__.py +5 -0
  2. odoo_driver/app.py +29 -0
  3. odoo_driver/cli.py +121 -0
  4. odoo_driver/default_cert/server.crt +22 -0
  5. odoo_driver/default_cert/server.key +28 -0
  6. odoo_driver/devices/__init__.py +4 -0
  7. odoo_driver/devices/device_display.py +147 -0
  8. odoo_driver/devices/device_payment.py +20 -0
  9. odoo_driver/devices/device_printer.py +25 -0
  10. odoo_driver/devices/device_scale.py +34 -0
  11. odoo_driver/devices/models/__init__.py +0 -0
  12. odoo_driver/devices/models/device_abstract.py +115 -0
  13. odoo_driver/devices/models/device_abstract_with_queue.py +84 -0
  14. odoo_driver/devices/models/device_task.py +10 -0
  15. odoo_driver/devices/scale_dialog06/__init__.py +0 -0
  16. odoo_driver/devices/scale_dialog06/scale_answer.py +101 -0
  17. odoo_driver/devices/scale_dialog06/scale_command.py +169 -0
  18. odoo_driver/devices/scale_dialog06/state_machine.py +93 -0
  19. odoo_driver/devices/tools/__init__.py +0 -0
  20. odoo_driver/devices/tools/serial_code.py +12 -0
  21. odoo_driver/flask_app/__init__.py +8 -0
  22. odoo_driver/flask_app/flask_app_display.py +20 -0
  23. odoo_driver/flask_app/flask_app_odoo.py +67 -0
  24. odoo_driver/flask_app/flask_app_payment.py +23 -0
  25. odoo_driver/flask_app/flask_app_printer.py +30 -0
  26. odoo_driver/flask_app/flask_app_scale.py +26 -0
  27. odoo_driver/flask_app/flask_app_website.py +115 -0
  28. odoo_driver/interface.py +162 -0
  29. odoo_driver/static/audio/photo-taken.wav +0 -0
  30. odoo_driver/static/css/bootstrap.min.css +6 -0
  31. odoo_driver/static/css/bootstrap.min.css.map +1 -0
  32. odoo_driver/static/css/odoo_pos_driver.css +3 -0
  33. odoo_driver/static/devices/display.png +0 -0
  34. odoo_driver/static/devices/display__aures__ocd_300.png +0 -0
  35. odoo_driver/static/devices/payment.png +0 -0
  36. odoo_driver/static/devices/payment__ingenico__move_5000.png +0 -0
  37. odoo_driver/static/devices/printer.png +0 -0
  38. odoo_driver/static/devices/printer__epson__tm_t20.png +0 -0
  39. odoo_driver/static/devices/scale.png +0 -0
  40. odoo_driver/static/devices/scale__mettler_toledo__ariva_s.png +0 -0
  41. odoo_driver/static/img/favicon.png +0 -0
  42. odoo_driver/static/img/odoo.png +0 -0
  43. odoo_driver/static/js/bootstrap.min.js +7 -0
  44. odoo_driver/static/js/bootstrap.min.js.map +1 -0
  45. odoo_driver/static/js/jquery.min.js +2 -0
  46. odoo_driver/static/js/webcam-easy.js +186 -0
  47. odoo_driver/static/readme/home_page.png +0 -0
  48. odoo_driver/static/readme/security_exception.png +0 -0
  49. odoo_driver/templates/application.html.jinja +24 -0
  50. odoo_driver/templates/disconnections.html.jinja +25 -0
  51. odoo_driver/templates/errored_tasks.html.jinja +27 -0
  52. odoo_driver/templates/home.html.jinja +429 -0
  53. odoo_driver/templates/layout.html.jinja +52 -0
  54. odoo_driver/templates/photo_booth.html.jinja +67 -0
  55. odoo_driver/templates/system.html.jinja +24 -0
  56. odoo_driver/templates/usb_devices.html.jinja +40 -0
  57. odoo_driver/translations/babel.cfg +2 -0
  58. odoo_driver/translations/fr/LC_MESSAGES/messages.mo +0 -0
  59. odoo_driver/translations/fr/LC_MESSAGES/messages.po +299 -0
  60. odoo_driver/translations/odoo-pos-driver.pot +294 -0
  61. odoo_driver/usb_device/__init__.py +0 -0
  62. odoo_driver/usb_device/usb_device_list.py +63 -0
  63. odoo_driver/usb_device/usb_device_tools.py +41 -0
  64. odoo_driver-0.9.6.dist-info/METADATA +306 -0
  65. odoo_driver-0.9.6.dist-info/RECORD +67 -0
  66. odoo_driver-0.9.6.dist-info/WHEEL +4 -0
  67. odoo_driver-0.9.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+ from single_source import get_version
4
+
5
+ __version__ = get_version(__name__, Path(__file__).parent.parent)
odoo_driver/app.py ADDED
@@ -0,0 +1,29 @@
1
+ from flask import Flask, request
2
+ from flask_babel import Babel
3
+ from flask_cors import CORS
4
+
5
+ # Initialize Flask Application
6
+ app = Flask(__name__)
7
+
8
+ # Include extra route files
9
+ from . import flask_app # noqa: F401,E402
10
+
11
+ # Handle CORS call
12
+ CORS(app, resources={r"/*": {"origins": "*"}})
13
+
14
+ # Handle translation via babel
15
+ app.config["BABEL_DEFAULT_LOCALE"] = "en"
16
+ babel = Babel(app)
17
+
18
+
19
+ def get_locale():
20
+ # Use the browser's language preferences to select an available translation
21
+ available_translations = [
22
+ str(translation) for translation in babel.list_translations()
23
+ ]
24
+ for lang in request.accept_languages.values():
25
+ if lang[:2] in available_translations:
26
+ return lang[:2]
27
+
28
+
29
+ babel.init_app(app, locale_selector=get_locale)
odoo_driver/cli.py ADDED
@@ -0,0 +1,121 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+ import importlib_resources
8
+ import simplejson
9
+ from click_loglevel import LogLevel
10
+ from gevent import pywsgi
11
+ from geventwebsocket.handler import WebSocketHandler
12
+ from loguru import logger
13
+
14
+ import odoo_driver
15
+
16
+ from .app import app
17
+ from .interface import interface
18
+
19
+ # Define loguru specific level for logging
20
+ # to make click_loglevel working
21
+ logging.addLevelName(5, "TRACE")
22
+ logging.addLevelName(25, "SUCCESS")
23
+
24
+
25
+ @click.command()
26
+ @click.option(
27
+ "-a",
28
+ "--address",
29
+ type=click.STRING,
30
+ default="0.0.0.0",
31
+ show_default=True,
32
+ help="Address on which the web service will be exposed",
33
+ )
34
+ @click.option(
35
+ "-p",
36
+ "--port",
37
+ type=click.INT,
38
+ default=8069,
39
+ show_default=True,
40
+ help="Port on which the web service will be exposed",
41
+ )
42
+ @click.option(
43
+ "--secure/--unsecure",
44
+ default=True,
45
+ show_default=True,
46
+ help="Option 'secure' exposes web service on https."
47
+ " Option 'unsecure' exposes web service on http.",
48
+ )
49
+ @click.option(
50
+ "-r",
51
+ "--refresh-devices-delay",
52
+ type=click.INT,
53
+ default=1,
54
+ show_default=True,
55
+ help="Interval in seconds between two device refreshes",
56
+ )
57
+ @click.option(
58
+ "--options",
59
+ type=click.STRING,
60
+ default="{}",
61
+ show_default=True,
62
+ help="""Extra JSON options to pass to devices. Exemple:"
63
+ " --options '{"scale": {"polynomial": "123456"}}'
64
+ """,
65
+ )
66
+ @click.option(
67
+ "-l",
68
+ "--log-level",
69
+ type=LogLevel(extra=["TRACE", "SUCCESS"]),
70
+ default="INFO",
71
+ show_default=True,
72
+ )
73
+ @click.option(
74
+ "-f",
75
+ "--log-folder",
76
+ type=click.Path(
77
+ exists=True, file_okay=False, writable=True, resolve_path=True, path_type=Path
78
+ ),
79
+ show_default=True,
80
+ )
81
+ @click.version_option(version=odoo_driver.__version__)
82
+ def main(log_level, address, port, refresh_devices_delay, secure, log_folder, options):
83
+ logger.success("Lauching odoo-driver ...")
84
+
85
+ logger.info(f"Application Path: {os.path.abspath(__file__)}")
86
+
87
+ # Log Configuration
88
+ # remove first default sink that is defined to 'INFO'
89
+ logger.remove()
90
+
91
+ # Add stdout logger
92
+ logger.add(sys.stderr, level=log_level)
93
+
94
+ # Add file logger
95
+ if log_folder:
96
+ log_path = log_folder / "odoo-driver_{time}.log"
97
+ logger.info(f"Log Path: {log_path}")
98
+ logger.add(log_path, level=log_level, rotation="1 week", enqueue=True)
99
+
100
+ # Initialize Interface
101
+ interface.initialize(
102
+ refresh_devices_delay=refresh_devices_delay,
103
+ options=simplejson.loads(options),
104
+ log_folder=log_folder,
105
+ )
106
+
107
+ kwargs = {"handler_class": WebSocketHandler}
108
+
109
+ # Handle HTTPS, if required
110
+ if secure:
111
+ cert_folder = importlib_resources.files("odoo_driver") / "default_cert"
112
+ server_key_path = cert_folder / "server.key"
113
+ server_cert_path = cert_folder / "server.crt"
114
+ kwargs.update({"keyfile": server_key_path, "certfile": server_cert_path})
115
+ http_server = pywsgi.WSGIServer((address, port), app, **kwargs)
116
+ prefix = secure and "https" or "http"
117
+ logger.success(
118
+ f"Serving odoo-driver (version: {odoo_driver.__version__})"
119
+ f" on {prefix}://{address}:{port} ..."
120
+ )
121
+ http_server.serve_forever()
@@ -0,0 +1,22 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDmTCCAoGgAwIBAgIUSJxXt4mpzmWmDHxjeN9/0l4TBJAwDQYJKoZIhvcNAQEL
3
+ BQAwXDELMAkGA1UEBhMCRlIxHTAbBgNVBAgMFEF1dmVyZ25lIFJob25lLUFscGVz
4
+ MQ0wCwYDVQQHDARMeW9uMQ0wCwYDVQQKDARHUkFQMRAwDgYDVQQLDAdJVCBUZWFt
5
+ MB4XDTI0MDgxMjE0Mzc0OVoXDTI1MDgxMjE0Mzc0OVowXDELMAkGA1UEBhMCRlIx
6
+ HTAbBgNVBAgMFEF1dmVyZ25lIFJob25lLUFscGVzMQ0wCwYDVQQHDARMeW9uMQ0w
7
+ CwYDVQQKDARHUkFQMRAwDgYDVQQLDAdJVCBUZWFtMIIBIjANBgkqhkiG9w0BAQEF
8
+ AAOCAQ8AMIIBCgKCAQEA1vrN9ArLWLgsySqqGuGLiU7sWG3MWpC1YHtiCf9LEOlq
9
+ gsmWQpkeUGb0qni6VsoT9M3sSNFS7TX0kmjEu5pE84IHmuWIvQoferl/PQWIqr7p
10
+ A3YTH9RxHQuII59LxfKzehfuZ+85CPYBOlSuLdovkd1+nm9ZcbyLotSeZ/G1JuTC
11
+ Gjg6JhbvHHh6oC8o1rqY2V+T5HgXTBqjHAogD14CMJVkQ7HbeNEQ3JfhyWgfBT6U
12
+ QYJkXcfZ4vI7TCZ8PiHMheoA4O3kIPzxogBwAxnNSs/T6LjkvXfNHVMLsfFrVY+h
13
+ wBbQz2QNbkSKLVrc+JRFBRqZWkvILqBcDsqKlvD+bwIDAQABo1MwUTAdBgNVHQ4E
14
+ FgQUtjczAx17jhbwMAPDpbGa1dQ8Im8wHwYDVR0jBBgwFoAUtjczAx17jhbwMAPD
15
+ pbGa1dQ8Im8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQz3v
16
+ 7Y1gOkYTcogihzczC5yXUCDOUDnQluUweTnFiNy4bSlD2DuOMvC8eC2eCZesRUwp
17
+ RPcwFQ9RwhNtVI0j+K1Ta5+R43J1ynjFWNed1UFo3oYv0WqeHon+W82mBQZ+x2Hn
18
+ fD98vRBxh0Kch9L00T2mpL2059uSM/CF2C/gXrFGLin9WXYdwjlvtuX2uTr20XzO
19
+ Xl9Tgumvqxvq3SqA68TBYtvpVRXAIgAkCOJLfH7GBKbqUg0dasrGB6MrOePUCyZn
20
+ xDTosZI9fyf0hNB6SdsbSYCd1c+zfh6/WwjMiUqQpNGecBB2i14nHFgQKg6qy8Fk
21
+ mnOKX0WobzE+IsJmbw==
22
+ -----END CERTIFICATE-----
@@ -0,0 +1,28 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDW+s30CstYuCzJ
3
+ Kqoa4YuJTuxYbcxakLVge2IJ/0sQ6WqCyZZCmR5QZvSqeLpWyhP0zexI0VLtNfSS
4
+ aMS7mkTzggea5Yi9Ch96uX89BYiqvukDdhMf1HEdC4gjn0vF8rN6F+5n7zkI9gE6
5
+ VK4t2i+R3X6eb1lxvIui1J5n8bUm5MIaODomFu8ceHqgLyjWupjZX5PkeBdMGqMc
6
+ CiAPXgIwlWRDsdt40RDcl+HJaB8FPpRBgmRdx9ni8jtMJnw+IcyF6gDg7eQg/PGi
7
+ AHADGc1Kz9PouOS9d80dUwux8WtVj6HAFtDPZA1uRIotWtz4lEUFGplaS8guoFwO
8
+ yoqW8P5vAgMBAAECggEAbUQgte9el4NU0BJtTyhgIbQJkJCOE2IFrjpIBEjLwJbC
9
+ 2iLXypXEwJzzskmWfFJNflnDxOcPvR11Z4z0oV2xugrXDrWnoF2qzlfGAh/z7MUH
10
+ 7ghvg/gYXejkkyZ9/uhyrJXJSO5gENvol9s+hHOnRf5MQv7ZeWOZnXNv5+kEZ/Vi
11
+ lAszy/oxj6kJOcjbeE3E2vNS1Yn9RC1GrGLl/UYTPli6lFZvKylbFQ3i4HePXej7
12
+ smreMu1BVHn6sr871dFqAyG9tW0hrYIUha+FYdgqU/lDhZLIzuxlKhwdxuuoBVDJ
13
+ dmPBhzeENoHXjHJpNQ3PMmlaajfUDQWWC4Nr17aWEQKBgQD35yBKpitbvX81LZfq
14
+ uU+djjDTiekdFfDjLO7wvhwihH1wJAaPFSmxzyinYie8Yhb0WHLhwfvmek06jlyM
15
+ zcQxt09cIsy+hi1vxWdMt/JI5suJle98tN3rPvp9ZmstY0Ua/q2Q8/PL1y5QMJR/
16
+ tH5b9xMmgPOrXX/5L47/thmkqwKBgQDeAGMVnC/m+ULEdxvxsqx/jbS1jo0sPfuN
17
+ 1+JfIm//VTAAcV9PJMx5dM+OpdKQACE7wlZZaqE7xr/odyFEei2OmTU8dV+LL2bN
18
+ KjRvkXLFZH8TaaAWO4+w2OkC1/DM2zahcgrjlDutXqkMLilS7PDhWjPNYtVxlIHM
19
+ QscZO8llTQKBgGqi6joSNq502iZm4WVfp0av5oLoXPVc/czLBFFchb+3XDoD1Spx
20
+ QQkygltWFp+rVhJvhaNlDVV8IIJiwJ702ENGhgWtFoOSNjNRJ/lRWxBO7xjU/TqY
21
+ lqld4uRUTSAuBojxKnoi5ik1+78U1pwEaSJoKr2yVJ7PToBDG/EVRPyXAoGAAVu9
22
+ P3eaL26dAvi7mgn+1jT2wxmhMYEble2ZO/3I86YAyi/nZmOSQLf/QR8wn6LwkR4K
23
+ SzfEzsnTiMeZ04Jv3ux/D+Sj9gUPOvgkI/6e1ihcQrElwp0QrwNUwaiyl35L4kcB
24
+ XA0fJFts618o75H1k1tq77nIFo7TEH3kG/pqrA0CgYAofu2NLjwqyTwWlmv46eyb
25
+ nLV1OXBIxGTRFeP1fqJN4b4iy/PsTt4cclRoO+oAFhVJp2/DyxO7hGToR2foA5w4
26
+ sU/I9zFuaddmG9y3hey8i1gBRdeGRT0F/fEyEv6hZMXr7w4OVTVMsi3VW0m7NalK
27
+ wqDKXF//CF6whZKdmOFmcQ==
28
+ -----END PRIVATE KEY-----
@@ -0,0 +1,4 @@
1
+ from .device_display import DeviceDisplay # noqa: F401
2
+ from .device_payment import DevicePayment # noqa: F401
3
+ from .device_printer import DevicePrinter # noqa: F401
4
+ from .device_scale import DeviceScale # noqa: F401
@@ -0,0 +1,147 @@
1
+ from flask_babel import gettext as _
2
+ from loguru import logger
3
+
4
+ from .models.device_abstract_with_queue import DeviceAbstractWithQueue
5
+ from .tools.serial_code import FF, US
6
+
7
+
8
+ class DeviceDisplay(DeviceAbstractWithQueue):
9
+ use_serial = True
10
+ device_type = "display"
11
+
12
+ @logger.catch
13
+ def _process_task_serial(self, serial, task):
14
+ lines_ascii = self._convert_task_to_ascii_lines(task)
15
+
16
+ self._clear_screen(serial)
17
+
18
+ for row, text in enumerate(lines_ascii, 1):
19
+ self._position_cursor(serial, 1, row)
20
+ self._send_message(serial, [text])
21
+
22
+ def _convert_task_to_ascii_lines(self, task):
23
+ lines_ascii = []
24
+
25
+ action = task.data["action"]
26
+
27
+ if action == "display_lines":
28
+ for line in task.data["text_lines"]:
29
+ lines_ascii.append(line)
30
+
31
+ elif action in [
32
+ "add_line",
33
+ "update_quantity",
34
+ "update_unit_price",
35
+ "update_discount",
36
+ ]:
37
+ # First Line
38
+ left_part = task.data.get("product_name")
39
+ right_part = self._prepare_percent(task.data.get("discount"))
40
+ line_1 = self._prepare_line(left_part, right_part)
41
+
42
+ # Second Line Line
43
+ left_part = (
44
+ self._prepare_qty(task.data.get("quantity"))
45
+ + "×"
46
+ + self._prepare_price(task.data.get("unit_price"))
47
+ )
48
+ right_part = self._prepare_price(task.data.get("total"))
49
+ line_2 = self._prepare_line(left_part, right_part)
50
+
51
+ lines_ascii = [line_1, line_2]
52
+
53
+ elif action == "remove_line":
54
+ lines_ascii = [
55
+ _("Deleting Line ..."),
56
+ self._prepare_line(task.data.get("product_name")),
57
+ ]
58
+
59
+ elif action == "payment_balance":
60
+ total = task.data.get("total")
61
+ total_paid = task.data.get("total_paid")
62
+ total_due = task.data.get("total_due")
63
+ total_to_pay = total - total_paid
64
+ line_1 = self._prepare_line(_("Total:"), self._prepare_price(total))
65
+ line_2 = ""
66
+ if total_paid != 0.0:
67
+ if total_to_pay > 0:
68
+ line_2 = self._prepare_line(
69
+ _("To Pay:"), self._prepare_price(total_to_pay)
70
+ )
71
+ elif total_due < 0:
72
+ line_2 = self._prepare_line(
73
+ _("Change:"), self._prepare_price(-total_due)
74
+ )
75
+ lines_ascii = [line_1, line_2]
76
+
77
+ else:
78
+ raise NotImplementedError(
79
+ f"{action} is not a valid action for the Display device."
80
+ )
81
+
82
+ return lines_ascii
83
+
84
+ def _clear_screen(self, serial):
85
+ self._send_message(serial, [FF], "Clear Screen")
86
+
87
+ def _position_cursor(self, serial, col=1, row=1):
88
+ self._send_message(
89
+ serial, [US, chr(36), chr(col), chr(row)], f"Move to col {col} / row {row}"
90
+ )
91
+
92
+ def _send_message(self, serial, message_list, action_description=""):
93
+ message_list = [x for x in message_list if x]
94
+ if not message_list:
95
+ return
96
+ message = b"".join([x.encode("cp858", errors="ignore") for x in message_list])
97
+ if action_description:
98
+ logger.debug(
99
+ f"Display: {action_description} with the message {message} ..."
100
+ )
101
+
102
+ serial.write(message)
103
+
104
+ def _display_float(self, float_repr, reduce_txt=True, suffix=""):
105
+ if reduce_txt:
106
+ while float_repr:
107
+ if float_repr[-1:] == "0":
108
+ float_repr = float_repr[:-1]
109
+ else:
110
+ break
111
+ if float_repr[-1:] == ".":
112
+ float_repr = float_repr[:-1]
113
+ return float_repr.replace(".", ",") + suffix
114
+
115
+ def _prepare_percent(self, percent):
116
+ try:
117
+ percent = float(percent)
118
+ except ValueError:
119
+ return ""
120
+ except TypeError:
121
+ return ""
122
+ if not percent:
123
+ return ""
124
+ return self._display_float("-{:.1f}".format(percent), suffix="%")
125
+
126
+ def _prepare_qty(self, quantity):
127
+ try:
128
+ quantity = float(quantity)
129
+ except ValueError:
130
+ quantity = 0.0
131
+ return self._display_float("{:.3f}".format(quantity))
132
+
133
+ def _prepare_price(self, price):
134
+ try:
135
+ price = float(price)
136
+ except ValueError:
137
+ price = 0.0
138
+ return self._display_float("{:.2f}".format(price), reduce_txt=False, suffix="€")
139
+
140
+ def _prepare_line(self, left_part, right_part=""):
141
+ result = left_part[:20].ljust(20, " ")
142
+ if right_part:
143
+ right_part = right_part[-20:]
144
+ result = result[0 : (20 - 1 - len(right_part))]
145
+ result += " " + right_part
146
+
147
+ return result
@@ -0,0 +1,20 @@
1
+ from telium import Telium, TeliumAsk
2
+
3
+ from .models.device_abstract import DeviceAbstract
4
+
5
+
6
+ class DevicePayment(DeviceAbstract):
7
+ device_type = "payment"
8
+
9
+ def push_amount(self, amount, currency_iso):
10
+ if not self.terminal_file:
11
+ return False
12
+ telium = Telium(self.terminal_file)
13
+ payment = TeliumAsk.new_payment(
14
+ amount,
15
+ payment_mode="debit",
16
+ target_currency=currency_iso,
17
+ )
18
+ result = telium.ask(payment)
19
+ telium.close()
20
+ return result
@@ -0,0 +1,25 @@
1
+ import base64
2
+ from io import BytesIO
3
+
4
+ from escpos.printer import Usb
5
+ from PIL import Image
6
+
7
+ from .models.device_abstract_with_queue import DeviceAbstractWithQueue
8
+
9
+
10
+ class DevicePrinter(DeviceAbstractWithQueue):
11
+ device_type = "printer"
12
+
13
+ def _process_task(self, task):
14
+ printer = Usb(self._usb_device.idVendor, self._usb_device.idProduct, 0)
15
+ (action, value) = task.data
16
+
17
+ if action == "print":
18
+ im = Image.open(BytesIO(base64.b64decode(value)))
19
+ printer.image(im)
20
+ printer.cut()
21
+
22
+ elif action == "open_cashbox":
23
+ printer.cashdraw(2)
24
+
25
+ printer.close()
@@ -0,0 +1,34 @@
1
+ from .models.device_abstract import DeviceAbstract
2
+ from .scale_dialog06 import state_machine
3
+
4
+
5
+ class DeviceScale(DeviceAbstract):
6
+ device_type = "scale"
7
+
8
+ def __init__(self, interface, options):
9
+ DeviceAbstract.__init__(self, interface, options)
10
+ self.last_weight = 0.0
11
+
12
+ def read_weight(self, unit_price, tare):
13
+ with self._get_serial() as serial:
14
+ sm = state_machine.Dialog06Machine(
15
+ serial,
16
+ self.get_option("polynomial", int, required=True),
17
+ unit_price,
18
+ tare,
19
+ )
20
+
21
+ while not sm.current_state.final:
22
+ sm.continue_communication()
23
+
24
+ # In a dialog06 context, if the scale return 21 (no change since the last weight)
25
+ # The scale doesn't provide the weight.
26
+ # in that 'valid use case', we return the last weight
27
+ if sm.last_scale_answer.error_number != "21":
28
+ self.last_weight = sm.last_scale_answer.weight
29
+
30
+ return {
31
+ "weight": self.last_weight,
32
+ "error_number": sm.last_scale_answer.error_number,
33
+ "error_description": sm.last_scale_answer.error_description,
34
+ }
File without changes
@@ -0,0 +1,115 @@
1
+ import serial.tools.list_ports
2
+ from flask_babel import gettext as _
3
+ from loguru import logger
4
+ from serial import Serial
5
+
6
+ from ...usb_device import usb_device_tools
7
+
8
+
9
+ class DeviceAbstract:
10
+ has_queue = False
11
+ use_serial = False
12
+ device_type = False
13
+
14
+ def __init__(self, interface, options):
15
+ self._interface = interface
16
+ self._usb_device = False
17
+ self.usb_device_name = False
18
+ self.terminal_file = False
19
+ self._usb_image_name = False
20
+ self._options = options
21
+
22
+ def get_option(self, option_name, option_type, required=False):
23
+ option = self._options.get(option_name, False)
24
+ if required and not option:
25
+ raise Exception(
26
+ f"{option_name} is required for the device {self.device_type}."
27
+ )
28
+ return option_type(option)
29
+
30
+ @property
31
+ def name(self):
32
+ if self.device_type == "display":
33
+ return _("Display")
34
+ elif self.device_type == "printer":
35
+ return _("Printer")
36
+ elif self.device_type == "payment":
37
+ return _("Payment Terminal")
38
+ elif self.device_type == "scale":
39
+ return _("Scale")
40
+ return "N/C"
41
+
42
+ @property
43
+ def usb_vendor_product_code(self):
44
+ return usb_device_tools.get_device_id_vendor_id_product(self._usb_device)
45
+
46
+ @property
47
+ def usb_serial_number(self):
48
+ return usb_device_tools.get_device_serial_number(self._usb_device)
49
+
50
+ @property
51
+ def usb_manufacturer(self):
52
+ return usb_device_tools.get_device_manufacturer(self._usb_device)
53
+
54
+ @property
55
+ def usb_product_name(self):
56
+ return usb_device_tools.get_device_product(self._usb_device)
57
+
58
+ @property
59
+ def is_connected(self):
60
+ return bool(self._usb_device)
61
+
62
+ @property
63
+ def disconnections(self):
64
+ return self._interface.disconnections.get(self.usb_vendor_product_code, [])
65
+
66
+ @property
67
+ def disconnections_qty(self):
68
+ return len(self.disconnections)
69
+
70
+ @property
71
+ def usb_image_name(self):
72
+ return self._usb_image_name or f"{self.device_type}.png"
73
+
74
+ # ###########################
75
+ # Device Section
76
+ # ###########################
77
+ def set_usb_device(self, usb_device, extra_info):
78
+ self._usb_device = usb_device
79
+ self.usb_device_name = extra_info.get("name", False)
80
+ self._usb_image_name = extra_info.get("image")
81
+ connected_comports = [x for x in serial.tools.list_ports.comports()]
82
+ for port in connected_comports:
83
+ if (port.vid, port.pid) == (
84
+ usb_device.idVendor,
85
+ usb_device.idProduct,
86
+ ):
87
+ self.terminal_file = port.device
88
+ self._logger("DEBUG", f"Terminal File found: {self.terminal_file}.")
89
+ break
90
+ if not self.terminal_file:
91
+ self._logger("DEBUG", "Terminal File not found.")
92
+
93
+ def remove_usb_device(self):
94
+ self._usb_device = False
95
+ self.usb_device_name = False
96
+ self._usb_image_name = False
97
+ self.terminal_file = False
98
+
99
+ def _get_serial(self):
100
+ if not self.terminal_file:
101
+ raise Exception(
102
+ "Unable to open a Serial connexion,"
103
+ " because terminal_file is not defined."
104
+ )
105
+ return Serial(self.terminal_file, 9600, timeout=0.05)
106
+
107
+ # ###########################
108
+ # Loggging Section
109
+ # ###########################
110
+ def _logger(self, level, message):
111
+ if self._usb_device:
112
+ extra_info = f" ({self.usb_device_name} - {self.usb_vendor_product_code})"
113
+ else:
114
+ extra_info = ""
115
+ logger.log(level, f"Device '{self.device_type}'{extra_info}: {message}")
@@ -0,0 +1,84 @@
1
+ import time
2
+ from queue import Queue
3
+ from threading import Thread
4
+
5
+ from .device_abstract import DeviceAbstract
6
+ from .device_task import DeviceTask
7
+
8
+
9
+ class DeviceAbstractWithQueue(DeviceAbstract, Thread):
10
+ has_queue = True
11
+
12
+ def __init__(self, interface, options, delay=0, max_queue_size=0):
13
+ """Initialize a new Device.
14
+ delay: time between processing two messages, in seconds.
15
+ """
16
+ DeviceAbstract.__init__(self, interface, options)
17
+ Thread.__init__(self)
18
+ self.errored_tasks = []
19
+ self._queue = Queue(max_queue_size)
20
+ self.delay = delay
21
+
22
+ @property
23
+ def queue_size(self):
24
+ return self._queue.qsize()
25
+
26
+ @property
27
+ def max_queue_size(self):
28
+ return self._queue.maxsize
29
+
30
+ @property
31
+ def errored_tasks_qty(self):
32
+ return len(self.errored_tasks)
33
+
34
+ # ###########################
35
+ # Thread / Task Section
36
+ # ###########################
37
+ def run(self):
38
+ while True:
39
+ if self._usb_device:
40
+ current_size = self._queue.qsize()
41
+ if current_size:
42
+ self._logger("INFO", f"Process one of the {current_size} task(s).")
43
+ else:
44
+ self._logger("INFO", "Waiting for message ...")
45
+ task = self._queue.get(True)
46
+ self._process_task_abstract(task)
47
+ time.sleep(self.delay)
48
+ else:
49
+ time.sleep(1)
50
+
51
+ def _process_task_abstract(self, task):
52
+ self._logger("DEBUG", f"Processing task {task.uuid} ...")
53
+ try:
54
+ if self.use_serial:
55
+ with self._get_serial() as serial:
56
+ self._process_task_serial(serial, task)
57
+ else:
58
+ self._process_task(task)
59
+ if task in self.errored_tasks:
60
+ self.errored_tasks.remove(task)
61
+ self._logger("SUCCESS", f"Task processed. UUID: {task.uuid}.")
62
+
63
+ except Exception as e:
64
+ self._logger("ERROR", f"Task {task.uuid}. Error{e}")
65
+ task.error = e
66
+ if task not in self.errored_tasks:
67
+ self._logger("INFO", f"Reenqueing task. UUID: {task.uuid} ...")
68
+ self.errored_tasks.append(task)
69
+ self._queue.put(task)
70
+ if self._usb_device:
71
+ self._interface._hook_usb_device_removed(self._usb_device, e)
72
+
73
+ def add_task(self, data):
74
+ if self.max_queue_size:
75
+ while self._queue.qsize() >= self._queue.maxsize:
76
+ self._logger("WARNING", "Removing obsolete task in queue.")
77
+ self._queue.get_nowait()
78
+ task = DeviceTask(data)
79
+ self._queue.put(task)
80
+ self._logger(
81
+ "INFO",
82
+ f"Task Enqueued ({self._queue.qsize()}/{self._queue.maxsize})."
83
+ f" UUID: {task.uuid}",
84
+ )
@@ -0,0 +1,10 @@
1
+ import uuid
2
+ from datetime import datetime
3
+
4
+
5
+ class DeviceTask:
6
+ def __init__(self, data):
7
+ self.create_date = datetime.now()
8
+ self.uuid = uuid.uuid4()
9
+ self.data = data
10
+ self.error = False
File without changes