Compare commits

...

29 Commits

Author SHA1 Message Date
522fc7c8c4 firmware: Disable performance monitor 2024-12-16 23:27:53 +00:00
f62f2414b0 controller: Fix sudden death outcome and add last round in half 2024-12-16 00:04:36 +00:00
f501dc046e controller: Improve shutdown handling 2024-12-15 23:34:39 +00:00
244a340764 controller: Add thread for HID handler to app 2024-12-15 17:41:07 +00:00
9543851867 firmware+controller: Add round start state 2024-12-15 17:30:08 +00:00
1caf43c6de firmware+controller: Add game start and over states 2024-12-15 15:36:52 +00:00
d9c063cba7 firmware+controller: Add generic in-game state 2024-12-15 14:37:31 +00:00
8adb89ef2b firmware+controller: Add pregame state 2024-12-15 13:57:31 +00:00
ea5f1a7902 firmware+controller: Add match found state 2024-12-15 12:05:36 +00:00
18831c3e0e firmware+controller: Queue start state 2024-12-14 16:26:34 +00:00
4d1074e60c firmware: Add repeating animations 2024-12-14 13:18:17 +00:00
514198d136 controller: Fix USB get_input_report on Windows 2024-12-13 17:57:49 +00:00
277eb4ee3b firmware+controller: Initial working HID state control 2024-12-13 17:51:46 +00:00
6d0db71305 firmware: Add menu and idle states 2024-12-13 16:32:53 +00:00
4fa183ab2d firmware: Finished (?) none state 2024-12-13 14:06:00 +00:00
76d8557f36 firmware: Semi-working emoji 2024-12-13 04:01:54 +00:00
facb46c068 firmware: Add second message to none state 2024-12-13 03:03:21 +00:00
e550c6d016 firmware: Call lv_disp_flush_ready() in vsync handler 2024-12-13 02:45:36 +00:00
84fb7553c5 firmware: Initial animation 2024-12-13 02:29:27 +00:00
0607f08b83 controller: Implement economy suggestions and add test 2024-12-12 23:07:29 +00:00
bb11a1607b controller: Initial state handling 2024-12-12 19:40:16 +00:00
78aa8005bd controller: Initial polling and parsing of Valorant presence 2024-12-12 17:12:13 +00:00
46cdc9cd43 controller: Initial Windows app 2024-12-12 14:19:20 +00:00
6de0451f33 controller: Add 24x24 icon 2024-12-12 13:51:50 +00:00
937cc46d80 controller: Add icon 2024-12-12 13:34:53 +00:00
68844ef4d3 controller: Tweak structure for Windows 2024-12-12 12:57:04 +00:00
b6f242a73a firmware: Add background image 2024-12-12 01:19:02 +00:00
8bdd1c018f firmware: Add Valorant font 2024-12-11 21:23:51 +00:00
680edbfd81 Update .envrc to use --impure 2024-12-11 20:01:30 +00:00
32 changed files with 2085 additions and 55 deletions

View File

@@ -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
View File

@@ -0,0 +1,5 @@
__pycache__/
/build/
/dist/
Valconomy.spec
.pytest_cache/

91
controller/app.py Normal file
View 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()

View File

@@ -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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
controller/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
controller/icon.xcf Normal file

Binary file not shown.

View File

@@ -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
View 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
View File

@@ -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
View 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)

View File

@@ -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
View File

@@ -3,3 +3,8 @@
sdkconfig
sdkconfig.old
*.swp
/main/font/*.c
/main/img/*.c
/assets/moon.png
/assets/star.png
/assets/sleep.png

Binary file not shown.

BIN
firmware/assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -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
View 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')

View File

@@ -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 ".")

View File

View File

View 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,

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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
View 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,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs,data,nvs,0x9000,24K,
4 phy_init,data,phy,0xf000,4K,
5 factory,app,factory,0x10000,4M,

View File

@@ -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