Compare commits

...

19 Commits

Author SHA1 Message Date
522fc7c8c4 firmware: Disable performance monitor 2024-12-16 23:27:53 +00:00
f62f2414b0 controller: Fix sudden death outcome and add last round in half 2024-12-16 00:04:36 +00:00
f501dc046e controller: Improve shutdown handling 2024-12-15 23:34:39 +00:00
244a340764 controller: Add thread for HID handler to app 2024-12-15 17:41:07 +00:00
9543851867 firmware+controller: Add round start state 2024-12-15 17:30:08 +00:00
1caf43c6de firmware+controller: Add game start and over states 2024-12-15 15:36:52 +00:00
d9c063cba7 firmware+controller: Add generic in-game state 2024-12-15 14:37:31 +00:00
8adb89ef2b firmware+controller: Add pregame state 2024-12-15 13:57:31 +00:00
ea5f1a7902 firmware+controller: Add match found state 2024-12-15 12:05:36 +00:00
18831c3e0e firmware+controller: Queue start state 2024-12-14 16:26:34 +00:00
4d1074e60c firmware: Add repeating animations 2024-12-14 13:18:17 +00:00
514198d136 controller: Fix USB get_input_report on Windows 2024-12-13 17:57:49 +00:00
277eb4ee3b firmware+controller: Initial working HID state control 2024-12-13 17:51:46 +00:00
6d0db71305 firmware: Add menu and idle states 2024-12-13 16:32:53 +00:00
4fa183ab2d firmware: Finished (?) none state 2024-12-13 14:06:00 +00:00
76d8557f36 firmware: Semi-working emoji 2024-12-13 04:01:54 +00:00
facb46c068 firmware: Add second message to none state 2024-12-13 03:03:21 +00:00
e550c6d016 firmware: Call lv_disp_flush_ready() in vsync handler 2024-12-13 02:45:36 +00:00
84fb7553c5 firmware: Initial animation 2024-12-13 02:29:27 +00:00
17 changed files with 1177 additions and 98 deletions

View File

