Coverage for HUD_main.pyw: 78%
384 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-15 19:33 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-15 19:33 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""Hud_main.py
5Main for FreePokerTools HUD.
6"""
8import codecs
9import contextlib
10import sys
11import os
12import time
13import logging
14import zmq
15from PyQt5.QtCore import QCoreApplication, QObject, QThread, pyqtSignal, Qt, QTimer
16from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
17from PyQt5.QtGui import QIcon
18from qt_material import apply_stylesheet
20import Configuration
21import Database
22import Hud
23import Options
24import Deck
26# Add a cache for frequently accessed data
27from cachetools import TTLCache
29# Logging configuration
30logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
31log = logging.getLogger("hud")
34class ZMQWorker(QThread):
35 error_occurred = pyqtSignal(str)
37 def __init__(self, zmq_receiver):
38 super().__init__()
39 self.zmq_receiver = zmq_receiver
40 self.is_running = True
42 def run(self):
43 while self.is_running:
44 try:
45 self.zmq_receiver.process_message()
46 except Exception as e:
47 log.error(f"Error in ZMQWorker: {e}")
48 self.error_occurred.emit(str(e))
49 time.sleep(0.01) # Short delay to avoid excessive CPU usage
51 def stop(self):
52 self.is_running = False
53 self.wait()
56class ZMQReceiver(QObject):
57 message_received = pyqtSignal(str)
59 def __init__(self, port="5555", parent=None):
60 super().__init__(parent)
61 self.context = zmq.Context()
62 self.socket = self.context.socket(zmq.PULL)
63 self.socket.connect(f"tcp://127.0.0.1:{port}")
64 log.info(f"ZMQ receiver connected on port {port}")
66 # Heartbeat configuration
67 self.poller = zmq.Poller()
68 self.poller.register(self.socket, zmq.POLLIN)
70 def process_message(self):
71 try:
72 socks = dict(self.poller.poll(1000)) # Timeout 1 seconde
73 if self.socket in socks and socks[self.socket] == zmq.POLLIN:
74 hand_id = self.socket.recv_string(zmq.NOBLOCK)
75 log.debug(f"Received hand ID: {hand_id}")
76 self.message_received.emit(hand_id)
77 else:
78 # Heartbeat
79 log.debug("Heartbeat: No message received")
80 except zmq.ZMQError as e:
81 if e.errno == zmq.EAGAIN:
82 pass # No message available
83 else:
84 log.error(f"ZMQ error: {e}")
86 def close(self):
87 self.socket.close()
88 self.context.term()
89 log.info("ZMQ receiver closed")
92class HUD_main(QObject):
93 """A main() object to own both the socket thread and the gui."""
95 def __init__(self, options, db_name="fpdb"):
96 self.options = options
97 QObject.__init__(self)
98 self.db_name = db_name
99 Configuration.set_logfile("HUD-log.txt")
100 self.config = Configuration.Config(file=options.config, dbname=options.dbname)
102 # Selecting the right module for the OS
103 if self.config.os_family == "Linux":
104 import XTables as Tables
105 elif self.config.os_family == "Mac":
106 import OSXTables as Tables
107 elif self.config.os_family in ("XP", "Win7"):
108 import WinTables as Tables
109 log.info(f"HUD_main starting: Using db name = {db_name}")
110 self.Tables = Tables # Assign Tables to self.Tables
112 # Configuration du logging
113 if not options.errorsToConsole:
114 fileName = os.path.join(self.config.dir_log, "HUD-errors.txt")
115 log.info(f"Note: error output is being diverted to {fileName}.")
116 log.info("Any major error will be reported there *only*.")
117 errorFile = codecs.open(fileName, "w", "utf-8")
118 sys.stderr = errorFile
119 log.info("HUD_main starting")
121 try:
122 # Connecting to the database
123 self.db_connection = Database.Database(self.config)
125 # HUD dictionary and parameters
126 self.hud_dict = {}
127 self.blacklist = []
128 self.hud_params = self.config.get_hud_ui_parameters()
129 self.deck = Deck.Deck(
130 self.config,
131 deck_type=self.hud_params["deck_type"],
132 card_back=self.hud_params["card_back"],
133 width=self.hud_params["card_wd"],
134 height=self.hud_params["card_ht"],
135 )
137 # Cache initialization
138 self.cache = TTLCache(maxsize=1000, ttl=300) # Cache of 1000 elements with a TTL of 5 minutes
140 # Initialisation ZMQ avec QThread
141 self.zmq_receiver = ZMQReceiver(parent=self)
142 self.zmq_receiver.message_received.connect(self.handle_message)
143 self.zmq_worker = ZMQWorker(self.zmq_receiver)
144 self.zmq_worker.error_occurred.connect(self.handle_worker_error)
145 self.zmq_worker.start()
147 # Main window
148 self.init_main_window()
150 log.debug("Main window initialized and shown.")
151 except Exception as e:
152 log.error(f"Error during HUD_main initialization: {e}")
153 raise
155 def handle_worker_error(self, error_message):
156 log.error(f"ZMQWorker encountered an error: {error_message}")
158 def init_main_window(self):
159 self.main_window = QWidget(None, Qt.Dialog | Qt.WindowMinimizeButtonHint | Qt.WindowCloseButtonHint)
160 if self.options.xloc is not None or self.options.yloc is not None:
161 x = int(self.options.xloc) if self.options.xloc is not None else self.main_window.x()
162 y = int(self.options.yloc) if self.options.yloc is not None else self.main_window.y()
163 self.main_window.move(x, y)
164 self.main_window.destroyed.connect(self.destroy)
165 self.vb = QVBoxLayout()
166 self.vb.setContentsMargins(2, 0, 2, 0)
167 self.main_window.setLayout(self.vb)
168 self.label = QLabel("Closing this window will exit from the HUD.")
169 self.main_window.closeEvent = self.close_event_handler
170 self.vb.addWidget(self.label)
171 self.main_window.setWindowTitle("HUD Main Window")
172 cards = os.path.join(self.config.graphics_path, "tribal.jpg")
173 if cards:
174 self.main_window.setWindowIcon(QIcon(cards))
176 # Timer for periodically checking tables
177 self.check_tables_timer = QTimer(self)
178 self.check_tables_timer.timeout.connect(self.check_tables)
179 self.check_tables_timer.start(800)
180 self.main_window.show()
182 def close_event_handler(self, event):
183 self.destroy()
184 event.accept()
186 def handle_message(self, hand_id):
187 # This method will be called in the main thread
188 self.read_stdin(hand_id)
190 def destroy(self, *args):
191 if hasattr(self, "zmq_receiver"):
192 self.zmq_receiver.close()
193 if hasattr(self, "zmq_worker"):
194 self.zmq_worker.stop()
195 log.info("Quitting normally")
196 QCoreApplication.quit()
198 def check_tables(self):
199 if len(self.hud_dict) == 0:
200 log.info("Waiting for hands ...")
201 for tablename, hud in list(self.hud_dict.items()):
202 status = hud.table.check_table()
203 if status == "client_destroyed":
204 self.client_destroyed(None, hud)
205 elif status == "client_moved":
206 self.client_moved(None, hud)
207 elif status == "client_resized":
208 self.client_resized(None, hud)
210 if self.config.os_family == "Mac":
211 for hud in self.hud_dict.values():
212 for aw in hud.aux_windows:
213 if not hasattr(aw, "m_windows"):
214 continue
215 for w in aw.m_windows.values():
216 if w.isVisible():
217 hud.table.topify(w)
219 def client_moved(self, widget, hud):
220 log.debug("Client moved event")
221 self.idle_move(hud)
223 def client_resized(self, widget, hud):
224 log.debug("Client resized event")
225 self.idle_resize(hud)
227 def client_destroyed(self, widget, hud):
228 log.debug("Client destroyed event")
229 self.kill_hud(None, hud.table.key)
231 def table_title_changed(self, widget, hud):
232 log.debug("Table title changed, killing current HUD")
233 self.kill_hud(None, hud.table.key)
235 def table_is_stale(self, hud):
236 log.debug("Moved to a new table, killing current HUD")
237 self.kill_hud(None, hud.table.key)
239 def kill_hud(self, event, table):
240 log.debug("kill_hud event")
241 self.idle_kill(table)
243 def blacklist_hud(self, event, table):
244 log.debug("blacklist_hud event")
245 self.blacklist.append(self.hud_dict[table].tablenumber)
246 self.idle_kill(table)
248 def create_HUD(self, new_hand_id, table, temp_key, max, poker_game, type, stat_dict, cards):
249 log.debug(f"Creating HUD for table {temp_key} and hand {new_hand_id}")
250 self.hud_dict[temp_key] = Hud.Hud(self, table, max, poker_game, type, self.config)
251 self.hud_dict[temp_key].table_name = temp_key
252 self.hud_dict[temp_key].stat_dict = stat_dict
253 self.hud_dict[temp_key].cards = cards
254 self.hud_dict[temp_key].max = max
256 table.hud = self.hud_dict[temp_key]
258 self.hud_dict[temp_key].hud_params["new_max_seats"] = None # trigger for seat layout change
260 for aw in self.hud_dict[temp_key].aux_windows:
261 aw.update_data(new_hand_id, self.db_connection)
263 self.idle_create(new_hand_id, table, temp_key, max, poker_game, type, stat_dict, cards)
264 log.debug(f"HUD for table {temp_key} created successfully.")
266 def update_HUD(self, new_hand_id, table_name, config):
267 log.debug(f"Updating HUD for table {table_name} and hand {new_hand_id}")
268 self.idle_update(new_hand_id, table_name, config)
270 def read_stdin(self, new_hand_id):
271 log.debug(f"Processing new hand id: {new_hand_id}")
272 print(f"Entering read_stdin with hand_id: {new_hand_id}")
274 self.hero, self.hero_ids = {}, {}
275 found = False
277 enabled_sites = self.config.get_supported_sites()
278 if not enabled_sites:
279 log.error("No enabled sites found")
280 print("No enabled sites found")
281 self.db_connection.connection.rollback()
282 self.destroy()
283 return
285 aux_disabled_sites = []
286 for i in enabled_sites:
287 if not self.config.get_site_parameters(i)["aux_enabled"]:
288 log.info(f"Aux disabled for site {i}")
289 print(f"Aux disabled for site {i}")
290 aux_disabled_sites.append(i)
292 self.db_connection.connection.rollback() # Libérer le verrou de l'itération précédente
294 if not found:
295 for site in enabled_sites:
296 print("not found ... site in enabled_site")
297 if result := self.db_connection.get_site_id(site):
298 site_id = result[0][0]
299 self.hero[site_id] = self.config.supported_sites[site].screen_name
300 self.hero_ids[site_id] = self.db_connection.get_player_id(self.config, site, self.hero[site_id])
301 if self.hero_ids[site_id] is not None:
302 found = True
303 else:
304 self.hero_ids[site_id] = -1
306 if new_hand_id != "":
307 log.debug("HUD_main.read_stdin: Hand processing starting.")
308 print("HUD_main.read_stdin: Hand processing starting.")
309 if new_hand_id in self.cache:
310 log.debug(f"Using cached data for hand {new_hand_id}")
311 print(f"Data found in cache for hand_id: {new_hand_id}")
312 table_info = self.cache[new_hand_id]
313 else:
314 print(f"Data not found in cache for hand_id: {new_hand_id}")
315 try:
316 table_info = self.db_connection.get_table_info(new_hand_id)
317 self.cache[new_hand_id] = table_info # Mise en cache des informations
318 except Exception as e:
319 log.error(f"Database error while processing hand {new_hand_id}: {e}", exc_info=True)
320 print("Database error while processing hand")
321 return
323 (table_name, max, poker_game, type, fast, site_id, site_name, num_seats, tour_number, tab_number) = table_info
325 if fast:
326 return
328 if site_name in aux_disabled_sites:
329 return
330 if site_name not in enabled_sites:
331 return
333 # Generating the temporary key
334 if type == "tour":
335 try:
336 log.debug("creating temp_key for tour")
337 # if len(table_name) >= 2 and table_name[-2].endswith(","):
338 # parts = table_name.split(",", 1)
339 # else:
340 # parts = table_name.split(" ", 1)
342 tab_number = tab_number.rsplit(" ", 1)[-1]
343 temp_key = f"{tour_number} Table {tab_number}"
344 log.debug(f"temp_key {temp_key}")
345 except ValueError:
346 log.error("Both tab_number and table_name not working")
347 else:
348 temp_key = table_name
350 # Managing table changes for tournaments
351 if type == "tour":
352 if temp_key in self.hud_dict:
353 if self.hud_dict[temp_key].table.has_table_title_changed(self.hud_dict[temp_key]):
354 log.debug("table has been renamed")
355 self.table_is_stale(self.hud_dict[temp_key])
356 return
357 else:
358 for k in self.hud_dict:
359 log.debug("check if the tournament number is in the hud_dict under a different table")
360 if k.startswith(tour_number):
361 self.table_is_stale(self.hud_dict[k])
362 continue
364 # Detection of max_seats and poker_game changes
365 if temp_key in self.hud_dict:
366 with contextlib.suppress(Exception):
367 newmax = self.hud_dict[temp_key].hud_params["new_max_seats"]
368 log.debug(f"newmax {newmax}")
369 if newmax and self.hud_dict[temp_key].max != newmax:
370 log.debug("going to kill_hud due to max seats change")
371 self.kill_hud("activate", temp_key)
372 while temp_key in self.hud_dict:
373 time.sleep(0.5)
374 max = newmax
375 self.hud_dict[temp_key].hud_params["new_max_seats"] = None
377 if self.hud_dict[temp_key].poker_game != poker_game:
378 with contextlib.suppress(Exception):
379 log.debug("going to kill_hud due to poker game change")
380 self.kill_hud("activate", temp_key)
381 while temp_key in self.hud_dict:
382 time.sleep(0.5)
384 # Updating or creating the HUD
385 if temp_key in self.hud_dict:
386 log.debug(f"update hud for hand {new_hand_id}")
387 self.db_connection.init_hud_stat_vars(
388 self.hud_dict[temp_key].hud_params["hud_days"], self.hud_dict[temp_key].hud_params["h_hud_days"]
389 )
390 stat_dict = self.db_connection.get_stats_from_hand(
391 new_hand_id, type, self.hud_dict[temp_key].hud_params, self.hero_ids[site_id], num_seats
392 )
393 log.debug(f"got stats for hand {new_hand_id}")
395 try:
396 self.hud_dict[temp_key].stat_dict = stat_dict
397 except KeyError:
398 log.error(f"hud_dict[{temp_key}] was not found")
399 log.error("will not send hand")
400 return
402 self.hud_dict[temp_key].cards = self.get_cards(new_hand_id, poker_game)
403 for aw in self.hud_dict[temp_key].aux_windows:
404 aw.update_data(new_hand_id, self.db_connection)
405 self.update_HUD(new_hand_id, temp_key, self.config)
406 log.debug(f"hud updated for table {temp_key} and hand {new_hand_id}")
407 else:
408 log.debug(f"create new hud for hand {new_hand_id}")
409 self.db_connection.init_hud_stat_vars(self.hud_params["hud_days"], self.hud_params["h_hud_days"])
410 stat_dict = self.db_connection.get_stats_from_hand(
411 new_hand_id, type, self.hud_params, self.hero_ids[site_id], num_seats
412 )
413 log.debug(f"got stats for hand {new_hand_id}")
415 hero_found = any(stat_dict[key]["screen_name"] == self.hero[site_id] for key in stat_dict)
416 if not hero_found:
417 log.info("HUD not created yet, because hero is not seated for this hand")
418 return
420 cards = self.get_cards(new_hand_id, poker_game)
421 table_kwargs = dict(table_name=table_name, tournament=tour_number, table_number=tab_number)
422 tablewindow = self.Tables.Table(self.config, site_name, **table_kwargs)
423 if tablewindow.number is None:
424 log.debug("tablewindow.number is none")
425 if type == "tour":
426 table_name = f"{tour_number} {tab_number}"
427 log.error(f"HUD create: table name {table_name} not found, skipping.")
428 return
429 elif tablewindow.number in self.blacklist:
430 return
431 else:
432 log.debug("tablewindow.number is not none")
433 tablewindow.key = temp_key
434 tablewindow.max = max
435 tablewindow.site = site_name
436 if hasattr(tablewindow, "number"):
437 log.debug("table window still exists")
438 self.create_HUD(new_hand_id, tablewindow, temp_key, max, poker_game, type, stat_dict, cards)
439 else:
440 log.error(f'Table "{table_name}" no longer exists')
441 return
443 def get_cards(self, new_hand_id, poker_game):
444 cards = self.db_connection.get_cards(new_hand_id)
445 if poker_game in ["holdem", "omahahi", "omahahilo"]:
446 comm_cards = self.db_connection.get_common_cards(new_hand_id)
447 cards["common"] = comm_cards["common"]
448 return cards
450 def idle_move(self, hud):
451 try:
452 hud.move_table_position()
453 for aw in hud.aux_windows:
454 aw.move_windows()
455 except Exception:
456 log.exception(f"Error moving HUD for table: {hud.table.title}.")
458 def idle_resize(self, hud):
459 try:
460 hud.resize_windows()
461 for aw in hud.aux_windows:
462 aw.resize_windows()
463 except Exception:
464 log.exception(f"Error resizing HUD for table: {hud.table.title}.")
466 def idle_kill(self, table):
467 try:
468 if table in self.hud_dict:
469 self.vb.removeWidget(self.hud_dict[table].tablehudlabel)
470 self.hud_dict[table].tablehudlabel.setParent(None)
471 self.hud_dict[table].kill()
472 del self.hud_dict[table]
473 self.main_window.resize(1, 1)
474 except Exception:
475 log.exception(f"Error killing HUD for table: {table}.")
477 def idle_create(self, new_hand_id, table, temp_key, max, poker_game, type, stat_dict, cards):
478 try:
479 newlabel = QLabel(f"{table.site} - {temp_key}")
480 log.debug(f"adding label {newlabel.text()}")
481 self.vb.addWidget(newlabel)
483 self.hud_dict[temp_key].tablehudlabel = newlabel
484 self.hud_dict[temp_key].tablenumber = table.number
485 self.hud_dict[temp_key].create(new_hand_id, self.config, stat_dict)
486 for m in self.hud_dict[temp_key].aux_windows:
487 m.create()
488 log.debug(f"idle_create new_hand_id {new_hand_id}")
489 m.update_gui(new_hand_id)
491 except Exception:
492 log.exception(f"Error creating HUD for hand {new_hand_id}.")
494 def idle_update(self, new_hand_id, table_name, config):
495 try:
496 log.debug(f"idle_update entered for {table_name} {new_hand_id}")
497 self.hud_dict[table_name].update(new_hand_id, config)
498 log.debug(f"idle_update update_gui {new_hand_id}")
499 for aw in self.hud_dict[table_name].aux_windows:
500 aw.update_gui(new_hand_id)
501 except Exception:
502 log.exception(f"Error updating HUD for hand {new_hand_id}.")
505if __name__ == "__main__":
506 (options, argv) = Options.fpdb_options()
508 app = QApplication([])
509 apply_stylesheet(app, theme="dark_purple.xml")
511 hm = HUD_main(options, db_name=options.dbname)
513 app.exec_()