diff --git a/controller/hid-test.py b/controller/hid-test.py
index 4e95389..b2a5685 100755
--- a/controller/hid-test.py
+++ b/controller/hid-test.py
@@ -1,17 +1,18 @@
 #!/usr/bin/env python3
-import hid
-
-USB_VID = 0x6969
-USB_PID = 0x0004
+import valconomy
 
 def main():
-  hid_handle = hid.device()
-  hid_handle.open(vendor_id=USB_VID, product_id=USB_PID)
+  h = valconomy.HIDValconomyHandler()
 
   try:
-    hid_handle.write(b'\x00test')
+    h.menu(None, False)
+    h.none()
+    h.menu(None, True)
+    h.idle(None)
+
+    h.service()
   finally:
-    hid_handle.close()
+    h.close()
 
 if __name__ == '__main__':
   main()
diff --git a/controller/valconomy.py b/controller/valconomy.py
index 17dc2eb..2ae168d 100644
--- a/controller/valconomy.py
+++ b/controller/valconomy.py
@@ -1,10 +1,13 @@
 import base64
 from dataclasses import dataclass
 from enum import Enum
+import errno
 import json
 import logging
 from pprint import pprint
 import os
+import queue
+import struct
 import time
 from typing import Callable
 
@@ -49,12 +52,12 @@ class RiotPlayerInfo:
     return f'{self.name}#{self.tag}'
 
 class ValorantLocalClient:
-  lockfile_path = os.path.join(os.environ['LOCALAPPDATA'], r'Riot Games\Riot Client\Config\lockfile')
   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 = {}
@@ -228,7 +231,76 @@ class ValconomyHandler:
       log.info('Hard luck...')
 
 class HIDValconomyHandler(ValconomyHandler):
-  pass
+  vid = 0x6969
+  pid = 0x0004
+
+  def __init__(self):
+    self.dev = None
+    self.running = True
+    self.queue = queue.Queue(128)
+
+  def close(self):
+    if self.dev is not None:
+      self.dev.close()
+
+  def _dev_ready(self):
+    try:
+      if self.dev is None:
+        self.dev = hid.device()
+        self.dev.open(vendor_id=self.vid, product_id=self.pid)
+        log.info(f'USB device opened')
+
+      # 2 bytes: report ID and returned value
+      # We get back the same report ID and value
+      data = self.dev.get_input_report(0, 2)
+      assert len(data) == 2
+      return data[1] == 1
+    except OSError:
+      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()
+
+      try:
+        while not self._dev_ready():
+          if not self.running:
+            break
+          time.sleep(0.1)
+
+        self._do(cmd, *vals, fmt=fmt)
+      except OSError as ex:
+        log.warn(f'USB device lost, state dequeuing stalled')
+        continue
+
+  def run(self):
+    while self.running:
+      while self.queue.empty():
+        time.sleep(0.5)
+
+      self.service()
+
+  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)
 
 class GameState(Enum):
   NONE = 0
diff --git a/firmware/main/ui.c b/firmware/main/ui.c
index 19e23c1..10faa29 100644
--- a/firmware/main/ui.c
+++ b/firmware/main/ui.c
@@ -280,7 +280,5 @@ void val_lvgl_ui(lv_display_t *disp) {
   lv_label_set_text(l_cfg, LV_SYMBOL_SETTINGS);
   lv_obj_center(l_cfg);
 
-  // val_ui_none();
-  // val_ui_menu(true);
-  val_ui_idle();
+  val_ui_none();
 }
diff --git a/firmware/main/usb.c b/firmware/main/usb.c
index 819e277..1a016fe 100644
--- a/firmware/main/usb.c
+++ b/firmware/main/usb.c
@@ -8,6 +8,7 @@
 #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)
 
@@ -77,24 +78,55 @@ uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) {
 // Invoked when received GET_REPORT control request
 // Application must fill buffer report's content and return its length.
 // Return zero will cause the stack to STALL request
-uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) {
+uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buf, uint16_t reqlen) {
     (void) instance;
-    (void) report_id;
-    (void) report_type;
-    (void) buffer;
-    (void) reqlen;
+    if (report_id != 0 || reqlen < 1) {
+      return 0;
+    }
 
-    return 0;
+    // ESP_LOGI(TAG, "Got %hu bytes report %hhu", reqlen, report_id);
+    // for (uint16_t i = 0; i < reqlen; i++) {
+    //   ESP_LOGI(TAG, "b: %02hhx", buf[i]);
+    // }
+    buf[0] = val_ui_state_ready();
+    return 1;
 }
 
 // Invoked when received SET_REPORT control request or
 // received data on OUT endpoint ( Report ID = 0, Type = 0 )
-void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) {
-  assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT);
-  ESP_LOGI(TAG, "Got %hu bytes report %hhu", bufsize, report_id);
-  for (uint16_t i = 0; i < bufsize; i++) {
-    ESP_LOGI(TAG, "b: %02hhx", buffer[i]);
+void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buf, uint16_t bufsize) {
+  (void) instance;
+  assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT && bufsize >= 1);
+  if (!val_lvgl_lock(-1)) {
+    ESP_LOGE(TAG, "Failed to grab LVGL lock");
+    return;
   }
+  if (buf[0] > ST_IDLE) {
+    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;
+  }
+
+ret:
+  val_lvgl_unlock();
 }
 
 void val_usb_init(void) {
diff --git a/firmware/main/usb.h b/firmware/main/usb.h
index 015bd15..204971b 100644
--- a/firmware/main/usb.h
+++ b/firmware/main/usb.h
@@ -5,4 +5,10 @@
 #define EPNUM_HID 0x01
 #define USB_EP_BUFSIZE 64
 
+typedef enum val_state {
+  ST_NONE = 0,
+  ST_MENU,
+  ST_IDLE
+} val_state_t;
+
 void val_usb_init(void);