@@ -1,6 +1,7 @@
import configparser import configparser
import logging import logging
import os import os
import signal
import sys import sys
import infi.systray import infi.systray
@@ -42,12 +43,16 @@ def main():
conf = configparser.ConfigParser() conf = configparser.ConfigParser()
conf.read_dict({ conf.read_dict({
'general': {'log_level': 'info'}, 'general': {
'log_level': 'info',
'dummy_impl': False,
},
'valorant': {'player_uuid': ''}, 'valorant': {'player_uuid': ''},
}) })
conf.read(CONFIG_FILE) conf.read(CONFIG_FILE)
with open(CONFIG_FILE, 'w') as f: with open(CONFIG_FILE, 'w') as f:
conf.write(f) conf.write(f)
use_dummy_impl = conf.getboolean('general', 'dummy_impl')
log_level = parse_log_level(conf['general']['log_level']) log_level = parse_log_level(conf['general']['log_level'])
logging.basicConfig( logging.basicConfig(
@@ -59,20 +64,28 @@ def main():
log.error(f'No player UUID set, exiting...') log.error(f'No player UUID set, exiting...')
sys.exit(1) sys.exit(1)
val_handler = valconomy.ValconomyHandler() 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_sm = valconomy.ValconomyStateMachine(conf['valorant']['player_uuid'], val_handler)
val_client = valconomy.ValorantLocalClient(val_sm.handle_presence) val_client = valconomy.ValorantLocalClient(val_sm.handle_presence)
def do_quit(tray: infi.systray.SysTrayIcon): def do_quit(*args):
log.info('Shutting down') log.info('Shutting down')
val_client.stop() val_client.stop()
if not use_dummy_impl:
val_handler.stop()
with infi.systray.SysTrayIcon( with infi.systray.SysTrayIcon(
data_file('icon.ico'), 'Valconomy', on_quit=do_quit, data_file('icon.ico'), 'Valconomy', on_quit=do_quit,
menu_options=( menu_options=(
('Open data directory', None, do_open_datadir), ('Open data directory', None, do_open_datadir),
)) as tray: )) as tray:
val_client.run() signal.signal(signal.SIGINT, do_quit)
signal.signal(signal.SIGTERM, do_quit)
val_client.run()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -1,17 +1,47 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import hid import logging
USB_VID = 0x6969 import valconomy
USB_PID = 0x0004 from valconomy import ValorantPlayerInfo, RiotPlayerInfo, EconomyDecision
def main(): def main():
hid_handle = hid.device() logging.basicConfig(
hid_handle.open(vendor_id=USB_VID, product_id=USB_PID) format='%(asctime)s %(name)s %(levelname)s %(message)s', level=logging.INFO)
h = valconomy.HIDValconomyHandler()
try: try:
hid_handle.write(b'\x00test') 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: finally:
hid_handle.close() h.close()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@@ -81,7 +81,8 @@ class TestSm:
('round_start', True, 2, 0, EconomyDecision.BONUS), ('round_start', True, 2, 0, EconomyDecision.BONUS),
('round_start', False, 2, 1, EconomyDecision.MATCH_TEAM), ('round_start', False, 2, 1, EconomyDecision.MATCH_TEAM),
] + [ ] + [
('round_start', False, 2, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(8)] + [ ('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, 10, EconomyDecision.BUY),
('round_start', False, 2, 11, EconomyDecision.SAVE), ('round_start', False, 2, 11, EconomyDecision.SAVE),
('round_start', False, 2, 12, EconomyDecision.BUY), ('round_start', False, 2, 12, EconomyDecision.BUY),
@@ -112,7 +113,8 @@ class TestSm:
('round_start', True, 1, 0, EconomyDecision.BUY), ('round_start', True, 1, 0, EconomyDecision.BUY),
('round_start', False, 1, 1, EconomyDecision.SAVE), ('round_start', False, 1, 1, EconomyDecision.SAVE),
] + [ ] + [
('round_start', False, 1, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(9)] + [ ('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, 11, EconomyDecision.BUY),
('round_start', False, 1, 12, EconomyDecision.BUY), ('round_start', False, 1, 12, EconomyDecision.BUY),
] + [ ] + [
@@ -133,8 +135,10 @@ class TestSm:
self.do(queue_type='swiftplay', game_state='PREGAME') self.do(queue_type='swiftplay', game_state='PREGAME')
self.do(queue_type='swiftplay', game_state='INGAME') self.do(queue_type='swiftplay', game_state='INGAME')
for i in range(5): for i in range(4):
self.do(queue_type='swiftplay', game_state='INGAME', enemy_score=1 + i) 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') self.do(queue_type='swiftplay', game_state='INGAME')
assert self.mock.seq == [ assert self.mock.seq == [
@@ -144,9 +148,15 @@ class TestSm:
('pregame', False), ('pregame', False),
'game_start', 'game_start',
('round_start', None, 0, 0, EconomyDecision.BUY), ('round_start', None, 0, 0, EconomyDecision.BUY),
] + [ ('round_start', False, 0, 1, EconomyDecision.BUY),
('round_start', False, 0, 1 + i, EconomyDecision.BUY) for i in range(4)] + [ ('round_start', False, 0, 2, EconomyDecision.BUY),
('game_over', False), ('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): def test_tdm(self):

View File

@@ -5,6 +5,9 @@ import json
import logging import logging
from pprint import pprint from pprint import pprint
import os import os
import queue
import struct
import threading
import time import time
from typing import Callable from typing import Callable
@@ -45,16 +48,20 @@ class RiotPlayerInfo:
valorant: ValorantPlayerInfo = None valorant: ValorantPlayerInfo = None
@classmethod
def dummy(cls, **kwargs):
return cls('00000000-0000-0000-0000-000000000000', 'Player', 'gamer', 'dnd', **kwargs)
def full_name(self) -> str: def full_name(self) -> str:
return f'{self.name}#{self.tag}' return f'{self.name}#{self.tag}'
class ValorantLocalClient: class ValorantLocalClient:
lockfile_path = os.path.join(os.environ['LOCALAPPDATA'], r'Riot Games\Riot Client\Config\lockfile')
poll_interval = 0.5 poll_interval = 0.5
def __init__(self, callback: Callable[[RiotPlayerInfo, bool], None]): def __init__(self, callback: Callable[[RiotPlayerInfo, bool], None]):
self.callback = callback 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.port, self.password = None, None
self.running = True self.running = True
self.players = {} self.players = {}
@@ -228,7 +235,143 @@ class ValconomyHandler:
log.info('Hard luck...') log.info('Hard luck...')
class HIDValconomyHandler(ValconomyHandler): class HIDValconomyHandler(ValconomyHandler):
pass 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): class GameState(Enum):
NONE = 0 NONE = 0
@@ -265,17 +408,16 @@ class ValconomyStateMachine:
won_last = True if p.valorant.score > self.score else False won_last = True if p.valorant.score > self.score else False
self.round_history.append(won_last) self.round_history.append(won_last)
over = False
if p.valorant.queue_type == 'swiftplay': if p.valorant.queue_type == 'swiftplay':
if p.valorant.score == 5 or p.valorant.enemy_score == 5: if p.valorant.score == 5 or p.valorant.enemy_score == 5:
# Game is over over = True
return
eco = EconomyDecision.BUY eco = EconomyDecision.BUY
else: else:
if p.valorant.score > 12 or p.valorant.enemy_score > 12: 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: if p.valorant.queue_type == 'unrated' or abs(p.valorant.score - p.valorant.enemy_score) >= 2:
# Match is over over = True
return
eco = EconomyDecision.MATCH_TEAM eco = EconomyDecision.MATCH_TEAM
rounds_played = p.valorant.score + p.valorant.enemy_score rounds_played = p.valorant.score + p.valorant.enemy_score
@@ -300,8 +442,12 @@ class ValconomyStateMachine:
elif rounds_played >= 24: elif rounds_played >= 24:
# Sudden death or overtime (buy either way) # Sudden death or overtime (buy either way)
eco = EconomyDecision.BUY eco = EconomyDecision.BUY
elif rounds_played == 11:
# Last round of half
eco = EconomyDecision.BUY
self.handler.round_start(p, won_last, eco) if not over:
self.handler.round_start(p, won_last, eco)
self.score = p.valorant.score self.score = p.valorant.score
self.score_enemy = p.valorant.enemy_score self.score_enemy = p.valorant.enemy_score
self.last = GameState.IN_GAME self.last = GameState.IN_GAME

3
firmware/.gitignore vendored
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -2,43 +2,6 @@
perSystem = { libMy, pkgs, ... }: perSystem = { libMy, pkgs, ... }:
let let
genImgsPy = pkgs.python3.withPackages (ps: with ps; [ pillow ]); genImgsPy = pkgs.python3.withPackages (ps: with ps; [ pillow ]);
genImgs = pkgs.writeScriptBin "gen-imgs" ''
#!${genImgsPy}/bin/python
import itertools
import os
from PIL import Image
imgs = ['bg.png']
for fname in imgs:
with Image.open(os.path.join('assets/', fname)) as img:
w = img.width
h = img.height
data = [(r >> 3) << 11 | (g >> 2) << 5 | (b >> 3) 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 uint16_t _data[] = {\n')
for group in itertools.batched(data, 20):
f.write(' ' + ' '.join(map(lambda i: f'{i:#x},', group)) + '\n')
f.write('};\n\n')
f.write(
f'const lv_image_dsc_t ui_img_{basename} = {{\n'
' .header.magic = LV_IMAGE_HEADER_MAGIC,\n'
' .header.cf = LV_COLOR_FORMAT_RGB565,\n'
f' .header.w = {w},\n'
f' .header.h = {h},\n'
' .data_size = sizeof(_data) * 2,\n'
' .data = (const uint8_t *)_data,\n'
'};\n')
'';
in in
{ {
devenv.shells.firmware = libMy.withRootdir { devenv.shells.firmware = libMy.withRootdir {
@@ -46,7 +9,7 @@
esp-idf-esp32s3 esp-idf-esp32s3
picocom picocom
lv_font_conv lv_font_conv
genImgs imagemagick
]; ];
env = { env = {
@@ -58,11 +21,18 @@
idf.py set-target esp32s3 idf.py set-target esp32s3
''; '';
gen-fonts.exec = '' gen-fonts.exec = ''
for s in 120; do for s in 40 180; do
DEBUG='*' lv_font_conv --font assets/Tungsten-Bold.ttf --bpp 4 --size $s -r 0x20-0x7F --no-compress \ 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 --format lvgl --lv-include lvgl.h --lv-font-name lv_font_tungsten_"$s" -o main/font/tungsten_"$s".c
done done
''; '';
gen-imgs.exec = ''
for e in moon star sleep; do
magick assets/''${e}_orig.png -resize 120x120 assets/$e.png
done
${genImgsPy}/bin/python gen-imgs.py
'';
}; };
}; };
}; };

54
firmware/gen-imgs.py Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import itertools
import os
import struct
from PIL import Image
def rgb2h(r, g, b):
return (r >> 3) << 11 | (g >> 2) << 5 | (b >> 3)
def bytes_line(bs):
return ' ' + ' '.join(map(lambda i: f'{i:#x},', bs)) + '\n'
imgs = ['bg.png', 'moon.png', 'star.png', 'sleep.png']
for fname in imgs:
alpha = []
with Image.open(os.path.join('assets/', fname)) as img:
w = img.width
h = img.height
if img.has_transparency_data:
has_alpha = True
data = [rgb2h(r, g, b) for r, g, b, _ in img.getdata()]
alpha = [a for _, _, _, a in img.getdata()]
else:
data = [rgb2h(r, g, b) for r, g, b in img.getdata()]
basename = os.path.splitext(fname)[0]
with open(os.path.join('main/img/', basename + '.c'), 'w') as f:
f.write(
'#include <inttypes.h>\n\n'
'#include "lvgl.h"\n\n'
'static const uint8_t _data[] = {\n')
for group in itertools.batched(data, 10):
bs = bytearray()
for c in group:
bs += struct.pack('<H', c)
f.write(bytes_line(bs))
for group in itertools.batched(alpha, 20):
f.write(bytes_line(group))
f.write('};\n\n')
f.write(
f'const lv_image_dsc_t ui_img_{basename} = {{\n'
' .header.magic = LV_IMAGE_HEADER_MAGIC,\n'
f' .header.cf = LV_COLOR_FORMAT_RGB565{"A8" if alpha else ""},\n'
f' .header.w = {w},\n'
f' .header.h = {h},\n'
' .data_size = sizeof(_data),\n'
' .data = _data,\n'
'};\n')

View File

@@ -1,3 +1,6 @@
idf_component_register( idf_component_register(
SRCS "valconomy.c" "ui.c" "lcd.c" "usb.c" "font/tungsten_120.c" "img/bg.c" SRCS
"font/tungsten_40.c" "font/tungsten_180.c"
"img/bg.c" "img/moon.c" "img/star.c" "img/sleep.c"
"valconomy.c" "ui.c" "lcd.c" "usb.c"
INCLUDE_DIRS ".") INCLUDE_DIRS ".")

View File

@@ -26,20 +26,38 @@
#define LVGL_TASK_PRIORITY 2 #define LVGL_TASK_PRIORITY 2
#define LVGL_TASK_STACK_SIZE (8 * 1024) #define LVGL_TASK_STACK_SIZE (8 * 1024)
#define WAIT_VSYNC 0
static const char *TAG = "valconomy-lcd"; 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 // 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; 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) { static bool val_on_vsync(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_ctx) {
return false; BaseType_t high_task_awoken = pdFALSE;
#if WAIT_VSYNC
if (xSemaphoreTakeFromISR(sem_gui_ready, &high_task_awoken) == pdTRUE) {
xSemaphoreGiveFromISR(sem_vsync_end, &high_task_awoken);
}
#endif
lv_disp_flush_ready(user_ctx);
return high_task_awoken == pdTRUE;
} }
static void val_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { 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); 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 // 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); esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, px_map);
lv_disp_flush_ready(disp);
} }
static void val_increase_lvgl_tick(void *arg) { static void val_increase_lvgl_tick(void *arg) {
@@ -225,6 +243,13 @@ lv_display_t *val_setup_lvgl(esp_lcd_panel_handle_t lcd_panel, esp_lcd_touch_han
// set the callback which can copy the rendered image to an area of the display // 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); 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_LOGI(TAG, "Register event callbacks");
esp_lcd_rgb_panel_event_callbacks_t cbs = { esp_lcd_rgb_panel_event_callbacks_t cbs = {
.on_vsync = val_on_vsync, .on_vsync = val_on_vsync,

View File

@@ -2,35 +2,704 @@
#include "ui.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 const lv_font_t *font_normal = &lv_font_montserrat_24;
static lv_color_t color_primary, color_secondary; static lv_color_t color_primary, color_secondary;
static lv_color_t color_text_hero; 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; 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) { static void b_cfg_cb(lv_event_t *e) {
lv_obj_t *box = lv_msgbox_create(NULL); lv_obj_t *box = lv_msgbox_create(NULL);
lv_msgbox_add_title(box, "Hello"); lv_msgbox_add_title(box, "Settings");
lv_msgbox_add_text(box, "test message"); lv_msgbox_add_text(box, "Sorry, there aren't any right now.");
lv_msgbox_add_close_button(box); lv_msgbox_add_close_button(box);
} }
static void ui_home() { static void setup_next_state() {
lv_obj_t *i_bg = lv_image_create(lv_screen_active()); assert(state_ready);
lv_image_set_src(i_bg, &ui_img_bg); state_ready = false;
lv_obj_t *l_test = lv_label_create(lv_screen_active()); if (at_active) {
lv_obj_add_style(l_test, &s_hero, 0); lv_anim_timeline_delete(at_active);
lv_label_set_text(l_test, "VAL TIME"); 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;
}
lv_obj_t *b_cfg = lv_button_create(lv_screen_active()); o_active = lv_obj_create(o_container);
lv_obj_align(b_cfg, LV_ALIGN_BOTTOM_RIGHT, -10, -10); lv_obj_center(o_active);
lv_obj_add_event_cb(b_cfg, b_cfg_cb, LV_EVENT_CLICKED, NULL); 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);
lv_obj_t *l_cfg = lv_label_create(b_cfg); at_active = lv_anim_timeline_create();
lv_label_set_text(l_cfg, LV_SYMBOL_SETTINGS); at_active_rep = lv_anim_timeline_create();
lv_obj_center(l_cfg); 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) { void val_lvgl_ui(lv_display_t *disp) {
@@ -38,17 +707,49 @@ void val_lvgl_ui(lv_display_t *disp) {
color_secondary = lv_color_hex(0xf7518f); color_secondary = lv_color_hex(0xf7518f);
color_text_hero = lv_palette_lighten(LV_PALETTE_GREY, 2); 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 // init default theme
lv_theme_default_init( lv_theme_default_init(
disp, color_primary, color_secondary, disp, color_primary, color_secondary,
true, // dark theme true, // dark theme
font_normal); font_normal);
// lv_sysmon_hide_performance(disp); 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_init(&s_hero);
lv_style_set_text_font(&s_hero, &lv_font_tungsten_120); lv_style_set_text_font(&s_hero, f_hero_emoji);
lv_style_set_text_color(&s_hero, color_text_hero); lv_style_set_text_color(&s_hero, color_text_hero);
ui_home(); 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();
} }

View File

@@ -2,8 +2,40 @@
#include "lvgl.h" #include "lvgl.h"
LV_FONT_DECLARE(lv_font_tungsten_120) 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_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); void val_lvgl_ui(lv_display_t *disp);

View File

@@ -7,6 +7,8 @@
#include "common.h" #include "common.h"
#include "usb.h" #include "usb.h"
#include "lcd.h"
#include "ui.h"
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN) #define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN)
@@ -76,24 +78,100 @@ uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) {
// Invoked when received GET_REPORT control request // Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length. // Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request // Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) { uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buf, uint16_t reqlen) {
(void) instance; (void) instance;
(void) report_id; if (report_id != 0 || reqlen < 1) {
(void) report_type; return 0;
(void) buffer; }
(void) reqlen;
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 // Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 ) // received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) { 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) {
assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT); (void) instance;
ESP_LOGI(TAG, "Got %hu bytes report %hhu", bufsize, report_id); assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT && bufsize >= 1);
for (uint16_t i = 0; i < bufsize; i++) { if (!val_lvgl_lock(-1)) {
ESP_LOGI(TAG, "b: %02hhx", buffer[i]); 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) { void val_usb_init(void) {

View File

@@ -5,4 +5,17 @@
#define EPNUM_HID 0x01 #define EPNUM_HID 0x01
#define USB_EP_BUFSIZE 64 #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); void val_usb_init(void);

View File

@@ -7,6 +7,7 @@ CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_120M=y CONFIG_ESPTOOLPY_FLASHFREQ_120M=y
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=n CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=n
CONFIG_SPIRAM=y CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_MODE_OCT=y
@@ -20,10 +21,10 @@ CONFIG_ESP_WIFI_DPP_SUPPORT=y
CONFIG_FREERTOS_HZ=1000 CONFIG_FREERTOS_HZ=1000
CONFIG_TINYUSB_DEBUG_LEVEL=0 CONFIG_TINYUSB_DEBUG_LEVEL=0
CONFIG_TINYUSB_HID_COUNT=1 CONFIG_TINYUSB_HID_COUNT=1
CONFIG_LV_DEF_REFR_PERIOD=24
CONFIG_LV_USE_LOG=y CONFIG_LV_USE_LOG=y
CONFIG_LV_LOG_PRINTF=y CONFIG_LV_LOG_PRINTF=y
CONFIG_LV_FONT_MONTSERRAT_24=y CONFIG_LV_FONT_MONTSERRAT_24=y
CONFIG_LV_USE_SYSMON=y CONFIG_LV_USE_SYSMON=y
CONFIG_LV_USE_PERF_MONITOR=y CONFIG_LV_USE_PERF_MONITOR=y
CONFIG_LV_USE_IMGFONT=y
CONFIG_IDF_EXPERIMENTAL_FEATURES=y CONFIG_IDF_EXPERIMENTAL_FEATURES=y