benchlab-pytools 3.0.0__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 (184) hide show
  1. benchlab/__init__.py +14 -0
  2. benchlab/__main__.py +21 -0
  3. benchlab/config/README.md +325 -0
  4. benchlab/config/__init__.py +9 -0
  5. benchlab/config/config_client.py +631 -0
  6. benchlab/config/config_manager.py +495 -0
  7. benchlab/config/config_tool.py +358 -0
  8. benchlab/config/diff.py +163 -0
  9. benchlab/config/examples/complete_config.json +56 -0
  10. benchlab/config/examples/rgb_lighting.json +24 -0
  11. benchlab/config/examples/simple_fan.json +32 -0
  12. benchlab/config/schema.py +138 -0
  13. benchlab/core/README.md +154 -0
  14. benchlab/core/__init__.py +78 -0
  15. benchlab/core/config.py +145 -0
  16. benchlab/core/datasource.py +1209 -0
  17. benchlab/core/datasource_manager.py +418 -0
  18. benchlab/core/device_registry.py +206 -0
  19. benchlab/core/discovery.py +92 -0
  20. benchlab/core/infrastructure.py +309 -0
  21. benchlab/core/process_manager.py +356 -0
  22. benchlab/core/retry.py +85 -0
  23. benchlab/core/shared_serial.py +235 -0
  24. benchlab/core/statistics.py +224 -0
  25. benchlab/csv_log/README.md +206 -0
  26. benchlab/csv_log/__init__.py +5 -0
  27. benchlab/csv_log/csv_logger_enhanced.py +416 -0
  28. benchlab/csv_log/message_batcher.py +361 -0
  29. benchlab/csv_log/requirements.txt +3 -0
  30. benchlab/csv_log/smart_retry.py +466 -0
  31. benchlab/graph/README.md +117 -0
  32. benchlab/graph/__init__.py +5 -0
  33. benchlab/graph/app.py +353 -0
  34. benchlab/graph/requirements.txt +2 -0
  35. benchlab/graph/runner.py +42 -0
  36. benchlab/graph/ui.py +288 -0
  37. benchlab/hwinfo/README.md +135 -0
  38. benchlab/hwinfo/__init__.py +5 -0
  39. benchlab/hwinfo/hwinfo_export.py +386 -0
  40. benchlab/hwinfo/requirements.txt +1 -0
  41. benchlab/launcher.py +387 -0
  42. benchlab/link/.env.EXAMPLE +10 -0
  43. benchlab/link/README.md +172 -0
  44. benchlab/link/__init__.py +1 -0
  45. benchlab/link/link_main.py +479 -0
  46. benchlab/main.py +462 -0
  47. benchlab/menu.py +729 -0
  48. benchlab/menu_classic.py +410 -0
  49. benchlab/mqtt/README.md +201 -0
  50. benchlab/mqtt/__init__.py +5 -0
  51. benchlab/mqtt/mqtt.config_template +2 -0
  52. benchlab/mqtt/mqtt_publisher.py +540 -0
  53. benchlab/mqtt/requirements.txt +3 -0
  54. benchlab/prefs.py +67 -0
  55. benchlab/restapi/.env.example +17 -0
  56. benchlab/restapi/__init__.py +5 -0
  57. benchlab/restapi/favicon.ico +0 -0
  58. benchlab/restapi/readme.md +202 -0
  59. benchlab/restapi/requirements.txt +5 -0
  60. benchlab/restapi/telemetry_api.py +808 -0
  61. benchlab/sources.py +515 -0
  62. benchlab/tools.py +147 -0
  63. benchlab/tui/README.md +266 -0
  64. benchlab/tui/__init__.py +5 -0
  65. benchlab/tui/config.py +477 -0
  66. benchlab/tui/requirements.txt +2 -0
  67. benchlab/tui/tui_core.py +1298 -0
  68. benchlab/tui/tui_main.py +418 -0
  69. benchlab/vu/README.md +64 -0
  70. benchlab/vu/VU-Server/NOTICE.md +13 -0
  71. benchlab/vu/VU-Server/README.md +80 -0
  72. benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
  73. benchlab/vu/VU-Server/__init__.py +0 -0
  74. benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  75. benchlab/vu/VU-Server/community_applications.md +13 -0
  76. benchlab/vu/VU-Server/config.yaml +9 -0
  77. benchlab/vu/VU-Server/database.py +256 -0
  78. benchlab/vu/VU-Server/dial_driver.py +534 -0
  79. benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
  80. benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
  81. benchlab/vu/VU-Server/dials/__init__.py +0 -0
  82. benchlab/vu/VU-Server/dials/base_logger.py +100 -0
  83. benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  84. benchlab/vu/VU-Server/installer/install.nsi +148 -0
  85. benchlab/vu/VU-Server/make_version.py +40 -0
  86. benchlab/vu/VU-Server/pylintrc +628 -0
  87. benchlab/vu/VU-Server/requirements.txt +9 -0
  88. benchlab/vu/VU-Server/serial_driver.py +244 -0
  89. benchlab/vu/VU-Server/server.py +670 -0
  90. benchlab/vu/VU-Server/server_config.py +255 -0
  91. benchlab/vu/VU-Server/server_dial_handler.py +334 -0
  92. benchlab/vu/VU-Server/vu_notifications.py +24 -0
  93. benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  94. benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  95. benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  96. benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
  97. benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
  98. benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
  99. benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
  100. benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
  101. benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
  102. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
  103. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
  104. benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
  105. benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
  106. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
  107. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
  108. benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
  109. benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
  110. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
  111. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
  112. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
  113. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
  114. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
  115. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
  116. benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
  117. benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
  118. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
  119. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
  120. benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
  121. benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  122. benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  123. benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
  124. benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
  125. benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
  126. benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
  127. benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
  128. benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
  129. benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
  130. benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
  131. benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
  132. benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
  133. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
  134. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
  135. benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
  136. benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
  137. benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
  138. benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  139. benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  140. benchlab/vu/VU-Server/www/favicon.ico +0 -0
  141. benchlab/vu/VU-Server/www/favicon.png +0 -0
  142. benchlab/vu/VU-Server/www/index.html +47 -0
  143. benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
  144. benchlab/vu/VU-Server/www/views/dial.html +286 -0
  145. benchlab/vu/VU-Server/www/views/footer.html +27 -0
  146. benchlab/vu/VU-Server/www/views/header.html +86 -0
  147. benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
  148. benchlab/vu/VU-Server/www/views/start.html +124 -0
  149. benchlab/vu/__init__.py +5 -0
  150. benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  151. benchlab/vu/assets/bl_dial_144x200.png +0 -0
  152. benchlab/vu/assets/bl_logo_144x200.png +0 -0
  153. benchlab/vu/devices.py +166 -0
  154. benchlab/vu/requirements.txt +24 -0
  155. benchlab/vu/sensors.py +18 -0
  156. benchlab/vu/vu_dial.config_template +90 -0
  157. benchlab/vu/vu_logo_gen.py +97 -0
  158. benchlab/vu/vu_server.config_template +5 -0
  159. benchlab/vu/vu_server_config.py +40 -0
  160. benchlab/vu/vu_server_manager.py +192 -0
  161. benchlab/vu/vu_tui.py +919 -0
  162. benchlab/vu/vu_updater.py +540 -0
  163. benchlab/wigidash/README.md +182 -0
  164. benchlab/wigidash/__init__.py +5 -0
  165. benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  166. benchlab/wigidash/assets/Inter.ttf +0 -0
  167. benchlab/wigidash/assets/benchlab.png +0 -0
  168. benchlab/wigidash/benchlab_fleet.py +150 -0
  169. benchlab/wigidash/benchlab_graph.py +517 -0
  170. benchlab/wigidash/benchlab_overview.py +426 -0
  171. benchlab/wigidash/benchlab_telemetry.py +105 -0
  172. benchlab/wigidash/benchlab_ui.py +190 -0
  173. benchlab/wigidash/benchlab_utils.py +182 -0
  174. benchlab/wigidash/requirements.txt +9 -0
  175. benchlab/wigidash/wigidash_device.py +541 -0
  176. benchlab/wigidash/wigidash_manager.py +342 -0
  177. benchlab/wigidash/wigidash_session.py +355 -0
  178. benchlab/wigidash/wigidash_usb.py +247 -0
  179. benchlab/wigidash/wigidash_widget.py +88 -0
  180. benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
  181. benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
  182. benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
  183. benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
  184. benchlab_pytools-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,670 @@
