diff --git a/CAN_Commander_FlipperZero/can_commander/application.fam b/CAN_Commander_FlipperZero/can_commander/application.fam index 37f6bdc..405278c 100644 --- a/CAN_Commander_FlipperZero/can_commander/application.fam +++ b/CAN_Commander_FlipperZero/can_commander/application.fam @@ -9,11 +9,13 @@ App( fap_category="GPIO", sources=[ "can_commander.c", + "libraries/controller.c", "libraries/can_commander_uart.c", "views/dashboard/dashboard_core.c", "views/dashboard/dashboard_read.c", "views/dashboard/dashboard_bittrack.c", "views/dashboard/dashboard_metric.c", + "views/dashboard/dashboard_controller.c", "scenes_config/scene_functions.c", "scenes/main_menu.c", "scenes/debug_menu.c", diff --git a/CAN_Commander_FlipperZero/can_commander/can_commander.c b/CAN_Commander_FlipperZero/can_commander/can_commander.c index 3f5db09..f775764 100644 --- a/CAN_Commander_FlipperZero/can_commander/can_commander.c +++ b/CAN_Commander_FlipperZero/can_commander/can_commander.c @@ -1469,7 +1469,11 @@ void app_action_tool_start(App* app, CcToolId tool_id, const char* args, const c } if(status == CcStatusOk) { - dashboard_set_mode(app, dashboard_mode_for_tool(tool_id)); + if (strcmp(label, "controller") == 0) { + dashboard_set_mode(app, AppDashboardGameController); + } else { + dashboard_set_mode(app, dashboard_mode_for_tool(tool_id)); + } } else { dashboard_set_mode(app, AppDashboardNone); } diff --git a/CAN_Commander_FlipperZero/can_commander/can_commander.h b/CAN_Commander_FlipperZero/can_commander/can_commander.h index 61dd7ce..afe844a 100644 --- a/CAN_Commander_FlipperZero/can_commander/can_commander.h +++ b/CAN_Commander_FlipperZero/can_commander/can_commander.h @@ -14,6 +14,7 @@ #include #include "libraries/can_commander_uart.h" +#include "libraries/controller.h" #include "scenes_config/scene_functions.h" #define PROGRAM_VERSION "v2.1.1" @@ -65,6 +66,7 @@ typedef enum { AppDashboardObdPid, AppDashboardDbcDecode, AppDashboardCustomInject, + AppDashboardGameController, } AppDashboardMode; typedef enum { diff --git a/CAN_Commander_FlipperZero/can_commander/libraries/controller.c b/CAN_Commander_FlipperZero/can_commander/libraries/controller.c new file mode 100644 index 0000000..46724c1 --- /dev/null +++ b/CAN_Commander_FlipperZero/can_commander/libraries/controller.c @@ -0,0 +1,613 @@ +// Source: https://github.com/expected-ingot/flipper-xinput/blob/master/app.c + +#include "controller.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define XBOX_SURFACE_EP_IN 0x81 // IN Endpoint 1 +#define HID_EP_SZ 0x10 + +typedef struct { + uint8_t x, y; +} Position; +enum { + UP = 1 << 0, + DOWN = 1 << 1, + LEFT = 1 << 2, + RIGHT = 1 << 3, + START = 1 << 4, + BACK = 1 << 5, + L3 = 1 << 6, + R3 = 1 << 7, + LEFT_BUMPER = 1 << 8, + RIGHT_BUMPER = 1 << 9, + LOGO = 1 << 10, + THE_VOID_ONE = 1 << 11, // https://www.partsnotincluded.com/wp-content/uploads/2019/03/X360_ButtonPackets.jpg + A = 1 << 12, + B = 1 << 13, + X = 1 << 14, + Y = 1 << 15, +} xbox_buttons; + +// "shit" variables are unknown variables. I couldn't find any other way to add those. /shrug + +struct xbox_control_surface { + // For the buttons, 0 means not pressed and 1 means pressed + uint8_t message_type; // 0x00 + uint8_t length; // 20 + uint16_t buttons; // Refer to xbox_buttons enum for flags + uint8_t left_trigger; // 0-255 + uint8_t right_trigger; + int16_t left_x; + int16_t left_y; + int16_t right_x; + int16_t right_y; + uint8_t shit[6]; +} FURI_PACKED; + +struct xbox_control_surface current_surface; + +struct usb_unknown0_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; // 0x00, 0x01, 0x01, 0x25 + uint8_t bEndpointAddress; // 0x81 IN endpoint 1 + uint8_t bMaxDataSize; + uint8_t shit5, shit6, shit7, shit8, shit9; // 0x00, 0x00, 0x00, 0x00, 0x13 + uint8_t bEndpointAddress2; // 0x01 OUT endpoint 1 + uint8_t bMaxDataSize2; + uint8_t shit10, shit11; // 0x00, 0x00 +}; + +struct usb_unknown1_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; + uint8_t bEndpointAddress; + uint8_t bMaxDataSize; + uint8_t shit5; + uint8_t bEndpointAddress2; + uint8_t bMaxDataSize2; + uint8_t shit6; + uint8_t bEndpointAddress3; + uint8_t bMaxDataSize3; + uint8_t shit7, shit8, shit9, shit10, shit11, shit12; + uint8_t bEndpointAddress4; + uint8_t bMaxDataSize4; + uint8_t shit13, shit14, shit15, shit16, shit17; +}; + +struct usb_unknown2_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; + uint8_t bEndpointAddress; + uint8_t bMaxDataSize; + uint8_t shit5; +}; + +struct usb_unknown3_descriptor { + uint8_t bLength; + uint8_t bDescriptorType; + uint8_t shit1, shit2, shit3, shit4; +}; + +struct XboxIntf0Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown0_descriptor unknown; + struct usb_endpoint_descriptor hid_ep_in; + struct usb_endpoint_descriptor hid_ep_out; +}; + +struct XboxIntf1Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown1_descriptor unknown; + struct usb_endpoint_descriptor hid_ep_in_2; + struct usb_endpoint_descriptor hid_ep_out_2; + struct usb_endpoint_descriptor hid_ep_in_3; + struct usb_endpoint_descriptor hid_ep_out_3; +}; + +struct XboxIntf2Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown2_descriptor unknown; + struct usb_endpoint_descriptor hid_ep_in_4; +}; + +struct XboxIntf3Descriptor { + struct usb_interface_descriptor hid; + struct usb_unknown3_descriptor unknown; +}; + +struct XboxConfigDescriptor { + struct usb_config_descriptor config; + struct XboxIntf0Descriptor intf_0; + struct XboxIntf1Descriptor intf_1; + struct XboxIntf2Descriptor intf_2; + struct XboxIntf3Descriptor intf_3; +} FURI_PACKED; + +struct usb_device_descriptor xbox_device_desc = { + .bLength = 0x12, + .bDescriptorType = 0x01, + .bcdUSB = 0x0200, + .bDeviceClass = 0xFF, // Vendor specific + .bDeviceSubClass = 0xFF, + .bDeviceProtocol = 0xFF, + .bMaxPacketSize0 = 0x08, + .idVendor = 0x045E, + .idProduct = 0x028E, + .bcdDevice = 0x0114, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01, +}; + +const struct XboxConfigDescriptor xbox_cfg_desc = { + .config = { + .bLength = 9, + .bDescriptorType = 0x02, // CONFIGURATION + .wTotalLength = 153, + .bNumInterfaces = 4, + .bConfigurationValue = 1, + .iConfiguration = 0, + .bmAttributes = 0b10100000, // Not self-powered, remote wake-up + .bMaxPower = USB_CFG_POWER_MA(500), + }, + .intf_0 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 0, + .bAlternateSetting = 0, + .bNumEndpoints = 2, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0x5D, + .bInterfaceProtocol = 0x01, + .iInterface = 0, + }, + .unknown = { // Unknown descriptor + .bLength = 17, + .bDescriptorType = 0x21, // UNKNOWN + .shit1 = 0x00, .shit2 = 0x01, .shit3 = 0x01, + .shit4 = 0x25, + .bEndpointAddress = 0x81, // IN Endpoint 1 + .bMaxDataSize = 20, + .shit5 = 0x00, .shit6 = 0x00, .shit7 = 0x00, .shit8 = 0x00, .shit9 = 0x13, + .bEndpointAddress2 = 0x01, // OUT Endpoint 1 + .bMaxDataSize2 = 8, + .shit10 = 0x00, .shit11 = 0x00, + }, + .hid_ep_in = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x81, // IN Endpoint 1 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 4, + }, + .hid_ep_out = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x01, // OUT Endpoint 1 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 8, + } + }, + .intf_1 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 1, + .bAlternateSetting = 0, + .bNumEndpoints = 4, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0x5D, + .bInterfaceProtocol = 0x03, + .iInterface = 0, + }, + .unknown = { + .bLength = 27, + .bDescriptorType = 0x21, // UNKNOWN + .shit1 = 0x00, + .shit2 = 0x01, + .shit3 = 0x01, + .shit4 = 0x01, + .bEndpointAddress = 0x82, // IN Endpoint 2 + .bMaxDataSize = 64, + .shit5 = 0x01, + .bEndpointAddress2 = 0x02, // OUT Endpoint 2 + .bMaxDataSize2 = 32, + .shit6 = 0x16, + .bEndpointAddress3 = 0x83, // IN Endpoint 3 + .bMaxDataSize3 = 0, + .shit7 = 0x00, + .shit8 = 0x00, + .shit9 = 0x00, + .shit10 = 0x00, + .shit11 = 0x00, + .shit12 = 0x16, + .bEndpointAddress4 = 0x03, // OUT Endpoint 3 + .bMaxDataSize4 = 0, + .shit13 = 0x00, + .shit14 = 0x00, + .shit15 = 0x00, + .shit16 = 0x00, + .shit17 = 0x00, + }, + .hid_ep_in_2 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x82, // IN Endpoint 2 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 2, + }, + .hid_ep_out_2 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x02, // OUT Endpoint 2 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 4, + }, + .hid_ep_in_3 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x83, // IN Endpoint 3 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 64, + }, + .hid_ep_out_3 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x03, // OUT Endpoint 3 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 16, + }, + }, + .intf_2 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 2, + .bAlternateSetting = 0, + .bNumEndpoints = 1, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0x5D, + .bInterfaceProtocol = 0x02, + .iInterface = 0, + }, + .unknown = { + .bLength = 9, + .bDescriptorType = 0x21, // UNKNOWN + .shit1 = 0x00, + .shit2 = 0x01, + .shit3 = 0x01, + .shit4 = 0x22, + .bEndpointAddress = 0x84, // IN Endpoint 4 + .bMaxDataSize = 7, + .shit5 = 0x00, + }, + .hid_ep_in_4 = { + .bLength = 7, + .bDescriptorType = 0x05, // ENDPOINT + .bEndpointAddress = 0x84, // IN Endpoint 4 + .bmAttributes = 0x03, + .wMaxPacketSize = 32, + .bInterval = 16, + } + }, + .intf_3 = { + .hid = { + .bLength = 9, + .bDescriptorType = 0x04, // INTERFACE + .bInterfaceNumber = 3, + .bAlternateSetting = 0, + .bNumEndpoints = 0, + .bInterfaceClass = 0xFF, // Vendor Specific + .bInterfaceSubClass = 0xFD, + .bInterfaceProtocol = 0x13, + .iInterface = 4, + }, + .unknown = { + .bLength = 6, + .bDescriptorType = 0x41, // UNKNOWN + .shit1 = 0x00, + .shit2 = 0x01, + .shit3 = 0x01, + .shit4 = 0x03, + }, + }, + +}; + +bool boot_protocol = false; +usbd_device* usb_dev; +FuriSemaphore* hid_semaphore = NULL; +bool hid_connected = false; +bool started = false; +bool running = false; + +void* hid_set_string_descr(char* str) { + furi_assert(str); + + size_t len = strlen(str); + struct usb_string_descriptor* dev_str_desc = malloc(len * 2 + 2); + dev_str_desc->bLength = len * 2 + 2; + dev_str_desc->bDescriptorType = USB_DTYPE_STRING; + for(size_t i = 0; i < len; i++) + dev_str_desc->wString[i] = str[i]; + + return dev_str_desc; +} + +void hid_txrx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) { + UNUSED(dev); + UNUSED(ep); + if(event == usbd_evt_eptx) { + furi_semaphore_release(hid_semaphore); + } else if(boot_protocol == true) { + //uint8_t message_type; + //usbd_ep_read(usb_dev, ep, &led_state, sizeof(led_state)); + } else { + //struct HidReportLED leds; + //usbd_ep_read(usb_dev, ep, &leds, sizeof(leds)); + //led_state = leds.led_state; + } +} +usbd_respond hid_ep_config(usbd_device* dev, uint8_t cfg) { + switch(cfg) { + case 0: + /* deconfiguring device */ + usbd_ep_deconfig(dev, XBOX_SURFACE_EP_IN); + usbd_reg_endpoint(dev, XBOX_SURFACE_EP_IN, 0); + return usbd_ack; + case 1: + /* configuring device */ + usbd_ep_config(dev, XBOX_SURFACE_EP_IN, USB_EPTYPE_INTERRUPT, 32); + usbd_reg_endpoint(dev, XBOX_SURFACE_EP_IN, hid_txrx_ep_callback); + //usbd_ep_write(dev, HID_EP_IN, 0, 0); + boot_protocol = false; /* BIOS will SET_PROTOCOL if it wants this */ + return usbd_ack; + default: + return usbd_fail; + } +} +usbd_respond hid_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) { + UNUSED(callback); + /* HID control requests */ + if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) == + (USB_REQ_INTERFACE | USB_REQ_CLASS) && + req->wIndex == 0) { + switch(req->bRequest) { + case USB_HID_SETIDLE: + return usbd_ack; + case USB_HID_GETREPORT: + //if(boot_protocol == true) { + // dev->status.data_ptr = &hid_report.keyboard.boot; + // dev->status.data_count = sizeof(hid_report.keyboard.boot); + //} else { + // dev->status.data_ptr = &hid_report; + // dev->status.data_count = sizeof(hid_report); + //} + return usbd_ack; + case USB_HID_SETPROTOCOL: + if(req->wValue == 0) + boot_protocol = true; + else if(req->wValue == 1) + boot_protocol = false; + else + return usbd_fail; + return usbd_ack; + default: + return usbd_fail; + } + } + if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) == + (USB_REQ_INTERFACE | USB_REQ_STANDARD) && + req->wIndex == 0 && req->bRequest == USB_STD_GET_DESCRIPTOR) { + switch(req->wValue >> 8) { + case USB_DTYPE_HID: + dev->status.data_ptr = (uint8_t*)&(xbox_cfg_desc.intf_0.hid); + dev->status.data_count = sizeof(xbox_cfg_desc.intf_0.hid); + return usbd_ack; + default: + return usbd_fail; + } + } + return usbd_fail; +} + +void hid_deinit(usbd_device* dev) { + running = false; + usbd_reg_config(dev, NULL); + usbd_reg_control(dev, NULL); +} +void hid_on_wakeup(usbd_device* dev) { + UNUSED(dev); + if(!hid_connected) { + hid_connected = true; + } +} +void hid_on_suspend(usbd_device* dev) { + UNUSED(dev); + if(hid_connected) { + hid_connected = false; + running = false; + furi_semaphore_release(hid_semaphore); + } +} + +bool hid_send_report() { + if((hid_semaphore == NULL) || (hid_connected == false) || (running == false)) return false; + FuriStatus status = furi_semaphore_acquire(hid_semaphore, 8 * 2); + if(status == FuriStatusErrorTimeout) { + return false; + } + furi_check(status == FuriStatusOk); + if(hid_connected == false) { + return false; + } + if (boot_protocol != true) { + usbd_ep_write(usb_dev, XBOX_SURFACE_EP_IN, ¤t_surface, sizeof(current_surface)); + } + return true; +} + +void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx); + +FuriHalUsbInterface usb_xbox = { + .init = hid_init, + .deinit = hid_deinit, + .wakeup = hid_on_wakeup, + .suspend = hid_on_suspend, + + .dev_descr = (struct usb_device_descriptor*)&xbox_device_desc, + + .str_manuf_descr = NULL, //USB_STRING_DESC("Microsoft Corporation"), + .str_prod_descr = NULL, //USB_STRING_DESC("Controller"), + .str_serial_descr = NULL, //USB_STRING_DESC("08FEC93"), + + .cfg_descr = (void*)&xbox_cfg_desc +}; + +void hid_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) { + UNUSED(intf); + UNUSED(ctx); + // FuriHalUsbHidConfig* cfg = (FuriHalUsbHidConfig*)ctx; + // UNUSED(cfg); + if(hid_semaphore == NULL) hid_semaphore = furi_semaphore_alloc(1, 1); + usb_dev = dev; + + usb_xbox.dev_descr->iManufacturer = 1; + usb_xbox.dev_descr->iProduct = 2; + usb_xbox.dev_descr->iSerialNumber = 3; + usb_xbox.dev_descr->idVendor = 0x045E; // Microsoft + usb_xbox.dev_descr->idProduct = 0x028E; // Xbox 360 Controller + + usb_xbox.str_manuf_descr = hid_set_string_descr("Microsoft Corporation"); + usb_xbox.str_prod_descr = hid_set_string_descr("Controller"); + usb_xbox.str_serial_descr = hid_set_string_descr("08FEC93"); + + usbd_reg_config(dev, hid_ep_config); + usbd_reg_control(dev, hid_control); + usbd_connect(dev, true); +} + +//////////////////////////////////////////////////////////////////////////////// + +void controller_start() { + if (started) return; + started = true; + running = false; + + furi_check(furi_hal_usb_set_config(&usb_xbox, NULL)); +} + +void controller_stop() { + if (!started) return; + started = false; + running = false; + + furi_check(furi_hal_usb_set_config(NULL, NULL)); + + // reboot the system to get usb working again + furi_hal_power_reset(); +} + +void controller_enable() { + running = true; +} + +bool controller_is_enabled() { + return running; +} + +void controller_handle(const CcEvent* event) { + if(!event || event->type != CcEventTypeCanFrame) { + return; + } + + const uint8_t *data = event->data.can_frame.data; + + // TODO: Should parse this from DBC or similar and allow user customization + switch (event->data.can_frame.id) { + case 0x224: // Brake (range 0-512) + // Take bytes 4 and 5 + uint16_t brake_value = (data[4] << 8) | data[5]; + + // Divide by 2 to get 0-256 + brake_value >>= 1; + if (brake_value > 255) brake_value = 255; + + current_surface.left_trigger = brake_value; + break; + + + case 0x361: // Gas (range 0-200) + // Take byte 2 + uint8_t gas_value = data[2]; + + // Multiply by 8 to make it easier to press + gas_value <<= 3; + + current_surface.right_trigger = gas_value; + break; + + + case 0x025: // Steering (range -2048-2047) + // Take the bottom 12 bits from bytes 0 and 1 + int16_t steer_value = (uint16_t)((data[0] << 12) | (data[1] << 4)); + + // Multiply by 250 to make it easier to press + #define STEER_MULT 250 + if (steer_value > 0 && steer_value > (32767/STEER_MULT)) steer_value = 32767; + else if (steer_value < 0 && steer_value < (-32767/STEER_MULT)) steer_value = -32767; + else steer_value *= STEER_MULT; + + // Reverse because my car uses negative=right + steer_value = -steer_value; + + current_surface.left_x = steer_value; + break; + + case 0x614: // Turn signals + // Signal is inverted (1=no signal, 0=signal) + bool left_turn_signal = (~data[3] >> 5) & 0b1; + bool right_turn_signal = (~data[3] >> 4) & 0b1; + + // Use left turn signal as handbrake + if (left_turn_signal) + current_surface.buttons |= A; + else + current_surface.buttons &= ~A; + + // Use right turn signal as menu button + if (right_turn_signal) + current_surface.buttons |= START; + else + current_surface.buttons &= ~START; + break; + } + + // Don't send data to the computer too quickly + static uint32_t last_ticks = 0; + if (furi_get_tick() - last_ticks >= furi_ms_to_ticks(50)) { + last_ticks = furi_get_tick(); + + hid_send_report(); + } +} \ No newline at end of file diff --git a/CAN_Commander_FlipperZero/can_commander/libraries/controller.h b/CAN_Commander_FlipperZero/can_commander/libraries/controller.h new file mode 100644 index 0000000..b0cfae1 --- /dev/null +++ b/CAN_Commander_FlipperZero/can_commander/libraries/controller.h @@ -0,0 +1,10 @@ +#pragma once + +#include "can_commander.h" + +void controller_start(); +void controller_stop(); +void controller_enable(); +void controller_handle(const CcEvent* event); + +bool controller_is_enabled(); \ No newline at end of file diff --git a/CAN_Commander_FlipperZero/can_commander/scenes/tools_monitor_menu.c b/CAN_Commander_FlipperZero/can_commander/scenes/tools_monitor_menu.c index 3856339..285f726 100644 --- a/CAN_Commander_FlipperZero/can_commander/scenes/tools_monitor_menu.c +++ b/CAN_Commander_FlipperZero/can_commander/scenes/tools_monitor_menu.c @@ -10,6 +10,7 @@ typedef enum { ToolsMonitorReverseAuto, ToolsMonitorValtrack, ToolsMonitorSpeed, + ToolsGameController, } ToolsMonitorMenuIndex; static void cancommander_scene_tools_monitor_menu_callback(void* context, uint32_t index) { @@ -100,6 +101,12 @@ void cancommander_scene_tools_monitor_menu_on_enter(void* context) { ToolsMonitorSpeed, cancommander_scene_tools_monitor_menu_callback, app); + submenu_add_item( + app->submenu, + "Game Controller", + ToolsGameController, + cancommander_scene_tools_monitor_menu_callback, + app); submenu_set_selected_item( app->submenu, @@ -189,6 +196,16 @@ bool cancommander_scene_tools_monitor_menu_on_event(void* context, SceneManagerE "CAN Speed Test"); return true; + case ToolsGameController: + cancommander_scene_tools_monitor_open_tool_args( + app, + CcToolReadAll, + "controller", + app->args_read_all, + sizeof(app->args_read_all), + "Game Controller"); + return true; + default: return false; } diff --git a/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_controller.c b/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_controller.c new file mode 100644 index 0000000..1005eb6 --- /dev/null +++ b/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_controller.c @@ -0,0 +1,85 @@ +#include "dashboard_i.h" + +#include + +bool dashboard_controller_draw(Canvas* canvas, const AppDashboardModel* dashboard) { + if(!dashboard || (dashboard->mode != AppDashboardGameController)) { + return false; + } + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, "Game Controller"); + + if (!controller_is_enabled()) { + canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, "USB mode changed."); + canvas_draw_str_aligned(canvas, 64, 34, AlignCenter, AlignCenter, "Connect to host"); + canvas_draw_str_aligned(canvas, 64, 45, AlignCenter, AlignCenter, "then press OK to"); + canvas_draw_str_aligned(canvas, 64, 63, AlignCenter, AlignBottom, "start."); + return true; + } + + char fps_text[24] = {0}; + snprintf(fps_text, sizeof(fps_text), "Rate: %lu fps", (unsigned long)dashboard->read_rate_fps); + + // TODO: Should have a more useful UI, maybe visualize the controller and the current inputs? + canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignCenter, "Running."); + canvas_draw_str_aligned(canvas, 64, 34, AlignCenter, AlignCenter, fps_text); + return true; +} + +bool dashboard_controller_input(App* app, const InputEvent* event) { + if(!app || !event) { + return false; + } + + if (event->type == InputTypeShort && event->key == InputKeyOk) { + controller_enable(); + return true; + } + + return false; +} + +void dashboard_controller_update(App* app, const CcEvent* event) { + if(!event || event->type != CcEventTypeCanFrame) { + return; + } + + controller_handle(event); + + with_view_model( + app->dashboard_view, + AppDashboardModel * model, + { + model->mode = AppDashboardGameController; + strncpy(model->title, "Game Controller", sizeof(model->title) - 1U); + model->title[sizeof(model->title) - 1U] = '\0'; + + if(model->mode == AppDashboardGameController) { + const uint32_t ts = event->data.can_frame.ts_ms; + if(model->read_rate_window_start_ms == 0U) { + model->read_rate_window_start_ms = ts; + model->read_rate_window_count = 0U; + model->read_rate_fps = 0U; + } + + const uint32_t elapsed = ts - model->read_rate_window_start_ms; + if(elapsed >= 1000U) { + const uint32_t sample_ms = (elapsed == 0U) ? 1U : elapsed; + model->read_rate_fps = + ((uint32_t)model->read_rate_window_count * 1000U) / sample_ms; + model->read_rate_window_start_ms = ts; + model->read_rate_window_count = 0U; + } + + if(model->read_rate_window_count < 0xFFFFU) { + model->read_rate_window_count++; + } + } else { + model->read_rate_window_start_ms = 0U; + model->read_rate_window_count = 0U; + model->read_rate_fps = 0U; + } + }, + false); +} diff --git a/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_core.c b/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_core.c index 3b4a6b2..1ab0c76 100644 --- a/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_core.c +++ b/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_core.c @@ -29,6 +29,10 @@ void dashboard_view_draw(Canvas* canvas, void* model) { return; } + if(dashboard_controller_draw(canvas, dashboard)) { + return; + } + dashboard_metric_draw(canvas, dashboard); } @@ -54,6 +58,10 @@ bool dashboard_view_input(InputEvent* event, void* context) { return true; } + if(dashboard_controller_input(app, event)) { + return true; + } + return false; } @@ -289,8 +297,12 @@ static void dashboard_init_mode(App* app, AppDashboardMode mode) { app, "CUSTOM INJECT", "Slot", slot_value, "", "Waiting for slot data"); } break; + case AppDashboardGameController: + controller_start(); + break; case AppDashboardNone: default: + controller_stop(); dashboard_apply_template(app, "CAN Commander", "Live Monitor", "--", "", ""); break; } @@ -417,6 +429,12 @@ bool dashboard_handle_event(App* app, const CcEvent* event) { return true; } return false; + case AppDashboardGameController: + if(event->type == CcEventTypeCanFrame) { + dashboard_controller_update(app, event); + return true; + } + return false; case AppDashboardNone: default: return false; diff --git a/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_i.h b/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_i.h index 0ba8832..00874af 100644 --- a/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_i.h +++ b/CAN_Commander_FlipperZero/can_commander/views/dashboard/dashboard_i.h @@ -197,3 +197,7 @@ void dashboard_update_unique_ids(App* app, const CcEvent* event); void dashboard_update_reverse(App* app, const CcEvent* event); void dashboard_update_dbc_decode(App* app, const CcEvent* event); void dashboard_update_custom_inject(App* app, const CcEvent* event); + +bool dashboard_controller_draw(Canvas* canvas, const AppDashboardModel* dashboard); +bool dashboard_controller_input(App* app, const InputEvent* event); +void dashboard_controller_update(App* app, const CcEvent* event);