Compare commits
29 Commits
0cdaefc992
...
master
Author | SHA1 | Date | |
---|---|---|---|
522fc7c8c4 | |||
f62f2414b0 | |||
f501dc046e | |||
244a340764 | |||
9543851867 | |||
1caf43c6de | |||
d9c063cba7 | |||
8adb89ef2b | |||
ea5f1a7902 | |||
18831c3e0e | |||
4d1074e60c | |||
514198d136 | |||
277eb4ee3b | |||
6d0db71305 | |||
4fa183ab2d | |||
76d8557f36 | |||
facb46c068 | |||
e550c6d016 | |||
84fb7553c5 | |||
0607f08b83 | |||
bb11a1607b | |||
78aa8005bd | |||
46cdc9cd43 | |||
6de0451f33 | |||
937cc46d80 | |||
68844ef4d3 | |||
b6f242a73a | |||
8bdd1c018f | |||
680edbfd81 |
@@ -1,2 +1,2 @@
|
||||
watch_file default.nix
|
||||
use flake ..#controller --override-input rootdir "file+file://"<(printf %s "$PWD"/../controller)
|
||||
use flake ..#controller --impure
|
||||
|
5
controller/.gitignore
vendored
Normal file
5
controller/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
/build/
|
||||
/dist/
|
||||
Valconomy.spec
|
||||
.pytest_cache/
|
91
controller/app.py
Normal file
91
controller/app.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import infi.systray
|
||||
|
||||
import pkg_resources # needed for pyinstaller?
|
||||
import valconomy
|
||||
|
||||
DATA_DIR = os.path.join(os.environ['LOCALAPPDATA'], 'Valconomy')
|
||||
LOG_FILE = os.path.join(DATA_DIR, 'app.log')
|
||||
CONFIG_FILE = os.path.join(DATA_DIR, 'config.ini')
|
||||
|
||||
log = logging.getLogger('valconomy-app')
|
||||
|
||||
def parse_log_level(level: str) -> int:
|
||||
match level.lower():
|
||||
case 'debug':
|
||||
return logging.DEBUG
|
||||
case 'info':
|
||||
return logging.INFO
|
||||
case 'warning':
|
||||
return logging.WARNING
|
||||
case 'error':
|
||||
return logging.ERROR
|
||||
case _:
|
||||
return logging.INFO
|
||||
|
||||
def data_file(fname: str) -> str:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), fname))
|
||||
|
||||
def do_open_datadir(tray: infi.systray.SysTrayIcon):
|
||||
os.startfile(DATA_DIR)
|
||||
|
||||
def main():
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
if sys.stderr is None:
|
||||
# running in console-less mode, redirect to log file
|
||||
sys.stderr = open(LOG_FILE, 'a')
|
||||
|
||||
conf = configparser.ConfigParser()
|
||||
conf.read_dict({
|
||||
'general': {
|
||||
'log_level': 'info',
|
||||
'dummy_impl': False,
|
||||
},
|
||||
'valorant': {'player_uuid': ''},
|
||||
})
|
||||
conf.read(CONFIG_FILE)
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
conf.write(f)
|
||||
use_dummy_impl = conf.getboolean('general', 'dummy_impl')
|
||||
|
||||
log_level = parse_log_level(conf['general']['log_level'])
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(name)s %(levelname)s %(message)s', level=log_level,
|
||||
stream=sys.stderr)
|
||||
log.info('Starting up')
|
||||
|
||||
if not conf['valorant']['player_uuid']:
|
||||
log.error(f'No player UUID set, exiting...')
|
||||
sys.exit(1)
|
||||
|
||||
if use_dummy_impl:
|
||||
val_handler = valconomy.ValconomyHandler()
|
||||
else:
|
||||
val_handler = valconomy.HIDValconomyHandler()
|
||||
val_handler.start()
|
||||
val_sm = valconomy.ValconomyStateMachine(conf['valorant']['player_uuid'], val_handler)
|
||||
|
||||
val_client = valconomy.ValorantLocalClient(val_sm.handle_presence)
|
||||
def do_quit(*args):
|
||||
log.info('Shutting down')
|
||||
val_client.stop()
|
||||
if not use_dummy_impl:
|
||||
val_handler.stop()
|
||||
|
||||
with infi.systray.SysTrayIcon(
|
||||
data_file('icon.ico'), 'Valconomy', on_quit=do_quit,
|
||||
menu_options=(
|
||||
('Open data directory', None, do_open_datadir),
|
||||
)) as tray:
|
||||
signal.signal(signal.SIGINT, do_quit)
|
||||
signal.signal(signal.SIGTERM, do_quit)
|
||||
val_client.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@@ -15,15 +15,16 @@
|
||||
};
|
||||
};
|
||||
|
||||
packages = with pkgs; [ ];
|
||||
packages = with pkgs; [
|
||||
imagemagick
|
||||
];
|
||||
|
||||
env = { };
|
||||
|
||||
scripts = {
|
||||
# build.exec = ''
|
||||
# cmake -S . -B build -D CMAKE_BUILD_TYPE=Debug -D PICO_STDIO_SEMIHOSTING=1
|
||||
# cmake --build build --parallel
|
||||
# '';
|
||||
gen-icon.exec = ''
|
||||
magick icon.png -resize 256x256 -define icon:auto-resize="256,128,96,64,48,32,24,16" icon.ico
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
1
controller/dist.bat
Normal file
1
controller/dist.bat
Normal file
@@ -0,0 +1 @@
|
||||
pyinstaller.exe -y --onedir --windowed --name Valconomy --icon icon.ico --add-data icon.ico:. app.py
|
47
controller/hid-test.py
Executable file
47
controller/hid-test.py
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
import logging
|
||||
|
||||
import valconomy
|
||||
from valconomy import ValorantPlayerInfo, RiotPlayerInfo, EconomyDecision
|
||||
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s %(name)s %(levelname)s %(message)s', level=logging.INFO)
|
||||
|
||||
h = valconomy.HIDValconomyHandler()
|
||||
try:
|
||||
h.menu(None, False)
|
||||
h.none()
|
||||
h.queue_start(RiotPlayerInfo.dummy(
|
||||
valorant=ValorantPlayerInfo()))
|
||||
h.menu(None, True)
|
||||
h.idle(None)
|
||||
h.queue_start(RiotPlayerInfo.dummy(
|
||||
valorant=ValorantPlayerInfo(queue_type='unrated', is_party_owner=True)))
|
||||
h.match_found(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(queue_type='premier-seasonmatch')))
|
||||
h.pregame(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(map='/Game/Maps/Bonsai/Bonsai')))
|
||||
h.match_found(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo()))
|
||||
h.pregame(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo()))
|
||||
h.game_generic(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(queue_type='ggteam')))
|
||||
h.game_start(None)
|
||||
h.game_over(None, False)
|
||||
h.game_over(None, True)
|
||||
h.round_start(
|
||||
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=3, enemy_score=7)),
|
||||
won=True, economy=EconomyDecision.SAVE)
|
||||
h.round_start(
|
||||
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=10, enemy_score=2)),
|
||||
won=False, economy=EconomyDecision.BUY)
|
||||
h.round_start(
|
||||
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=10, enemy_score=2)),
|
||||
won=None, economy=EconomyDecision.MATCH_TEAM)
|
||||
h.round_start(
|
||||
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=2, enemy_score=0)),
|
||||
won=True, economy=EconomyDecision.BONUS)
|
||||
|
||||
h.service()
|
||||
finally:
|
||||
h.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
BIN
controller/icon.ico
Normal file
BIN
controller/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 148 KiB |
BIN
controller/icon.png
Normal file
BIN
controller/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
BIN
controller/icon.xcf
Normal file
BIN
controller/icon.xcf
Normal file
Binary file not shown.
@@ -5,4 +5,12 @@ description = "Valconomy controller"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"hidapi>=0.14.0.post4",
|
||||
"infi-systray>=0.1.12; sys_platform == 'win32'",
|
||||
"requests>=2.32.3",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pyinstaller>=6.11.1",
|
||||
"pytest>=8.3.4",
|
||||
]
|
||||
|
180
controller/test_sm.py
Normal file
180
controller/test_sm.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import valconomy
|
||||
from valconomy import ValorantPlayerInfo, RiotPlayerInfo, EconomyDecision
|
||||
|
||||
class DummyValHandler:
|
||||
def __init__(self):
|
||||
self.seq = []
|
||||
|
||||
def none(self):
|
||||
self.seq.append('none')
|
||||
|
||||
def menu(self, info: RiotPlayerInfo, was_idle: bool=False):
|
||||
self.seq.append(('menu', was_idle))
|
||||
|
||||
def idle(self, info: RiotPlayerInfo):
|
||||
self.seq.append('idle')
|
||||
|
||||
def queue_start(self, info: RiotPlayerInfo):
|
||||
self.seq.append(('queue_start', info.valorant.is_party_owner and info.valorant.queue_type == 'unrated'))
|
||||
|
||||
def match_found(self, info: RiotPlayerInfo):
|
||||
self.seq.append(('match_found', info.valorant.queue_type == 'premier-seasonmatch'))
|
||||
|
||||
def pregame(self, info: RiotPlayerInfo):
|
||||
self.seq.append(('pregame', info.valorant.map == '/Game/Maps/Bonsai/Bonsai'))
|
||||
|
||||
def game_generic(self, info: RiotPlayerInfo):
|
||||
t = info.valorant.queue_type
|
||||
if info.valorant.max_party_size == 12:
|
||||
t = 'custom'
|
||||
elif not t:
|
||||
t = 'unknown'
|
||||
|
||||
self.seq.append(('game_generic', t))
|
||||
|
||||
def game_start(self, info: RiotPlayerInfo):
|
||||
self.seq.append('game_start')
|
||||
|
||||
def round_start(self, info: RiotPlayerInfo, won: bool, economy: EconomyDecision):
|
||||
self.seq.append(('round_start', won, info.valorant.score, info.valorant.enemy_score, economy))
|
||||
|
||||
def game_over(self, info: RiotPlayerInfo, won: bool):
|
||||
self.seq.append(('game_over', won))
|
||||
|
||||
class TestSm:
|
||||
def setup_method(self, method):
|
||||
self.mock = DummyValHandler()
|
||||
self.sm = valconomy.ValconomyStateMachine('dummy', self.mock)
|
||||
|
||||
def do(self, **kwargs):
|
||||
gone = kwargs.pop('gone', False)
|
||||
info = RiotPlayerInfo(kwargs.pop('uuid', 'dummy'), 'Team Player', 'gamer', 'unknown')
|
||||
info.valorant = ValorantPlayerInfo(**kwargs)
|
||||
self.sm.handle_presence(info, gone)
|
||||
|
||||
def test_comp_normal(self):
|
||||
self.do(game_state='MENUS')
|
||||
self.do(game_state='MENUS', queue_type='competitive')
|
||||
|
||||
for _ in range(2):
|
||||
self.do(game_state='MENUS', party_state='MATCHMAKING', queue_type='competitive')
|
||||
for _ in range(3):
|
||||
self.do(game_state='MENUS', party_state='MATCHMADE_GAME_STARTING', queue_type='competitive')
|
||||
|
||||
self.do(queue_type='competitive', game_state='PREGAME', map='/Game/Maps/Bonsai/Bonsai')
|
||||
self.do(queue_type='competitive', game_state='INGAME', map='/Game/Maps/Bonsai/Bonsai')
|
||||
for _ in range(2):
|
||||
self.do(queue_type='competitive', game_state='INGAME', map='/Game/Maps/Bonsai/Bonsai', score=1, enemy_score=0)
|
||||
self.do(queue_type='competitive', game_state='INGAME', map='/Game/Maps/Bonsai/Bonsai', score=2, enemy_score=0)
|
||||
for i in range(14):
|
||||
self.do(queue_type='competitive', game_state='INGAME', map='/Game/Maps/Bonsai/Bonsai', score=2, enemy_score=i)
|
||||
self.do(queue_type='competitive', game_state='INGAME', map='/Game/Maps/Bonsai/Bonsai', score=0, enemy_score=0)
|
||||
|
||||
assert self.mock.seq == [
|
||||
('menu', False),
|
||||
('queue_start', False),
|
||||
('match_found', False),
|
||||
('pregame', True),
|
||||
'game_start',
|
||||
('round_start', None, 0, 0, EconomyDecision.BUY),
|
||||
('round_start', True, 1, 0, EconomyDecision.BUY),
|
||||
('round_start', True, 2, 0, EconomyDecision.BONUS),
|
||||
('round_start', False, 2, 1, EconomyDecision.MATCH_TEAM),
|
||||
] + [
|
||||
('round_start', False, 2, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(7)] + [
|
||||
('round_start', False, 2, 9, EconomyDecision.BUY), # last round in half
|
||||
('round_start', False, 2, 10, EconomyDecision.BUY),
|
||||
('round_start', False, 2, 11, EconomyDecision.SAVE),
|
||||
('round_start', False, 2, 12, EconomyDecision.BUY),
|
||||
('game_over', False)
|
||||
]
|
||||
|
||||
def test_comp_overtime(self):
|
||||
for _ in range(2):
|
||||
self.do(queue_type='competitive', game_state='INGAME')
|
||||
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=1)
|
||||
for i in range(13):
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=1, enemy_score=i)
|
||||
for i in range(12):
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=1 + i, enemy_score=12)
|
||||
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=13, enemy_score=12)
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=13, enemy_score=13)
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=13, enemy_score=14)
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=14, enemy_score=14)
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=15, enemy_score=14)
|
||||
self.do(queue_type='competitive', game_state='INGAME', score=16, enemy_score=14)
|
||||
self.do(queue_type='competitive', game_state='INGAME')
|
||||
|
||||
assert self.mock.seq == [
|
||||
'game_start',
|
||||
('round_start', None, 0, 0, EconomyDecision.BUY),
|
||||
('round_start', True, 1, 0, EconomyDecision.BUY),
|
||||
('round_start', False, 1, 1, EconomyDecision.SAVE),
|
||||
] + [
|
||||
('round_start', False, 1, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(8)] + [
|
||||
('round_start', False, 1, 10, EconomyDecision.BUY), # last round in half
|
||||
('round_start', False, 1, 11, EconomyDecision.BUY),
|
||||
('round_start', False, 1, 12, EconomyDecision.BUY),
|
||||
] + [
|
||||
('round_start', True, 2 + i, 12, EconomyDecision.BUY) for i in range(12)] + [
|
||||
('round_start', False, 13, 13, EconomyDecision.BUY),
|
||||
('round_start', False, 13, 14, EconomyDecision.BUY),
|
||||
('round_start', True, 14, 14, EconomyDecision.BUY),
|
||||
('round_start', True, 15, 14, EconomyDecision.BUY),
|
||||
('game_over', True),
|
||||
]
|
||||
|
||||
def test_swiftplay(self):
|
||||
self.do(game_state='MENUS')
|
||||
self.do(game_state='MENUS', queue_type='swiftplay')
|
||||
|
||||
self.do(game_state='MENUS', party_state='MATCHMAKING', queue_type='swiftplay')
|
||||
self.do(game_state='MENUS', party_state='MATCHMADE_GAME_STARTING', queue_type='swiftplay')
|
||||
|
||||
self.do(queue_type='swiftplay', game_state='PREGAME')
|
||||
self.do(queue_type='swiftplay', game_state='INGAME')
|
||||
for i in range(4):
|
||||
self.do(queue_type='swiftplay', game_state='INGAME', enemy_score=1 + i)
|
||||
for i in range(5):
|
||||
self.do(queue_type='swiftplay', game_state='INGAME', score=1 + i, enemy_score=4)
|
||||
self.do(queue_type='swiftplay', game_state='INGAME')
|
||||
|
||||
assert self.mock.seq == [
|
||||
('menu', False),
|
||||
('queue_start', False),
|
||||
('match_found', False),
|
||||
('pregame', False),
|
||||
'game_start',
|
||||
('round_start', None, 0, 0, EconomyDecision.BUY),
|
||||
('round_start', False, 0, 1, EconomyDecision.BUY),
|
||||
('round_start', False, 0, 2, EconomyDecision.BUY),
|
||||
('round_start', False, 0, 3, EconomyDecision.BUY),
|
||||
('round_start', False, 0, 4, EconomyDecision.BUY),
|
||||
('round_start', True, 1, 4, EconomyDecision.BUY),
|
||||
('round_start', True, 2, 4, EconomyDecision.BUY),
|
||||
('round_start', True, 3, 4, EconomyDecision.BUY),
|
||||
('round_start', True, 4, 4, EconomyDecision.BUY),
|
||||
('game_over', True),
|
||||
]
|
||||
|
||||
def test_tdm(self):
|
||||
self.do(game_state='MENUS')
|
||||
self.do(game_state='MENUS', queue_type='hurm')
|
||||
|
||||
self.do(game_state='MENUS', party_state='MATCHMAKING', queue_type='hurm')
|
||||
self.do(game_state='MENUS', party_state='MATCHMADE_GAME_STARTING', queue_type='hurm')
|
||||
|
||||
self.do(queue_type='hurm', game_state='PREGAME')
|
||||
self.do(queue_type='hurm', game_state='INGAME')
|
||||
for i in range(100):
|
||||
self.do(queue_type='hurm', game_state='INGAME', enemy_score=1 + i)
|
||||
self.do(queue_type='hurm', game_state='INGAME')
|
||||
|
||||
assert self.mock.seq == [
|
||||
('menu', False),
|
||||
('queue_start', False),
|
||||
('match_found', False),
|
||||
('game_generic', 'hurm'),
|
||||
]
|
229
controller/uv.lock
generated
229
controller/uv.lock
generated
@@ -1,6 +1,57 @@
|
||||
version = 1
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hidapi"
|
||||
version = "0.14.0.post4"
|
||||
@@ -22,6 +73,155 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/a6/0d43ac0be00db25fb0c2c6125e15a3e3536196c9a7cd806d50ebfb37b375/hidapi-0.14.0.post4-cp313-cp313-win_amd64.whl", hash = "sha256:87218eeba366c871adcc273407aacbabab781d6a964919712d5583eded5ca50f", size = 69749 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infi-systray"
|
||||
version = "0.1.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/10/c323bbafafe9ef59721f3a4f242afb4ec1ff958d16ebe0f44c4bae032e3a/infi.systray-0.1.12.tar.gz", hash = "sha256:635bc10fabd3ba60a2382922fdec1e4e47efaea4b8c5ea7e437f6cdedae884d3", size = 8207 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/de/e97c6111025d5a95753bcdab2db30846e1a9eb3cc57aafec98c088b1f649/infi.systray-0.1.12-py3-none-any.whl", hash = "sha256:4041ab3693f7f00a6b2b2d7b553bf60fce8941708b83e32261ca40926952ed0d", size = 9234 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2023.2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
{ name = "macholib", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pefile", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pyinstaller-hooks-contrib" },
|
||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/d4/54f5f5c73b803e6256ea97ffc6ba8a305d9a5f57f85f9b00b282512bf18a/pyinstaller-6.11.1.tar.gz", hash = "sha256:491dfb4d9d5d1d9650d9507daec1ff6829527a254d8e396badd60a0affcb72ef", size = 4249772 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/15/b0f1c0985ee32fcd2f6ad9a486ef94e4db3fef9af025a3655e76cb708009/pyinstaller-6.11.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:44e36172de326af6d4e7663b12f71dbd34e2e3e02233e181e457394423daaf03", size = 991780 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/0f/9f54cb18abe2b1d89051bc9214c0cb40d7b5f4049c151c315dacc067f4a2/pyinstaller-6.11.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6d12c45a29add78039066a53fb05967afaa09a672426072b13816fe7676abfc4", size = 711739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f7/79d10830780eff8339bfa793eece1df4b2459e35a712fc81983e8536cc29/pyinstaller-6.11.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ddc0fddd75f07f7e423da1f0822e389a42af011f9589e0269b87e0d89aa48c1f", size = 714053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/f7/9961ef02cdbd2dbb1b1a215292656bd0ea72a83aafd8fb6373513849711e/pyinstaller-6.11.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d6475559c4939f0735122989611d7f739ed3bf02f666ce31022928f7a7e4fda", size = 719133 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/4d/7f854842a1ce798de762a0b0bc5d5a4fc26ad06164a98575dc3c54abed1f/pyinstaller-6.11.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:e21c7806e34f40181e7606926a14579f848bfb1dc52cbca7eea66eccccbfe977", size = 709591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/e0/00d29fc90c3ba50620c61554e26ebb4d764569507be7cd1c8794aa696f9a/pyinstaller-6.11.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:32c742a24fe65d0702958fadf4040f76de85859c26bec0008766e5dbabc5b68f", size = 710068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/57/d14b44a69f068d2caaee49d15e45f9fa0f37c6a2d2ad778c953c1722a1ca/pyinstaller-6.11.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:208c0ef6dab0837a0a273ea32d1a3619a208e3d1fe3fec3785eea71a77fd00ce", size = 714439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/01/256824bb57ca208099c86c2fb289f888ca7732580e91ced48fa14e5903b2/pyinstaller-6.11.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ad84abf465bcda363c1d54eafa76745d77b6a8a713778348377dc98d12a452f7", size = 710457 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f0/98c9138f5f0ff17462f1ad6d712dcfa643b9a283d6238d464d8145bc139d/pyinstaller-6.11.1-py3-none-win32.whl", hash = "sha256:2e8365276c5131c9bef98e358fbc305e4022db8bedc9df479629d6414021956a", size = 1280261 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/08/f43080614b3e8bce481d4dfd580e579497c7dcdaf87656d9d2ad912e5796/pyinstaller-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7ac83c0dc0e04357dab98c487e74ad2adb30e7eb186b58157a8faf46f1fa796f", size = 1340482 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/56/953c6594cb66e249563854c9cc04ac5a055c6c99d1614298feeaeaa9b87e/pyinstaller-6.11.1-py3-none-win_arm64.whl", hash = "sha256:35e6b8077d240600bb309ed68bb0b1453fd2b7ab740b66d000db7abae6244423", size = 1267519 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2024.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/6a/9d0057e312b85fbd17a79e1c1955d115fd9bbc78b85bab757777c8ef2307/pyinstaller_hooks_contrib-2024.10.tar.gz", hash = "sha256:8a46655e5c5b0186b5e527399118a9b342f10513eb1425c483fa4f6d02e8800c", size = 140592 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/64/445861ee7a5fd32874c0f6cfe8222aacc8feda22539332e0d8ff50dadec6/pyinstaller_hooks_contrib-2024.10-py3-none-any.whl", hash = "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10", size = 338417 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "75.6.0"
|
||||
@@ -31,13 +231,40 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valconomy"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "hidapi" },
|
||||
{ name = "infi-systray", marker = "sys_platform == 'win32'" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "hidapi", specifier = ">=0.14.0.post4" }]
|
||||
requires-dist = [
|
||||
{ name = "hidapi", specifier = ">=0.14.0.post4" },
|
||||
{ name = "infi-systray", marker = "sys_platform == 'win32'", specifier = ">=0.1.12" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pyinstaller", specifier = ">=6.11.1" },
|
||||
{ name = "pytest", specifier = ">=8.3.4" },
|
||||
]
|
||||
|
533
controller/valconomy.py
Executable file → Normal file
533
controller/valconomy.py
Executable file → Normal file
@@ -1,17 +1,526 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import json
|
||||
import logging
|
||||
from pprint import pprint
|
||||
import os
|
||||
import queue
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
import hid
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
USB_VID = 0x6969
|
||||
USB_PID = 0x0004
|
||||
# Local HTTPS connection, we need to not verify the cert
|
||||
urllib3.disable_warnings()
|
||||
# Disable so we can debug without a log for every request
|
||||
logging.getLogger('urllib3.connectionpool').disabled = True
|
||||
|
||||
def main():
|
||||
hid_handle = hid.device()
|
||||
hid_handle.open(vendor_id=USB_VID, product_id=USB_PID)
|
||||
log = logging.getLogger('valconomy')
|
||||
|
||||
try:
|
||||
hid_handle.write(b'\x00test')
|
||||
finally:
|
||||
hid_handle.close()
|
||||
@dataclass
|
||||
class ValorantPlayerInfo:
|
||||
is_idle: bool = False
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
is_party_owner: bool = True
|
||||
max_party_size: int = 5
|
||||
party_size: int = 1
|
||||
party_state: str = None
|
||||
|
||||
queue_type: str = None
|
||||
game_state: str = None
|
||||
|
||||
map: str = None
|
||||
team: str = None
|
||||
score: int = 0
|
||||
enemy_score: int = 0
|
||||
|
||||
@dataclass
|
||||
class RiotPlayerInfo:
|
||||
uuid: str
|
||||
name: str
|
||||
tag: str
|
||||
state: str
|
||||
|
||||
valorant: ValorantPlayerInfo = None
|
||||
|
||||
@classmethod
|
||||
def dummy(cls, **kwargs):
|
||||
return cls('00000000-0000-0000-0000-000000000000', 'Player', 'gamer', 'dnd', **kwargs)
|
||||
|
||||
def full_name(self) -> str:
|
||||
return f'{self.name}#{self.tag}'
|
||||
|
||||
class ValorantLocalClient:
|
||||
poll_interval = 0.5
|
||||
|
||||
def __init__(self, callback: Callable[[RiotPlayerInfo, bool], None]):
|
||||
self.callback = callback
|
||||
|
||||
self.lockfile_path = os.path.join(os.environ['LOCALAPPDATA'], r'Riot Games\Riot Client\Config\lockfile')
|
||||
self.port, self.password = None, None
|
||||
self.running = True
|
||||
self.players = {}
|
||||
|
||||
def _load_credentials(self):
|
||||
if not os.path.exists(self.lockfile_path):
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(self.lockfile_path) as f:
|
||||
for line in f:
|
||||
if line.startswith('Riot Client'):
|
||||
client, pid, self.port, self.password, proto = line.rstrip().split(':')
|
||||
assert proto == 'https'
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def _do_poll(self):
|
||||
try:
|
||||
resp = requests.get(
|
||||
f'https://127.0.0.1:{self.port}/chat/v4/presences',
|
||||
auth=('riot', self.password), verify=False)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except (requests.HTTPError, requests.ConnectionError) as ex:
|
||||
# Most likely one of these means the Riot Client shut down
|
||||
log.error(f'Failed to make request to Riot Client: {ex}')
|
||||
return False
|
||||
|
||||
next_players = {}
|
||||
for p in data['presences']:
|
||||
if p['product'] != 'valorant':
|
||||
continue
|
||||
|
||||
v = json.loads(base64.b64decode(p.pop('private'))) # Valorant-specfic
|
||||
# pprint(p)
|
||||
# pprint(v)
|
||||
|
||||
val_info = ValorantPlayerInfo(
|
||||
is_idle=v['isIdle'], is_party_owner=v['isPartyOwner'],
|
||||
max_party_size=v['maxPartySize'], party_size=v['partySize'],
|
||||
party_state=v['partyState'],
|
||||
queue_type=v['queueId'] or None, game_state=v['sessionLoopState'],
|
||||
map=v['matchMap'] or None,
|
||||
score=v['partyOwnerMatchScoreAllyTeam'], enemy_score=v['partyOwnerMatchScoreEnemyTeam'])
|
||||
match v['partyOwnerMatchCurrentTeam']:
|
||||
case 'Red':
|
||||
val_info.team = 'attackers'
|
||||
case 'Blue':
|
||||
val_info.team = 'defenders'
|
||||
|
||||
info = RiotPlayerInfo(p['puuid'], p['game_name'], p['game_tag'], p['state'], valorant=val_info)
|
||||
|
||||
last = self.players.get(info.uuid)
|
||||
if info != last:
|
||||
self.callback(info, False)
|
||||
next_players[info.uuid] = info
|
||||
|
||||
for uuid in self.players.keys() - next_players.keys():
|
||||
self.callback(self.players[uuid], True)
|
||||
|
||||
self.players = next_players
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
while self.running:
|
||||
if self.password is None:
|
||||
if not self._load_credentials():
|
||||
time.sleep(self.poll_interval)
|
||||
continue
|
||||
log.info('Detected Riot Client credentials, starting polling')
|
||||
|
||||
if not self._do_poll():
|
||||
for p in self.players.values():
|
||||
self.callback(p, True)
|
||||
self.players = {}
|
||||
self.port, self.password = None, None
|
||||
|
||||
time.sleep(self.poll_interval)
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
class EconomyDecision(Enum):
|
||||
BUY = 0
|
||||
SAVE = 1
|
||||
BONUS = 2
|
||||
MATCH_TEAM = 3
|
||||
|
||||
class ValconomyHandler:
|
||||
# Valorant isn't open
|
||||
def none(self):
|
||||
log.info('Val Time soon?')
|
||||
|
||||
# Welcome the user (back) to the game!
|
||||
def menu(self, info: RiotPlayerInfo, was_idle: bool=False):
|
||||
if was_idle:
|
||||
log.info(f"Welcome back, {info.name}!")
|
||||
else:
|
||||
log.info(f"It's Val Time, {info.name}!")
|
||||
|
||||
# The user is idle in the menu...
|
||||
def idle(self, info: RiotPlayerInfo):
|
||||
log.info(f'Come back soon, {info.name}...')
|
||||
|
||||
# Just entered queue
|
||||
def queue_start(self, info: RiotPlayerInfo):
|
||||
if info.valorant.is_party_owner and info.valorant.queue_type == 'unrated':
|
||||
log.info('Uhhhh should that be comp, MoonStar?')
|
||||
else:
|
||||
log.info(f'Hope you find a game quickly, {info.name}!')
|
||||
|
||||
def match_found(self, info: RiotPlayerInfo):
|
||||
if info.valorant.queue_type == 'premier-seasonmatch':
|
||||
log.info('Do the Cosmonauts proud!')
|
||||
else:
|
||||
log.info("I hope it's not Split...")
|
||||
|
||||
# Loaded into the agent select
|
||||
def pregame(self, info: RiotPlayerInfo):
|
||||
if info.valorant.map == '/Game/Maps/Bonsai/Bonsai':
|
||||
log.info('Ewwwww, Split....')
|
||||
else:
|
||||
log.info('Pick a good agent!')
|
||||
|
||||
# Game where we're not providing economy help
|
||||
def game_generic(self, info: RiotPlayerInfo):
|
||||
match info.valorant.queue_type:
|
||||
case 'hurm':
|
||||
log.info('Have a good TDM!')
|
||||
case 'deathmatch':
|
||||
log.info('Have a good deathmatch!')
|
||||
case 'ggteam':
|
||||
log.info('Have a good Escalation!')
|
||||
case 'spikerush':
|
||||
log.info('Have a good Spike Rush!')
|
||||
case _:
|
||||
if info.valorant.max_party_size == 12:
|
||||
log.info('Have a good custom!')
|
||||
else:
|
||||
log.info('Have a good game!')
|
||||
|
||||
# Loaded into the game
|
||||
def game_start(self, info: RiotPlayerInfo):
|
||||
log.info('OK best of luck!')
|
||||
|
||||
# Round started
|
||||
def round_start(self, info: RiotPlayerInfo, won: bool, economy: EconomyDecision):
|
||||
msg = f'Start round {info.valorant.score}-{info.valorant.enemy_score}'
|
||||
if won is not None:
|
||||
msg += f' ({"won" if won else "lost"})'
|
||||
log.info(msg)
|
||||
|
||||
match economy:
|
||||
case EconomyDecision.BUY:
|
||||
log.info('You should buy!')
|
||||
case EconomyDecision.SAVE:
|
||||
log.info('Time to save...')
|
||||
case EconomyDecision.BONUS:
|
||||
log.info('Keep your gun from last round!')
|
||||
case EconomyDecision.MATCH_TEAM:
|
||||
log.info('Follow the team economy!')
|
||||
|
||||
def game_over(self, info: RiotPlayerInfo, won: bool):
|
||||
if won:
|
||||
log.info('Well played!')
|
||||
else:
|
||||
log.info('Hard luck...')
|
||||
|
||||
class HIDValconomyHandler(ValconomyHandler):
|
||||
vid = 0x6969
|
||||
pid = 0x0004
|
||||
|
||||
def __init__(self):
|
||||
self.dev = None
|
||||
self.running = True
|
||||
self.queue = queue.Queue(128)
|
||||
self._thread = None
|
||||
|
||||
def close(self):
|
||||
if self.dev is not None:
|
||||
self.dev.close()
|
||||
|
||||
def _dev_ready(self):
|
||||
try:
|
||||
if self.dev is None:
|
||||
dev = hid.device()
|
||||
dev.open(vendor_id=self.vid, product_id=self.pid)
|
||||
self.dev = dev
|
||||
log.info(f'USB device opened')
|
||||
|
||||
# 2 bytes: report ID and returned value
|
||||
# We get back the same report ID and value
|
||||
# Set to report size + 1 to satisfy W*ndoze
|
||||
data = self.dev.get_input_report(0, 65)
|
||||
assert len(data) == 2
|
||||
return data[1] == 1
|
||||
except OSError as ex:
|
||||
if self.dev is not None:
|
||||
log.warning(f'USB device lost')
|
||||
self.dev = None
|
||||
return False
|
||||
|
||||
def _do(self, cmd, *vals, fmt=None):
|
||||
if fmt is None:
|
||||
fmt = ''
|
||||
|
||||
fmt = '<BB' + fmt
|
||||
# Prepend report ID 0
|
||||
data = struct.pack(fmt, *(0, cmd) + vals)
|
||||
self.dev.write(data)
|
||||
|
||||
def _enq(self, cmd, *vals, fmt=None):
|
||||
self.queue.put((cmd, vals, fmt))
|
||||
|
||||
def service(self):
|
||||
while not self.queue.empty():
|
||||
cmd, vals, fmt = self.queue.get()
|
||||
|
||||
while not self._dev_ready():
|
||||
if not self.running:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
try:
|
||||
self._do(cmd, *vals, fmt=fmt)
|
||||
except OSError as ex:
|
||||
log.warning(f'USB device lost, state dequeuing stalled')
|
||||
|
||||
def run(self):
|
||||
while self.running:
|
||||
while self.queue.empty():
|
||||
if not self.running:
|
||||
return
|
||||
time.sleep(0.5)
|
||||
|
||||
self.service()
|
||||
|
||||
def start(self):
|
||||
assert self._thread is None
|
||||
self._thread = threading.Thread(name='HIDValconomyHandler', target=self.run)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self._thread is None or not self.running:
|
||||
return
|
||||
self.running = False
|
||||
self._thread.join()
|
||||
|
||||
def none(self):
|
||||
self._enq(0)
|
||||
|
||||
def menu(self, info: RiotPlayerInfo, was_idle: bool=False):
|
||||
self._enq(1, 1 if was_idle else 0, fmt='B')
|
||||
|
||||
def idle(self, info: RiotPlayerInfo):
|
||||
self._enq(2)
|
||||
|
||||
def queue_start(self, info: RiotPlayerInfo):
|
||||
ms_not_comp = info.valorant.is_party_owner and info.valorant.queue_type == 'unrated'
|
||||
self._enq(3, 1 if ms_not_comp else 0, fmt='B')
|
||||
|
||||
def match_found(self, info: RiotPlayerInfo):
|
||||
self._enq(4, 1 if info.valorant.queue_type == 'premier-seasonmatch' else 0, fmt='B')
|
||||
|
||||
def pregame(self, info: RiotPlayerInfo):
|
||||
self._enq(5, 1 if info.valorant.map == '/Game/Maps/Bonsai/Bonsai' else 0, fmt='B')
|
||||
|
||||
def game_generic(self, info: RiotPlayerInfo):
|
||||
match info.valorant.queue_type:
|
||||
case 'hurm': # tdm
|
||||
gm = 0
|
||||
case 'deathmatch':
|
||||
gm = 1
|
||||
case 'ggteam': # escalation
|
||||
gm = 2
|
||||
case 'spikerush':
|
||||
gm = 3
|
||||
case _:
|
||||
if info.valorant.max_party_size == 12: # custom
|
||||
gm = 4
|
||||
else:
|
||||
gm = 5
|
||||
|
||||
self._enq(6, gm, fmt='B')
|
||||
|
||||
def game_start(self, info: RiotPlayerInfo):
|
||||
self._enq(7)
|
||||
|
||||
def round_start(self, info: RiotPlayerInfo, won: bool, economy: EconomyDecision):
|
||||
match won:
|
||||
case False:
|
||||
won_val = 0
|
||||
case True:
|
||||
won_val = 1
|
||||
case None:
|
||||
won_val = 2
|
||||
|
||||
self._enq(
|
||||
8,
|
||||
info.valorant.score, info.valorant.enemy_score,
|
||||
won_val,
|
||||
economy.value,
|
||||
fmt='BBBB')
|
||||
|
||||
def game_over(self, info: RiotPlayerInfo, won: bool):
|
||||
self._enq(9, 1 if won else 0, fmt='B')
|
||||
|
||||
class GameState(Enum):
|
||||
NONE = 0
|
||||
MENU = 1
|
||||
IDLE = 2
|
||||
IN_QUEUE = 3
|
||||
GAME_FOUND = 4
|
||||
PRE_GAME = 5
|
||||
GAME_START = 6
|
||||
IN_GAME = 7
|
||||
GAME_OVER = 8
|
||||
IN_GAME_GENERIC = 9
|
||||
|
||||
class ValconomyStateMachine:
|
||||
def __init__(self, player_uuid: str, handler: ValconomyHandler):
|
||||
self.uuid = player_uuid
|
||||
self.handler = handler
|
||||
|
||||
self.last = None
|
||||
self.score = -1
|
||||
self.score_enemy = -1
|
||||
self.round_history = []
|
||||
|
||||
def handle_state(self, s: GameState, p: RiotPlayerInfo):
|
||||
log.info(f'{self.last} -> {s}')
|
||||
|
||||
if s == GameState.IN_GAME:
|
||||
if p.valorant.score == self.score and p.valorant.enemy_score == self.score_enemy:
|
||||
# Same score line
|
||||
return
|
||||
|
||||
won_last = None
|
||||
if self.score != -1:
|
||||
won_last = True if p.valorant.score > self.score else False
|
||||
self.round_history.append(won_last)
|
||||
|
||||
over = False
|
||||
if p.valorant.queue_type == 'swiftplay':
|
||||
if p.valorant.score == 5 or p.valorant.enemy_score == 5:
|
||||
over = True
|
||||
|
||||
eco = EconomyDecision.BUY
|
||||
else:
|
||||
if p.valorant.score > 12 or p.valorant.enemy_score > 12:
|
||||
if p.valorant.queue_type == 'unrated' or abs(p.valorant.score - p.valorant.enemy_score) >= 2:
|
||||
over = True
|
||||
|
||||
eco = EconomyDecision.MATCH_TEAM
|
||||
rounds_played = p.valorant.score + p.valorant.enemy_score
|
||||
if p.valorant.enemy_score == 12:
|
||||
# Enemy about to win!
|
||||
eco = EconomyDecision.BUY
|
||||
elif rounds_played in (0, 12):
|
||||
# First round (of half)
|
||||
eco = EconomyDecision.BUY
|
||||
elif rounds_played in (1, 13):
|
||||
# Second round (of half)
|
||||
eco = EconomyDecision.BUY if won_last else EconomyDecision.SAVE
|
||||
elif rounds_played in (2, 14):
|
||||
# Third round (of half)
|
||||
match self.round_history[-2:]:
|
||||
case [True, True]:
|
||||
eco = EconomyDecision.BONUS
|
||||
case [True, False]:
|
||||
eco = EconomyDecision.SAVE
|
||||
case [False, _]:
|
||||
eco = EconomyDecision.BUY
|
||||
elif rounds_played >= 24:
|
||||
# Sudden death or overtime (buy either way)
|
||||
eco = EconomyDecision.BUY
|
||||
elif rounds_played == 11:
|
||||
# Last round of half
|
||||
eco = EconomyDecision.BUY
|
||||
|
||||
if not over:
|
||||
self.handler.round_start(p, won_last, eco)
|
||||
self.score = p.valorant.score
|
||||
self.score_enemy = p.valorant.enemy_score
|
||||
self.last = GameState.IN_GAME
|
||||
return
|
||||
|
||||
if s == self.last:
|
||||
return
|
||||
|
||||
if s != GameState.GAME_OVER:
|
||||
self.score = self.score_enemy = -1
|
||||
self.round_history = []
|
||||
match s:
|
||||
case GameState.IN_GAME_GENERIC:
|
||||
self.handler.game_generic(p)
|
||||
case GameState.GAME_START:
|
||||
self.handler.game_start(p)
|
||||
case GameState.PRE_GAME:
|
||||
self.handler.pregame(p)
|
||||
case GameState.GAME_FOUND:
|
||||
self.handler.match_found(p)
|
||||
case GameState.IN_QUEUE:
|
||||
self.handler.queue_start(p)
|
||||
|
||||
case GameState.IDLE:
|
||||
self.handler.idle(p)
|
||||
case GameState.MENU:
|
||||
self.handler.menu(p, self.last == GameState.IDLE)
|
||||
|
||||
case GameState.NONE:
|
||||
self.handler.none()
|
||||
|
||||
case GameState.GAME_OVER:
|
||||
self.handler.game_over(p, self.score > self.score_enemy)
|
||||
|
||||
self.last = s
|
||||
|
||||
def handle_presence(self, p: RiotPlayerInfo, gone: bool):
|
||||
log.debug(f'{repr(p)}, gone: {gone}')
|
||||
if p.uuid != self.uuid:
|
||||
return
|
||||
|
||||
if gone:
|
||||
self.handle_state(GameState.NONE, p)
|
||||
return
|
||||
|
||||
if p.valorant.party_state == 'MATCHMADE_GAME_STARTING':
|
||||
self.handle_state(GameState.GAME_FOUND, p)
|
||||
return
|
||||
if p.valorant.party_state == 'MATCHMAKING':
|
||||
self.handle_state(GameState.IN_QUEUE, p)
|
||||
return
|
||||
|
||||
if p.valorant.queue_type in ('unrated', 'competitive', 'premier-seasonmatch', 'swiftplay'):
|
||||
if p.valorant.game_state == 'PREGAME':
|
||||
self.handle_state(GameState.PRE_GAME, p)
|
||||
return
|
||||
if p.valorant.game_state == 'INGAME':
|
||||
if self.score == -1 and p.valorant.score == p.valorant.enemy_score == 0:
|
||||
self.handle_state(GameState.GAME_START, p)
|
||||
|
||||
if (self.score > 0 or self.score_enemy > 0) and p.valorant.score == p.valorant.enemy_score == 0:
|
||||
self.handle_state(GameState.GAME_OVER, p)
|
||||
else:
|
||||
self.handle_state(GameState.IN_GAME, p)
|
||||
return
|
||||
|
||||
if p.valorant.game_state in ('PREGAME', 'INGAME'):
|
||||
self.handle_state(GameState.IN_GAME_GENERIC, p)
|
||||
return
|
||||
|
||||
if p.valorant.game_state == 'MENUS':
|
||||
if p.valorant.is_idle:
|
||||
self.handle_state(GameState.IDLE, p)
|
||||
return
|
||||
|
||||
self.handle_state(GameState.MENU, p)
|
||||
|
@@ -1,2 +1,2 @@
|
||||
watch_file default.nix
|
||||
use flake ..#firmware --override-input rootdir "file+file://"<(printf %s "$PWD"/../firmware)
|
||||
use flake ..#firmware --impure
|
||||
|
5
firmware/.gitignore
vendored
5
firmware/.gitignore
vendored
@@ -3,3 +3,8 @@
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
*.swp
|
||||
/main/font/*.c
|
||||
/main/img/*.c
|
||||
/assets/moon.png
|
||||
/assets/star.png
|
||||
/assets/sleep.png
|
||||
|
BIN
firmware/assets/Tungsten-Bold.ttf
Normal file
BIN
firmware/assets/Tungsten-Bold.ttf
Normal file
Binary file not shown.
BIN
firmware/assets/bg.png
Normal file
BIN
firmware/assets/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
firmware/assets/moon_orig.png
Normal file
BIN
firmware/assets/moon_orig.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
BIN
firmware/assets/sleep_orig.png
Normal file
BIN
firmware/assets/sleep_orig.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
BIN
firmware/assets/star_orig.png
Normal file
BIN
firmware/assets/star_orig.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@@ -1,9 +1,15 @@
|
||||
{
|
||||
perSystem = { libMy, pkgs, ... }: {
|
||||
perSystem = { libMy, pkgs, ... }:
|
||||
let
|
||||
genImgsPy = pkgs.python3.withPackages (ps: with ps; [ pillow ]);
|
||||
in
|
||||
{
|
||||
devenv.shells.firmware = libMy.withRootdir {
|
||||
packages = with pkgs; [
|
||||
esp-idf-esp32s3
|
||||
picocom
|
||||
lv_font_conv
|
||||
imagemagick
|
||||
];
|
||||
|
||||
env = {
|
||||
@@ -11,13 +17,22 @@
|
||||
};
|
||||
|
||||
scripts = {
|
||||
# build.exec = ''
|
||||
# cmake -S . -B build -D CMAKE_BUILD_TYPE=Debug -D PICO_STDIO_SEMIHOSTING=1
|
||||
# cmake --build build --parallel
|
||||
# '';
|
||||
init.exec = ''
|
||||
idf.py set-target esp32s3
|
||||
'';
|
||||
gen-fonts.exec = ''
|
||||
for s in 40 180; do
|
||||
DEBUG='*' lv_font_conv --font assets/Tungsten-Bold.ttf --bpp 4 --size $s -r 0x20-0x7F --no-compress \
|
||||
--format lvgl --lv-include lvgl.h --lv-font-name lv_font_tungsten_"$s" -o main/font/tungsten_"$s".c
|
||||
done
|
||||
'';
|
||||
gen-imgs.exec = ''
|
||||
for e in moon star sleep; do
|
||||
magick assets/''${e}_orig.png -resize 120x120 assets/$e.png
|
||||
done
|
||||
|
||||
${genImgsPy}/bin/python gen-imgs.py
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
54
firmware/gen-imgs.py
Executable file
54
firmware/gen-imgs.py
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
import itertools
|
||||
import os
|
||||
import struct
|
||||
|
||||
from PIL import Image
|
||||
|
||||
def rgb2h(r, g, b):
|
||||
return (r >> 3) << 11 | (g >> 2) << 5 | (b >> 3)
|
||||
|
||||
def bytes_line(bs):
|
||||
return ' ' + ' '.join(map(lambda i: f'{i:#x},', bs)) + '\n'
|
||||
|
||||
imgs = ['bg.png', 'moon.png', 'star.png', 'sleep.png']
|
||||
for fname in imgs:
|
||||
alpha = []
|
||||
with Image.open(os.path.join('assets/', fname)) as img:
|
||||
w = img.width
|
||||
h = img.height
|
||||
|
||||
if img.has_transparency_data:
|
||||
has_alpha = True
|
||||
data = [rgb2h(r, g, b) for r, g, b, _ in img.getdata()]
|
||||
alpha = [a for _, _, _, a in img.getdata()]
|
||||
else:
|
||||
data = [rgb2h(r, g, b) for r, g, b in img.getdata()]
|
||||
|
||||
basename = os.path.splitext(fname)[0]
|
||||
with open(os.path.join('main/img/', basename + '.c'), 'w') as f:
|
||||
f.write(
|
||||
'#include <inttypes.h>\n\n'
|
||||
'#include "lvgl.h"\n\n'
|
||||
'static const uint8_t _data[] = {\n')
|
||||
|
||||
for group in itertools.batched(data, 10):
|
||||
bs = bytearray()
|
||||
for c in group:
|
||||
bs += struct.pack('<H', c)
|
||||
|
||||
f.write(bytes_line(bs))
|
||||
for group in itertools.batched(alpha, 20):
|
||||
f.write(bytes_line(group))
|
||||
|
||||
f.write('};\n\n')
|
||||
|
||||
f.write(
|
||||
f'const lv_image_dsc_t ui_img_{basename} = {{\n'
|
||||
' .header.magic = LV_IMAGE_HEADER_MAGIC,\n'
|
||||
f' .header.cf = LV_COLOR_FORMAT_RGB565{"A8" if alpha else ""},\n'
|
||||
f' .header.w = {w},\n'
|
||||
f' .header.h = {h},\n'
|
||||
' .data_size = sizeof(_data),\n'
|
||||
' .data = _data,\n'
|
||||
'};\n')
|
@@ -1,3 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS "valconomy.c" "ui.c" "lcd.c" "usb.c"
|
||||
SRCS
|
||||
"font/tungsten_40.c" "font/tungsten_180.c"
|
||||
"img/bg.c" "img/moon.c" "img/star.c" "img/sleep.c"
|
||||
"valconomy.c" "ui.c" "lcd.c" "usb.c"
|
||||
INCLUDE_DIRS ".")
|
||||
|
0
firmware/main/font/.gitkeep
Normal file
0
firmware/main/font/.gitkeep
Normal file
0
firmware/main/img/.gitkeep
Normal file
0
firmware/main/img/.gitkeep
Normal file
@@ -26,20 +26,38 @@
|
||||
#define LVGL_TASK_PRIORITY 2
|
||||
#define LVGL_TASK_STACK_SIZE (8 * 1024)
|
||||
|
||||
#define WAIT_VSYNC 0
|
||||
|
||||
static const char *TAG = "valconomy-lcd";
|
||||
|
||||
// LVGL library is not thread-safe, we will call LVGL APIs from different tasks, so use a mutex to protect it
|
||||
static SemaphoreHandle_t lvgl_mtx = NULL;
|
||||
#if WAIT_VSYNC
|
||||
static SemaphoreHandle_t sem_vsync_end = NULL;
|
||||
static SemaphoreHandle_t sem_gui_ready = NULL;
|
||||
#endif
|
||||
|
||||
static bool val_on_vsync(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_ctx) {
|
||||
return false;
|
||||
BaseType_t high_task_awoken = pdFALSE;
|
||||
#if WAIT_VSYNC
|
||||
if (xSemaphoreTakeFromISR(sem_gui_ready, &high_task_awoken) == pdTRUE) {
|
||||
xSemaphoreGiveFromISR(sem_vsync_end, &high_task_awoken);
|
||||
}
|
||||
#endif
|
||||
|
||||
lv_disp_flush_ready(user_ctx);
|
||||
return high_task_awoken == pdTRUE;
|
||||
}
|
||||
|
||||
static void val_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
|
||||
esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data(disp);
|
||||
#if WAIT_VSYNC
|
||||
xSemaphoreGive(sem_gui_ready);
|
||||
xSemaphoreTake(sem_vsync_end, portMAX_DELAY);
|
||||
#endif
|
||||
|
||||
// pass the draw buffer to the driver
|
||||
esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, px_map);
|
||||
lv_disp_flush_ready(disp);
|
||||
}
|
||||
|
||||
static void val_increase_lvgl_tick(void *arg) {
|
||||
@@ -225,6 +243,13 @@ lv_display_t *val_setup_lvgl(esp_lcd_panel_handle_t lcd_panel, esp_lcd_touch_han
|
||||
// set the callback which can copy the rendered image to an area of the display
|
||||
lv_display_set_flush_cb(display, val_lvgl_flush_cb);
|
||||
|
||||
#if WAIT_VSYNC
|
||||
sem_vsync_end = xSemaphoreCreateBinary();
|
||||
assert(sem_vsync_end);
|
||||
sem_gui_ready = xSemaphoreCreateBinary();
|
||||
assert(sem_gui_ready);
|
||||
#endif
|
||||
|
||||
ESP_LOGI(TAG, "Register event callbacks");
|
||||
esp_lcd_rgb_panel_event_callbacks_t cbs = {
|
||||
.on_vsync = val_on_vsync,
|
||||
|
@@ -2,32 +2,754 @@
|
||||
|
||||
#include "ui.h"
|
||||
|
||||
static const lv_font_t *font_normal = &lv_font_montserrat_14;
|
||||
const char *val_ext_gamemodes[] = {
|
||||
"TDM",
|
||||
"DEATHMATCH",
|
||||
"ESCALATION",
|
||||
"SPIKE RUSH",
|
||||
"CUSTOM",
|
||||
"GAME",
|
||||
};
|
||||
|
||||
static const char* M_VAL_TIME = "VAL TIME?";
|
||||
static const char *val_eco_titles[] = {
|
||||
"BUY",
|
||||
"SAVE",
|
||||
"BONUS",
|
||||
"MATCH TEAM",
|
||||
};
|
||||
static const char *val_eco_subtitles[] = {
|
||||
"SPEND ALL YOUR MONEY!",
|
||||
"MAKE SURE YOU CAN BUY NEXT ROUND!",
|
||||
"KEEP YOUR GUN FROM LAST ROUND!",
|
||||
"FOLLOW THE TEAM ECONOMY",
|
||||
};
|
||||
|
||||
static const lv_font_t *font_normal = &lv_font_montserrat_24;
|
||||
|
||||
static lv_color_t color_primary, color_secondary;
|
||||
static lv_color_t color_text_hero, color_text_subtitle;
|
||||
static lv_color_t color_text_won, color_text_lost;
|
||||
|
||||
static lv_style_t s_hero, s_subtitle;
|
||||
|
||||
static lv_obj_t *o_container = NULL;
|
||||
|
||||
static lv_anim_timeline_t *at_active = NULL;
|
||||
static lv_anim_timeline_t *at_active_rep = NULL;
|
||||
static lv_obj_t *o_active = NULL;
|
||||
static bool state_ready = true;
|
||||
|
||||
static const void* imgfont_get_path(
|
||||
const lv_font_t *font, uint32_t unicode, uint32_t unicode_next, int32_t *offset_y, void *user_data) {
|
||||
LV_UNUSED(font);
|
||||
LV_UNUSED(unicode_next);
|
||||
LV_UNUSED(offset_y);
|
||||
LV_UNUSED(user_data);
|
||||
|
||||
switch (unicode) {
|
||||
case 0x1F319:
|
||||
return &ui_img_moon;
|
||||
case 0x2B50:
|
||||
return &ui_img_star;
|
||||
case 0x1F634:
|
||||
return &ui_img_sleep;
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void b_cfg_cb(lv_event_t *e) {
|
||||
lv_obj_t *box = lv_msgbox_create(NULL);
|
||||
lv_msgbox_add_title(box, "Hello");
|
||||
lv_msgbox_add_text(box, "test message");
|
||||
lv_msgbox_add_title(box, "Settings");
|
||||
lv_msgbox_add_text(box, "Sorry, there aren't any right now.");
|
||||
lv_msgbox_add_close_button(box);
|
||||
}
|
||||
|
||||
static void ui_home() {
|
||||
static void setup_next_state() {
|
||||
assert(state_ready);
|
||||
state_ready = false;
|
||||
|
||||
if (at_active) {
|
||||
lv_anim_timeline_delete(at_active);
|
||||
at_active = NULL;
|
||||
}
|
||||
if (at_active_rep) {
|
||||
lv_anim_timeline_delete(at_active_rep);
|
||||
at_active_rep = NULL;
|
||||
}
|
||||
if (o_active) {
|
||||
lv_obj_delete(o_active);
|
||||
o_active = NULL;
|
||||
}
|
||||
|
||||
o_active = lv_obj_create(o_container);
|
||||
lv_obj_center(o_active);
|
||||
lv_obj_set_style_bg_opa(o_active, LV_OPA_0, 0);
|
||||
lv_obj_set_style_border_width(o_active, 0, 0);
|
||||
lv_obj_set_size(o_active, lv_pct(100), lv_pct(100));
|
||||
lv_obj_set_scrollbar_mode(o_active, LV_SCROLLBAR_MODE_OFF);
|
||||
|
||||
at_active = lv_anim_timeline_create();
|
||||
at_active_rep = lv_anim_timeline_create();
|
||||
lv_anim_timeline_set_repeat_count(at_active_rep, LV_ANIM_REPEAT_INFINITE);
|
||||
}
|
||||
|
||||
bool val_ui_state_ready() {
|
||||
return state_ready;
|
||||
}
|
||||
|
||||
static void anim_state_ready_cb(lv_anim_t *anim) {
|
||||
lv_anim_timeline_start(at_active_rep);
|
||||
state_ready = true;
|
||||
}
|
||||
static void anim_x_cb(void *var, int32_t v) {
|
||||
lv_obj_set_x(var, v);
|
||||
}
|
||||
static void anim_y_cb(void *var, int32_t v) {
|
||||
lv_obj_set_y(var, v);
|
||||
}
|
||||
static void anim_opa_cb(void *var, int32_t v) {
|
||||
lv_obj_set_style_opa(var, v, 0);
|
||||
}
|
||||
|
||||
static void anim_val_time_text(lv_anim_t *anim) {
|
||||
lv_label_set_text_static(anim->var, M_VAL_TIME);
|
||||
}
|
||||
void val_ui_none() {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, M_VAL_TIME);
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_static(l_subtitle, "SOME VAL IS ALWAYS ON THE MENU...");
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
lv_obj_set_pos(
|
||||
l_subtitle,
|
||||
-(lv_obj_get_width(l_main) - lv_obj_get_width(l_subtitle)) / 2,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
|
||||
lv_label_set_text_static(l_main, "HELLO \U0001F319\u2B50!");
|
||||
lv_obj_update_layout(o_active);
|
||||
|
||||
// Animations
|
||||
lv_anim_t a_hello_in;
|
||||
lv_anim_init(&a_hello_in);
|
||||
lv_anim_set_var(&a_hello_in, l_main);
|
||||
lv_anim_set_values(
|
||||
&a_hello_in,
|
||||
(lv_obj_get_height(o_container) - lv_obj_get_height(l_main)) / 2, 0);
|
||||
lv_anim_set_exec_cb(&a_hello_in, anim_y_cb);
|
||||
lv_anim_set_path_cb(&a_hello_in, lv_anim_path_ease_in);
|
||||
lv_anim_set_duration(&a_hello_in, 500);
|
||||
|
||||
lv_anim_t a_hello_out;
|
||||
lv_anim_init(&a_hello_out);
|
||||
lv_anim_set_var(&a_hello_out, l_main);
|
||||
lv_anim_set_values(&a_hello_out, 255, 0);
|
||||
lv_anim_set_exec_cb(&a_hello_out, anim_opa_cb);
|
||||
lv_anim_set_path_cb(&a_hello_out, lv_anim_path_linear);
|
||||
lv_anim_set_duration(&a_hello_out, 500);
|
||||
|
||||
lv_anim_t a_val;
|
||||
lv_anim_init(&a_val);
|
||||
lv_anim_set_early_apply(&a_val, false);
|
||||
lv_anim_set_var(&a_val, l_main);
|
||||
lv_anim_set_values(&a_val, 0, 255);
|
||||
lv_anim_set_start_cb(&a_val, anim_val_time_text);
|
||||
lv_anim_set_exec_cb(&a_val, anim_opa_cb);
|
||||
lv_anim_set_path_cb(&a_val, lv_anim_path_linear);
|
||||
lv_anim_set_duration(&a_val, 500);
|
||||
|
||||
lv_anim_t a_sub;
|
||||
lv_anim_init(&a_sub);
|
||||
lv_anim_set_var(&a_sub, l_subtitle);
|
||||
lv_anim_set_values(&a_sub, 0, 255);
|
||||
lv_anim_set_exec_cb(&a_sub, anim_opa_cb);
|
||||
lv_anim_set_path_cb(&a_sub, lv_anim_path_linear);
|
||||
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
|
||||
lv_anim_set_duration(&a_sub, 750);
|
||||
|
||||
lv_anim_timeline_add(at_active, 0, &a_hello_in);
|
||||
lv_anim_timeline_add(at_active, 3000, &a_hello_out);
|
||||
lv_anim_timeline_add(at_active, 3250, &a_val);
|
||||
lv_anim_timeline_add(at_active, 3750, &a_sub);
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_ui_menu(bool was_idle) {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, "LET'S GO");
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_static(l_subtitle, was_idle ? "WELCOME BACK!" : "GET QUEUEING!");
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
|
||||
// Animations
|
||||
lv_anim_t a_title;
|
||||
lv_anim_init(&a_title);
|
||||
lv_anim_set_var(&a_title, l_main);
|
||||
lv_anim_set_values(
|
||||
&a_title,
|
||||
-(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2, 0);
|
||||
lv_anim_set_exec_cb(&a_title, anim_y_cb);
|
||||
lv_anim_set_path_cb(&a_title, lv_anim_path_ease_in);
|
||||
lv_anim_set_duration(&a_title, 500);
|
||||
|
||||
lv_anim_t a_sub;
|
||||
lv_anim_init(&a_sub);
|
||||
lv_anim_set_var(&a_sub, l_subtitle);
|
||||
lv_anim_set_values(
|
||||
&a_sub,
|
||||
(lv_obj_get_height(o_container) + lv_obj_get_height(l_subtitle)) / 2,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
lv_anim_set_exec_cb(&a_sub, anim_y_cb);
|
||||
lv_anim_set_path_cb(&a_sub, lv_anim_path_ease_in);
|
||||
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
|
||||
lv_anim_set_duration(&a_sub, 500);
|
||||
|
||||
lv_anim_timeline_add(at_active, 0, &a_title);
|
||||
lv_anim_timeline_add(at_active, 0, &a_sub);
|
||||
|
||||
// Repeating loop swing back and forth
|
||||
lv_anim_t a_title_swing;
|
||||
lv_anim_init(&a_title_swing);
|
||||
lv_anim_set_early_apply(&a_title_swing, false);
|
||||
lv_anim_set_var(&a_title_swing, l_main);
|
||||
lv_anim_set_exec_cb(&a_title_swing, anim_x_cb);
|
||||
|
||||
// Left
|
||||
lv_anim_set_path_cb(&a_title_swing, lv_anim_path_ease_out);
|
||||
lv_anim_set_values(&a_title_swing, 0, -10);
|
||||
lv_anim_set_duration(&a_title_swing, 500);
|
||||
lv_anim_timeline_add(at_active_rep, 0, &a_title_swing);
|
||||
|
||||
// Left to right
|
||||
lv_anim_set_path_cb(&a_title_swing, lv_anim_path_ease_in_out);
|
||||
lv_anim_set_values(&a_title_swing, -10, 10);
|
||||
lv_anim_set_duration(&a_title_swing, 1000);
|
||||
lv_anim_timeline_add(at_active_rep, 500, &a_title_swing);
|
||||
|
||||
// Right to centre
|
||||
lv_anim_set_path_cb(&a_title_swing, lv_anim_path_ease_in);
|
||||
lv_anim_set_values(&a_title_swing, 10, 0);
|
||||
lv_anim_set_duration(&a_title_swing, 500);
|
||||
lv_anim_timeline_add(at_active_rep, 1500, &a_title_swing);
|
||||
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_ui_idle() {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, "\U0001F634");
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_static(l_subtitle, "COME BACK SOON...");
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
lv_obj_set_y(
|
||||
l_subtitle,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
|
||||
// Animations
|
||||
lv_anim_t a_fade_in;
|
||||
lv_anim_init(&a_fade_in);
|
||||
lv_anim_set_values(&a_fade_in, 0, 255);
|
||||
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
|
||||
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
|
||||
lv_anim_set_duration(&a_fade_in, 1000);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_main);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_subtitle);
|
||||
lv_anim_set_completed_cb(&a_fade_in, anim_state_ready_cb);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
static void anim_text_color_mix_hero_sub(void *var, int32_t v) {
|
||||
lv_obj_set_style_text_color(
|
||||
var, lv_color_mix(color_text_hero, color_text_subtitle, v), 0);
|
||||
}
|
||||
void val_ui_queue_start(bool ms_not_comp) {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, "SEARCHING...");
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_static(l_subtitle, ms_not_comp ? "UHHH SHOULD THAT BE COMP?" : "HOPE YOU FIND A GAME QUICKLY!");
|
||||
|
||||
lv_obj_t *spinner = lv_spinner_create(o_active);
|
||||
lv_obj_set_size(spinner, 100, 100);
|
||||
lv_obj_center(spinner);
|
||||
lv_obj_set_style_arc_color(spinner, color_text_hero, LV_PART_INDICATOR);
|
||||
lv_spinner_set_anim_params(spinner, 1500, 200);
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
const int32_t offset = -60;
|
||||
lv_obj_set_y(l_main, offset);
|
||||
lv_obj_set_y(
|
||||
l_subtitle,
|
||||
offset + (lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
lv_obj_set_y(
|
||||
spinner,
|
||||
lv_obj_get_height(o_container) / 4);
|
||||
|
||||
// Animations
|
||||
lv_anim_t a_title;
|
||||
lv_anim_init(&a_title);
|
||||
lv_anim_set_var(&a_title, l_main);
|
||||
lv_anim_set_values(
|
||||
&a_title,
|
||||
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_main)) / 2, 0);
|
||||
lv_anim_set_exec_cb(&a_title, anim_x_cb);
|
||||
lv_anim_set_path_cb(&a_title, lv_anim_path_ease_out);
|
||||
lv_anim_set_duration(&a_title, 750);
|
||||
|
||||
lv_anim_t a_sub;
|
||||
lv_anim_init(&a_sub);
|
||||
lv_anim_set_var(&a_sub, l_subtitle);
|
||||
lv_anim_set_values(
|
||||
&a_sub,
|
||||
(lv_obj_get_width(o_container) + lv_obj_get_width(l_subtitle)) / 2, 0);
|
||||
lv_anim_set_exec_cb(&a_sub, anim_x_cb);
|
||||
lv_anim_set_path_cb(&a_sub, lv_anim_path_ease_out);
|
||||
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
|
||||
lv_anim_set_duration(&a_sub, 750);
|
||||
|
||||
lv_anim_timeline_add(at_active, 0, &a_title);
|
||||
lv_anim_timeline_add(at_active, 0, &a_sub);
|
||||
|
||||
if (ms_not_comp) {
|
||||
lv_anim_t a_warn;
|
||||
lv_anim_init(&a_warn);
|
||||
lv_anim_set_early_apply(&a_warn, false);
|
||||
lv_anim_set_var(&a_warn, l_subtitle);
|
||||
lv_anim_set_path_cb(&a_warn, lv_anim_path_linear);
|
||||
lv_anim_set_duration(&a_warn, 1000);
|
||||
lv_anim_set_exec_cb(&a_warn, anim_text_color_mix_hero_sub);
|
||||
|
||||
lv_anim_set_values(&a_warn, 0, 255);
|
||||
lv_anim_timeline_add(at_active_rep, 0, &a_warn);
|
||||
|
||||
lv_anim_set_values(&a_warn, 255, 0);
|
||||
lv_anim_timeline_add(at_active_rep, 1000, &a_warn);
|
||||
}
|
||||
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_ui_match_found(bool is_premier) {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, "MATCH FOUND");
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_static(l_subtitle, is_premier ? "DO THE COSMONAUTS PROUD!" : "I HOPE IT'S NOT SPLIT...");
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
|
||||
// Animations
|
||||
lv_anim_t a_title;
|
||||
lv_anim_init(&a_title);
|
||||
lv_anim_set_var(&a_title, l_main);
|
||||
lv_anim_set_values(
|
||||
&a_title,
|
||||
(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2, 0);
|
||||
lv_anim_set_exec_cb(&a_title, anim_y_cb);
|
||||
lv_anim_set_path_cb(&a_title, lv_anim_path_ease_in);
|
||||
lv_anim_set_duration(&a_title, 500);
|
||||
|
||||
lv_anim_t a_sub;
|
||||
lv_anim_init(&a_sub);
|
||||
lv_anim_set_var(&a_sub, l_subtitle);
|
||||
lv_anim_set_values(
|
||||
&a_sub,
|
||||
-(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
lv_anim_set_exec_cb(&a_sub, anim_y_cb);
|
||||
lv_anim_set_path_cb(&a_sub, lv_anim_path_ease_in);
|
||||
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
|
||||
lv_anim_set_duration(&a_sub, 500);
|
||||
|
||||
lv_anim_timeline_add(at_active, 0, &a_title);
|
||||
lv_anim_timeline_add(at_active, 0, &a_sub);
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_ui_pregame(bool is_split) {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, "CHOOSE AGENT");
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_static(l_subtitle, is_split ? "EWWW, SPLIT..." : "PICK SOMETHING GOOD!");
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
lv_obj_set_y(
|
||||
l_subtitle,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
|
||||
// Animations
|
||||
lv_anim_t a_fade_in;
|
||||
lv_anim_init(&a_fade_in);
|
||||
lv_anim_set_values(&a_fade_in, 0, 255);
|
||||
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
|
||||
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
|
||||
lv_anim_set_duration(&a_fade_in, 500);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_main);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_subtitle);
|
||||
lv_anim_set_completed_cb(&a_fade_in, anim_state_ready_cb);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
|
||||
lv_anim_t a_warn;
|
||||
lv_anim_init(&a_warn);
|
||||
lv_anim_set_early_apply(&a_warn, false);
|
||||
lv_anim_set_var(&a_warn, l_main);
|
||||
lv_anim_set_path_cb(&a_warn, lv_anim_path_linear);
|
||||
lv_anim_set_duration(&a_warn, 2000);
|
||||
lv_anim_set_exec_cb(&a_warn, anim_text_color_mix_hero_sub);
|
||||
|
||||
lv_anim_set_values(&a_warn, 255, 0);
|
||||
lv_anim_timeline_add(at_active_rep, 0, &a_warn);
|
||||
|
||||
lv_anim_set_values(&a_warn, 0, 255);
|
||||
lv_anim_timeline_add(at_active_rep, 2000, &a_warn);
|
||||
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_ui_game_generic(const char *gamemode) {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, "HAVE FUN!");
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_fmt(l_subtitle, "ENJOY YOUR %s!", gamemode);
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
lv_obj_set_y(
|
||||
l_subtitle,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
|
||||
// Animations
|
||||
lv_anim_t a_title;
|
||||
lv_anim_init(&a_title);
|
||||
lv_anim_set_var(&a_title, l_main);
|
||||
lv_anim_set_values(
|
||||
&a_title,
|
||||
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_main)) / 2, 0);
|
||||
lv_anim_set_exec_cb(&a_title, anim_x_cb);
|
||||
lv_anim_set_path_cb(&a_title, lv_anim_path_bounce);
|
||||
lv_anim_set_duration(&a_title, 750);
|
||||
|
||||
lv_anim_t a_sub;
|
||||
lv_anim_init(&a_sub);
|
||||
lv_anim_set_var(&a_sub, l_subtitle);
|
||||
lv_anim_set_values(
|
||||
&a_sub,
|
||||
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_subtitle)) / 2,
|
||||
-(lv_obj_get_width(l_main) - lv_obj_get_width(l_subtitle)) / 2);
|
||||
lv_anim_set_exec_cb(&a_sub, anim_x_cb);
|
||||
lv_anim_set_path_cb(&a_sub, lv_anim_path_bounce);
|
||||
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
|
||||
lv_anim_set_duration(&a_sub, 750);
|
||||
|
||||
lv_anim_timeline_add(at_active, 0, &a_title);
|
||||
lv_anim_timeline_add(at_active, 0, &a_sub);
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_ui_game_start() {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, "GOOD LUCK!");
|
||||
|
||||
state_ready = true;
|
||||
}
|
||||
|
||||
static void anim_text_color_mix_hero_won(void *var, int32_t v) {
|
||||
lv_obj_set_style_text_color(
|
||||
var, lv_color_mix(color_text_hero, color_text_won, v), 0);
|
||||
}
|
||||
static void anim_text_color_mix_hero_lost(void *var, int32_t v) {
|
||||
lv_obj_set_style_text_color(
|
||||
var, lv_color_mix(color_text_hero, color_text_lost, v), 0);
|
||||
}
|
||||
void val_ui_round_start(uint8_t score, uint8_t score_enemy, val_won_t won, val_eco_decision_t eco) {
|
||||
assert(won <= ROUND_NONE && eco <= ECO_MATCH_TEAM);
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_score = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_score, &s_hero, 0);
|
||||
lv_obj_center(l_score);
|
||||
lv_label_set_text_fmt(l_score, "%hhu-%hhu", score, score_enemy);
|
||||
|
||||
lv_obj_t *l_eco = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_eco, &s_hero, 0);
|
||||
lv_obj_center(l_eco);
|
||||
lv_label_set_text_static(l_eco, val_eco_titles[eco]);
|
||||
|
||||
lv_obj_t *l_advice = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_advice, &s_subtitle, 0);
|
||||
lv_obj_center(l_advice);
|
||||
lv_label_set_text_static(l_advice, val_eco_subtitles[eco]);
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
lv_obj_set_y(
|
||||
l_score,
|
||||
-(lv_obj_get_height(l_score) / 2) - 20);
|
||||
lv_obj_set_y(
|
||||
l_eco,
|
||||
lv_obj_get_height(l_eco) / 2);
|
||||
|
||||
// Animations
|
||||
// Score and eco fade in
|
||||
lv_anim_t a_fade_in;
|
||||
lv_anim_init(&a_fade_in);
|
||||
lv_anim_set_values(&a_fade_in, 0, 255);
|
||||
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
|
||||
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
|
||||
lv_anim_set_duration(&a_fade_in, 750);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_score);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_eco);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
|
||||
// Advice slide up
|
||||
lv_anim_t a_advice;
|
||||
lv_anim_init(&a_advice);
|
||||
lv_anim_set_var(&a_advice, l_advice);
|
||||
lv_anim_set_values(
|
||||
&a_advice,
|
||||
(lv_obj_get_height(o_container) + lv_obj_get_height(l_advice)) / 2,
|
||||
lv_obj_get_height(l_eco) + (lv_obj_get_height(l_advice) / 2) + 5);
|
||||
lv_anim_set_exec_cb(&a_advice, anim_y_cb);
|
||||
lv_anim_set_path_cb(&a_advice, lv_anim_path_ease_out);
|
||||
lv_anim_set_duration(&a_advice, 750);
|
||||
|
||||
if (won != ROUND_NONE) {
|
||||
// Score colour
|
||||
lv_anim_t a_score;
|
||||
lv_anim_init(&a_score);
|
||||
lv_anim_set_var(&a_score, l_score);
|
||||
lv_anim_set_path_cb(&a_score, lv_anim_path_ease_in);
|
||||
lv_anim_set_exec_cb(
|
||||
&a_score,
|
||||
won == ROUND_WON ? anim_text_color_mix_hero_won : anim_text_color_mix_hero_lost);
|
||||
lv_anim_set_duration(&a_score, 2000);
|
||||
lv_anim_set_values(&a_score, 0, 255);
|
||||
lv_anim_set_completed_cb(&a_score, anim_state_ready_cb);
|
||||
lv_anim_timeline_add(at_active, 0, &a_score);
|
||||
} else {
|
||||
lv_anim_set_completed_cb(&a_advice, anim_state_ready_cb);
|
||||
}
|
||||
lv_anim_timeline_add(at_active, 0, &a_advice);
|
||||
|
||||
// Eco flash
|
||||
lv_anim_t a_eco;
|
||||
lv_anim_init(&a_eco);
|
||||
lv_anim_set_early_apply(&a_eco, false);
|
||||
lv_anim_set_var(&a_eco, l_eco);
|
||||
lv_anim_set_path_cb(&a_eco, lv_anim_path_linear);
|
||||
lv_anim_set_duration(&a_eco, 1000);
|
||||
lv_anim_set_exec_cb(&a_eco, anim_text_color_mix_hero_sub);
|
||||
|
||||
lv_anim_set_values(&a_eco, 255, 0);
|
||||
lv_anim_timeline_add(at_active_rep, 0, &a_eco);
|
||||
|
||||
lv_anim_set_values(&a_eco, 0, 255);
|
||||
lv_anim_timeline_add(at_active_rep, 1000, &a_eco);
|
||||
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_ui_game_over(bool won) {
|
||||
setup_next_state();
|
||||
|
||||
// Widgets
|
||||
lv_obj_t *l_main = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_main, &s_hero, 0);
|
||||
lv_obj_center(l_main);
|
||||
lv_label_set_text_static(l_main, won ? "VICTORY" : "DEFEAT");
|
||||
lv_obj_set_style_text_color(l_main, won ? color_text_won : color_text_lost, 0);
|
||||
|
||||
lv_obj_t *l_subtitle = lv_label_create(o_active);
|
||||
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
|
||||
lv_obj_center(l_subtitle);
|
||||
lv_label_set_text_static(l_subtitle, won ? "WELL PLAYED!" : "HARD LUCK...");
|
||||
|
||||
lv_obj_update_layout(o_active);
|
||||
lv_obj_set_y(
|
||||
l_subtitle,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
|
||||
// Animations
|
||||
if (won) {
|
||||
lv_anim_t a_fly;
|
||||
lv_anim_init(&a_fly);
|
||||
lv_anim_set_path_cb(&a_fly, lv_anim_path_ease_out);
|
||||
lv_anim_set_duration(&a_fly, 750);
|
||||
|
||||
// Main text from top-left
|
||||
// Y
|
||||
lv_anim_set_var(&a_fly, l_main);
|
||||
lv_anim_set_exec_cb(&a_fly, anim_y_cb);
|
||||
lv_anim_set_values(
|
||||
&a_fly,
|
||||
-(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2, 0);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fly);
|
||||
|
||||
// X
|
||||
lv_anim_set_exec_cb(&a_fly, anim_x_cb);
|
||||
lv_anim_set_values(
|
||||
&a_fly,
|
||||
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_main)) / 2, 0);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fly);
|
||||
|
||||
|
||||
// Subtitle from bottom-right
|
||||
// Y
|
||||
lv_anim_set_var(&a_fly, l_subtitle);
|
||||
lv_anim_set_exec_cb(&a_fly, anim_y_cb);
|
||||
lv_anim_set_values(
|
||||
&a_fly,
|
||||
(lv_obj_get_height(o_container) + lv_obj_get_height(l_subtitle)) / 2,
|
||||
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fly);
|
||||
|
||||
// X
|
||||
lv_anim_set_exec_cb(&a_fly, anim_x_cb);
|
||||
lv_anim_set_values(
|
||||
&a_fly,
|
||||
(lv_obj_get_width(o_container) + lv_obj_get_width(l_subtitle)) / 2, 0);
|
||||
lv_anim_set_completed_cb(&a_fly, anim_state_ready_cb);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fly);
|
||||
} else {
|
||||
lv_anim_t a_fade_in;
|
||||
lv_anim_init(&a_fade_in);
|
||||
lv_anim_set_values(&a_fade_in, 0, 255);
|
||||
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
|
||||
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
|
||||
lv_anim_set_duration(&a_fade_in, 1000);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_main);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
|
||||
lv_anim_set_var(&a_fade_in, l_subtitle);
|
||||
lv_anim_set_completed_cb(&a_fade_in, anim_state_ready_cb);
|
||||
lv_anim_timeline_add(at_active, 0, &a_fade_in);
|
||||
}
|
||||
|
||||
lv_anim_timeline_start(at_active);
|
||||
}
|
||||
|
||||
void val_lvgl_ui(lv_display_t *disp) {
|
||||
color_primary = lv_color_hex(0xff4655);
|
||||
color_secondary = lv_color_hex(0xf7518f);
|
||||
|
||||
color_text_hero = lv_palette_lighten(LV_PALETTE_GREY, 2);
|
||||
color_text_subtitle = lv_palette_darken(LV_PALETTE_GREY, 1);
|
||||
color_text_won = lv_palette_lighten(LV_PALETTE_GREEN, 1);
|
||||
color_text_lost = lv_palette_darken(LV_PALETTE_RED, 4);
|
||||
|
||||
// init default theme
|
||||
lv_theme_default_init(
|
||||
disp, color_primary, color_secondary,
|
||||
true, // dark theme
|
||||
font_normal);
|
||||
lv_sysmon_hide_performance(disp);
|
||||
|
||||
lv_font_t *f_hero_emoji = lv_imgfont_create(120, imgfont_get_path, NULL);
|
||||
assert(f_hero_emoji);
|
||||
f_hero_emoji->fallback = &lv_font_tungsten_180;
|
||||
|
||||
lv_style_init(&s_hero);
|
||||
lv_style_set_text_font(&s_hero, f_hero_emoji);
|
||||
lv_style_set_text_color(&s_hero, color_text_hero);
|
||||
|
||||
lv_style_init(&s_subtitle);
|
||||
lv_style_set_text_font(&s_subtitle, &lv_font_tungsten_40);
|
||||
lv_style_set_text_color(&s_subtitle, color_text_subtitle);
|
||||
|
||||
// Background
|
||||
lv_obj_t *i_bg = lv_image_create(lv_screen_active());
|
||||
lv_image_set_src(i_bg, &ui_img_bg);
|
||||
|
||||
// Content container
|
||||
o_container = lv_obj_create(lv_screen_active());
|
||||
lv_obj_set_style_bg_opa(o_container, LV_OPA_0, 0);
|
||||
lv_obj_set_style_border_width(o_container, 0, 0);
|
||||
lv_obj_set_style_pad_all(o_container, 0, 0);
|
||||
lv_obj_set_size(o_container, lv_pct(100), lv_pct(100));
|
||||
lv_obj_set_scrollbar_mode(o_container, LV_SCROLLBAR_MODE_OFF);
|
||||
|
||||
// Settings button
|
||||
lv_obj_t *b_cfg = lv_button_create(lv_screen_active());
|
||||
lv_obj_align(b_cfg, LV_ALIGN_BOTTOM_RIGHT, -10, -10);
|
||||
lv_obj_add_event_cb(b_cfg, b_cfg_cb, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_add_event_cb(b_cfg, b_cfg_cb, LV_EVENT_CLICKED, NULL);
|
||||
|
||||
lv_obj_t *l_cfg = lv_label_create(b_cfg);
|
||||
lv_label_set_text(l_cfg, LV_SYMBOL_SETTINGS);
|
||||
lv_obj_center(l_cfg);
|
||||
}
|
||||
|
||||
void val_lvgl_ui(lv_display_t *disp) {
|
||||
// init default theme
|
||||
lv_theme_default_init(
|
||||
disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED),
|
||||
true, // dark theme
|
||||
font_normal);
|
||||
// lv_sysmon_hide_performance(disp);
|
||||
|
||||
ui_home();
|
||||
val_ui_none();
|
||||
}
|
||||
|
@@ -2,4 +2,40 @@
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
extern const char *val_ext_gamemodes[];
|
||||
#define VAL_EXT_GAMEMODES_SIZE 6
|
||||
|
||||
typedef enum val_eco_decision {
|
||||
ECO_BUY,
|
||||
ECO_SAVE,
|
||||
ECO_BONUS,
|
||||
ECO_MATCH_TEAM,
|
||||
} val_eco_decision_t;
|
||||
typedef enum val_won {
|
||||
ROUND_LOST,
|
||||
ROUND_WON,
|
||||
ROUND_NONE,
|
||||
} val_won_t;
|
||||
|
||||
LV_FONT_DECLARE(lv_font_tungsten_40)
|
||||
LV_FONT_DECLARE(lv_font_tungsten_180)
|
||||
|
||||
LV_IMAGE_DECLARE(ui_img_bg);
|
||||
LV_IMAGE_DECLARE(ui_img_moon);
|
||||
LV_IMAGE_DECLARE(ui_img_star);
|
||||
LV_IMAGE_DECLARE(ui_img_sleep);
|
||||
|
||||
bool val_ui_state_ready();
|
||||
|
||||
void val_ui_none();
|
||||
void val_ui_menu(bool was_idle);
|
||||
void val_ui_idle();
|
||||
void val_ui_queue_start(bool ms_not_comp);
|
||||
void val_ui_match_found(bool is_premier);
|
||||
void val_ui_pregame(bool is_split);
|
||||
void val_ui_game_generic(const char *gamemode);
|
||||
void val_ui_game_start();
|
||||
void val_ui_round_start(uint8_t score, uint8_t score_enemy, val_won_t won, val_eco_decision_t eco);
|
||||
void val_ui_game_over(bool won);
|
||||
|
||||
void val_lvgl_ui(lv_display_t *disp);
|
||||
|
@@ -7,6 +7,8 @@
|
||||
|
||||
#include "common.h"
|
||||
#include "usb.h"
|
||||
#include "lcd.h"
|
||||
#include "ui.h"
|
||||
|
||||
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN)
|
||||
|
||||
@@ -76,24 +78,100 @@ uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) {
|
||||
// Invoked when received GET_REPORT control request
|
||||
// Application must fill buffer report's content and return its length.
|
||||
// Return zero will cause the stack to STALL request
|
||||
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) {
|
||||
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buf, uint16_t reqlen) {
|
||||
(void) instance;
|
||||
(void) report_id;
|
||||
(void) report_type;
|
||||
(void) buffer;
|
||||
(void) reqlen;
|
||||
if (report_id != 0 || reqlen < 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
// ESP_LOGI(TAG, "Got %hu bytes report %hhu", reqlen, report_id);
|
||||
// for (uint16_t i = 0; i < reqlen; i++) {
|
||||
// ESP_LOGI(TAG, "b: %02hhx", buf[i]);
|
||||
// }
|
||||
buf[0] = val_ui_state_ready();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Invoked when received SET_REPORT control request or
|
||||
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
|
||||
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) {
|
||||
assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT);
|
||||
ESP_LOGI(TAG, "Got %hu bytes report %hhu", bufsize, report_id);
|
||||
for (uint16_t i = 0; i < bufsize; i++) {
|
||||
ESP_LOGI(TAG, "b: %02hhx", buffer[i]);
|
||||
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buf, uint16_t bufsize) {
|
||||
(void) instance;
|
||||
assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT && bufsize >= 1);
|
||||
if (!val_lvgl_lock(-1)) {
|
||||
ESP_LOGE(TAG, "Failed to grab LVGL lock");
|
||||
return;
|
||||
}
|
||||
if (buf[0] > ST_GAME_OVER) {
|
||||
ESP_LOGW(TAG, "Unknown state %hhu", buf[0]);
|
||||
goto ret;
|
||||
}
|
||||
if (!val_ui_state_ready()) {
|
||||
goto ret;
|
||||
}
|
||||
|
||||
switch (buf[0]) {
|
||||
case ST_NONE:
|
||||
val_ui_none();
|
||||
break;
|
||||
case ST_MENU:
|
||||
if (bufsize < 2) {
|
||||
ESP_LOGE(TAG, "Invalid ST_MENU command");
|
||||
goto ret;
|
||||
}
|
||||
val_ui_menu((bool)buf[1]);
|
||||
break;
|
||||
case ST_IDLE:
|
||||
val_ui_idle();
|
||||
break;
|
||||
case ST_QUEUE_START:
|
||||
if (bufsize < 2) {
|
||||
ESP_LOGE(TAG, "Invalid ST_QUEUE_START command");
|
||||
goto ret;
|
||||
}
|
||||
val_ui_queue_start((bool)buf[1]);
|
||||
break;
|
||||
case ST_MATCH_FOUND:
|
||||
if (bufsize < 2) {
|
||||
ESP_LOGE(TAG, "Invalid ST_MATCH_FOUND command");
|
||||
goto ret;
|
||||
}
|
||||
val_ui_match_found((bool)buf[1]);
|
||||
break;
|
||||
case ST_PREGAME:
|
||||
if (bufsize < 2) {
|
||||
ESP_LOGE(TAG, "Invalid ST_PREGAME command");
|
||||
goto ret;
|
||||
}
|
||||
val_ui_pregame((bool)buf[1]);
|
||||
break;
|
||||
case ST_GAME_GENERIC:
|
||||
if (bufsize < 2 || buf[1] > VAL_EXT_GAMEMODES_SIZE) {
|
||||
ESP_LOGE(TAG, "Invalid ST_PREGAME command");
|
||||
goto ret;
|
||||
}
|
||||
val_ui_game_generic(val_ext_gamemodes[buf[1]]);
|
||||
break;
|
||||
case ST_GAME_START:
|
||||
val_ui_game_start();
|
||||
break;
|
||||
case ST_ROUND_START:
|
||||
if (bufsize < 5 || buf[3] > ROUND_NONE || buf[4] > ECO_MATCH_TEAM) {
|
||||
ESP_LOGE(TAG, "Invalid ST_ROUND_START command");
|
||||
goto ret;
|
||||
}
|
||||
val_ui_round_start(buf[1], buf[2], buf[3], buf[4]);
|
||||
break;
|
||||
case ST_GAME_OVER:
|
||||
if (bufsize < 2) {
|
||||
ESP_LOGE(TAG, "Invalid ST_GAME_OVER command");
|
||||
goto ret;
|
||||
}
|
||||
val_ui_game_over((bool)buf[1]);
|
||||
break;
|
||||
}
|
||||
|
||||
ret:
|
||||
val_lvgl_unlock();
|
||||
}
|
||||
|
||||
void val_usb_init(void) {
|
||||
|
@@ -5,4 +5,17 @@
|
||||
#define EPNUM_HID 0x01
|
||||
#define USB_EP_BUFSIZE 64
|
||||
|
||||
typedef enum val_state {
|
||||
ST_NONE = 0,
|
||||
ST_MENU,
|
||||
ST_IDLE,
|
||||
ST_QUEUE_START,
|
||||
ST_MATCH_FOUND,
|
||||
ST_PREGAME,
|
||||
ST_GAME_GENERIC,
|
||||
ST_GAME_START,
|
||||
ST_ROUND_START,
|
||||
ST_GAME_OVER,
|
||||
} val_state_t;
|
||||
|
||||
void val_usb_init(void);
|
||||
|
5
firmware/partitions.csv
Normal file
5
firmware/partitions.csv
Normal file
@@ -0,0 +1,5 @@
|
||||
# ESP-IDF Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs,data,nvs,0x9000,24K,
|
||||
phy_init,data,phy,0xf000,4K,
|
||||
factory,app,factory,0x10000,4M,
|
|
@@ -6,6 +6,8 @@ CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=n
|
||||
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
|
||||
CONFIG_ESPTOOLPY_FLASHFREQ_120M=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_COMPILER_OPTIMIZATION_PERF=y
|
||||
CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=n
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
@@ -19,7 +21,10 @@ CONFIG_ESP_WIFI_DPP_SUPPORT=y
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_TINYUSB_DEBUG_LEVEL=0
|
||||
CONFIG_TINYUSB_HID_COUNT=1
|
||||
CONFIG_LV_DEF_REFR_PERIOD=24
|
||||
CONFIG_LV_USE_LOG=y
|
||||
CONFIG_LV_LOG_PRINTF=y
|
||||
CONFIG_LV_FONT_MONTSERRAT_24=y
|
||||
CONFIG_LV_USE_SYSMON=y
|
||||
CONFIG_LV_USE_PERF_MONITOR=y
|
||||
CONFIG_LV_USE_IMGFONT=y
|
||||
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
|
||||
|
Reference in New Issue
Block a user