1
+ import sys
2
+ import os
3
+ import signal
4
+ import argparse
5
+ import zlib
6
+ import time
7
+ import re
8
+ from mimetypes import guess_type
9
+ from dials.base_logger import logger, set_logger_level
10
+ from tornado.web import Application, RequestHandler, Finish, StaticFileHandler
11
+ from tornado.ioloop import IOLoop, PeriodicCallback
12
+ from dial_driver import DialSerialDriver
13
+ from server_config import ServerConfig
14
+ from server_dial_handler import ServerDialHandler
15
+ from vu_notifications import show_error_msg, show_info_msg, show_warning_msg
16
+ from serial.serialutil import SerialException
17
+
18
+ BASEDIR_NAME = os.path.dirname(__file__)
19
+ BASEDIR_PATH = os.path.abspath(BASEDIR_NAME)
20
+ WEB_ROOT = os.path.join(BASEDIR_PATH, 'www')
21
+
22
+ def pid_lock(service_name, create=True):
23
+ file_name = "service.{}.pid.lock".format(service_name)
24
+ pid_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), file_name)
25
+
26
+ if create:
27
+ pid = os.getpid()
28
+ with open(pid_file, "w", encoding="utf-8") as file:
29
+ file.write(str(pid))
30
+ else:
31
+ if os.path.exists(pid_file):
32
+ os.remove(pid_file)
33
+
34
+ class BaseHandler(RequestHandler):
35
+ def initialize(self, handler, config):
36
+ self.handler = handler # pylint: disable=attribute-defined-outside-init
37
+ self.config = config # pylint: disable=attribute-defined-outside-init
38
+ self.upload_path = os.path.join(os.path.dirname(__file__), 'upload') # pylint: disable=attribute-defined-outside-init
39
+
40
+ def set_default_headers(self):
41
+ self.set_header("Access-Control-Allow-Origin", "*")
42
+ # self.set_header("Access-Control-Allow-Headers", "x-requested-with")
43
+ # self.set_header('Access-Control-Allow-Methods', ' PUT, DELETE, OPTIONS, GET')
44
+ self.set_header('Access-Control-Allow-Methods', 'POST, GET')
45
+ self.set_header('Content-Type', 'application/json')
46
+
47
+ # self.set_header("Access-Control-Allow-Origin", "Origin");
48
+ # self.set_header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
49
+ # self.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
50
+ # self.set_header("Access-Control-Allow-Credentials", "true");
51
+
52
+ # Helper function to send response
53
+ def send_response(self, status, message='', data=None, status_code=200):
54
+ resp = {'status': status, 'message': message, 'data': data}
55
+ self.set_status(status_code)
56
+ self.write(resp)
57
+ self.finish()
58
+
59
+ def api_key_has_access_to_dial(self, gaugeUID, api_key=None):
60
+ if api_key is None:
61
+ api_key = self.get_argument('key', None)
62
+
63
+ if not self.config.api_key_has_access_to_dial(api_key, gaugeUID):
64
+ return False
65
+ return True
66
+
67
+ def is_valid_api_key(self):
68
+ if not self.config.is_valid_api_key(self.get_argument('key', None)):
69
+ return False
70
+ return True
71
+
72
+ def valid_admin_key(self):
73
+ admin_key = self.get_argument('admin_key', None)
74
+ if not admin_key:
75
+ logger.error("Missing API key")
76
+ self.send_response(status='fail', message='Invalid or missing API key.', status_code=401)
77
+ return False
78
+
79
+ if not self.config.validate_admin_key(admin_key):
80
+ logger.error("Invalid API key")
81
+ self.send_response(status='fail', message='Invalid or missing API key.', status_code=401)
82
+ return False
83
+ return True
84
+
85
+ def get_file_crc(self, filepath):
86
+ if not os.path.exists(filepath):
87
+ logger.error(f"File {filepath} does not exist!")
88
+ return "00000000"
89
+
90
+ with open(filepath, 'rb') as fh:
91
+ fileCrc = 0
92
+ while True:
93
+ s = fh.read(65536)
94
+ if not s:
95
+ break
96
+ fileCrc = zlib.crc32(s, fileCrc)
97
+ return "%08X" % (fileCrc & 0xFFFFFFFF)
98
+
99
+
100
+ class Device_Status_Handler(BaseHandler):
101
+ def get(self, dial_uid):
102
+ logger.debug(f"Request:STATUS - Device:{dial_uid}")
103
+ dial = self.handler.get_dial_info(dial_uid=dial_uid)
104
+ if dial is not None:
105
+ return self.send_response(status='ok', data=dial)
106
+ return self.send_response(status='fail', message='Invalid dial_uid or device is offline.')
107
+
108
+ class Device_Set_Handler(BaseHandler):
109
+ def get(self, dial_uid):
110
+ value = self.get_argument('value', 0)
111
+ logger.debug(f"Request:SET - Device:{dial_uid} To:{value}")
112
+
113
+ # Validate API key
114
+ if not self.is_valid_api_key():
115
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
116
+
117
+ if self.handler.dial_set_percent(dial_uid=dial_uid, value=value):
118
+ return self.send_response(status='ok', message='Update queued')
119
+ return self.send_response(status='fail', message='Invalid dial_uid or device is offline.')
120
+
121
+ class Device_SetRaw_Handler(BaseHandler):
122
+ def get(self, dial_uid):
123
+ value = self.get_argument('value', 0)
124
+ logger.debug(f"Request:SET_RAW - Device:{dial_uid} To:{value}")
125
+
126
+ # Validate API key
127
+ if not self.is_valid_api_key():
128
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
129
+
130
+ if self.handler.dial_set_raw(dial_uid=dial_uid, value=value):
131
+ return self.send_response(status='ok', message='Dial RAW value updated', status_code=201)
132
+ return self.send_response(status='fail', message='Invalid dial_uid or device is offline.', status_code=503)
133
+
134
+ class Device_Backlight_Handler(BaseHandler):
135
+ def get(self, dial_uid):
136
+ red = self.get_argument('red', 0)
137
+ green = self.get_argument('green', 0)
138
+ blue = self.get_argument('blue', 0)
139
+ white = self.get_argument('white', 0)
140
+
141
+ logger.debug(f"Request:BACKLIGHT - Device:{dial_uid} To: (red:{red} green:{green} blue:{blue} white:{white})")
142
+
143
+ # Validate API key
144
+ if not self.is_valid_api_key():
145
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
146
+
147
+ if self.handler.dial_set_backlight(dial_uid=dial_uid, red=red, green=green, blue=blue, white=white):
148
+ return self.send_response(status='ok', message='Update queued', status_code=201)
149
+ return self.send_response(status='fail', message='Invalid dial_uid or device is offline.', status_code=503)
150
+
151
+ class Device_Set_Image(BaseHandler):
152
+ def post(self, dial_uid):
153
+ get_force = self.get_argument('force', False)
154
+
155
+ force_img_update = bool(get_force is True)
156
+
157
+ logger.debug(f"Request:SET_IMAGE - Device:{dial_uid}")
158
+
159
+ # Validate API key
160
+ if not self.is_valid_api_key():
161
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
162
+
163
+ # Store new image
164
+ img_file = self.handle_image_upload(dial_uid)
165
+ if not img_file:
166
+ logger.error("Handle image upload failed")
167
+ return self.send_response(status='fail', message='image upload failed', status_code=503)
168
+
169
+ current_img = os.path.join(self.upload_path, f'img_{dial_uid}')
170
+ new_img = os.path.join(self.upload_path, f'tmp_{dial_uid}')
171
+
172
+ # If this is a different image from existing one
173
+ if self.different_image_uploaded(current_img, new_img) or force_img_update:
174
+
175
+ # Remove existing image (if exists)
176
+ if os.path.exists(current_img):
177
+ os.remove(current_img)
178
+ # Move (rename) new image and set as current
179
+ os.rename(new_img, current_img)
180
+
181
+
182
+ if self.handler.dial_set_image(dial_uid=dial_uid, image_file=current_img):
183
+ return self.send_response(status='ok', status_code=201)
184
+ return self.send_response(status='fail', message='Invalid dial_uid or device is offline.', status_code=503)
185
+
186
+ logger.debug(f"Skipping dial `{dial_uid}` image update. Contents already match.")
187
+ return self.send_response(status='ok', message='Image CRC already maches existing one. Skipping update.')
188
+
189
+ def handle_image_upload(self, dial_uid):
190
+ self.make_upload_folder()
191
+ image_data = self.request.files.get('imgfile', None)
192
+ if image_data is None:
193
+ logger.error("imgfile field missing from request.")
194
+ return None
195
+
196
+ # First upload image as temporary
197
+ file_path = os.path.join(self.upload_path, f'tmp_{dial_uid}')
198
+
199
+ with open(file_path, 'wb') as img:
200
+ img.write(image_data[0]['body'])
201
+
202
+ return file_path
203
+
204
+ def different_image_uploaded(self, old, new):
205
+ if self.get_file_crc(old) != self.get_file_crc(new):
206
+ return True
207
+ return False
208
+
209
+ def make_upload_folder(self):
210
+ if not os.path.exists(self.upload_path):
211
+ os.makedirs(self.upload_path)
212
+
213
+ class Dial_Get_Image(BaseHandler):
214
+ def get(self, gaugeUID):
215
+ self.set_header("Content-Type", "image/png")
216
+
217
+ logger.debug("Request: GET_IMAGE")
218
+ dial_image = os.path.join(os.path.dirname(__file__), 'upload', f'img_{gaugeUID}')
219
+
220
+ if os.path.exists(dial_image):
221
+ filepath = dial_image
222
+ logger.debug(f"Serving image from {filepath}")
223
+ else:
224
+ filepath = os.path.join(os.path.dirname(__file__), 'upload', 'img_blank')
225
+ logger.debug(f"Serving DEFAULT image from {filepath}")
226
+
227
+ try:
228
+ with open(filepath, 'rb') as f:
229
+ data = f.read()
230
+ self.write(data)
231
+ return self.finish()
232
+ except IOError as e:
233
+ logger.error(e)
234
+ return self.send_response(status='fail', message='Internal sever error!', status_code=500)
235
+
236
+ class Dial_Get_Image_CRC(BaseHandler):
237
+ def get(self, gaugeUID):
238
+ logger.debug("Request: GET_IMAGE_CRC")
239
+
240
+ img_file = os.path.join(os.path.dirname(__file__), 'upload', f'img_{gaugeUID}')
241
+
242
+ crc = self.get_file_crc(img_file)
243
+ return self.send_response(status='ok', data=crc)
244
+
245
+ class Dial_Get_List(BaseHandler):
246
+ def get(self):
247
+ logger.debug("Request: DEVICE_LIST")
248
+
249
+ # Validate API key
250
+ if not self.is_valid_api_key():
251
+ return self.send_response(status='fail', message='Missing API key!', status_code=403)
252
+
253
+ if not self.is_valid_api_key():
254
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
255
+
256
+ dials = self.handler.get_dial_info()
257
+ # logger.debug(dials)
258
+
259
+ # Reshape dials data to respond with only relevant information
260
+ dialData = []
261
+ for uid in dials:
262
+ tmp_dial = {
263
+ 'uid' : uid,
264
+ 'dial_name': dials[uid]['dial_name'],
265
+ 'value': dials[uid]['value'],
266
+ 'backlight': dials[uid]['backlight'],
267
+ 'image_file' : dials[uid]['image_file']
268
+ }
269
+ # Remove unused keys
270
+ tmp_dial['backlight'].pop('white', None)
271
+
272
+ # If key has access to dial
273
+ if self.api_key_has_access_to_dial(gaugeUID=uid):
274
+ dialData.append(tmp_dial)
275
+
276
+ return self.send_response(status='ok', data=dialData)
277
+
278
+
279
+ class Dial_Provision(BaseHandler):
280
+ def get(self):
281
+
282
+ logger.debug("Request: PROVISION_NEW_DIALS")
283
+
284
+ # Validate master key
285
+ if not self.valid_admin_key():
286
+ return False
287
+
288
+ dials = self.handler.provision_dials()
289
+ logger.debug(dials)
290
+
291
+ return self.send_response(status='ok', data=dials)
292
+
293
+ class Dial_Set_Dial_Name(BaseHandler):
294
+ def get(self, gaugeUID):
295
+ new_name = self.get_argument('name', None)
296
+ logger.debug(f"Request:SET_NAME - Device:{gaugeUID} To: friendly name={new_name}")
297
+
298
+ # Validate API key
299
+ if not self.is_valid_api_key():
300
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
301
+
302
+ if new_name is not None:
303
+ # Dial name should be 3 or more characters
304
+ if len(new_name) < 3:
305
+ return self.send_response(status='fail', message='Dial name should be at least 3 characters long.', status_code=400)
306
+
307
+ # Limit dial name to 30 characters
308
+ if len(new_name) > 30:
309
+ return self.send_response(status='fail', message='Dial name should be 30 characters or less.', status_code=400)
310
+
311
+ # Verify valid name
312
+ if not re.search(r"^[a-z0-9\-_\ ]*$", new_name, re.IGNORECASE):
313
+ return self.send_response(status='fail', message='Invalid characters! Only `A-Z`, `0-9`, `-`, `_` and space allowed.', status_code=400)
314
+
315
+ # Finally update dial name
316
+ ret = self.config.update_dial_db_cell(dial_uid=gaugeUID, cell='dial_name', value=new_name)
317
+ if ret:
318
+ return self.send_response(status='ok', status_code=201)
319
+ return self.send_response(status='fail', message='Can not update dial name! Dial does not exist?', status_code=406)
320
+ return self.send_response(status='fail', message='Device not present!', status_code=406)
321
+
322
+ class Dial_Reload_Device_Info(BaseHandler):
323
+ def get(self, gaugeUID):
324
+
325
+ logger.debug(f"Request:GET_INFO - Device:{gaugeUID}")
326
+
327
+ # Validate API key
328
+ if not self.is_valid_api_key():
329
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
330
+
331
+ dial_info = self.handler.dial_reload_info_from_hardware(gaugeUID)
332
+ return self.send_response(status='ok', data=dial_info)
333
+
334
+ class Dial_Set_Calibration(BaseHandler):
335
+ def get(self, gaugeUID):
336
+ dac_calibration = self.get_argument('value', None)
337
+ logger.debug(f"Request:SET_CALIBRATION - Device:{gaugeUID} To: value={dac_calibration}")
338
+
339
+ # Validate API key
340
+ if not self.is_valid_api_key():
341
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
342
+
343
+ if dac_calibration is not None:
344
+ self.handler.dial_set_calibration(dial_uid=gaugeUID, value=dac_calibration, fullScale=False)
345
+ return self.send_response(status='ok', message="Calibration value updated", status_code=201)
346
+ return self.send_response(status='fail', message="Device not present", status_code=406)
347
+
348
+ class Dial_Set_Easing_Dial(BaseHandler):
349
+ def get(self, gaugeUID):
350
+ step = self.get_argument('step', None)
351
+ period = self.get_argument('period', None)
352
+ logger.debug(f"Request:SET_EASING_DIAL - Device:{gaugeUID} Step:{step} Period:{period}")
353
+
354
+ # Validate API key
355
+ if not self.is_valid_api_key():
356
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
357
+
358
+ if step is None and period is None:
359
+ return self.send_response(status='fail', message="Please provide at least one of required parameters (`step` or `period`)", status_code=400)
360
+
361
+ if self.handler.dial_set_easing_dial(dial_uid=gaugeUID, step=step, period=period):
362
+ values_dict = { 'easing_dial_step': int(step), 'easing_dial_period': int(period) }
363
+ self.config.update_dial_db_cell_with_dict(gaugeUID, values_dict)
364
+ self.handler.dial_reload_info_from_database(gaugeUID)
365
+ return self.send_response(status='ok')
366
+ return self.send_response(status='fail', message="Device not present", status_code=406)
367
+
368
+ class Dial_Set_Easing_Backlight(BaseHandler):
369
+ def get(self, gaugeUID):
370
+ step = self.get_argument('step', None)
371
+ period = self.get_argument('period', None)
372
+ logger.debug(f"Request:SET_EASING_BACKLIGHT - Device:{gaugeUID} Step:{step} Period:{period}")
373
+
374
+ # Validate API key
375
+ if not self.is_valid_api_key():
376
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
377
+
378
+ if step is None and period is None:
379
+ return self.send_response(status='fail', message="Please provide at least one of required parameters (`step` or `period`)", status_code=400)
380
+
381
+ if self.handler.dial_set_easing_backlight(dial_uid=gaugeUID, step=step, period=period):
382
+ values_dict = { 'easing_backlight_step': step, 'easing_backlight_period': period }
383
+ self.config.update_dial_db_cell_with_dict(gaugeUID, values_dict)
384
+ self.handler.dial_reload_info_from_database(gaugeUID)
385
+ return self.send_response(status='ok')
386
+ return self.send_response(status='fail', message="Device not present", status_code=406)
387
+
388
+ class Dial_Get_Easing_Config(BaseHandler):
389
+ def get(self, gaugeUID):
390
+ logger.debug(f"Request:GET_EASING_CONFIG - Device:{gaugeUID}")
391
+
392
+ # Validate API key
393
+ if not self.is_valid_api_key():
394
+ return self.send_response(status='fail', message='Unauthorized', status_code=401)
395
+
396
+ # TODO: Implement in dial handler
397
+ return self.send_response(status='ok', message="not supported yet")
398
+
399
+ # -- Keys --
400
+ class Admin_Keys_List(BaseHandler):
401
+ def get(self):
402
+ logger.debug("Request:Admin_Keys_List")
403
+
404
+ # Validate master key
405
+ if not self.valid_admin_key():
406
+ return False # Above function already sends response
407
+
408
+ keys = self.config.list_keys()
409
+
410
+ ret = []
411
+ for _, value in keys.items():
412
+ # value.pop('dials', None)
413
+ ret.append(value)
414
+ return self.send_response(status='ok', data=ret)
415
+
416
+ class Admin_Keys_Create(BaseHandler):
417
+ def post(self):
418
+ logger.debug("Request:Admin_Keys_Create")
419
+
420
+ # Validate master key
421
+ if not self.valid_admin_key():
422
+ return False # Above function already sends response
423
+
424
+ key_name = self.get_argument('name', 'Not set')
425
+ dial_access = self.get_argument('dials', None)
426
+ priviledges = self.get_argument('priviledges', 1)
427
+
428
+ if dial_access:
429
+ dials = dial_access.split(';')
430
+ else:
431
+ dials = None
432
+
433
+ new_key = self.config.create_api_key(key_name, priviledges)
434
+
435
+ if dials:
436
+ self.config.api_key_add_dial_access(new_key, dials)
437
+
438
+ return self.send_response(status='ok', data=new_key)
439
+
440
+ class Admin_Keys_Update(BaseHandler):
441
+ def post(self):
442
+ logger.debug("Request:Admin_Keys_Update")
443
+
444
+ dial_list = self.get_argument('dials', None)
445
+ key = self.get_argument('key', None)
446
+ name = self.get_argument('name', None)
447
+
448
+ # Validate master key
449
+ if not self.valid_admin_key():
450
+ return False # Above function already sends response
451
+
452
+ # Check if API key exists
453
+ if not self.is_valid_api_key():
454
+ return self.send_response(status='fail', message='Invalid key selected!')
455
+
456
+ logger.debug(dial_list)
457
+ logger.debug(key)
458
+ logger.debug(name)
459
+
460
+ if dial_list is None and key is None:
461
+ return self.send_response(status='fail', message='Key, Key name and Dial list are all empty. Aborting.', status_code=400)
462
+
463
+ # Update key
464
+ if name is not None:
465
+ if not self.config.update_api_key(key_uid=key, key_name=name):
466
+ return self.send_response(status='fail', message='Failed to update key!')
467
+
468
+ # Update dial access
469
+ if dial_list:
470
+ dial_list = dial_list.split(';')
471
+ if self.config.api_key_add_dial_access(key, dial_list):
472
+ return self.send_response(status='ok', message='Key updated!')
473
+
474
+ return self.send_response(status='fail', message='Failed to update key!')
475
+
476
+ class Admin_Keys_Remove(BaseHandler):
477
+ def get(self):
478
+ logger.debug("Request:Admin_Keys_Remove")
479
+ key_uid = self.get_argument('key', None)
480
+
481
+ # Validate master key
482
+ if not self.valid_admin_key():
483
+ return False # Above function already sends response
484
+
485
+ # Check if API key exists
486
+ if not self.is_valid_api_key():
487
+ return self.send_response(status='fail', message='Invalid key selected!')
488
+
489
+ if not self.config.delete_api_key(key_uid):
490
+ return self.send_response(status='fail', message='Failed to remove key!')
491
+ return self.send_response(status='ok', message='Key removed!')
492
+
493
+ # -- Default 404 --
494
+ class Default_404_Handler(RequestHandler):
495
+ # Override prepare() instead of get() to cover all possible HTTP methods.
496
+ def prepare(self):
497
+ self.set_status(404)
498
+ resp = {'status': 'fail', 'message': 'Unsupported method'}
499
+ self.write(resp)
500
+ raise Finish()
501
+
502
+ class FileHandler(RequestHandler):
503
+ def get(self, path=None):
504
+ if path:
505
+ logger.debug(f"Requesting: {path}")
506
+ file_location = os.path.join(WEB_ROOT, path)
507
+ else:
508
+ file_location = os.path.join(WEB_ROOT, 'index.html')
509
+
510
+ if not os.path.isfile(file_location):
511
+ logger.error(f"Requested file can not be found: {path}")
512
+ self.set_status(404)
513
+ resp = {'status': 'fail', 'message': 'Page not found'}
514
+ self.write(resp)
515
+ raise Finish()
516
+ content_type, _ = guess_type(file_location)
517
+ self.add_header('Content-Type', content_type)
518
+ with open(file_location, encoding="utf-8") as source_file:
519
+ self.write(source_file.read())
520
+
521
+
522
+ class Dial_API_Service(Application):
523
+ def __init__(self):
524
+ pid_lock('server', True)
525
+ signal.signal(signal.SIGTERM, self.signal_handler)
526
+ signal.signal(signal.SIGINT, self.signal_handler)
527
+
528
+ logger.info("Loading server config...")
529
+ self.config = ServerConfig('config.yaml')
530
+
531
+ # If config contains COM port, use it. Otherwise try to find it
532
+ hardware_config = self.config.get_hardware_config()
533
+ port = hardware_config.get('port', None)
534
+ if port:
535
+ self.serialPort = port
536
+ else:
537
+ self.serialPort = DialSerialDriver.find_gauge_hub()
538
+ if self.serialPort is None:
539
+ logger.error("Could not find VU1 Dials Hub. Please make sure it's plugged in and (if necessary) drivers are installed.")
540
+ show_error_msg("Hub not found", "Could not find VU1 Hub on the USB bus.\r\n"\
541
+ "Please make sure it is plugged in and (if necessary) drivers are installed.\r\n"\
542
+ "Then restart the VU Server application.\r\nVU server application will close now.")
543
+ sys.exit(0)
544
+ # raise Exception("Could not find VU1 Dials Hub. Please make sure it's plugged in and (if necessary) drivers are installed.")
545
+
546
+ logger.info("VU1 HUB port: {}".format(self.serialPort))
547
+ self.dial_driver = DialSerialDriver(self.serialPort)
548
+ self.dial_handler = ServerDialHandler(self.dial_driver, self.config)
549
+
550
+ # If we don't see any dials, try looking/provisioning some
551
+ if len(self.dial_handler.dials) <= 1:
552
+ logger.info("No additional dials found. Searching the bus for new ones...")
553
+ self.dial_handler.provision_dials(num_attempts=3)
554
+
555
+ handlers_config = { "handler":self.dial_handler, "config":self.config }
556
+ self.handlers = [
557
+ (r"/api/v0/dial/provision", Dial_Provision, handlers_config),
558
+ (r"/api/v0/dial/list", Dial_Get_List, handlers_config),
559
+ (r"/api/v0/dial/([0-9A-F]*?)/status", Device_Status_Handler, handlers_config),
560
+ (r"/api/v0/dial/([0-9A-F]*?)/set", Device_Set_Handler, handlers_config),
561
+ (r"/api/v0/dial/([0-9A-F]*?)/setRaw", Device_SetRaw_Handler, handlers_config),
562
+ (r"/api/v0/dial/([0-9A-F]*?)/image/set", Device_Set_Image, handlers_config),
563
+ (r"/api/v0/dial/([0-9A-F]*?)/image/get", Dial_Get_Image, handlers_config),
564
+ (r"/api/v0/dial/([0-9A-F]*?)/image/crc", Dial_Get_Image_CRC, handlers_config),
565
+ (r"/api/v0/dial/([0-9A-F]*?)/backlight", Device_Backlight_Handler, handlers_config),
566
+ (r"/api/v0/dial/([0-9A-F]*?)/name", Dial_Set_Dial_Name, handlers_config),
567
+ (r"/api/v0/dial/([0-9A-F]*?)/reload", Dial_Reload_Device_Info, handlers_config),
568
+ (r"/api/v0/dial/([0-9A-F]*?)/calibrate", Dial_Set_Calibration, handlers_config),
569
+ (r"/api/v0/dial/([0-9A-F]*?)/easing/dial", Dial_Set_Easing_Dial, handlers_config),
570
+ (r"/api/v0/dial/([0-9A-F]*?)/easing/backlight", Dial_Set_Easing_Backlight, handlers_config),
571
+ (r"/api/v0/dial/([0-9A-F]*?)/easing/get", Dial_Get_Easing_Config, handlers_config),
572
+ (r"/api/v0/admin/keys/list", Admin_Keys_List, handlers_config),
573
+ (r"/api/v0/admin/keys/create", Admin_Keys_Create, handlers_config),
574
+ (r"/api/v0/admin/keys/remove", Admin_Keys_Remove, handlers_config),
575
+ (r"/api/v0/admin/keys/update", Admin_Keys_Update, handlers_config),
576
+ (r"/", FileHandler),
577
+ (r'/(.*)', StaticFileHandler, {'path': WEB_ROOT}),
578
+ ]
579
+
580
+ self.server_settings = {
581
+ "debug": True,
582
+ "autoreload": False,
583
+ # "autoreload": True,
584
+ "default_handler_class": Default_404_Handler,
585
+ }
586
+
587
+ def signal_handler(self, signal, frame):
588
+ pid_lock('server', False)
589
+ self.shut_down_dials()
590
+ IOLoop.current().add_callback_from_signal(self.shutdown_server)
591
+ print('\r\nYou pressed Ctrl+C!')
592
+ show_info_msg("CTRL+C", "CTRL+C pressed.\r\nVU Server app will exit now.") # Remove if becomes annoying
593
+ sys.exit(0)
594
+
595
+ def shut_down_dials(self):
596
+ print("Shutting down dials...")
597
+ try:
598
+ for dial in self.dial_driver.dials:
599
+ logger.debug(f"Shutting down dial {dial}")
600
+ self.dial_driver.set_dial(dialID=dial, value=0)
601
+ self.dial_driver.dial_set_backlight(device=dial, red=0, green=0, blue=0, white=0)
602
+
603
+ except Exception as e:
604
+ logger.error("Failed to gracefully shut down dials")
605
+ logger.error(f"Error: {e}")
606
+
607
+ def shutdown_server(self):
608
+ logger.info('Stopping API server')
609
+ logger.info('Will shutdown in 3 seconds ...')
610
+ io_loop = IOLoop.instance()
611
+ deadline = time.time() + 3
612
+
613
+ def stop_loop():
614
+ now = time.time()
615
+ if now < deadline and (io_loop._callbacks or io_loop._timeouts):
616
+ io_loop.add_timeout(now + 1, stop_loop)
617
+ else:
618
+ io_loop.stop()
619
+ logger.info('Shutdown')
620
+ stop_loop()
621
+
622
+ def run_forever(self):
623
+ logger.info("Karanovic Research Dials - Starting API server")
624
+ app = Application(self.handlers, **self.server_settings)
625
+
626
+ # Port from config.yaml or default 5340
627
+ server_config = self.config.get_server_config()
628
+ port = server_config.get('port', 5340)
629
+ master_key = server_config.get('master_key', None)
630
+ dial_update_period = server_config.get('dial_update_period', 1000)
631
+ logger.info(f"VU1 API server is listening on http://localhost:{port}")
632
+ app.listen(port)
633
+
634
+ if master_key is not None:
635
+ logger.info("Master Key is present in config.yaml (or using default)")
636
+ logger.info(f"Provide '{master_key}' to your main application.")
637
+ logger.info("to allow it to manage this server and the VU dials.")
638
+ else:
639
+ show_error_msg(title='Key missing from config', message='Entry "master_key" is missing from the "config.yaml"!')
640
+ logger.error("Master Key is MISSING from config.yaml")
641
+ logger.error("Check your 'config.yaml' or add it manually under 'server' section.")
642
+ sys.exit(0)
643
+
644
+ pc = PeriodicCallback(self.dial_handler.periodic_dial_update, dial_update_period)
645
+ pc.start()
646
+
647
+ IOLoop.instance().start()
648
+
649
+
650
+ def main(cmd_args=None):
651
+ if cmd_args is None:
652
+ set_logger_level('info')
653
+ else:
654
+ set_logger_level(cmd_args.logging)
655
+ try:
656
+ Dial_API_Service().run_forever()
657
+ except SerialException:
658
+ logger.exception("VU Dials API service - Serial port access denied")
659
+ show_warning_msg("Serial Port Access Denied", "VU Server failed to start. Access to serial port denied.\r\nVU Server already running?")
660
+ except Exception as e:
661
+ logger.exception("VU Dials API service crashed during setup.")
662
+ logger.exception(f"Unhandled exception: ({type(e)}) {e}")
663
+ show_error_msg("Crashed", "VU Server has crashed unexpectedly!\r\nPlease check log files for more information.")
664
+ os._exit(0)
665
+
666
+ if __name__ == '__main__':
667
+ parser = argparse.ArgumentParser(description='Karanovic Research - VU Dials API service')
668
+ parser.add_argument('-l', '--logging', type=str, default='info', help='Set logging level. Default is `info`')
669
+ args = parser.parse_args()
670
+ main(args)