Compare commits
38 Commits
4ac2bbba62
...
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 | |||
0cdaefc992 | |||
4817c80df3 | |||
a35f1cccc1 | |||
1ff784bc3d | |||
475f5626dd | |||
768ee410d8 | |||
db683dbc71 | |||
1d5853e91c | |||
df62eb0fe6 |
2
controller/.envrc
Normal file
2
controller/.envrc
Normal file
@@ -0,0 +1,2 @@
|
||||
watch_file default.nix
|
||||
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/
|
1
controller/.python-version
Normal file
1
controller/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
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()
|
31
controller/default.nix
Normal file
31
controller/default.nix
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
perSystem = { libMy, pkgs, ... }: {
|
||||
devenv.shells.controller = libMy.withRootdir {
|
||||
languages = {
|
||||
python = {
|
||||
enable = true;
|
||||
version = "3.13";
|
||||
libraries = with pkgs; [
|
||||
zlib # required by hidapi (?)
|
||||
];
|
||||
uv = {
|
||||
enable = true;
|
||||
sync.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
packages = with pkgs; [
|
||||
imagemagick
|
||||
];
|
||||
|
||||
env = { };
|
||||
|
||||
scripts = {
|
||||
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.
16
controller/pyproject.toml
Normal file
16
controller/pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "valconomy"
|
||||
version = "0.1.0"
|
||||
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'),
|
||||
]
|
270
controller/uv.lock
generated
Normal file
270
controller/uv.lock
generated
Normal file
@@ -0,0 +1,270 @@
|
||||
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"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/72/21ccaaca6ffb06f544afd16191425025d831c2a6d318635e9c8854070f2d/hidapi-0.14.0.post4.tar.gz", hash = "sha256:48fce253e526d17b663fbf9989c71c7ef7653ced5f4be65f1437c313fb3dbdf6", size = 174388 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/c7/8601f03a6eeeac35655245177b50bb00e707f3392e0a79c34637f8525207/hidapi-0.14.0.post4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6f96ae777e906f0a9d6f75e873313145dfec2b774f558bfcae8ba34f09792460", size = 70358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/5d/7376cf339fbe6fca26048e3c7e183ef4d99c046cc5d8378516a745914327/hidapi-0.14.0.post4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6439fc9686518d0336fac8c5e370093279f53c997540065fce131c97567118d8", size = 68034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/5a/4bca20898c699810f016d2719b980fc57fe36d5012d03eca7a89ace98547/hidapi-0.14.0.post4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2acadb4f1ae569c4f73ddb461af8733e8f5efcb290c3d0ef1b0671ba793b0ae3", size = 1075570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c6/66e6b7c27297249bc737115dff4a1e819d3e0e73885160a3104ebec7ac13/hidapi-0.14.0.post4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:884fa003d899113e14908bd3b519c60b48fc3cec0410264dcbdad1c4a8fc2e8d", size = 1081482 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a8/21e9860eddeefd0dc41b3f7e6e81cd9ff53c2b07130f57776b56a1dddc66/hidapi-0.14.0.post4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2d466b995f8ff387d68c052d3b74ee981a4ddc4f1a99f32f2dc7022273dc11", size = 1069549 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/01/3adf46a7ea5bf31f12e09d4392e1810e662101ba6611214ea6e2c35bea7a/hidapi-0.14.0.post4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e1f6409854c0a8ed4d1fdbe88d5ee4baf6f19996d1561f76889a132cb083574d", size = 698200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/19/db15cd21bef1b0dc8ef4309c5734b64affb7e88540efd3c090f153cdae0b/hidapi-0.14.0.post4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bca568a2b7d0d454c7921d70b1cc44f427eb6f95961b6d7b3b9b4532d0de74ef", size = 671554 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/23/f896ee8f0977710c354bd1b9ac6d5206c12842bd39d78a357c866f8ec6b6/hidapi-0.14.0.post4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f14ac2737fd6f58d88d2e6bf8ebd03aac7b486c14d3f570b7b1d0013d61b726", size = 703897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/5e/3c93bb12b01392b538870bc710786fee86a9ced074a8b5c091a59786ee07/hidapi-0.14.0.post4-cp313-cp313-win32.whl", hash = "sha256:b6b9c4dbf7d7e2635ff129ce6ea82174865c073b75888b8b97dda5a3d9a70493", size = 62688 },
|
||||
{ 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"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/54/292f26c208734e9a7f067aea4a7e282c080750c4546559b58e2e45413ca0/setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", size = 1337429 }
|
||||
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" },
|
||||
{ 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" },
|
||||
]
|
526
controller/valconomy.py
Normal file
526
controller/valconomy.py
Normal file
@@ -0,0 +1,526 @@
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
log = logging.getLogger('valconomy')
|
||||
|
||||
@dataclass
|
||||
class ValorantPlayerInfo:
|
||||
is_idle: bool = False
|
||||
|
||||
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")
|
||||
use flake ..#firmware --impure
|
||||
|
7
firmware/.gitignore
vendored
7
firmware/.gitignore
vendored
@@ -1,3 +1,10 @@
|
||||
/build/
|
||||
/managed_components/
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
73
firmware/dependencies.lock
Normal file
73
firmware/dependencies.lock
Normal file
@@ -0,0 +1,73 @@
|
||||
dependencies:
|
||||
espressif/esp_lcd_touch:
|
||||
component_hash: 779b4ba2464a3ae85681e4b860caa5fdc35801458c23f3039ee761bae7f442a4
|
||||
dependencies:
|
||||
- name: idf
|
||||
registry_url: https://components.espressif.com
|
||||
require: private
|
||||
version: '>=4.4.2'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
version: 1.1.2
|
||||
espressif/esp_lcd_touch_gt911:
|
||||
component_hash: 145801c24c7ecfa2c43eea0dd5e87fd5bde5653eb9a48d619c7bad60b20a1acf
|
||||
dependencies:
|
||||
- name: espressif/esp_lcd_touch
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: ^1.1.0
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=4.4.2'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.1.1~2
|
||||
espressif/esp_tinyusb:
|
||||
component_hash: 4878831be091116ec8d0e5eaedcae54a5e9866ebf15a44ef101886fd42c0b91f
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
- name: espressif/tinyusb
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: '>=0.14.2'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.5.0
|
||||
espressif/tinyusb:
|
||||
component_hash: 8a9e23b8cdc733b51fed357979139f5ae63a2fed3ce4e7c41d505f685f7d741a
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
targets:
|
||||
- esp32s2
|
||||
- esp32s3
|
||||
- esp32p4
|
||||
version: 0.17.0~1
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.3.1
|
||||
lvgl/lvgl:
|
||||
component_hash: 096c69af22eaf8a2b721e3913da91918c5e6bf1a762a113ec01f401aa61337a0
|
||||
dependencies: []
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 9.2.2
|
||||
direct_dependencies:
|
||||
- espressif/esp_lcd_touch_gt911
|
||||
- espressif/esp_tinyusb
|
||||
- idf
|
||||
- lvgl/lvgl
|
||||
manifest_hash: a301075b9c0715323f037caea9def414e47e90934bf7fd0c3b2c9d8ffa30bc49
|
||||
target: esp32s3
|
||||
version: 2.0.0
|
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,2 +1,6 @@
|
||||
idf_component_register(SRCS "valconomy.c"
|
||||
INCLUDE_DIRS ".")
|
||||
idf_component_register(
|
||||
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 ".")
|
||||
|
15
firmware/main/common.h
Normal file
15
firmware/main/common.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
#define SCL_SPEED 100000
|
||||
|
||||
#define LCD_HRES 800
|
||||
#define LCD_VRES 480
|
||||
|
||||
extern char val_dev_serial[13];
|
||||
|
||||
extern i2c_master_bus_handle_t i2c_bus_handle;
|
||||
extern i2c_master_dev_handle_t exio_cfg_handle, exio_o_handle;
|
0
firmware/main/font/.gitkeep
Normal file
0
firmware/main/font/.gitkeep
Normal file
19
firmware/main/idf_component.yml
Normal file
19
firmware/main/idf_component.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/esp_tinyusb: "^1.5.0"
|
||||
lvgl/lvgl: "^9.2.2"
|
||||
esp_lcd_touch_gt911: "^1.1.1"
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: ">=4.1.0"
|
||||
# # Put list of dependencies here
|
||||
# # For components maintained by Espressif:
|
||||
# component: "~1.0.0"
|
||||
# # For 3rd party components:
|
||||
# username/component: ">=1.0.0,<2.0.0"
|
||||
# username2/component2:
|
||||
# version: "~1.0.0"
|
||||
# # For transient dependencies `public` flag can be set.
|
||||
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
0
firmware/main/img/.gitkeep
Normal file
0
firmware/main/img/.gitkeep
Normal file
280
firmware/main/lcd.c
Normal file
280
firmware/main/lcd.c
Normal file
@@ -0,0 +1,280 @@
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
#include "esp_lcd_touch_gt911.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "lcd.h"
|
||||
|
||||
#define LCD_PIXEL_SIZE 2
|
||||
|
||||
// in ms
|
||||
#define LVGL_TICK_PERIOD 2
|
||||
#define LVGL_TASK_MAX_DELAY 500
|
||||
#define LVGL_TASK_MIN_DELAY 1
|
||||
#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) {
|
||||
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);
|
||||
}
|
||||
|
||||
static void val_increase_lvgl_tick(void *arg) {
|
||||
/* Tell LVGL how many milliseconds has elapsed */
|
||||
lv_tick_inc(LVGL_TICK_PERIOD);
|
||||
}
|
||||
|
||||
static void val_lvgl_port_task(void *arg) {
|
||||
ESP_LOGI(TAG, "Starting LVGL task");
|
||||
uint32_t task_delay_ms = LVGL_TASK_MAX_DELAY;
|
||||
while (1) {
|
||||
// Lock the mutex due to the LVGL APIs are not thread-safe
|
||||
if (val_lvgl_lock(-1)) {
|
||||
task_delay_ms = lv_timer_handler();
|
||||
// Release the mutex
|
||||
val_lvgl_unlock();
|
||||
}
|
||||
|
||||
if (task_delay_ms > LVGL_TASK_MAX_DELAY) {
|
||||
task_delay_ms = LVGL_TASK_MAX_DELAY;
|
||||
} else if (task_delay_ms < LVGL_TASK_MIN_DELAY) {
|
||||
task_delay_ms = LVGL_TASK_MIN_DELAY;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
|
||||
}
|
||||
}
|
||||
|
||||
static void val_lvgl_touch_read(lv_indev_t *indev, lv_indev_data_t *data) {
|
||||
uint16_t touchpad_x[1] = {0};
|
||||
uint16_t touchpad_y[1] = {0};
|
||||
uint8_t touchpad_cnt = 0;
|
||||
|
||||
// Read touch controller data
|
||||
esp_lcd_touch_handle_t touch_panel = lv_indev_get_user_data(indev);
|
||||
esp_lcd_touch_read_data(touch_panel);
|
||||
|
||||
// Get coordinates
|
||||
bool touchpad_pressed = esp_lcd_touch_get_coordinates(touch_panel, touchpad_x, touchpad_y, NULL, &touchpad_cnt, 1);
|
||||
|
||||
if (touchpad_pressed && touchpad_cnt > 0) {
|
||||
data->point.x = touchpad_x[0];
|
||||
data->point.y = touchpad_y[0];
|
||||
data->state = LV_INDEV_STATE_PR;
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
}
|
||||
}
|
||||
|
||||
bool val_lvgl_lock(int timeout_ms) {
|
||||
// Convert timeout in milliseconds to FreeRTOS ticks
|
||||
// If `timeout_ms` is set to -1, the program will block until the condition is met
|
||||
const TickType_t timeout_ticks = (timeout_ms == -1) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
||||
return xSemaphoreTakeRecursive(lvgl_mtx, timeout_ticks) == pdTRUE;
|
||||
}
|
||||
|
||||
void val_lvgl_unlock(void) {
|
||||
xSemaphoreGiveRecursive(lvgl_mtx);
|
||||
}
|
||||
|
||||
esp_lcd_touch_handle_t val_setup_touch(void) {
|
||||
ESP_LOGI(TAG, "Install touch panel driver");
|
||||
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
|
||||
io_config.scl_speed_hz = SCL_SPEED;
|
||||
esp_lcd_panel_io_handle_t io_handle = NULL;
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_handle, &io_config, &io_handle));
|
||||
|
||||
esp_lcd_touch_io_gt911_config_t gt911_config = {
|
||||
.dev_addr = io_config.dev_addr,
|
||||
};
|
||||
|
||||
esp_lcd_touch_config_t tp_cfg = {
|
||||
.x_max = LCD_HRES,
|
||||
.y_max = LCD_VRES,
|
||||
.rst_gpio_num = -1,
|
||||
.int_gpio_num = 4,
|
||||
.levels = {
|
||||
.reset = 0,
|
||||
.interrupt = 0,
|
||||
},
|
||||
.flags = {
|
||||
.swap_xy = 0,
|
||||
.mirror_x = 0,
|
||||
.mirror_y = 0,
|
||||
},
|
||||
.driver_data = >911_config,
|
||||
};
|
||||
|
||||
esp_lcd_touch_handle_t tp;
|
||||
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_gt911(io_handle, &tp_cfg, &tp));
|
||||
return tp;
|
||||
}
|
||||
|
||||
esp_lcd_panel_handle_t val_setup_lcd(void) {
|
||||
uint8_t i2c_b;
|
||||
i2c_b = 0x01; // Output enable
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(exio_cfg_handle, &i2c_b, 1, -1));
|
||||
|
||||
i2c_b = 0x00; // Reset everything
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(exio_o_handle, &i2c_b, 1, -1));
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
i2c_b = 0x0c; // Pull LCD + touch out of reset
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(exio_o_handle, &i2c_b, 1, -1));
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
ESP_LOGI(TAG, "Install RGB LCD panel driver");
|
||||
esp_lcd_panel_handle_t panel_handle = NULL;
|
||||
esp_lcd_rgb_panel_config_t panel_config = {
|
||||
.data_width = 16, // RGB565
|
||||
.psram_trans_align = 64,
|
||||
.num_fbs = 2,
|
||||
|
||||
.clk_src = LCD_CLK_SRC_DEFAULT,
|
||||
.disp_gpio_num = -1, // Only accessible via GPIO expander
|
||||
.pclk_gpio_num = 7,
|
||||
.vsync_gpio_num = 3,
|
||||
.hsync_gpio_num = 46,
|
||||
.de_gpio_num = 5,
|
||||
.data_gpio_nums = {
|
||||
14, // B3
|
||||
38, // B4
|
||||
18, // B5
|
||||
17, // B6
|
||||
10, // B7
|
||||
39, // G2
|
||||
0, // G3
|
||||
45, // G4
|
||||
48, // G5
|
||||
47, // G6
|
||||
21, // G7
|
||||
1, // R3
|
||||
2, // R4
|
||||
42, // R5
|
||||
41, // R6
|
||||
40, // R7
|
||||
},
|
||||
.timings = {
|
||||
.pclk_hz = 18 * 1000 * 1000, // 18000000 / (hs+hbp+hfp+hres) / (vs+vbp+hfp+vres) = ~42Hz
|
||||
.h_res = LCD_HRES,
|
||||
.v_res = LCD_VRES,
|
||||
.hsync_back_porch = 8,
|
||||
.hsync_front_porch = 8,
|
||||
.hsync_pulse_width = 4,
|
||||
.vsync_back_porch = 16,
|
||||
.vsync_front_porch = 16,
|
||||
.vsync_pulse_width = 4,
|
||||
.flags = {
|
||||
.pclk_active_neg = true,
|
||||
},
|
||||
},
|
||||
.flags.fb_in_psram = true, // allocate frame buffer in PSRAM
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle));
|
||||
|
||||
ESP_LOGI(TAG, "Initialize RGB LCD panel");
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
|
||||
|
||||
ESP_LOGI(TAG, "Turn on LCD backlight");
|
||||
i2c_b = 0x0e; // LCD + touch out of reset, backlight on
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(exio_o_handle, &i2c_b, 1, -1));
|
||||
|
||||
return panel_handle;
|
||||
}
|
||||
|
||||
lv_display_t *val_setup_lvgl(esp_lcd_panel_handle_t lcd_panel, esp_lcd_touch_handle_t touch_panel) {
|
||||
ESP_LOGI(TAG, "Initialize LVGL library");
|
||||
lv_init();
|
||||
// create a lvgl display
|
||||
lv_display_t *display = lv_display_create(LCD_HRES, LCD_VRES);
|
||||
// associate the rgb panel handle to the display
|
||||
lv_display_set_user_data(display, lcd_panel);
|
||||
// set color depth
|
||||
lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
|
||||
|
||||
// create draw buffers
|
||||
void *buf1 = NULL;
|
||||
void *buf2 = NULL;
|
||||
ESP_LOGI(TAG, "Use frame buffers as LVGL draw buffers");
|
||||
ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(lcd_panel, 2, &buf1, &buf2));
|
||||
// set LVGL draw buffers and direct mode
|
||||
lv_display_set_buffers(display, buf1, buf2, LCD_HRES * LCD_VRES * LCD_PIXEL_SIZE, LV_DISPLAY_RENDER_MODE_DIRECT);
|
||||
|
||||
// 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,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_lcd_rgb_panel_register_event_callbacks(lcd_panel, &cbs, display));
|
||||
|
||||
lv_indev_t *indev = lv_indev_create();
|
||||
lv_indev_set_user_data(indev, touch_panel);
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
||||
lv_indev_set_read_cb(indev, val_lvgl_touch_read);
|
||||
|
||||
ESP_LOGI(TAG, "Install LVGL tick timer");
|
||||
// Tick interface for LVGL (using esp_timer to generate 2ms periodic event)
|
||||
const esp_timer_create_args_t lvgl_tick_timer_args = {
|
||||
.callback = &val_increase_lvgl_tick,
|
||||
.name = "lvgl_tick"
|
||||
};
|
||||
esp_timer_handle_t lvgl_tick_timer = NULL;
|
||||
ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, LVGL_TICK_PERIOD * 1000));
|
||||
|
||||
lvgl_mtx = xSemaphoreCreateRecursiveMutex();
|
||||
assert(lvgl_mtx);
|
||||
ESP_LOGI(TAG, "Create LVGL task");
|
||||
xTaskCreate(val_lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, NULL, LVGL_TASK_PRIORITY, NULL);
|
||||
|
||||
return display;
|
||||
}
|
16
firmware/main/lcd.h
Normal file
16
firmware/main/lcd.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
#include "esp_lcd_touch_gt911.h"
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
bool val_lvgl_lock(int timeout_ms);
|
||||
void val_lvgl_unlock(void);
|
||||
|
||||
esp_lcd_touch_handle_t val_setup_touch(void);
|
||||
esp_lcd_panel_handle_t val_setup_lcd(void);
|
||||
lv_display_t *val_setup_lvgl(esp_lcd_panel_handle_t lcd_panel, esp_lcd_touch_handle_t touch_panel);
|
755
firmware/main/ui.c
Normal file
755
firmware/main/ui.c
Normal file
@@ -0,0 +1,755 @@
|
||||
#include "lvgl.h"
|
||||
|
||||
#include "ui.h"
|
||||
|
||||
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, "Settings");
|
||||
lv_msgbox_add_text(box, "Sorry, there aren't any right now.");
|
||||
lv_msgbox_add_close_button(box);
|
||||
}
|
||||
|
||||
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_t *l_cfg = lv_label_create(b_cfg);
|
||||
lv_label_set_text(l_cfg, LV_SYMBOL_SETTINGS);
|
||||
lv_obj_center(l_cfg);
|
||||
|
||||
val_ui_none();
|
||||
}
|
41
firmware/main/ui.h
Normal file
41
firmware/main/ui.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#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);
|
196
firmware/main/usb.c
Normal file
196
firmware/main/usb.c
Normal file
@@ -0,0 +1,196 @@
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "tinyusb.h"
|
||||
#include "class/hid/hid_device.h"
|
||||
|
||||
#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)
|
||||
|
||||
static const char *TAG = "valconomy-usb";
|
||||
|
||||
// See https://github.com/hathach/tinyusb/blob/cb22301f91f0465a5578be35d9be284657ddd31d/src/common/tusb_types.h#L331
|
||||
const tusb_desc_device_t val_usb_dev_descriptor = {
|
||||
.bLength = sizeof(tusb_desc_device_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
// BCD-coded USB version (?)
|
||||
.bcdUSB = 0x0200,
|
||||
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
|
||||
.idVendor = 0x6969,
|
||||
.idProduct = 0x0004,
|
||||
// BCD-coded device version (0.1)
|
||||
.bcdDevice = 0x0010,
|
||||
|
||||
// Indices of strings
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
|
||||
.bNumConfigurations = 0x01,
|
||||
};
|
||||
|
||||
const char* val_usb_string_descriptor[5] = {
|
||||
// 0: supported language is English (0x0409)
|
||||
(char[]){0x09, 0x04},
|
||||
// 1: Manufacturer
|
||||
"/dev/player0",
|
||||
// 2: Product
|
||||
"Valorant Economy Helper",
|
||||
// 3: Serials, should use chip ID
|
||||
val_dev_serial,
|
||||
// 4: HID
|
||||
"Valconomy HID interface",
|
||||
};
|
||||
|
||||
const uint8_t val_hid_report_descriptor[] = {
|
||||
TUD_HID_REPORT_DESC_GENERIC_INOUT(USB_EP_BUFSIZE),
|
||||
};
|
||||
|
||||
const uint8_t val_hid_conf_descriptor[] = {
|
||||
// Configuration number, interface count, string index, total length, attribute, power in mA
|
||||
TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
|
||||
// HID requires an IN endpoint even if we don't care about it
|
||||
// Interface number, string index, protocol, report descriptor len, EP Out & In address, size & polling interval (ms)
|
||||
// 0x80 in endpoint address indicates IN
|
||||
TUD_HID_INOUT_DESCRIPTOR(0, 4, HID_ITF_PROTOCOL_NONE, sizeof(val_hid_report_descriptor), EPNUM_HID, 0x80 | EPNUM_HID, USB_EP_BUFSIZE, 100)
|
||||
};
|
||||
|
||||
// TinyUSB callbacks
|
||||
|
||||
// Invoked when received GET HID REPORT DESCRIPTOR request
|
||||
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
|
||||
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) {
|
||||
// We use only one interface and one HID report descriptor, so we can ignore parameter 'instance'
|
||||
return val_hid_report_descriptor;
|
||||
}
|
||||
|
||||
// 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* buf, uint16_t reqlen) {
|
||||
(void) instance;
|
||||
if (report_id != 0 || reqlen < 1) {
|
||||
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* 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) {
|
||||
ESP_LOGI(TAG, "Initializing USB");
|
||||
|
||||
const tinyusb_config_t cfg = {
|
||||
.device_descriptor = &val_usb_dev_descriptor,
|
||||
.string_descriptor = val_usb_string_descriptor,
|
||||
.string_descriptor_count = sizeof(val_usb_string_descriptor) / sizeof(char *),
|
||||
.external_phy = false,
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
// HID configuration descriptor for full-speed and high-speed are the same
|
||||
.fs_configuration_descriptor = val_hid_conf_descriptor,
|
||||
.hs_configuration_descriptor = val_hid_conf_descriptor,
|
||||
.qualifier_descriptor = NULL,
|
||||
#else
|
||||
.configuration_descriptor = val_hid_conf_descriptor,
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
};
|
||||
ESP_ERROR_CHECK(tinyusb_driver_install(&cfg));
|
||||
}
|
21
firmware/main/usb.h
Normal file
21
firmware/main/usb.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "tinyusb_types.h"
|
||||
|
||||
#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);
|
@@ -1,46 +1,76 @@
|
||||
#include <stdio.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_chip_info.h"
|
||||
#include "esp_flash.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "usb.h"
|
||||
#include "lcd.h"
|
||||
#include "ui.h"
|
||||
|
||||
static const char *TAG = "valconomy";
|
||||
|
||||
char val_dev_serial[13];
|
||||
|
||||
i2c_master_bus_handle_t i2c_bus_handle;
|
||||
i2c_master_dev_handle_t exio_cfg_handle, exio_o_handle;
|
||||
|
||||
static void val_i2c_master_init(void) {
|
||||
ESP_LOGI(TAG, "Init I2C");
|
||||
i2c_master_bus_config_t i2c_mst_config = {
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.scl_io_num = 9,
|
||||
.sda_io_num = 8,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_mst_config, &i2c_bus_handle));
|
||||
|
||||
i2c_device_config_t exio_dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x24, // CH422G config register
|
||||
.scl_speed_hz = SCL_SPEED,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus_handle, &exio_dev_cfg, &exio_cfg_handle));
|
||||
exio_dev_cfg.device_address = 0x38;
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus_handle, &exio_dev_cfg, &exio_o_handle));
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
printf("Hello world!\n");
|
||||
// Use MAC address for serial number
|
||||
uint8_t mac[6] = { 0 };
|
||||
ESP_ERROR_CHECK(esp_read_mac(mac, ESP_MAC_EFUSE_FACTORY));
|
||||
snprintf(
|
||||
val_dev_serial, sizeof(val_dev_serial),
|
||||
"%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
|
||||
/* Print chip information */
|
||||
esp_chip_info_t chip_info;
|
||||
uint32_t flash_size;
|
||||
esp_chip_info(&chip_info);
|
||||
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
|
||||
CONFIG_IDF_TARGET,
|
||||
chip_info.cores,
|
||||
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
|
||||
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
|
||||
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
|
||||
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");
|
||||
val_i2c_master_init();
|
||||
|
||||
unsigned major_rev = chip_info.revision / 100;
|
||||
unsigned minor_rev = chip_info.revision % 100;
|
||||
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
|
||||
if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
|
||||
printf("Get flash size failed");
|
||||
return;
|
||||
}
|
||||
val_usb_init();
|
||||
|
||||
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
|
||||
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");
|
||||
esp_lcd_panel_handle_t lcd_panel = val_setup_lcd();
|
||||
assert(lcd_panel);
|
||||
esp_lcd_touch_handle_t touch_panel = val_setup_touch();
|
||||
assert(touch_panel);
|
||||
|
||||
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
|
||||
lv_display_t *display = val_setup_lvgl(lcd_panel, touch_panel);
|
||||
assert(display);
|
||||
|
||||
for (int i = 10; i >= 0; i--) {
|
||||
printf("Restarting in %d seconds...\n", i);
|
||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
||||
}
|
||||
printf("Restarting now.\n");
|
||||
fflush(stdout);
|
||||
esp_restart();
|
||||
ESP_LOGI(TAG, "Display LVGL UI");
|
||||
// Lock the mutex due to the LVGL APIs are not thread-safe
|
||||
assert(val_lvgl_lock(-1));
|
||||
val_lvgl_ui(display);
|
||||
val_lvgl_unlock();
|
||||
}
|
||||
|
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,
|
|
@@ -1,4 +1,30 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) 5.3.1 Project Minimal Configuration
|
||||
#
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
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
|
||||
CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y
|
||||
CONFIG_SPIRAM_RODATA=y
|
||||
CONFIG_SPIRAM_SPEED_120M=y
|
||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
|
||||
CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y
|
||||
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
|
||||
CONFIG_ESP_WIFI_DPP_SUPPORT=y
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_TINYUSB_DEBUG_LEVEL=0
|
||||
CONFIG_TINYUSB_HID_COUNT=1
|
||||
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
|
||||
|
384
flake.lock
generated
Normal file
384
flake.lock
generated
Normal file
@@ -0,0 +1,384 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"git-hooks": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1728672398,
|
||||
"narHash": "sha256-KxuGSoVUFnQLB2ZcYODW7AVPAh9JqRlD5BrfsC/Q4qs=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "aac51f698309fd0f381149214b7eee213c66ef0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "latest",
|
||||
"repo": "cachix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"cachix": "cachix",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nix": "nix",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733788855,
|
||||
"narHash": "sha256-sGn4o9KFoGRSWDQlBKpv8dkNQ2/ODS9APopZD1/FP2Y=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "d59fee8696cd48f69cf79f65992269df9891ba86",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"esp-dev": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733098767,
|
||||
"narHash": "sha256-XLxNWOclBjSrzbbLQoOUNWyuF306/R0n4mMGxT3a698=",
|
||||
"owner": "mirrexagon",
|
||||
"repo": "nixpkgs-esp-dev",
|
||||
"rev": "31ee58005f43e93a6264e3667c9bf5c31b368733",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mirrexagon",
|
||||
"repo": "nixpkgs-esp-dev",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"devenv",
|
||||
"nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712014858,
|
||||
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_2": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733312601,
|
||||
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1730302582,
|
||||
"narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"libgit2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1697646580,
|
||||
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-parts": "flake-parts",
|
||||
"libgit2": "libgit2",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-23-11": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs-regression": [
|
||||
"devenv"
|
||||
],
|
||||
"pre-commit-hooks": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1727438425,
|
||||
"narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.24",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1730531603,
|
||||
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1733096140,
|
||||
"narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
|
||||
}
|
||||
},
|
||||
"nixpkgs-python": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733319315,
|
||||
"narHash": "sha256-cFQBdRmtIZFVjr2P6NkaCOp7dddF93BC0CXBwFZFaN0=",
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"rev": "01263eeb28c09f143d59cd6b0b7c4cc8478efd48",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1717432640,
|
||||
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1733749988,
|
||||
"narHash": "sha256-+5qdtgXceqhK5ZR1YbP1fAUsweBIrhL38726oIEAtDs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bc27f0fde01ce4e1bfec1ab122d72b7380278e68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"esp-dev": "esp-dev",
|
||||
"flake-parts": "flake-parts_2",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"nixpkgs-python": "nixpkgs-python",
|
||||
"rootdir": "rootdir"
|
||||
}
|
||||
},
|
||||
"rootdir": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"narHash": "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY=",
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
},
|
||||
"original": {
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
@@ -4,6 +4,8 @@
|
||||
devenv.url = "github:cachix/devenv";
|
||||
devenv.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
nixpkgs-python.url = "github:cachix/nixpkgs-python";
|
||||
nixpkgs-python.inputs.nixpkgs.follows = "nixpkgs";
|
||||
esp-dev.url = "github:mirrexagon/nixpkgs-esp-dev";
|
||||
esp-dev.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
@@ -19,6 +21,7 @@
|
||||
devenv.flakeModule
|
||||
|
||||
./firmware
|
||||
./controller
|
||||
];
|
||||
systems = [ "x86_64-linux" ];
|
||||
|
||||
|
Reference in New Issue
Block a user