Commit 9ab100f3 authored by Antoine Lima's avatar Antoine Lima

New package structure (Babyfut as root)

Many changes but mainly changing things like `from ui import ..`
to `from Babyfut.ui import ..`
parent fd0e6b96
......@@ -13,7 +13,7 @@ The UI is done with PyQt5.
* Make sure to have Python>3.5 installed (`sudo apt-get install python3` otherwise)
* Make sure to have PyQt5 installed (`sudo pip install pyqt5` otherwise)
* Compile the .ui and .rc files: `./devtools.sh ui`
* Execute main.py: `python3 babyfut.py`
* Execute the software: `./devtools.sh exec`
## Edit the UI
The UI is mostly created using Qt-Designer:
......
......@@ -17,10 +17,10 @@ from PyQt5.QtWidgets import QMainWindow, QApplication
def getContent(path):
contentFolder = join(dirname(dirname(abspath(__file__))), 'content')
return join(contentFolder, path)
def getMainWin():
from ui.mainwin import MainWin
from Babyfut.ui.mainwin import MainWin
# Global function to find the (open) QMainWindow in application
for widget in QApplication.instance().topLevelWidgets():
if isinstance(widget, QMainWindow):
......@@ -28,46 +28,46 @@ def getMainWin():
return None
if __name__=='__main__':
from ui.mainwin import MainWin
from modules import GameModule
from player import Side
from settings import Settings
from input import GPIOThread
from database import Database
from replay import Replay as ReplayThread
print(__package__)
from Babyfut.ui.mainwin import MainWin
from Babyfut.modules import GameModule
from Babyfut.core.player import Side
from Babyfut.core.settings import Settings
from Babyfut.core.input import GPIOThread
from Babyfut.core.database import Database
from Babyfut.core.replay import Replay as ReplayThread
try:
#logging.basicConfig(filename='babyfoot.log', level=logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)
app = QtWidgets.QApplication(sys.argv)
lang = Settings['ui.language']
qtTranslator = QtCore.QTranslator()
if lang!='en' and qtTranslator.load("translations/babyfut_{}.qm".format(lang)):
app.installTranslator(qtTranslator)
myapp = MainWin()
if ReplayThread.isCamAvailable():
threadReplay = ReplayThread(Side.Left)
threadReplay.start()
myapp.dispatchMessage({'replayThread': threadReplay}, toType=GameModule)
threadGPIO = GPIOThread(myapp)
threadGPIO.start()
myapp.show()
app.exec_()
threadGPIO.stop()
if ReplayThread.isCamAvailable():
threadReplay.stop()
threadReplay.join()
threadGPIO.join()
finally:
GPIOThread.clean()
Database.instance().close()
......@@ -11,42 +11,42 @@ class DatabaseError(Exception):
class Database():
__db = None
def __init__(self):
if not Database.__db:
from babyfut import getContent
from Babyfut.babyfut import getContent
db_path = getContent('babyfut.sqlite')
self._connection = sqlite3.connect(db_path)
@staticmethod
def instance():
'''
Singleton
'''
if not Database.__db:
Database.__db = Database()
return Database.__db
@property
def _cursor(self):
return self._connection.cursor()
def select_one(self, query, *args):
res = self._cursor.execute(query, args).fetchone()
if not res:
raise DatabaseError('Query \"{}\" returned nothing with args {}'.format(query, args))
return res
def select_guest_team(self):
return self.select_one('SELECT id FROM players WHERE fname LIKE "guest"')[0]
def insert_team(self, players, goals):
if len(players)<2:
players.append(None)
self._cursor.execute('INSERT INTO Teams (nGoals, player1, player2) VALUES (?, ?, ?)', (goals, players[0], players[1],))
self._connection.commit()
return self._cursor.execute('SELECT seq FROM sqlite_sequence WHERE name="Teams"').fetchone()[0]
......@@ -56,7 +56,7 @@ class Database():
self._connection.commit()
def select_all_rfid(self, debug=False):
from settings import Settings
from Babyfut.core.settings import Settings
if Settings['app.mode']=='prod':
return self._cursor.execute('SELECT rfid FROM Players WHERE rfid>0').fetchall()
else:
......@@ -65,6 +65,6 @@ class Database():
def delete_player(self, playerID):
self._cursor.execute('DELETE FROM Players WHERE id==?', (playerID,))
self._connection.commit()
def close(self):
self._connection.close()
......@@ -8,13 +8,12 @@ import logging
import pyautogui # PyPi library
from threading import Thread
from babyfut import OnRasp
from Babyfut.babyfut import OnRasp
from Babyfut.core.player import Side
if OnRasp:
import RPi.GPIO as GPIO
from player import Side
class GPIOThread(Thread):
_keyButtonBindings = {
26: 'up',
......@@ -29,7 +28,7 @@ class GPIOThread(Thread):
Thread.__init__(self)
self.dispatcher = dispatcher
self.continueRunning = True
if OnRasp:
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
......@@ -54,10 +53,10 @@ class GPIOThread(Thread):
key = GPIOThread._keyButtonBindings[button_pin]
logging.debug('Sending {} as {}'.format(button_pin, key))
pyautogui.press(key)
def stop(self):
self.continueRunning = False
@staticmethod
def clean():
if OnRasp:
......
......@@ -8,12 +8,13 @@ Created on Wed Apr 18 18:34:40 2018
import logging
from enum import Enum
from database import Database, DatabaseError
from PyQt5.QtCore import Qt, QCoreApplication
from PyQt5.QtWidgets import QDialog
from ui.consent_dialog_ui import Ui_Dialog as ConsentDialogUI
from Babyfut.babyfut import getMainWin
from Babyfut.core.database import Database, DatabaseError
from Babyfut.ui.consent_dialog_ui import Ui_Dialog as ConsentDialogUI
class Side(Enum):
'''
......@@ -22,17 +23,17 @@ class Side(Enum):
Undef = -1
Left = 0
Right = 1
@property
def opposite(self):
return Side.Right if self==Side.Left else Side.Left
return Side.Right if self==Side.Left else Side.Left
class ConsentDialog(QDialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
self.ui = ConsentDialogUI()
self.ui.setupUi(self)
self.ui.txtConsent.setHtml(QCoreApplication.translate('consent', '''<p>
You are about to connect yourself for the first time. We will need to access:
<ul>
......@@ -40,29 +41,29 @@ class ConsentDialog(QDialog):
<li>Your Picture (if public)</li>
<li>...</li>
</ul>
</p>
</p>
<p>
It is possible to play withtout connecting yourslef, but this will allow you to keep track of your score and to provide a better experience for you and the ones you play with!
<br/><br/>
Do you agree with this?
</p>'''))
def keyPressEvent(self, e):
if e.key()==Qt.Key_Return:
self.accept()
else:
self.reject()
class Player():
__query_infos = 'SELECT id, fname, lname, pic FROM Players WHERE rfid==?'
__query_time_goals_games = 'SELECT SUM(Matchs.duration) AS timePlayed, SUM(Teams.nGoals) AS goalsScored, COUNT(*) AS gamesPlayed FROM Teams INNER JOIN Matchs ON (Teams.id==Matchs.winningTeam OR Teams.id==Matchs.losingTeam) WHERE (Teams.player1==? OR player2==?)'
__query_victories = 'SELECT COUNT(*) AS victories FROM Players INNER JOIN Teams ON (Players.id==Teams.player1 OR Players.id==Teams.player2) INNER JOIN Matchs ON (Teams.id==Matchs.winningTeam) WHERE Players.id==?'
_placeholder_pic_path = ':ui/img/placeholder_head.jpg'
_first_time = True # Debug
def __init__(self, id, rfid, fname, lname, pic_path, stats):
self.id = id
self.rfid = rfid
......@@ -70,52 +71,60 @@ class Player():
self.lname = lname
self.pic_path = pic_path if pic_path else Player._placeholder_pic_path # Default pic if None
self.stats = stats
@staticmethod
def fromRFID(rfid):
db = Database.instance()
try:
# Retrieve generic informations
id, fname, lname, pic = db.select_one(Player.__query_infos, rfid)
# Ask for consent
if rfid==-2 and Player._first_time:
import babyfut
consentDialog = ConsentDialog(babyfut.getMainWin())
consentDialog = ConsentDialog(getMainWin())
consentDialog.exec()
if not consentDialog.result()==QDialog.Accepted:
Player._first_time = False
return PlayerGuest
# Retrieve stats
stats = {}
stats['time_played'], stats['goals_scored'], stats['games_played'] = db.select_one(Player.__query_time_goals_games, id, id)
stats['victories'], = db.select_one(Player.__query_victories, id)
for key, val in stats.items():
if val==None:
stats[key] = 0
return Player(id, rfid, fname, lname, pic, stats)
except DatabaseError as e:
logging.warn('DB Error: {}'.format(e))
return PlayerGuest
def displayImg(self, containerWidget):
containerWidget.setStyleSheet('border-image: url({});'.format(self.pic_path))
def displayImg(self, container_widget):
self.pic_container = container_widget
if self.pic_path.startswith('http'):
# Download from the internet
container_widget.setStyleSheet('border-image: url({});'.format(Player._placeholder_pic_path))
else:
# Already downloaded and stored locally
container_widget.setStyleSheet('border-image: url({});'.format(self.pic_path))
def save(self):
'''
Update or create the player in database
'''
pass
# DONT SAVE PIC PATH, IT IS CHANGED FOR THE LOCAL PATH
@property
def name(self):
return '{} {}'.format(self.fname, self.lname.upper())
@property
def stats_property(self):
'''
......@@ -129,9 +138,9 @@ class Player():
self.time_played = stats['time_played']
self.goals_scored = stats['goals_scored']
self.games_played = stats['games_played']
return Stat(self.stats)
@staticmethod
def allStoredPlayers():
return [Player.fromRFID(rfid) for rfid, in Database.instance().select_all_rfid()]
......
......@@ -6,8 +6,8 @@
from threading import Thread, Event
from babyfut import getContent, OnRasp
from settings import Settings
from Babyfut.babyfut import getContent, OnRasp
from Babyfut.core.settings import Settings
if OnRasp:
import picamera
......@@ -17,7 +17,7 @@ class Replay(Thread):
Thread.__init__(self)
self.replayPath = getContent('Replay {}.mp4'.format(side.name))
self.shutdown = False
self.start_flag = Event()
self.stop_flag = Event()
self.stopped_flag = Event()
......@@ -33,12 +33,12 @@ class Replay(Thread):
def start_recording(self):
if OnRasp:
self.start_flag.set()
def stop_recording(self):
if OnRasp:
self.stop_flag.set()
self.stopped_flag.wait()
# Clear all control flags
self.stop_flag.clear()
self.start_flag.clear()
......@@ -49,20 +49,20 @@ class Replay(Thread):
def stop(self):
self.start_flag.set()
self.shutdown = True
def run(self):
while not self.shutdown:
self.start_flag.wait()
if not self.shutdown:
self.cam.start_recording(self.stream, Settings['picam.format'])
try:
while not self.stop_flag.is_set():
self.cam.wait_recording(1)
finally :
self.cam.stop_recording()
self.stream.copy_to(self.replayPath)
self.stream.clear()
......@@ -71,11 +71,11 @@ class Replay(Thread):
self.cam.close()
self.stream.close()
@classmethod
def Dummy(cls):
return getContent('Replay Left.mp4')
@staticmethod
def isCamAvailable():
return OnRasp # and other checks (ToDo)
......@@ -8,7 +8,7 @@ import json
class MyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Setting):
if isinstance(obj, Setting):
return obj.__dict__
elif isinstance(obj, String) and obj=='settingsPath':
return None
......@@ -17,65 +17,65 @@ class MyEncoder(json.JSONEncoder):
class Setting(object):
TypeName = ''
def __init__(self, value):
self.type = type(self).TypeName
self.value = value
class SettingBoolean(Setting):
TypeName = 'boolean'
def __init__(self, value):
Setting.__init__(self, value)
class SettingCombo(Setting):
TypeName = 'combo'
def __init__(self, value, values):
Setting.__init__(self, value)
self.values = values
if self.value not in values:
raise ValueError('Setting value {} not in list of possible values {}'.format(self.value, self.values))
class SettingRange(Setting):
TypeName = 'range'
def __init__(self, value, limits):
Setting.__init__(self, value)
self.range = [min(limits), max(limits)]
if self.value<self.range[0] or self.value>self.range[1]:
raise ValueError('Setting value {} not in range {}'.format(self.value, (self.lower_limit, self.upper_limit)))
class SettingsHolder(object):
def __init__(self, settingsPath):
self.settingsPath = settingsPath
self.loadSettingsFromJSON()
def __delitem__(self, key):
pass
def __getitem__(self, key):
subkeys = SettingsHolder._parseKey(key)
if len(subkeys) == 2:
return getattr(self, subkeys[0])[subkeys[1]].value
elif len(subkeys) == 1:
return getattr(self, subkeys[0]).value
else:
raise IndexError('Invalid key {}'.format(key))
def __setitem__(self, key, value):
subkeys = SettingsHolder._parseKey(key)
if len(subkeys) == 2:
getattr(self, subkeys[0])[subkeys[1]].value = value
elif len(subkeys) == 1:
getattr(self, subkeys[0]).value = value
else:
raise IndexError('Invalid key {}'.format(key))
@staticmethod
def _parseKey(key):
return [k for k in key.split('.') if k]
......@@ -83,18 +83,18 @@ class SettingsHolder(object):
def loadSettingsFromJSON(self):
with open(self.settingsPath, 'r') as f:
content = json.load(f)
# Outer loop, setting category
for cat in content:
setattr(self, cat, dict())
content_outer = content[cat]
# Inner loop, setting type
for name in content_outer:
content_inner = content_outer[name]
typeName = content_inner['type']
value = content_inner['value']
# Switch over types
if typeName == SettingBoolean.TypeName:
setting = SettingBoolean(value)
......@@ -108,14 +108,14 @@ class SettingsHolder(object):
getattr(self, cat)[name] = setting
def saveSettingsToJSON(self):
# Deletes the settings path member to prevent it from being saved in the JSON
# Deletes the settings path member to prevent it from being saved in the JSON
settingsPath = self.settingsPath
del self.settingsPath
with open(settingsPath, 'w') as f:
json.dump(self.__dict__, f, cls=MyEncoder, indent=4)
self.settingsPath = settingsPath
from babyfut import getContent
from Babyfut.babyfut import getContent
Settings = SettingsHolder(getContent('settings.json'))
......@@ -7,25 +7,25 @@ cd "$( dirname "${BASH_SOURCE[0]}" )"
case "$1" in
"ui"|"update_ui"|"build_ui")
echo "Building UI..."
echo " Modules"
pyuic5 --import-from=ui ui/main.ui -o ui/main_ui.py
pyuic5 --import-from=ui ui/menu.ui -o ui/menu_ui.py
pyuic5 --import-from=ui ui/game.ui -o ui/game_ui.py
pyuic5 --import-from=ui ui/endgame.ui -o ui/endgame_ui.py
pyuic5 --import-from=ui ui/options.ui -o ui/options_ui.py
pyuic5 --import-from=ui ui/authquick.ui -o ui/authquick_ui.py
pyuic5 --import-from=ui ui/authleague.ui -o ui/authleague_ui.py
pyuic5 --import-from=ui ui/leaderboard.ui -o ui/leaderboard_ui.py
pyuic5 --import-from=ui ui/privacy.ui -o ui/privacy_ui.py
pyuic5 --import-from=Babyfut.ui ui/main.ui -o ui/main_ui.py
pyuic5 --import-from=Babyfut.ui ui/menu.ui -o ui/menu_ui.py
pyuic5 --import-from=Babyfut.ui ui/game.ui -o ui/game_ui.py
pyuic5 --import-from=Babyfut.ui ui/endgame.ui -o ui/endgame_ui.py
pyuic5 --import-from=Babyfut.ui ui/options.ui -o ui/options_ui.py
pyuic5 --import-from=Babyfut.ui ui/authquick.ui -o ui/authquick_ui.py
pyuic5 --import-from=Babyfut.ui ui/authleague.ui -o ui/authleague_ui.py
pyuic5 --import-from=Babyfut.ui ui/leaderboard.ui -o ui/leaderboard_ui.py
pyuic5 --import-from=Babyfut.ui ui/privacy.ui -o ui/privacy_ui.py
echo " Custom Widgets"
pyuic5 --import-from=ui ui/playerlist.ui -o ui/playerlist_ui.py
pyuic5 --import-from=ui ui/delete_dialog.ui -o ui/delete_dialog_ui.py
pyuic5 --import-from=ui ui/consent_dialog.ui -o ui/consent_dialog_ui.py
pyuic5 --import-from=Babyfut.ui ui/playerlist.ui -o ui/playerlist_ui.py
pyuic5 --import-from=Babyfut.ui ui/delete_dialog.ui -o ui/delete_dialog_ui.py
pyuic5 --import-from=Babyfut.ui ui/consent_dialog.ui -o ui/consent_dialog_ui.py
echo " Resources"
pyrcc5 -root /ui ui/assets.qrc -o ui/assets_rc.py
pyrcc5 -root /Babyfut/ui ui/assets.qrc -o ui/assets_rc.py
echo "Done."
;;
"clean"|"clear")
......@@ -36,6 +36,7 @@ case "$1" in
rm -rf __pycache__
rm -rf ui/__pycache__
rm -rf core/__pycache__
rm -rf modules/__pycache__
echo "Done."
;;
......@@ -58,6 +59,10 @@ case "$1" in
bash ./devtools.sh "clean"
bash ./devtools.sh "all"
;;
"exec")
cd ..
python -m Babyfut.babyfut
;;
*)
echo "Unknown command \"$1\". See content of script for available commands."
;;
......
from modules.authquick import AuthQuickModule
from modules.authleague import AuthLeagueModule
from modules.game import GameModule
from modules.endgame import EndGameModule
from modules.menu import MenuModule
from modules.options import OptionsModule
from modules.leaderboard import LeaderboardModule
from modules.privacy import PrivacyModule
from Babyfut.modules.authquick import AuthQuickModule
from Babyfut.modules.authleague import AuthLeagueModule
from Babyfut.modules.game import GameModule
from Babyfut.modules.endgame import EndGameModule
from Babyfut.modules.menu import MenuModule
from Babyfut.modules.options import OptionsModule
from Babyfut.modules.leaderboard import LeaderboardModule
from Babyfut.modules.privacy import PrivacyModule
......@@ -8,9 +8,9 @@ import logging
from PyQt5.QtCore import Qt
import modules
from module import Module
from player import Side, Player
from Babyfut import modules
from Babyfut.core.module import Module
from Babyfut.core.player import Side, Player
class AuthModuleBase(Module):
def __init__(self, parent, widget):
......@@ -20,12 +20,12 @@ class AuthModuleBase(Module):
def load(self):
pass
def unload(self):
self.createPlayerList()
self.numPlayers = 0
def other(self, **kwargs):
def other(self, **kwargs):
for key, val in kwargs.items():
if key=='rfid' and 'source' in kwargs:
side = kwargs['source']
......@@ -35,10 +35,10 @@ class AuthModuleBase(Module):
def keyPressEvent(self, e):
if e.key() == Qt.Key_Escape:
self.handleCancel()
elif e.key() == Qt.Key_Return:
self.handleDone()
elif e.key() == Qt.Key_Left or e.key() == Qt.Key_Right:
side = Side.Left if e.key() == Qt.Key_Left else Side.Right
rfid = -(2 + self.numPlayers%5)
......@@ -46,7 +46,7 @@ class AuthModuleBase(Module):
def createPlayerList(self):
logging.warning('Base function meant to be reimplemented')
def handleCancel(self):
self.switchModule(modules.MenuModule)
......
......@@ -7,57 +7,56 @@
import logging
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import QAbstractItemView
from modules.auth import AuthModuleBase
from ui.authleague_ui import Ui_Form as AuthLeagueWidget
from player import Side, PlayerEmpty
from Babyfut.modules.auth import AuthModuleBase
from Babyfut.ui.authleague_ui import Ui_Form as AuthLeagueWidget
from Babyfut.core.player import Side, PlayerEmpty
class AuthLeagueModule(AuthModuleBase):
def __init__(self, parent):
super().__init__(parent, AuthLeagueWidget())
def load(self):
logging.debug('Loading AuthLeagueModule')
super().load()
self.addPlayer(Side.Left, PlayerEmpty)
def unload(self):
logging.debug('Loading AuthLeagueModule')
super().unload()
self.ui.playersList.clear()
def createPlayerList(self):
'''
Duplicates the player list to be the same on both sides.
That way, adding a player on the left or on the right have the exact same effect,
and thus the AuthModuleBase code can remain generic.
'''
l = list()
self.players = {Side.Left: l, Side.Right: l}
def addPlayer(self, side, player):
_translate = QCoreApplication.translate
# Add the player if not already in the list
if all([p.id!=player.id for p in self.players[side]]):
if player!=PlayerEmpty:
self.players[side].append(player)
# Update the left side description
player.displayImg(self.ui.img)
self.ui.lblName.setText(player.name)
self.ui.lblStat1.setText(_translate('{} Victories').format(player.stats['victories']))
self.ui.lblStat2.setText(_translate('{} Games Played').format(player.stats['games_played']))
self.ui.lblStat3.setText(_translate('{} Goals Scored').format(player.stats['goals_scored']))
self.ui.lblStat1.setText(_translate('Form', '{} Victories').format(player.stats['victories']))
self.ui.lblStat2.setText(_translate('Form', '{} Games Played').format(player.stats['games_played']))
self.ui.lblStat3.setText(_translate('Form', '{} Goals Scored').format(player.stats['goals_scored']))
if player!=PlayerEmpty:
# Update the right side list, making sure that the added player is showed
self.ui.playersList.addItem('{}. {}'.format(len(self.players[side]), player.name))