USB device-mode support (#613)
This commit is contained in:
@@ -26,7 +26,8 @@ enum BtHidDeviceMode {
|
||||
BT_HID_DEVICE_MODE_MOUSE,
|
||||
/** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */
|
||||
BT_HID_DEVICE_MODE_KEYBOARD_MOUSE,
|
||||
/** Gamepad (report ID 1, 8 bytes: 2-byte buttons + 6-byte axes). */
|
||||
/** Gamepad (report ID 1, 8 bytes: 5-byte axes X/Y/Rx/Ry/Z, 1-byte hat/dpad, 2-byte
|
||||
* buttons[10] padded). */
|
||||
BT_HID_DEVICE_MODE_GAMEPAD,
|
||||
};
|
||||
|
||||
@@ -90,7 +91,7 @@ struct BtHidDeviceApi {
|
||||
error_t (*send_mouse)(struct Device* device, const uint8_t* report, size_t len);
|
||||
|
||||
/**
|
||||
* Send a gamepad HID report (8 bytes: buttons[2] + axes[6]).
|
||||
* Send a gamepad HID report (8 bytes: axes[5] + hat[1] + buttons[2]).
|
||||
* @param[in] device the HID device child device
|
||||
* @param[in] report pointer to the 8-byte gamepad report
|
||||
* @param[in] len number of bytes (up to 8)
|
||||
|
||||
@@ -43,6 +43,20 @@ struct KeyboardKeyData {
|
||||
* separately. Drivers whose hardware cannot report Alt leave this false.
|
||||
*/
|
||||
bool alt;
|
||||
/**
|
||||
* @brief Standard USB HID keyboard usage code for this key (USB HID Usage Tables, page 0x07),
|
||||
* or 0 if this driver doesn't compute one (most don't - `key` is the only field most consumers
|
||||
* need). Populated by drivers whose hardware layout maps cleanly onto HID usage codes, so
|
||||
* consumers that want to mirror physical key presses as real HID reports (e.g. USB HID output)
|
||||
* don't have to reverse-engineer one out of `key`'s LVGL/ASCII encoding - which is lossy for
|
||||
* keys with no ASCII/LVGL representation at all, e.g. F1-F12.
|
||||
*/
|
||||
uint8_t hid_keycode;
|
||||
/**
|
||||
* @brief HID modifier bitmask (report byte 0: bit0=LeftCtrl, bit1=LeftShift, bit2=LeftAlt,
|
||||
* bit3=LeftGui, bit4-7=Right variants) matching hid_keycode, or 0 if hid_keycode is 0.
|
||||
*/
|
||||
uint8_t hid_modifier;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Device;
|
||||
struct DeviceType;
|
||||
struct UsbInterfaceContribution;
|
||||
|
||||
// ---- USB device-mode CDC-ACM addon ----
|
||||
|
||||
/**
|
||||
* USB CDC-ACM addon API (present a serial console over USB device mode). Unlike MSC/HID/MIDI,
|
||||
* CDC is not a primary USB device class and does not go through
|
||||
* usb_device_controller_claim()/release() - it's a devicetree-presence addon the USB device
|
||||
* controller composites into whichever primary class is active (or stands alone with none
|
||||
* active), toggled per-board via the usbdevicecdc0 devicetree child's status. This API is
|
||||
* exposed by that child device and called by the USB device controller itself, not by primary
|
||||
* class drivers.
|
||||
*/
|
||||
struct UsbCdcDeviceApi {
|
||||
/**
|
||||
* @param[in] device the CDC child device
|
||||
* @return true if this board's usbdevicecdc0 child is present and enabled (status != "disabled")
|
||||
*/
|
||||
bool (*is_present)(struct Device* device);
|
||||
|
||||
/**
|
||||
* Build this CDC instance's descriptor contribution, requesting interface/endpoint numbers
|
||||
* from the controller via usb_device_controller_allocate_interfaces(). Called by the USB
|
||||
* device controller itself during claim(), after the primary class's own contribution has
|
||||
* already been allocated.
|
||||
*
|
||||
* `interface_string_index` is the string-descriptor index CDC's own interface string will
|
||||
* land at once the controller appends it to the primary's string table (always
|
||||
* primary_string_descriptor_count, since CDC's string is always appended last) - CDC bakes
|
||||
* this index into its TUD_CDC_DESCRIPTOR bytes directly since it can't be patched after the
|
||||
* fact. `out_interface_string` is CDC's own interface string text for the controller to
|
||||
* append at that same index.
|
||||
*
|
||||
* @param[in] device the CDC child device
|
||||
* @param[in] controller the USB device controller (to call allocate_interfaces() on)
|
||||
* @param[in] interface_string_index string index this contribution's descriptor bytes must reference
|
||||
* @param[out] out_contribution the built fragment
|
||||
* @param[out] out_interface_string CDC's interface string text, to append at interface_string_index
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*build_contribution)(struct Device* device, struct Device* controller,
|
||||
uint8_t interface_string_index,
|
||||
struct UsbInterfaceContribution* out_contribution,
|
||||
const char** out_interface_string);
|
||||
|
||||
/**
|
||||
* Install the CDC ACM interface and the log-mirroring vprintf hook. Called by the USB device
|
||||
* controller after claim() successfully installs the composite descriptor.
|
||||
* @param[in] device the CDC child device
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*start_console)(struct Device* device);
|
||||
|
||||
/**
|
||||
* Uninstall the CDC ACM interface and restore the previous vprintf hook. Called by the USB
|
||||
* device controller before release() uninstalls the TinyUSB driver.
|
||||
* @param[in] device the CDC child device
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*stop_console)(struct Device* device);
|
||||
};
|
||||
|
||||
extern const struct DeviceType USB_CDC_DEVICE_TYPE;
|
||||
|
||||
/**
|
||||
* Find the first started USB CDC device and take a reference on it.
|
||||
* @return the device with an outstanding reference, or NULL if none is available - caller must
|
||||
* call device_put() exactly once when done, same as device_get_first_active_by_type().
|
||||
*/
|
||||
struct Device* usb_cdc_device_get(void);
|
||||
|
||||
bool usb_cdc_device_is_present(struct Device* device);
|
||||
error_t usb_cdc_device_build_contribution(struct Device* device, struct Device* controller,
|
||||
uint8_t interface_string_index,
|
||||
struct UsbInterfaceContribution* out_contribution,
|
||||
const char** out_interface_string);
|
||||
error_t usb_cdc_device_start_console(struct Device* device);
|
||||
error_t usb_cdc_device_stop_console(struct Device* device);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,192 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Device;
|
||||
struct DeviceType;
|
||||
|
||||
// ---- USB device-mode (peripheral) controller ----
|
||||
|
||||
/**
|
||||
* Which USB device-mode class currently owns the single TinyUSB device-mode slot.
|
||||
* Only one class may be active at a time - see usb_device_controller_claim(). CDC is not part of
|
||||
* this enum: it's an addon that composites into whichever primary class is active (or stands
|
||||
* alone with none active) rather than a primary itself - see usb_cdc_device.h.
|
||||
*/
|
||||
enum UsbDeviceClass {
|
||||
USB_DEVICE_CLASS_NONE,
|
||||
USB_DEVICE_CLASS_MSC,
|
||||
USB_DEVICE_CLASS_HID_KEYBOARD,
|
||||
USB_DEVICE_CLASS_MIDI,
|
||||
};
|
||||
|
||||
/**
|
||||
* A block of raw USB interface-descriptor bytes contributed by one class (MSC/HID/MIDI/CDC) to
|
||||
* the composite configuration descriptor the controller assembles at claim() time.
|
||||
*
|
||||
* Interface and endpoint numbers inside the descriptor bytes must already be renumbered by the
|
||||
* contributor to the base values the controller handed back via usb_device_controller_allocate_interfaces()
|
||||
* - the controller does not parse or patch the bytes, it only concatenates them behind the
|
||||
* config-descriptor header it writes itself.
|
||||
*
|
||||
* `hs_descriptor_bytes`/`hs_descriptor_bytes_len` are optional (leave both NULL/0 if this
|
||||
* contributor's bytes don't differ between full-speed and high-speed, e.g. HID/MIDI/CDC's
|
||||
* endpoint sizes are already speed-independent for their traffic) - only used when
|
||||
* TUD_OPT_HIGH_SPEED is set, mirroring the fs_/hs_configuration_descriptor split
|
||||
* tinyusb_config_t itself has (see MSC, whose bulk endpoint max-packet-size legitimately differs:
|
||||
* 64 bytes FS vs 512 bytes HS).
|
||||
*/
|
||||
struct UsbInterfaceContribution {
|
||||
const uint8_t* descriptor_bytes; // one or more TUD_*_DESCRIPTOR blocks, back to back (full-speed)
|
||||
size_t descriptor_bytes_len;
|
||||
const uint8_t* hs_descriptor_bytes; // optional high-speed variant; NULL to reuse descriptor_bytes
|
||||
size_t hs_descriptor_bytes_len; // must equal descriptor_bytes_len (same interface/endpoint layout, only sizes differ)
|
||||
uint8_t interface_count; // interfaces consumed (MSC/HID/CDC=1, MIDI=2)
|
||||
uint8_t in_endpoint_count; // IN endpoints consumed (excluding EP0)
|
||||
uint8_t out_endpoint_count; // OUT endpoints consumed
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface/endpoint numbers assigned to one contributor by the controller before it built its
|
||||
* descriptor bytes. Endpoint numbers are direction-local (IN and OUT each number from 1), as is
|
||||
* standard for USB - usb_device_controller_allocate_interfaces() hands out the next free *pair*
|
||||
* per direction, not a single shared counter.
|
||||
*/
|
||||
struct UsbInterfaceAllocation {
|
||||
uint8_t first_interface_number;
|
||||
uint8_t first_in_endpoint; // e.g. 0x81, 0x82, ... or 0 if in_endpoint_count was 0
|
||||
uint8_t first_out_endpoint; // e.g. 0x01, 0x02, ... or 0 if out_endpoint_count was 0
|
||||
};
|
||||
|
||||
/**
|
||||
* A fully-described primary class descriptor plus device/string descriptor metadata, submitted
|
||||
* to claim(). CDC (if enabled) is appended by the controller itself - primary contributors never
|
||||
* see or reference CDC's interface numbers.
|
||||
*/
|
||||
struct UsbDeviceClaimConfig {
|
||||
// Actually a `tusb_desc_device_t*` (TinyUSB's device descriptor struct) - void* here so this
|
||||
// kernel header doesn't need to depend on TinyUSB's own headers; the controller and every
|
||||
// contributor already build against TinyUSB directly and cast accordingly. Mutable: the
|
||||
// controller patches the class triad (bDeviceClass/SubClass/Protocol) in place at claim()
|
||||
// time depending on whether CDC is composited in.
|
||||
void* device_descriptor;
|
||||
const char* const* string_descriptor;
|
||||
size_t string_descriptor_count;
|
||||
struct UsbInterfaceContribution primary; // MSC or HID or MIDI's contribution
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared owner of the TinyUSB device-mode (peripheral) stack. Exactly one primary USB device
|
||||
* class (mass storage, HID, MIDI) may be installed at a time - callers claim the slot before use
|
||||
* and release it when done. CDC is a separate, orthogonal addon (see usb_cdc_device.h) that the
|
||||
* controller composites into whichever primary is active, independent of the claim/release cycle.
|
||||
* This exists so multiple independent drivers (MSC, HID, MIDI, CDC) can share the single
|
||||
* underlying `tinyusb_driver_install()` call, composite descriptor assembly, and any
|
||||
* board-specific PHY routing without needing to know about each other.
|
||||
*/
|
||||
struct UsbDeviceControllerApi {
|
||||
/**
|
||||
* Starts a new interface/endpoint allocation session, resetting the numbering counters
|
||||
* allocate_interfaces() hands out. Call once, before building any descriptor bytes, as the
|
||||
* first step of a claim() attempt (i.e. before the primary class's own allocate_interfaces()
|
||||
* call) - claim() itself calls this again internally for CDC's allocation, so primary
|
||||
* contributors only need to call it for their own single call.
|
||||
*
|
||||
* There is no explicit abort/cancel: a session abandoned after begin_claim() (e.g. a
|
||||
* contributor's own allocate_interfaces() call fails and it returns early without calling
|
||||
* claim()) is simply reset by the next begin_claim() call, which unconditionally reinitializes
|
||||
* the allocation state regardless of whether the previous session ever finished.
|
||||
*
|
||||
* @param[in] device the USB device controller device
|
||||
* @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*begin_claim)(struct Device* device);
|
||||
|
||||
/**
|
||||
* Ask the controller for the next free interface number and endpoint pair, before building
|
||||
* descriptor bytes. Call once per contributor (primary class, and CDC internally) per
|
||||
* begin_claim() session, in the order the resulting descriptor should list interfaces:
|
||||
* primary class first, then CDC (the controller enforces this order internally for CDC;
|
||||
* primary contributors just call this once for themselves, after begin_claim()).
|
||||
* @param[in] device the USB device controller device
|
||||
* @param[in] interface_count number of interfaces this contributor needs
|
||||
* @param[in] in_endpoint_count number of IN endpoints needed (0 if none)
|
||||
* @param[in] out_endpoint_count number of OUT endpoints needed (0 if none)
|
||||
* @param[out] out_allocation the assigned numbers
|
||||
* @retval ERROR_INVALID_STATE if called without a preceding begin_claim()
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*allocate_interfaces)(struct Device* device, uint8_t interface_count,
|
||||
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
|
||||
struct UsbInterfaceAllocation* out_allocation);
|
||||
|
||||
/**
|
||||
* Claim the USB device-mode slot for the given primary class and install the composite
|
||||
* descriptor (primary + CDC, if the board's usbdevicecdc0 child is enabled). Must be called
|
||||
* after begin_claim() and the primary's own allocate_interfaces() call, using the same
|
||||
* device-mode session (no other claim()/begin_claim() calls in between).
|
||||
* @param[in] device the USB device controller device
|
||||
* @param[in] usb_class the class to claim the slot for
|
||||
* @param[in] config the primary class's descriptor contribution and metadata
|
||||
* @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*claim)(struct Device* device, enum UsbDeviceClass usb_class,
|
||||
const struct UsbDeviceClaimConfig* config);
|
||||
|
||||
/**
|
||||
* Release the USB device-mode slot. Stops the CDC console (if it was composited in),
|
||||
* disconnects from the host, uninstalls the TinyUSB driver, and restores any board-specific
|
||||
* PHY routing. Only the current holder may release.
|
||||
* @param[in] device the USB device controller device
|
||||
* @param[in] usb_class the class releasing the slot; must match the current holder
|
||||
* @retval ERROR_INVALID_STATE if usb_class does not hold the slot
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*release)(struct Device* device, enum UsbDeviceClass usb_class);
|
||||
|
||||
/**
|
||||
* @param[in] device the USB device controller device
|
||||
* @return the class currently holding the slot, or USB_DEVICE_CLASS_NONE if free
|
||||
*/
|
||||
enum UsbDeviceClass (*get_active_class)(struct Device* device);
|
||||
|
||||
/**
|
||||
* @param[in] device the USB device controller device
|
||||
* @return true if a usbdevicecdc0 child device is present and enabled on this board
|
||||
*/
|
||||
bool (*is_cdc_enabled)(struct Device* device);
|
||||
};
|
||||
|
||||
extern const struct DeviceType USB_DEVICE_CONTROLLER_TYPE;
|
||||
|
||||
/**
|
||||
* Find the first started USB device controller and take a reference on it.
|
||||
* @return the device with an outstanding reference, or NULL if none is available - caller must
|
||||
* call device_put() exactly once when done, same as device_get_first_active_by_type().
|
||||
*/
|
||||
struct Device* usb_device_controller_get(void);
|
||||
|
||||
error_t usb_device_controller_begin_claim(struct Device* device);
|
||||
error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t interface_count,
|
||||
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
|
||||
struct UsbInterfaceAllocation* out_allocation);
|
||||
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class,
|
||||
const struct UsbDeviceClaimConfig* config);
|
||||
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class);
|
||||
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device);
|
||||
bool usb_device_controller_is_cdc_enabled(struct Device* device);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Device;
|
||||
struct DeviceType;
|
||||
|
||||
// ---- USB HID device mode ----
|
||||
|
||||
/**
|
||||
* Selects the HID report descriptor installed when this device operates as a USB HID
|
||||
* peripheral. Mirrors BtHidDeviceMode's shape (bluetooth_hid_device.h) so callers that want to
|
||||
* support both transports use the same mode taxonomy and send_* call shape.
|
||||
*/
|
||||
enum UsbHidDeviceMode {
|
||||
/** Keyboard (report ID 1, boot-protocol-compatible 8 bytes) + Consumer (report ID 2, 2 bytes). */
|
||||
USB_HID_DEVICE_MODE_KEYBOARD,
|
||||
/** Mouse only (report ID 1, 4 bytes). */
|
||||
USB_HID_DEVICE_MODE_MOUSE,
|
||||
/** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */
|
||||
USB_HID_DEVICE_MODE_KEYBOARD_MOUSE,
|
||||
/** Gamepad (report ID 1, 8 bytes: 5-byte axes, 1-byte hat/dpad, 2-byte buttons[10] padded). */
|
||||
USB_HID_DEVICE_MODE_GAMEPAD,
|
||||
};
|
||||
|
||||
/**
|
||||
* USB HID device profile API (present this device as a USB HID peripheral to a host).
|
||||
* This API is exposed by a child device of the USB device controller.
|
||||
*/
|
||||
struct UsbHidDeviceApi {
|
||||
/**
|
||||
* Claim the USB device-mode slot and start advertising as a USB HID device with the given
|
||||
* mode.
|
||||
* @param[in] device the HID device child device
|
||||
* @param[in] mode the HID device mode (keyboard, mouse, keyboard+mouse, gamepad)
|
||||
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*start)(struct Device* device, enum UsbHidDeviceMode mode);
|
||||
|
||||
/**
|
||||
* Stop presenting as a USB HID device and release the USB device-mode slot.
|
||||
* @param[in] device the HID device child device
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*stop)(struct Device* device);
|
||||
|
||||
/**
|
||||
* Override the USB product name string reported to the host (iProduct descriptor). Only
|
||||
* takes effect on the next start() - the descriptor is fixed for the lifetime of a claimed
|
||||
* session, matching bluetooth_set_device_name()'s pattern of being set before starting
|
||||
* advertising. If never called, each mode falls back to its own default name (e.g.
|
||||
* "Tactility Keyboard", "Tactility Mouse").
|
||||
* @param[in] device the HID device child device
|
||||
* @param[in] name the product name (copied; safe to free/reuse the caller's buffer after
|
||||
* this call returns)
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*set_name)(struct Device* device, const char* name);
|
||||
|
||||
/**
|
||||
* Send a keyboard HID report (report ID 1: modifier, reserved, keycodes[6] - 8 bytes after
|
||||
* the ID byte). Only valid in USB_HID_DEVICE_MODE_KEYBOARD or _KEYBOARD_MOUSE.
|
||||
* @param[in] device the HID device child device
|
||||
* @param[in] report pointer to the 8-byte keyboard report (modifier, reserved, keycode[6])
|
||||
* @param[in] len number of bytes (up to 8)
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*send_keyboard)(struct Device* device, const uint8_t* report, size_t len);
|
||||
|
||||
/**
|
||||
* Send a consumer control HID report (report ID 2: 16-bit usage code, little-endian).
|
||||
* Only valid in USB_HID_DEVICE_MODE_KEYBOARD or _KEYBOARD_MOUSE.
|
||||
* @param[in] device the HID device child device
|
||||
* @param[in] report pointer to the 2-byte consumer report
|
||||
* @param[in] len number of bytes (up to 2)
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*send_consumer)(struct Device* device, const uint8_t* report, size_t len);
|
||||
|
||||
/**
|
||||
* Send a mouse HID report (buttons, X, Y, wheel - 4 bytes). Report ID 1 in
|
||||
* USB_HID_DEVICE_MODE_MOUSE, report ID 3 in _KEYBOARD_MOUSE.
|
||||
* @param[in] device the HID device child device
|
||||
* @param[in] report pointer to the 4-byte mouse report
|
||||
* @param[in] len number of bytes (up to 4)
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*send_mouse)(struct Device* device, const uint8_t* report, size_t len);
|
||||
|
||||
/**
|
||||
* Send a gamepad HID report (report ID 1: axes[5] (X,Y,Rx,Ry,Z), hat/dpad[1] (low nibble),
|
||||
* buttons[2] (10 buttons + 6 padding bits) - 8 bytes). Only valid in
|
||||
* USB_HID_DEVICE_MODE_GAMEPAD. See hid_report_map_gamepad in hid_report_descriptors.cpp for
|
||||
* the full wire layout.
|
||||
* @param[in] device the HID device child device
|
||||
* @param[in] report pointer to the 8-byte gamepad report
|
||||
* @param[in] len number of bytes (up to 8)
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*send_gamepad)(struct Device* device, const uint8_t* report, size_t len);
|
||||
|
||||
/**
|
||||
* @param[in] device the HID device child device
|
||||
* @return true when a USB host has mounted the device and the HID interface is ready
|
||||
*/
|
||||
bool (*is_connected)(struct Device* device);
|
||||
};
|
||||
|
||||
extern const struct DeviceType USB_HID_DEVICE_TYPE;
|
||||
|
||||
/**
|
||||
* Find the first started USB HID device child device and take a reference on it.
|
||||
* @return the device with an outstanding reference, or NULL if none is available - caller must
|
||||
* call device_put() exactly once when done, same as device_get_first_active_by_type().
|
||||
*/
|
||||
struct Device* usb_hid_device_get(void);
|
||||
|
||||
error_t usb_hid_device_start(struct Device* device, enum UsbHidDeviceMode mode);
|
||||
error_t usb_hid_device_stop(struct Device* device);
|
||||
error_t usb_hid_device_set_name(struct Device* device, const char* name);
|
||||
error_t usb_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len);
|
||||
error_t usb_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len);
|
||||
error_t usb_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len);
|
||||
error_t usb_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len);
|
||||
bool usb_hid_device_is_connected(struct Device* device);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Device;
|
||||
struct DeviceType;
|
||||
|
||||
// ---- USB MIDI device mode ----
|
||||
|
||||
/**
|
||||
* USB MIDI device profile API (present this device as a USB MIDI peripheral to a host).
|
||||
* This API is exposed by a child device of the USB device controller. Mirrors
|
||||
* bluetooth_midi.h's shape (no mode enum - unlike HID, MIDI has exactly one device profile) so
|
||||
* the two transports stay symmetric for callers that want to support both.
|
||||
*/
|
||||
struct UsbMidiDeviceApi {
|
||||
/**
|
||||
* Claim the USB device-mode slot and start advertising as a USB MIDI device.
|
||||
* @param[in] device the MIDI device child device
|
||||
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*start)(struct Device* device);
|
||||
|
||||
/**
|
||||
* Stop presenting as a USB MIDI device and release the USB device-mode slot.
|
||||
* @param[in] device the MIDI device child device
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*stop)(struct Device* device);
|
||||
|
||||
/**
|
||||
* Override the USB product name string reported to the host (iProduct descriptor). Only
|
||||
* takes effect on the next start() - matches bluetooth_set_device_name()'s pattern of being
|
||||
* set before starting advertising. If never called, falls back to "Tactility MIDI Device".
|
||||
* @param[in] device the MIDI device child device
|
||||
* @param[in] name the product name (copied; safe to free/reuse the caller's buffer after
|
||||
* this call returns)
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*set_name)(struct Device* device, const char* name);
|
||||
|
||||
/**
|
||||
* Send raw MIDI message bytes over the USB MIDI connection.
|
||||
* @param[in] device the MIDI device child device
|
||||
* @param[in] msg the raw MIDI bytes
|
||||
* @param[in] len the number of bytes
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*send)(struct Device* device, const uint8_t* msg, size_t len);
|
||||
|
||||
/**
|
||||
* @param[in] device the MIDI device child device
|
||||
* @return true when a USB host has mounted the device and the MIDI interface is ready
|
||||
*/
|
||||
bool (*is_connected)(struct Device* device);
|
||||
};
|
||||
|
||||
extern const struct DeviceType USB_MIDI_DEVICE_TYPE;
|
||||
|
||||
/**
|
||||
* Find the first started USB MIDI device child device and take a reference on it.
|
||||
* @return the device with an outstanding reference, or NULL if none is available - caller must
|
||||
* call device_put() exactly once when done, same as device_get_first_active_by_type().
|
||||
*/
|
||||
struct Device* usb_midi_device_get(void);
|
||||
|
||||
error_t usb_midi_device_start(struct Device* device);
|
||||
error_t usb_midi_device_stop(struct Device* device);
|
||||
error_t usb_midi_device_set_name(struct Device* device, const char* name);
|
||||
error_t usb_midi_device_send(struct Device* device, const uint8_t* msg, size_t len);
|
||||
bool usb_midi_device_is_connected(struct Device* device);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Device;
|
||||
struct DeviceType;
|
||||
|
||||
// ---- USB mass-storage device mode ----
|
||||
|
||||
/** Which backing storage is exposed to the USB host as a mass-storage volume. */
|
||||
enum UsbMscDeviceSource {
|
||||
USB_MSC_DEVICE_SOURCE_SDMMC,
|
||||
USB_MSC_DEVICE_SOURCE_FLASH,
|
||||
};
|
||||
|
||||
/** Fired when the exposed volume's mount state changes (host mounted/unmounted it). */
|
||||
typedef void (*UsbMscDeviceMountChangedCallback)(bool mounted, void* context);
|
||||
|
||||
/**
|
||||
* USB mass-storage device profile API (present the board's SD card or internal flash as a USB
|
||||
* mass-storage device to a host).
|
||||
*/
|
||||
struct UsbMscDeviceApi {
|
||||
/**
|
||||
* Claim the USB device-mode slot and expose the given storage source as a USB mass-storage
|
||||
* volume.
|
||||
* @warning the caller must ensure the backing storage is unmounted/quiesced from local use
|
||||
* before calling this - a source exposed to a USB host while still locally mounted
|
||||
* risks filesystem corruption from two concurrent writers. Tactility's own flash-MSC
|
||||
* path (see UsbTusb.cpp / UsbSettingsApp) enforces this via a dedicated reboot-into-
|
||||
* MSC boot flow rather than unmounting live, so local access never overlaps with USB
|
||||
* exposure; local availability is only restored by rebooting back to normal OS.
|
||||
* @param[in] device the MSC device child device
|
||||
* @param[in] source which backing storage to expose
|
||||
* @param[in] source_handle the backing storage handle: a `sdmmc_card_t*` when source is
|
||||
* USB_MSC_DEVICE_SOURCE_SDMMC, or a pointer to a `wl_handle_t` (i.e. `wl_handle_t*`)
|
||||
* when source is USB_MSC_DEVICE_SOURCE_FLASH - passed by address, not cast through
|
||||
* `void*` by value, since `wl_handle_t` is a plain integer type where 0 is a valid
|
||||
* handle and would collide with the nullptr/"no handle" check otherwise. The caller
|
||||
* resolves this - platform-esp32 has no business knowing which wear-levelling
|
||||
* partition Tactility mounted as /data.
|
||||
* @param[in] mount_changed_cb optional callback fired on host mount/unmount, nullable
|
||||
* @param[in] context passed back to mount_changed_cb, nullable
|
||||
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
|
||||
* @retval ERROR_INVALID_ARGUMENT if source_handle is invalid for the given source
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t (*start)(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
|
||||
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context);
|
||||
|
||||
/**
|
||||
* Stop presenting as a USB mass-storage device and release the USB device-mode slot.
|
||||
* @param[in] device the MSC device child device
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t (*stop)(struct Device* device);
|
||||
|
||||
/**
|
||||
* @param[in] device the MSC device child device
|
||||
* @return true when a USB host currently has the volume mounted
|
||||
*/
|
||||
bool (*is_connected)(struct Device* device);
|
||||
};
|
||||
|
||||
extern const struct DeviceType USB_MSC_DEVICE_TYPE;
|
||||
|
||||
/**
|
||||
* Find the first started USB MSC device child device and take a reference on it.
|
||||
* @return the device with an outstanding reference, or NULL if none is available - caller must
|
||||
* call device_put() exactly once when done, same as device_get_first_active_by_type().
|
||||
*/
|
||||
struct Device* usb_msc_device_get(void);
|
||||
|
||||
error_t usb_msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
|
||||
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context);
|
||||
error_t usb_msc_device_stop(struct Device* device);
|
||||
bool usb_msc_device_is_connected(struct Device* device);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -10,10 +10,12 @@ extern "C" {
|
||||
error_t keyboard_read_key(Device* device, KeyboardKeyData* data) {
|
||||
const auto* driver = device_get_driver(device);
|
||||
|
||||
// Default the modifier fields here rather than in each driver: only drivers whose hardware can
|
||||
// report modifiers set them, and the rest would otherwise leave whatever the caller's stack held.
|
||||
// Default the modifier/HID fields here rather than in each driver: only drivers whose hardware
|
||||
// can report them set them, and the rest would otherwise leave whatever the caller's stack held.
|
||||
data->ctrl = false;
|
||||
data->alt = false;
|
||||
data->hid_keycode = 0;
|
||||
data->hid_modifier = 0;
|
||||
|
||||
return KEYBOARD_DRIVER_API(driver)->read_key(device, data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#include <tactility/drivers/usb_cdc_device.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
|
||||
#define USB_CDC_DEVICE_API(device) ((const struct UsbCdcDeviceApi*)device_get_driver(device)->api)
|
||||
|
||||
extern "C" {
|
||||
|
||||
const struct DeviceType USB_CDC_DEVICE_TYPE = {
|
||||
.name = "usb-cdc-device",
|
||||
};
|
||||
|
||||
struct Device* usb_cdc_device_get() {
|
||||
struct Device* found = nullptr;
|
||||
device_get_first_active_by_type(&USB_CDC_DEVICE_TYPE, &found);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool usb_cdc_device_is_present(struct Device* device) {
|
||||
return USB_CDC_DEVICE_API(device)->is_present(device);
|
||||
}
|
||||
|
||||
error_t usb_cdc_device_build_contribution(struct Device* device, struct Device* controller,
|
||||
uint8_t interface_string_index,
|
||||
struct UsbInterfaceContribution* out_contribution,
|
||||
const char** out_interface_string) {
|
||||
return USB_CDC_DEVICE_API(device)->build_contribution(device, controller, interface_string_index, out_contribution, out_interface_string);
|
||||
}
|
||||
|
||||
error_t usb_cdc_device_start_console(struct Device* device) {
|
||||
return USB_CDC_DEVICE_API(device)->start_console(device);
|
||||
}
|
||||
|
||||
error_t usb_cdc_device_stop_console(struct Device* device) {
|
||||
return USB_CDC_DEVICE_API(device)->stop_console(device);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,45 @@
|
||||
#include <tactility/drivers/usb_device_controller.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
|
||||
#define USB_DEVICE_CONTROLLER_API(device) ((const struct UsbDeviceControllerApi*)device_get_driver(device)->api)
|
||||
|
||||
extern "C" {
|
||||
|
||||
const struct DeviceType USB_DEVICE_CONTROLLER_TYPE = {
|
||||
.name = "usb-device-controller",
|
||||
};
|
||||
|
||||
struct Device* usb_device_controller_get() {
|
||||
struct Device* found = nullptr;
|
||||
device_get_first_active_by_type(&USB_DEVICE_CONTROLLER_TYPE, &found);
|
||||
return found;
|
||||
}
|
||||
|
||||
error_t usb_device_controller_begin_claim(struct Device* device) {
|
||||
return USB_DEVICE_CONTROLLER_API(device)->begin_claim(device);
|
||||
}
|
||||
|
||||
error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t interface_count,
|
||||
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
|
||||
struct UsbInterfaceAllocation* out_allocation) {
|
||||
return USB_DEVICE_CONTROLLER_API(device)->allocate_interfaces(device, interface_count, in_endpoint_count, out_endpoint_count, out_allocation);
|
||||
}
|
||||
|
||||
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class, const struct UsbDeviceClaimConfig* config) {
|
||||
return USB_DEVICE_CONTROLLER_API(device)->claim(device, usb_class, config);
|
||||
}
|
||||
|
||||
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class) {
|
||||
return USB_DEVICE_CONTROLLER_API(device)->release(device, usb_class);
|
||||
}
|
||||
|
||||
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device) {
|
||||
return USB_DEVICE_CONTROLLER_API(device)->get_active_class(device);
|
||||
}
|
||||
|
||||
bool usb_device_controller_is_cdc_enabled(struct Device* device) {
|
||||
return USB_DEVICE_CONTROLLER_API(device)->is_cdc_enabled(device);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,51 @@
|
||||
#include <tactility/drivers/usb_hid_device.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
|
||||
#define USB_HID_DEVICE_API(device) ((const struct UsbHidDeviceApi*)device_get_driver(device)->api)
|
||||
|
||||
extern "C" {
|
||||
|
||||
const struct DeviceType USB_HID_DEVICE_TYPE = {
|
||||
.name = "usb-hid-device",
|
||||
};
|
||||
|
||||
struct Device* usb_hid_device_get() {
|
||||
struct Device* found = nullptr;
|
||||
device_get_first_active_by_type(&USB_HID_DEVICE_TYPE, &found);
|
||||
return found;
|
||||
}
|
||||
|
||||
error_t usb_hid_device_start(struct Device* device, enum UsbHidDeviceMode mode) {
|
||||
return USB_HID_DEVICE_API(device)->start(device, mode);
|
||||
}
|
||||
|
||||
error_t usb_hid_device_stop(struct Device* device) {
|
||||
return USB_HID_DEVICE_API(device)->stop(device);
|
||||
}
|
||||
|
||||
error_t usb_hid_device_set_name(struct Device* device, const char* name) {
|
||||
return USB_HID_DEVICE_API(device)->set_name(device, name);
|
||||
}
|
||||
|
||||
error_t usb_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) {
|
||||
return USB_HID_DEVICE_API(device)->send_keyboard(device, report, len);
|
||||
}
|
||||
|
||||
error_t usb_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) {
|
||||
return USB_HID_DEVICE_API(device)->send_consumer(device, report, len);
|
||||
}
|
||||
|
||||
error_t usb_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) {
|
||||
return USB_HID_DEVICE_API(device)->send_mouse(device, report, len);
|
||||
}
|
||||
|
||||
error_t usb_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) {
|
||||
return USB_HID_DEVICE_API(device)->send_gamepad(device, report, len);
|
||||
}
|
||||
|
||||
bool usb_hid_device_is_connected(struct Device* device) {
|
||||
return USB_HID_DEVICE_API(device)->is_connected(device);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <tactility/drivers/usb_midi_device.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
|
||||
#define USB_MIDI_DEVICE_API(device) ((const struct UsbMidiDeviceApi*)device_get_driver(device)->api)
|
||||
|
||||
extern "C" {
|
||||
|
||||
const struct DeviceType USB_MIDI_DEVICE_TYPE = {
|
||||
.name = "usb-midi-device",
|
||||
};
|
||||
|
||||
struct Device* usb_midi_device_get() {
|
||||
struct Device* found = nullptr;
|
||||
device_get_first_active_by_type(&USB_MIDI_DEVICE_TYPE, &found);
|
||||
return found;
|
||||
}
|
||||
|
||||
error_t usb_midi_device_start(struct Device* device) {
|
||||
return USB_MIDI_DEVICE_API(device)->start(device);
|
||||
}
|
||||
|
||||
error_t usb_midi_device_stop(struct Device* device) {
|
||||
return USB_MIDI_DEVICE_API(device)->stop(device);
|
||||
}
|
||||
|
||||
error_t usb_midi_device_set_name(struct Device* device, const char* name) {
|
||||
return USB_MIDI_DEVICE_API(device)->set_name(device, name);
|
||||
}
|
||||
|
||||
error_t usb_midi_device_send(struct Device* device, const uint8_t* msg, size_t len) {
|
||||
return USB_MIDI_DEVICE_API(device)->send(device, msg, len);
|
||||
}
|
||||
|
||||
bool usb_midi_device_is_connected(struct Device* device) {
|
||||
return USB_MIDI_DEVICE_API(device)->is_connected(device);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,32 @@
|
||||
#include <tactility/drivers/usb_msc_device.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
|
||||
#define USB_MSC_DEVICE_API(device) ((const struct UsbMscDeviceApi*)device_get_driver(device)->api)
|
||||
|
||||
extern "C" {
|
||||
|
||||
const struct DeviceType USB_MSC_DEVICE_TYPE = {
|
||||
.name = "usb-msc-device",
|
||||
};
|
||||
|
||||
struct Device* usb_msc_device_get() {
|
||||
struct Device* found = nullptr;
|
||||
device_get_first_active_by_type(&USB_MSC_DEVICE_TYPE, &found);
|
||||
return found;
|
||||
}
|
||||
|
||||
error_t usb_msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
|
||||
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context) {
|
||||
return USB_MSC_DEVICE_API(device)->start(device, source, source_handle, mount_changed_cb, context);
|
||||
}
|
||||
|
||||
error_t usb_msc_device_stop(struct Device* device) {
|
||||
return USB_MSC_DEVICE_API(device)->stop(device);
|
||||
}
|
||||
|
||||
bool usb_msc_device_is_connected(struct Device* device) {
|
||||
return USB_MSC_DEVICE_API(device)->is_connected(device);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -33,9 +33,13 @@
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/drivers/trackball.h>
|
||||
#include <tactility/drivers/uart_controller.h>
|
||||
#include <tactility/drivers/usb_device_controller.h>
|
||||
#include <tactility/drivers/usb_hid_device.h>
|
||||
#include <tactility/drivers/usb_host_hid.h>
|
||||
#include <tactility/drivers/usb_host_midi.h>
|
||||
#include <tactility/drivers/usb_host_msc.h>
|
||||
#include <tactility/drivers/usb_midi_device.h>
|
||||
#include <tactility/drivers/usb_msc_device.h>
|
||||
#include <tactility/drivers/wifi.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/filesystem/file_mutex.h>
|
||||
@@ -422,6 +426,40 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
// drivers/usb_host_msc
|
||||
DEFINE_MODULE_SYMBOL(usb_msc_eject),
|
||||
DEFINE_MODULE_SYMBOL(USB_HOST_MSC_TYPE),
|
||||
// drivers/usb_device_controller
|
||||
DEFINE_MODULE_SYMBOL(usb_device_controller_get),
|
||||
DEFINE_MODULE_SYMBOL(usb_device_controller_begin_claim),
|
||||
DEFINE_MODULE_SYMBOL(usb_device_controller_allocate_interfaces),
|
||||
DEFINE_MODULE_SYMBOL(usb_device_controller_claim),
|
||||
DEFINE_MODULE_SYMBOL(usb_device_controller_release),
|
||||
DEFINE_MODULE_SYMBOL(usb_device_controller_get_active_class),
|
||||
DEFINE_MODULE_SYMBOL(usb_device_controller_is_cdc_enabled),
|
||||
DEFINE_MODULE_SYMBOL(USB_DEVICE_CONTROLLER_TYPE),
|
||||
// drivers/usb_hid_device
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_get),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_start),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_stop),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_set_name),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_send_keyboard),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_send_consumer),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_send_mouse),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_send_gamepad),
|
||||
DEFINE_MODULE_SYMBOL(usb_hid_device_is_connected),
|
||||
DEFINE_MODULE_SYMBOL(USB_HID_DEVICE_TYPE),
|
||||
// drivers/usb_msc_device
|
||||
DEFINE_MODULE_SYMBOL(usb_msc_device_get),
|
||||
DEFINE_MODULE_SYMBOL(usb_msc_device_start),
|
||||
DEFINE_MODULE_SYMBOL(usb_msc_device_stop),
|
||||
DEFINE_MODULE_SYMBOL(usb_msc_device_is_connected),
|
||||
DEFINE_MODULE_SYMBOL(USB_MSC_DEVICE_TYPE),
|
||||
// drivers/usb_midi_device
|
||||
DEFINE_MODULE_SYMBOL(usb_midi_device_get),
|
||||
DEFINE_MODULE_SYMBOL(usb_midi_device_start),
|
||||
DEFINE_MODULE_SYMBOL(usb_midi_device_stop),
|
||||
DEFINE_MODULE_SYMBOL(usb_midi_device_set_name),
|
||||
DEFINE_MODULE_SYMBOL(usb_midi_device_send),
|
||||
DEFINE_MODULE_SYMBOL(usb_midi_device_is_connected),
|
||||
DEFINE_MODULE_SYMBOL(USB_MIDI_DEVICE_TYPE),
|
||||
// concurrent/dispatcher
|
||||
DEFINE_MODULE_SYMBOL(dispatcher_alloc),
|
||||
DEFINE_MODULE_SYMBOL(dispatcher_free),
|
||||
|
||||
Reference in New Issue
Block a user