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