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,255 @@
1
+ # pylint: disable=E1101
2
+ import os
3
+ from ruamel.yaml import YAML
4
+ # import yaml
5
+ from dials.base_logger import logger
6
+ from vu_notifications import show_error_msg, show_warning_msg
7
+ import database as db
8
+
9
+ class ServerConfig:
10
+ config_path = None
11
+ server = None
12
+ hardware = None
13
+ server_default = {'hostname': 'localhost', 'port': 3000, 'communication_timeout': 10, 'master_key': 'cTpAWYuRpA2zx75Yh961Cg' }
14
+ hardware_default = {'port': None }
15
+ dials = {}
16
+ api_keys = {}
17
+ database = None
18
+
19
+ def __init__(self, config_file='config.yaml'):
20
+ self.config_path = os.path.join(os.path.dirname(__file__), config_file)
21
+ logger.info(f"VU1 config yaml file: {self.config_path}")
22
+ self.database = db.DialsDB(init_if_missing=True)
23
+ self._load_config() # Load configuration from .yaml file
24
+ self._load_API_keys() # Load API keys from `api_keys` section
25
+ self.debug_config()
26
+
27
+ # Save current configuration to .yaml file
28
+ def _save_config(self):
29
+ config = None
30
+ yaml = YAML(typ='safe', pure=True)
31
+
32
+ if os.path.exists(self.config_path):
33
+ with open(self.config_path, 'r', encoding="utf-8") as file:
34
+ config = yaml.load(file) # pylint: disable=assignment-from-no-return
35
+
36
+ if not config:
37
+ config = {}
38
+
39
+ # Update server config
40
+ if not config.get('server'):
41
+ config['server'] = self.server
42
+ config['port'] = self.server['port']
43
+ config['communication_timeout'] = 5000
44
+ else:
45
+ config['server']['hostname'] = self.server['hostname']
46
+ config['server']['port'] = self.server['port']
47
+ config['server']['communication_timeout'] = self.server['communication_timeout']
48
+
49
+ with open(self.config_path, 'w', encoding="utf-8") as file:
50
+ yaml.dump(config, file)
51
+
52
+ # Read .yaml config file
53
+ def _load_config(self):
54
+ if not os.path.exists(self.config_path):
55
+ logger.error(f"Can not load config. Config file '{self.config_path}' does not exist!")
56
+ show_error_msg("Can not find config.yaml", f"Config file '{self.config_path}' is missing!\r\n"\
57
+ "Please fix this issue by creating a default config.yaml file in the VU Server directory.\r\n"\
58
+ "Using default values for this session.")
59
+ self._create_default_config()
60
+ return False
61
+
62
+ yaml = YAML(typ='safe', pure=True)
63
+ with open(self.config_path, 'r', encoding="utf-8") as file:
64
+ cfg = yaml.load(file) # pylint: disable=assignment-from-no-return
65
+
66
+ if cfg is None:
67
+ show_error_msg("Empty/corrupt config file!", "Config file exists but it is empty or corrupt!\r\n"\
68
+ "Using defaul values for this session.")
69
+ self._create_default_config()
70
+ return False
71
+
72
+ # Check that config file meets the minimum requirements
73
+ if not isinstance(cfg, dict) or 'server' not in cfg or 'hardware' not in cfg:
74
+ show_warning_msg("Missing Key", f"Config file '{self.config_path}' \r\n"\
75
+ "Must have valid entries for 'server' and 'hardware' configuration!\r\n"\
76
+ "Using defaul values for this session.")
77
+ cfg = {}
78
+ cfg['server'] = self.server_default
79
+ cfg['hardware'] = self.hardware_default
80
+
81
+ elif not isinstance(cfg['server'], dict):
82
+ show_warning_msg("Invalid server config", f"Config file '{self.config_path}' \r\n"\
83
+ "Has invalid `server` config entry.\r\n"\
84
+ "Using defaul values for this session.")
85
+ self._force_default_config()
86
+ return False
87
+
88
+ elif not isinstance(cfg['hardware'], dict):
89
+ show_warning_msg("Invalid server config", f"Config file '{self.config_path}' \r\n"\
90
+ "Has invalid `hardware` config entry.\r\n"\
91
+ "Using defaul values for this session.")
92
+ self._force_default_config()
93
+ return False
94
+
95
+ elif ('hostname' not in cfg['server'] or
96
+ 'port' not in cfg['server'] or
97
+ 'communication_timeout' not in cfg['server'] or
98
+ 'master_key' not in cfg['server']):
99
+ show_warning_msg("Missing Key", f"Config file '{self.config_path}' \r\n"\
100
+ "must have `hostname`, `port`, `communication_timeout` and `master_key` entries!\r\n"\
101
+ "Using defaul values for this session.")
102
+ self._force_default_config()
103
+ return False
104
+
105
+ elif 'port' not in cfg['hardware']:
106
+ show_warning_msg("Missing Key", f"Config file '{self.config_path}' \r\n"\
107
+ "must have hardware `port` entry! (it can be left empty)\r\n"\
108
+ "Using defaul values for this session.")
109
+ self._force_default_config()
110
+ return False
111
+
112
+ # Load yaml values
113
+ self.server = cfg.get('server', self.server_default)
114
+ self.hardware = cfg.get('hardware', self.hardware_default)
115
+
116
+ return True
117
+
118
+ def _force_default_config(self):
119
+ self.server = self.server_default
120
+ self.hardware = self.hardware_default
121
+
122
+ def _create_default_config(self):
123
+ logger.info("Using default config values")
124
+ self.server = self.server_default
125
+ self.hardware = self.hardware_default
126
+
127
+ # Load API keys from config file
128
+ def _load_API_keys(self):
129
+ # Make sure .yaml master key exists in the database
130
+ self.database.api_update_master(self.server['master_key'])
131
+
132
+ # Load all API keys from the database
133
+ self.api_keys = self.database.api_key_list()
134
+
135
+ def reload_API_keys(self):
136
+ # Load all API keys from the database
137
+ self.api_keys = self.database.api_key_list()
138
+
139
+ def update_dial_db_cell(self, dial_uid, cell, value):
140
+ try:
141
+ ret = self.database.dial_update_cell(dial_uid=dial_uid, cell=cell, value=value)
142
+ if ret:
143
+ self.dials[dial_uid][cell] = value
144
+ return True
145
+ return False
146
+ except Exception as e:
147
+ logger.error(e)
148
+ return False
149
+
150
+ def update_dial_db_cell_with_dict(self, dial_uid, values_dict):
151
+ try:
152
+ self.database.dial_update_cell_with_dict(dial_uid=dial_uid, values_dict=values_dict)
153
+ except Exception as e:
154
+ logger.error(e)
155
+ return
156
+
157
+ # Read dial information stored in the DB and append to existing list
158
+ def append_dial_info_from_db(self, dial_list):
159
+ for key, dial in enumerate(dial_list):
160
+ dial_info = self.database.fetch_dial_info_or_create_default(dial['uid'])
161
+
162
+ dial_list[key]['dial_name'] = dial_info['dial_name']
163
+ dial_list[key]['fw_hash'] = dial_info['dial_build_hash']
164
+ dial_list[key]['fw_version'] = dial_info['dial_fw_version']
165
+ dial_list[key]['hw_version'] = dial_info['dial_hw_version']
166
+ dial_list[key]['protocol_version'] = dial_info['dial_protocol_version']
167
+ dial_list[key]['easing']['dial_step'] = dial_info['easing_dial_step']
168
+ dial_list[key]['easing']['dial_period'] = dial_info['easing_dial_period']
169
+ dial_list[key]['easing']['backlight_step'] = dial_info['easing_backlight_step']
170
+ dial_list[key]['easing']['backlight_period'] = dial_info['easing_backlight_period']
171
+
172
+ self.dials[dial['uid']] = dial
173
+
174
+ return dial_list
175
+
176
+ def dial_fetch_db_info(self, dial_uid):
177
+ return self.database.fetch_dial_info_or_create_default(dial_uid)
178
+
179
+ # Print out .yaml config
180
+ def debug_config(self):
181
+ logger.debug(f"\t Host: {self.server['hostname']}")
182
+ logger.debug("--- Server Config ---")
183
+ logger.debug(f"\t Port: {self.server['port']}")
184
+ logger.debug(f"\t Serial Timeout: {self.server['communication_timeout']}")
185
+ logger.debug(f"\t Master Key: {self.server['master_key']}")
186
+
187
+ logger.debug("--- API Keys ---")
188
+ logger.debug(f"\t There are {len(self.api_keys)} API keys loaded")
189
+
190
+ def get_server_config(self):
191
+ return self.server
192
+
193
+ def get_hardware_config(self):
194
+ return self.hardware
195
+
196
+ def create_api_key(self, key_name, priviledges=0):
197
+ generated_key = self.database.api_key_generate(key_name=key_name, level=priviledges)
198
+ logger.info(f"Generated API key '{generated_key}' (key_name:'{key_name}', priviledges:'{priviledges}')")
199
+ self.list_keys(reload=True)
200
+ return generated_key
201
+
202
+ def update_api_key(self, key_uid, key_name):
203
+ # Update key
204
+ if not self.database.api_key_update(key_uid=key_uid, key_name=key_name):
205
+ return False
206
+ return True
207
+
208
+ def delete_api_key(self, key_uid):
209
+ if not self.database.api_key_delete(key_uid=key_uid):
210
+ return False
211
+ self.list_keys(reload=True)
212
+ return True
213
+
214
+ def list_keys(self, reload=False):
215
+ if reload:
216
+ self.api_keys = self.database.api_key_list()
217
+ return self.api_keys
218
+
219
+ def api_key_add_dial_access(self, key, dials):
220
+ res = self.database.api_key_add_dial_access(key, dials)
221
+ if res:
222
+ self.list_keys(reload=True)
223
+ return res
224
+
225
+ # Returns True if provided key is listed as master key
226
+ # Otherwise return False
227
+ def validate_admin_key(self, key):
228
+ if not self.is_valid_api_key(key):
229
+ logger.debug(f"API key `{key}` does not exist")
230
+ return False
231
+
232
+ if self.api_keys[key]['priviledges'] >= 99:
233
+ return True
234
+ logger.debug("Key exists but not admin key.")
235
+ return False
236
+
237
+
238
+ # Returns True if API key existis, otherwise returns False
239
+ def is_valid_api_key(self, key):
240
+ if key in self.api_keys.keys():
241
+ return True
242
+ return False
243
+
244
+ # Returns True if API key has access to dial UID, otherwise retrns False
245
+ def api_key_has_access_to_dial(self, key, dial):
246
+ if not self.is_valid_api_key(key):
247
+ return False
248
+
249
+ # Master key has wildcard access
250
+ if self.api_keys[key]['priviledges'] >= 99:
251
+ return True
252
+
253
+ if dial in self.api_keys[key]['dials']:
254
+ return True
255
+ return False
@@ -0,0 +1,334 @@
1
+ import os
2
+ from time import time, sleep
3
+ from math import trunc
4
+ from dials.base_logger import logger
5
+
6
+ # ServerDialHandler Class
7
+ # ---
8
+ # This class handles all the requests coming from the server.
9
+ # It stores update requests (value, backlight, image etc) that are coming from the server API calls.
10
+ # It will also periodically update the dials
11
+ # ---
12
+ # 'periodic_dial_update' function is called periodically from the main server loop
13
+ #
14
+ class ServerDialHandler:
15
+ dials = {}
16
+ hub_info = {}
17
+ communication_timeout = 3
18
+
19
+ def __init__(self, dial_driver, server_config):
20
+ self.dial_driver = dial_driver
21
+ self.server_config = server_config
22
+
23
+ # Communication timeout
24
+ cfg = self.server_config.get_server_config()
25
+ self.communication_timeout = cfg.get('communication_timeout', 3)
26
+ logger.info(f"Communication timeout set to {self.communication_timeout} seconds")
27
+
28
+ logger.debug("Retrieving list of dials")
29
+ self._reload_dials(True)
30
+
31
+ logger.debug("Reconfiguring dials with stored behaviour")
32
+ self._send_db_config_to_dials()
33
+
34
+ logger.debug("Setting all dials percentage to 0")
35
+ self.dial_driver.set_all_dials_to(0)
36
+
37
+ logger.debug("Server dial handler up and running.")
38
+
39
+ def periodic_dial_update(self):
40
+ updated = 0
41
+ ret=0
42
+
43
+ ret = self._periodic_update_dial_values()
44
+ updated = updated + ret
45
+
46
+ ret = self._periodic_update_dial_backlight()
47
+ updated = updated + ret
48
+
49
+ ret = self._periodic_update_dial_images()
50
+ updated = updated + ret
51
+
52
+ if updated <=0:
53
+ self._periodic_keep_alive()
54
+
55
+ def _convert_to_int(self, value):
56
+ try:
57
+ if not isinstance(value, int):
58
+ value = trunc(int(float(value)))
59
+ except Exception as e:
60
+ logger.error(e)
61
+ logger.error("Failed to convert value `{value}`to int. Defaulting to 0")
62
+ value = 0
63
+
64
+ return value
65
+
66
+ def _reload_dials(self, rescan=False):
67
+ # Get dial list from the dial driver (actual list reported from the hub)
68
+ dials = self.dial_driver.get_dial_list(rescan)
69
+
70
+ if len(dials)<=0:
71
+ logger.error("No dials connected to the bus!")
72
+ return
73
+
74
+ # 1 - Inform config/db what is the list of dials that we currently see
75
+ # 2 - Update handler information with any information retrieved from the database
76
+ # dials = self.server_config.append_dial_info_from_db(dials)
77
+ self.server_config.append_dial_info_from_db(dials)
78
+
79
+ # Dial HUB uses indexes to address each dial. On the server side we use UID for flexibility
80
+ # and also so that we can uniquely identify each dial.
81
+ for dial in dials:
82
+ dial['value'] = 0
83
+ dial['backlight'] = {'red':0, 'green':0, 'blue':0, 'white':0 }
84
+ dial['image_file'] = self._check_upload_for_dial_image(dial['uid'])
85
+ dial['update_deadline'] = time()
86
+ dial['value_changed'] = False
87
+ dial['backlight_changed'] = True
88
+ dial['image_changed'] = False
89
+ self.dials[dial['uid']] = dial
90
+
91
+ def _send_db_config_to_dials(self):
92
+ for _, dial in self.dials.items():
93
+ dial_step = dial['easing']['dial_step']
94
+ dial_period = dial['easing']['dial_period']
95
+ backlight_step = dial['easing']['backlight_step']
96
+ backlight_period = dial['easing']['backlight_period']
97
+
98
+ logger.debug(f"Configuring dial `{dial['uid']}`")
99
+ logger.debug(f"\tDial:{dial_step}% per {dial_period}ms")
100
+ logger.debug(f"\tBacklight {backlight_step}% {backlight_period}ms")
101
+ self.dial_set_easing_dial(dial['uid'], step=dial_step, period=dial_period)
102
+ self.dial_set_easing_backlight(dial['uid'], step=backlight_step, period=backlight_period)
103
+
104
+ def _check_upload_for_dial_image(self, dial_uid):
105
+ filename = f'img_{dial_uid}'
106
+ filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'upload', filename)
107
+ if os.path.exists(filepath):
108
+ return filename
109
+
110
+ return 'img_blank'
111
+
112
+ # TODO: Update to send multiple/all dial values in one go instead one-by-one
113
+ def _periodic_update_dial_values(self):
114
+ updated = 0
115
+ for _, dial in self.dials.items():
116
+ if dial['value_changed']:
117
+ self.dial_driver.dial_single_set_percent(dial['index'], dial['value'])
118
+ dial['value_changed'] = False
119
+ dial['update_deadline'] = time() + self.communication_timeout
120
+ updated = updated+1
121
+ if updated>0:
122
+ logger.debug(f"Updated {updated} dial values.")
123
+ return updated
124
+
125
+ def _periodic_update_dial_backlight(self):
126
+ updated = 0
127
+ for _, dial in self.dials.items():
128
+ if dial['backlight_changed']:
129
+ self.dial_driver.dial_set_backlight(dial['index'],
130
+ dial['backlight']['red'],
131
+ dial['backlight']['green'],
132
+ dial['backlight']['blue'],
133
+ dial['backlight']['white']
134
+ )
135
+ dial['backlight_changed'] = False
136
+ dial['update_deadline'] = time() + self.communication_timeout
137
+ updated = updated+1
138
+ if updated>0:
139
+ logger.debug(f"Updated {updated} dial backlight(s).")
140
+ return updated
141
+
142
+ def _periodic_update_dial_images(self):
143
+ updated = 0
144
+ for _, dial in self.dials.items():
145
+ if dial['image_changed']:
146
+ logger.debug("Updating images")
147
+ self.dial_driver.update_display(device=dial['index'], imageFile=dial['image_file'])
148
+ dial['update_deadline'] = time() + self.communication_timeout
149
+ dial['image_changed'] = False
150
+ updated = updated+1
151
+ return updated
152
+
153
+ def _periodic_keep_alive(self):
154
+ #FIXME!
155
+ return
156
+ # for _, dial in self.dials.items():
157
+ # if time() >= dial['update_deadline']:
158
+ # logger.info("Keeping communication alive")
159
+ # self.dial_driver.dial_send_keep_comm_alive(device=dial['index'])
160
+ # dial['image_changed'] = False
161
+
162
+ def _dial_exists(self, dial_uid):
163
+ return dial_uid in self.dials
164
+
165
+ def provision_dials(self, num_attempts = 3):
166
+ logger.debug(f"Provisioning new dials (with {num_attempts} attempts)")
167
+ for _ in range(num_attempts):
168
+ self.dial_driver.provision_dials()
169
+ sleep(0.2)
170
+ logger.debug("Retrieving list of dials")
171
+ self._reload_dials(True)
172
+
173
+ def get_dial_info(self, dial_uid=None):
174
+ if dial_uid is not None:
175
+ return self.dials.get(dial_uid, None)
176
+ return self.dials
177
+
178
+ def dial_set_percent(self, dial_uid, value):
179
+ if not self._dial_exists(dial_uid):
180
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
181
+ return False
182
+
183
+ value = self._convert_to_int(value)
184
+
185
+ # Check if already at value
186
+ if self.dials[dial_uid]['value'] == value:
187
+ logger.debug(f"Dial {dial_uid} already at {value}")
188
+ return True
189
+
190
+ logger.debug(f"Queueing dial {dial_uid} value update to {value}")
191
+ self.dials[dial_uid]['value'] = value
192
+ self.dials[dial_uid]['value_changed'] = True
193
+ return True
194
+
195
+ # Debug function, mainly used for dial offset/calibration
196
+ def dial_set_raw(self, dial_uid, value):
197
+ if not self._dial_exists(dial_uid):
198
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
199
+ return False
200
+
201
+ value = self._convert_to_int(value)
202
+ self.dial_driver.dial_single_set_raw(self.dials[dial_uid]['index'], value)
203
+ return True
204
+
205
+
206
+ # Debug function, mainly used for dial offset/calibration
207
+ def dial_set_calibration(self, dial_uid, value, fullScale=False):
208
+ if not self._dial_exists(dial_uid):
209
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
210
+ return False
211
+
212
+ value = self._convert_to_int(value)
213
+ self.dial_driver.dial_calibrate(self.dials[dial_uid]['index'], value, fullScale)
214
+ return True
215
+
216
+ def dial_set_easing_dial(self, dial_uid, step=None, period=None):
217
+ if not self._dial_exists(dial_uid):
218
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
219
+ return False
220
+
221
+ if step is not None:
222
+ step = self._convert_to_int(step)
223
+ self.dial_driver.dial_easing_dial_step(self.dials[dial_uid]['index'], step)
224
+
225
+ if period is not None:
226
+ period = self._convert_to_int(period)
227
+ self.dial_driver.dial_easing_dial_period(self.dials[dial_uid]['index'], period)
228
+
229
+ return True
230
+
231
+ def dial_set_easing_backlight(self, dial_uid, step=None, period=None):
232
+ if not self._dial_exists(dial_uid):
233
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
234
+ return False
235
+
236
+ if step is not None:
237
+ step = self._convert_to_int(step)
238
+ self.dial_driver.dial_easing_backlight_step(self.dials[dial_uid]['index'], step)
239
+
240
+ if period is not None:
241
+ period = self._convert_to_int(period)
242
+ self.dial_driver.dial_easing_backlight_period(self.dials[dial_uid]['index'], period)
243
+
244
+ return True
245
+
246
+ def dial_set_backlight(self, dial_uid, red, green, blue, white):
247
+ if not self._dial_exists(dial_uid):
248
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
249
+ return False
250
+
251
+ red = self._convert_to_int(red)
252
+ green = self._convert_to_int(green)
253
+ blue = self._convert_to_int(blue)
254
+ white = self._convert_to_int(white)
255
+
256
+ red = min(red, 100)
257
+ green = min(green, 100)
258
+ blue = min(blue, 100)
259
+ blue = min(blue, 100)
260
+ white = min(white, 100)
261
+
262
+ new_value = {'red':red, 'green':green, 'blue':blue, 'white':white }
263
+
264
+ # Check if already at value
265
+ if self.dials[dial_uid]['backlight'] == new_value:
266
+ logger.debug(f"Dial {dial_uid} already at {red}:{green}:{blue}:{white}")
267
+ return True
268
+
269
+ logger.debug(f"Queueing dial {dial_uid} RGBW update to {red}:{green}:{blue}:{white}")
270
+ self.dials[dial_uid]['backlight'] = {'red':red, 'green':green, 'blue':blue, 'white':white }
271
+ self.dials[dial_uid]['backlight_changed'] = True
272
+ return True
273
+
274
+ def dial_set_image(self, dial_uid, image_file):
275
+ if not self._dial_exists(dial_uid):
276
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
277
+ return False
278
+
279
+ logger.debug(f"Queueing dial {dial_uid} background image to {image_file}")
280
+ self.dials[dial_uid]['image_file'] = image_file
281
+ self.dials[dial_uid]['image_changed'] = True
282
+ return True
283
+
284
+ def dial_reload_info_from_hardware(self, dial_uid):
285
+ if not self._dial_exists(dial_uid):
286
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
287
+ return False
288
+
289
+ deviceIndex = int(self.dials[dial_uid]['index'])
290
+
291
+ fw_hash = self.dial_driver.dial_get_fw_hash(deviceIndex)
292
+ fw_version = self.dial_driver.dial_get_fw_version(deviceIndex)
293
+ hw_version = self.dial_driver.dial_get_hw_version(deviceIndex)
294
+ protocol_version = self.dial_driver.dial_get_protocol_version(deviceIndex)
295
+ deviceEasing = self.dial_driver.dial_easing_get_config(deviceIndex) # Read dial easing config
296
+
297
+ self.dials[dial_uid]['fw_hash'] = fw_hash
298
+ self.dials[dial_uid]['fw_version'] = fw_version
299
+ self.dials[dial_uid]['hw_version'] = hw_version
300
+ self.dials[dial_uid]['protocol_version'] = protocol_version
301
+ self.dials[dial_uid]['easing']['dial_step'] = deviceEasing['dial_step']
302
+ self.dials[dial_uid]['easing']['dial_period'] = deviceEasing['dial_period']
303
+ self.dials[dial_uid]['easing']['backlight_step'] = deviceEasing['backlight_step']
304
+ self.dials[dial_uid]['easing']['backlight_period'] = deviceEasing['backlight_period']
305
+
306
+ self.server_config.update_dial_db_cell(dial_uid, 'dial_build_hash', fw_hash)
307
+ self.server_config.update_dial_db_cell(dial_uid, 'dial_fw_version', fw_version)
308
+ self.server_config.update_dial_db_cell(dial_uid, 'dial_hw_version', hw_version)
309
+ self.server_config.update_dial_db_cell(dial_uid, 'dial_protocol_version', protocol_version)
310
+ self.server_config.update_dial_db_cell(dial_uid, 'easing_dial_step', deviceEasing['dial_step'])
311
+ self.server_config.update_dial_db_cell(dial_uid, 'easing_dial_period', deviceEasing['dial_period'])
312
+ self.server_config.update_dial_db_cell(dial_uid, 'easing_backlight_step', deviceEasing['backlight_step'])
313
+ self.server_config.update_dial_db_cell(dial_uid, 'easing_backlight_period', deviceEasing['backlight_period'])
314
+
315
+ return self.dials[dial_uid]
316
+
317
+
318
+ def dial_reload_info_from_database(self, dial_uid):
319
+ if not self._dial_exists(dial_uid):
320
+ logger.error(f"Dial {dial_uid} does not exist in dial list.")
321
+ return False
322
+
323
+ dial_info = self.server_config.dial_fetch_db_info(dial_uid)
324
+
325
+ self.dials[dial_uid]['fw_hash'] = dial_info['dial_build_hash']
326
+ self.dials[dial_uid]['fw_version'] = dial_info['dial_fw_version']
327
+ self.dials[dial_uid]['hw_version'] = dial_info['dial_hw_version']
328
+ self.dials[dial_uid]['protocol_version'] = dial_info['dial_protocol_version']
329
+ self.dials[dial_uid]['easing']['dial_step'] = dial_info['easing_dial_step']
330
+ self.dials[dial_uid]['easing']['dial_period'] = dial_info['easing_dial_period']
331
+ self.dials[dial_uid]['easing']['backlight_step'] = dial_info['easing_backlight_step']
332
+ self.dials[dial_uid]['easing']['backlight_period'] = dial_info['easing_backlight_period']
333
+
334
+ return self.dials[dial_uid]
@@ -0,0 +1,24 @@
1
+ # pylint: disable=import-outside-toplevel
2
+ import sys
3
+
4
+ # TODO: Add support for Linux and Mac OS
5
+
6
+ if sys.platform.lower() == "win32":
7
+ import ctypes
8
+
9
+ def show_error_msg(title, message):
10
+ ctypes.windll.user32.MessageBoxW(0, message, f"VU Server - {title}", 16)
11
+
12
+ def show_warning_msg(title, message):
13
+ ctypes.windll.user32.MessageBoxW(0, message, f"VU Server - {title}", 48)
14
+
15
+ def show_info_msg(title, message):
16
+ ctypes.windll.user32.MessageBoxW(0, message, f"VU Server - {title}", 64)
17
+
18
+ else:
19
+ def show_error_msg(title, message):
20
+ pass
21
+ def show_warning_msg(title, message):
22
+ pass
23
+ def show_info_msg(title, message):
24
+ pass