Coverage for HUD_main.pyw: 78%
378 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-18 00:10 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-18 00:10 +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}")
273 self.hero, self.hero_ids = {}, {}
274 found = False
276 enabled_sites = self.config.get_supported_sites()
277 if not enabled_sites:
278 log.error("No enabled sites found")
279 self.db_connection.connection.rollback()
280 self.destroy()
281 return
283 aux_disabled_sites = []
284 for i in enabled_sites:
285 if not self.config.get_site_parameters(i)["aux_enabled"]:
286 log.info(f"Aux disabled for site {i}")
287 aux_disabled_sites.append(i)
289 self.db_connection.connection.rollback() # Libérer le verrou de l'itération précédente
291 if not found:
292 for site in enabled_sites:
293 log.debug("not found ... site in enabled_site")
294 if result := self.db_connection.get_site_id(site):
295 site_id = result[0][0]
296 self.hero[site_id] = self.config.supported_sites[site].screen_name
297 self.hero_ids[site_id] = self.db_connection.get_player_id(self.config, site, self.hero[site_id])
298 if self.hero_ids[site_id] is not None:
299 found = True
300 else:
301 self.hero_ids[site_id] = -1
303 if new_hand_id != "":
304 log.debug("HUD_main.read_stdin: Hand processing starting.")
305 if new_hand_id in self.cache:
306 log.debug(f"Using cached data for hand {new_hand_id}")
307 table_info = self.cache[new_hand_id]
308 else:
309 log.debug(f"Data not found in cache for hand_id: {new_hand_id}")
310 try:
311 table_info = self.db_connection.get_table_info(new_hand_id)
312 self.cache[new_hand_id] = table_info # Mise en cache des informations
313 except Exception as e:
314 log.error(f"Database error while processing hand {new_hand_id}: {e}", exc_info=True)
315 return
317 (table_name, max, poker_game, type, fast, site_id, site_name, num_seats, tour_number, tab_number) = table_info
319 if fast:
320 return
322 if site_name in aux_disabled_sites:
323 return
324 if site_name not in enabled_sites:
325 return
327 # Generating the temporary key
328 if type == "tour":
329 try:
330 log.debug("creating temp_key for tour")
331 # if len(table_name) >= 2 and table_name[-2].endswith(","):
332 # parts = table_name.split(",", 1)
333 # else:
334 # parts = table_name.split(" ", 1)
336 tab_number = tab_number.rsplit(" ", 1)[-1]
337 temp_key = f"{tour_number} Table {tab_number}"
338 log.debug(f"temp_key {temp_key}")
339 except ValueError:
340 log.error("Both tab_number and table_name not working")
341 else:
342 temp_key = table_name
344 # Managing table changes for tournaments
345 if type == "tour":
346 if temp_key in self.hud_dict:
347 if self.hud_dict[temp_key].table.has_table_title_changed(self.hud_dict[temp_key]):
348 log.debug("table has been renamed")
349 self.table_is_stale(self.hud_dict[temp_key])
350 return
351 else:
352 for k in self.hud_dict:
353 log.debug("check if the tournament number is in the hud_dict under a different table")
354 if k.startswith(tour_number):
355 self.table_is_stale(self.hud_dict[k])
356 continue
358 # Detection of max_seats and poker_game changes
359 if temp_key in self.hud_dict:
360 with contextlib.suppress(Exception):
361 newmax = self.hud_dict[temp_key].hud_params["new_max_seats"]
362 log.debug(f"newmax {newmax}")
363 if newmax and self.hud_dict[temp_key].max != newmax:
364 log.debug("going to kill_hud due to max seats change")
365 self.kill_hud("activate", temp_key)
366 while temp_key in self.hud_dict:
367 time.sleep(0.5)
368 max = newmax
369 self.hud_dict[temp_key].hud_params["new_max_seats"] = None
371 if self.hud_dict[temp_key].poker_game != poker_game:
372 with contextlib.suppress(Exception):
373 log.debug("going to kill_hud due to poker game change")
374 self.kill_hud("activate", temp_key)
375 while temp_key in self.hud_dict:
376 time.sleep(0.5)
378 # Updating or creating the HUD
379 if temp_key in self.hud_dict:
380 log.debug(f"update hud for hand {new_hand_id}")
381 self.db_connection.init_hud_stat_vars(
382 self.hud_dict[temp_key].hud_params["hud_days"], self.hud_dict[temp_key].hud_params["h_hud_days"]
383 )
384 stat_dict = self.db_connection.get_stats_from_hand(
385 new_hand_id, type, self.hud_dict[temp_key].hud_params, self.hero_ids[site_id], num_seats
386 )
387 log.debug(f"got stats for hand {new_hand_id}")
389 try:
390 self.hud_dict[temp_key].stat_dict = stat_dict
391 except KeyError:
392 log.error(f"hud_dict[{temp_key}] was not found")
393 log.error("will not send hand")
394 return
396 self.hud_dict[temp_key].cards = self.get_cards(new_hand_id, poker_game)
397 for aw in self.hud_dict[temp_key].aux_windows:
398 aw.update_data(new_hand_id, self.db_connection)
399 self.update_HUD(new_hand_id, temp_key, self.config)
400 log.debug(f"hud updated for table {temp_key} and hand {new_hand_id}")
401 else:
402 log.debug(f"create new hud for hand {new_hand_id}")
403 self.db_connection.init_hud_stat_vars(self.hud_params["hud_days"], self.hud_params["h_hud_days"])
404 stat_dict = self.db_connection.get_stats_from_hand(
405 new_hand_id, type, self.hud_params, self.hero_ids[site_id], num_seats
406 )
407 log.debug(f"got stats for hand {new_hand_id}")
409 hero_found = any(stat_dict[key]["screen_name"] == self.hero[site_id] for key in stat_dict)
410 if not hero_found:
411 log.info("HUD not created yet, because hero is not seated for this hand")
412 return
414 cards = self.get_cards(new_hand_id, poker_game)
415 table_kwargs = dict(table_name=table_name, tournament=tour_number, table_number=tab_number)
416 tablewindow = self.Tables.Table(self.config, site_name, **table_kwargs)
417 if tablewindow.number is None:
418 log.debug("tablewindow.number is none")
419 if type == "tour":
420 table_name = f"{tour_number} {tab_number}"
421 log.error(f"HUD create: table name {table_name} not found, skipping.")
422 return
423 elif tablewindow.number in self.blacklist:
424 return
425 else:
426 log.debug("tablewindow.number is not none")
427 tablewindow.key = temp_key
428 tablewindow.max = max
429 tablewindow.site = site_name
430 if hasattr(tablewindow, "number"):
431 log.debug("table window still exists")
432 self.create_HUD(new_hand_id, tablewindow, temp_key, max, poker_game, type, stat_dict, cards)
433 else:
434 log.error(f'Table "{table_name}" no longer exists')
435 return
437 def get_cards(self, new_hand_id, poker_game):
438 cards = self.db_connection.get_cards(new_hand_id)
439 if poker_game in ["holdem", "omahahi", "omahahilo"]:
440 comm_cards = self.db_connection.get_common_cards(new_hand_id)
441 cards["common"] = comm_cards["common"]
442 return cards
444 def idle_move(self, hud):
445 try:
446 hud.move_table_position()
447 for aw in hud.aux_windows:
448 aw.move_windows()
449 except Exception:
450 log.exception(f"Error moving HUD for table: {hud.table.title}.")
452 def idle_resize(self, hud):
453 try:
454 hud.resize_windows()
455 for aw in hud.aux_windows:
456 aw.resize_windows()
457 except Exception:
458 log.exception(f"Error resizing HUD for table: {hud.table.title}.")
460 def idle_kill(self, table):
461 try:
462 if table in self.hud_dict:
463 self.vb.removeWidget(self.hud_dict[table].tablehudlabel)
464 self.hud_dict[table].tablehudlabel.setParent(None)
465 self.hud_dict[table].kill()
466 del self.hud_dict[table]
467 self.main_window.resize(1, 1)
468 except Exception:
469 log.exception(f"Error killing HUD for table: {table}.")
471 def idle_create(self, new_hand_id, table, temp_key, max, poker_game, type, stat_dict, cards):
472 try:
473 newlabel = QLabel(f"{table.site} - {temp_key}")
474 log.debug(f"adding label {newlabel.text()}")
475 self.vb.addWidget(newlabel)
477 self.hud_dict[temp_key].tablehudlabel = newlabel
478 self.hud_dict[temp_key].tablenumber = table.number
479 self.hud_dict[temp_key].create(new_hand_id, self.config, stat_dict)
480 for m in self.hud_dict[temp_key].aux_windows:
481 m.create()
482 log.debug(f"idle_create new_hand_id {new_hand_id}")
483 m.update_gui(new_hand_id)
485 except Exception:
486 log.exception(f"Error creating HUD for hand {new_hand_id}.")
488 def idle_update(self, new_hand_id, table_name, config):
489 try:
490 log.debug(f"idle_update entered for {table_name} {new_hand_id}")
491 self.hud_dict[table_name].update(new_hand_id, config)
492 log.debug(f"idle_update update_gui {new_hand_id}")
493 for aw in self.hud_dict[table_name].aux_windows:
494 aw.update_gui(new_hand_id)
495 except Exception:
496 log.exception(f"Error updating HUD for hand {new_hand_id}.")
499if __name__ == "__main__":
500 (options, argv) = Options.fpdb_options()
502 app = QApplication([])
503 apply_stylesheet(app, theme="dark_purple.xml")
505 hm = HUD_main(options, db_name=options.dbname)
507 app.exec_()