Skip to content

Instantly share code, notes, and snippets.

@sonnny
Created January 5, 2024 13:50
Show Gist options
  • Select an option

  • Save sonnny/06a95a17ed4041a2a5d03ad5038ef173 to your computer and use it in GitHub Desktop.

Select an option

Save sonnny/06a95a17ed4041a2a5d03ad5038ef173 to your computer and use it in GitHub Desktop.
demo of pico w bluetooth using an nes gamepad, demo of pio irq, send signal to program from pio
demo of nes gamepad using pico w bluetooth
source from https://codeberg.org/chipfire/rppico-bt-gamepad
/****************************
* bt_gamepad.c
**********************/
/**
* Copyright (c) 2023 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdio.h>
#include "btstack.h"
#include "pico/cyw43_arch.h"
#include "pico/btstack_cyw43.h"
#include "pico/stdlib.h"
#include "server_common.h"
#include "nes_gamepad.h"
// this is the same report descriptor used by the gamepad USB example,
// with an added report ID which USB doesn't demand in case there's only one report.
uint8_t const reportDescriptor[] = {
0x05, 0x01,// Usage Page(Generic Desktop)
0x09, 0x05,// Usage(Gamepad)
0xa1, 0x01,// Collection(Application)
0x85, 0x01,// ReportID(1)
0xa1, 0x00,// Collection(Physical)
0x75, 0x08,// Report Size(8)
0x95, 0x02,// Report Count(2)
0x15, 0x81,// Logical Minimum(-127)
0x25, 0x7f,// Logical Maximum(127)
0x19, 0x30,// Usage Minimum(X)
0x29, 0x31,// Usage Maximum(Y)
0x81, 0x02,// Input(Data, Variable, Absolut)
0x75, 0x01,// Report Size(1)
0x95, 0x02,// Report Count(2)
0x15, 0x00,// Logical Minimum(0)
0x25, 0x01,// Logical Maximum(1)
0x19, 0x3d,// Usage Minimum(Start)
0x29, 0x3e,// Usage Maximum(Select)
0x81, 0x02,// Input(Data, Variable, Absolute)
0x05, 0x09,// Usage Page(Button)
0x19, 0x01,// Usage Minimum(Button 1)
0x29, 0x02,// Usage Maximum(Button 2)
0x81, 0x02,// Input(Data, Variable, Absolute)
0x75, 0x01,// Report Size(4)
0x95, 0x02,// Report Count(1)
0x81, 0x03,// Input(Constant, Variable)
0xc0,// End Collection
0xc0// End Collection
};
// data structures for callback handlers
static btstack_packet_callback_registration_t hci_event_callback_registration;
static btstack_packet_callback_registration_t sm_event_callback_registration;
uint8_t report[] = {0x00, 0x00, 0x00};
size_t const reportSize = 3;
// NOTE: I've never seen suspend being used so far
bool inSuspend = false;
// turn on/off LED to indicate suspend
void suspendNotification(int suspended) {
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, !suspended);
inSuspend = !suspended;
}
int main() {
uint gpState = 0xff, gpStateLast = 0;
stdio_init_all();
// remove comment to get some more time to attach your serial console
//sleep_ms(2000);
// initialize CYW43 driver architecture (will enable BT if/because CYW43_ENABLE_BLUETOOTH == 1)
if (cyw43_arch_init()) {
printf("failed to initialise cyw43_arch\n");
return -1;
}
// start gamepad reading code which uses programmable IO
if(gamepadStart(pio0, 8, 6, 7, &gpState) != 0) {
printf("Oops: failed to initialize gamepad!\n");
return -1;
}
printf("init done, starting bluetooth stack.\n");
// init Btstack
l2cap_init();
// init state machine and configure state machine to handle bonding
sm_init();
sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION | SM_AUTHREQ_BONDING);
// initialize ATT server, but don't set read and write callback functions yet.
// This is done afterwards in gamepadBtInit().
// profile_data comes from bt_nes_gamepad.h which is generated by Btstack.
att_server_init(profile_data, NULL, NULL);
// inform about BTstack state
hci_event_callback_registration.callback = &packet_handler;
hci_add_event_handler(&hci_event_callback_registration);
// register for SM events
sm_event_callback_registration.callback = &packet_handler;
sm_add_event_handler(&sm_event_callback_registration);
// packet handler is registered by the HID code.
// initialize the HID handler.
gamepadBtInit(reportDescriptor, sizeof(reportDescriptor), suspendNotification);
// turn on bluetooth!
hci_power_control(HCI_POWER_ON);
// in this loop, we check whether input data has changed. If so, we notify the client, if one is connected.
while(true) {
sleep_ms(20);
if(gpState != ~gpStateLast) {
// we need to invert gpState since the NES gamepad buttons report HIGH when not pressed
gpStateLast = ~gpState;
// prepare the report, then tell Btstack we want to send a notification.
// When sending HID reports using HID over GATT profile, we MUST NOT include the report ID (which would go in Byte 0, data
// items would follow: B1 = X, B2 = Y, B3 = buttons; This format is used when transmitting the report over USB).
// The report ID is added by the report host's Bluetooth stack, see HID over GATT spec, section 4.8.1
report[0] = (gpStateLast & 0x01) ? 0x7f : ((gpStateLast & 0x02) ? 0x81 : 0x00);// X axis; bit 0 is right, bit 1 left
report[1] = (gpStateLast & 0x04) ? 0x7f : ((gpStateLast & 0x08) ? 0x81 : 0x00);// Y axis; bit 3 is down, bit 2 is up
// start and select are reported separately in bits 0 and 1, A and B in bits 2 and 3.
report[2] = ((gpStateLast & 0x10) >> 4) | ((gpStateLast & 0x20) >> 4) | ((gpStateLast & 0x80) >> 5) | ((gpStateLast & 0x40) >> 3);
// transmit the report we just generated. requestReportTransmission() checks whether
// notifications are enabled, in case they're not, nothing happens.
// if(!inSuspend)
requestReportTransmission();
}
}
return 0;
}
/****************************
* nes_gamepad.c
**********************/
#include <stdio.h>
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "hardware/irq.h"
#include "build/nes_gamepad.pio.h"
uint pioProgOffset;
int pioSm;
PIO pioUsed;
uint *gpState;
void gamepadIrqHandler() {
// the interrupt handler, not much exiting stuff going on here.
// We just need to clear the interrupt and read the new data from the SM FIFO.
// clear interrupt first
pio_interrupt_clear(pioUsed, 0);
// then read current status word from SM FIFO and write it to the configured destination.
*gpState = pio_sm_get(pioUsed, pioSm);
}
int gamepadStart(PIO pio, uint dataPin, uint clkPin, uint psPin, uint *gpStateVar) {
gpState = NULL;
if(gpStateVar == NULL)
return -1;
// try to get a PIO SM, returns -1 if none is available.
pioSm = pio_claim_unused_sm(pio, false);
// if no SM was available, fail
if(pioSm < 0)
return -1;
// check whether the PIO has sufficient program space available. If not, fail.
if(!pio_can_add_program(pio, &NES_controller_interface_program))
return -1;
// for debugging
// printf("Pins assigned: %u %u %u SM used: %d\n", dataPin, clkPin, psPin, pioSm);
pioUsed = pio;
gpState = gpStateVar;
pioProgOffset = pio_add_program(pio, &NES_controller_interface_program);
nesControllerProgramInit(pio, (uint) pioSm, pioProgOffset, dataPin, clkPin, psPin);
// set up interrupt handler and source. Making the IRQ configurable would be nice.
irq_set_exclusive_handler(pio_get_index(pio) ? PIO1_IRQ_0 : PIO0_IRQ_0, gamepadIrqHandler);
irq_set_enabled(pio_get_index(pio) ? PIO1_IRQ_0 : PIO0_IRQ_0, true);
irq_set_priority(pio_get_index(pio) ? PIO1_IRQ_0 : PIO0_IRQ_0, PICO_LOWEST_IRQ_PRIORITY);
pio_set_irq0_source_enabled(pio, pis_interrupt0 + pioSm, true);
// enable the SM, that also starts the program
pio_sm_set_enabled(pio, pioSm, true);
return 0;
}
// can be used for debugging
uint getPioProgOff() {
return pioProgOffset;
}
uint getPioSm() {
return pioSm;
}
/*****************************
* nes_gamepad.h
****************************/
#include "hardware/pio.h"
#ifndef NES_GAMEPAD_H__
#define NES_GAMEPAD_H__
int gamepadStart(PIO pio, uint dataPin, uint clkPin, uint psPin, uint *gpStateVar);
uint getPioProgOff();
uint getPioSm();
#endif
/********************************
* nes_gamepad.pio
*****************************/
.program NES_controller_interface
.side_set 1
; every instruction ins delayed another three cycles so we can get a really slow clock.
.wrap_target
; first assert P/S and toggle the clock once to latch data
set PINS, 1 side 0 [3]
; set counter register
set X, 7 side 1 [3]
; deassert P/S to switch to shift register mode
set PINS, 0 side 1 [3]
readBits:
; read one bit, then jump back until we've read 8
in PINS, 1 side 0 [3]
jmp X-- readBits side 1 [3]
; notify CPU that new data has arrived
irq 0 side 1 [3]
.wrap
% c-sdk {
static inline void nesControllerProgramInit(PIO pio, uint sm, uint offset, uint dataPin, uint clkPin, uint psPin) {
// initialize GPIO pins. This changes their function to PIO.
// The pins are used as follows:
// dataPin is only read from (in)
// clkPin is controlled via side set
// psPin is controlled using set
pio_gpio_init(pio, dataPin);
pio_gpio_init(pio, clkPin);
pio_gpio_init(pio, psPin);
// configure pin directions
pio_sm_set_pindirs_with_mask(pio, sm, (1 << clkPin) | (1 << psPin), (1 << dataPin) | (1 << clkPin) | (1 << psPin));
pio_sm_config c = NES_controller_interface_program_get_default_config(offset);
// configure input, set and sideset pins. Only for set more than one pin can be configured at once.
sm_config_set_in_pins(&c, dataPin);
sm_config_set_set_pins(&c, psPin, 1);
sm_config_set_sideset_pins(&c, clkPin);
// configure sideset to use 1 bit, be mandatory and control pin values.
// NOTE: that's already done by get_default_config(), not required here.
//sm_config_set_sideset(&c, 1, false, false);
// join FIFOs as we only need the RX FIFO
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
// set the clock divider to 35400 so the gamepad state is read every 20 ms (50 Hz)
// each readout takes 20 instructions, leading to 80 cycles of the SM clock, i.e.,
// each clock cycle must last 250 us (4 kHz). To get a 4 kHz clock from the 141.6 MHz
// system clock we must divide it by 35,400.
sm_config_set_clkdiv_int_frac(&c, 35400, 0);
// output shift register isn't used so we don't need to configure it.
// configure the input shift register: Shift left, enable auto push, the register is
// considered full after eight bits have been shifted in.
sm_config_set_in_shift(&c, false, true, 8);
// initialize SM
pio_sm_init(pio, sm, offset, &c);
// the PIO program isn't started here, that's done by gamepadStart().
}
%}
/*****************************
* bt_nes_gamepad.gatt
***************************/
PRIMARY_SERVICE, GAP_SERVICE
CHARACTERISTIC, GAP_DEVICE_NAME, READ, "NESblue Gamepad"
PRIMARY_SERVICE, GATT_SERVICE
CHARACTERISTIC, GATT_DATABASE_HASH, READ,
PRIMARY_SERVICE, ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE
// report map maps the USB HID descriptor
CHARACTERISTIC, ORG_BLUETOOTH_CHARACTERISTIC_REPORT_MAP, DYNAMIC | READ,
CHARACTERISTIC, ORG_BLUETOOTH_CHARACTERISTIC_REPORT, DYNAMIC | READ | NOTIFY | ENCRYPTION_KEY_SIZE_16,
// reference descriptor containing report ID (1) and type (input == 1)
REPORT_REFERENCE, READ, 1, 1
// we only have one input report, no output (to send data to the device) or
// feature (to configure the device).
// contains HID version (2B; here: 1.11), country code (1B) and device information (1B; see section 2.10 of BT HID service spec)
// here: device is not remote wakeable (Bit 0) and normally connectable (Bit 1)
CHARACTERISTIC, ORG_BLUETOOTH_CHARACTERISTIC_HID_INFORMATION, READ, 01 11 00 02
// HID control point is used by the client to suspend our device (see section 2.11 of BT HID service spec)
CHARACTERISTIC, ORG_BLUETOOTH_CHARACTERISTIC_HID_CONTROL_POINT, DYNAMIC | WRITE_WITHOUT_RESPONSE,
/**********************************
* server_common.h
*****************************/
/**
* Copyright (c) 2023 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef SERVER_COMMON_H_
#define SERVER_COMMON_H_
// these variables are defined in other source files
extern int le_notification_enabled;
extern uint8_t report[];
extern size_t const reportSize;
extern uint8_t const profile_data[];
void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
uint16_t att_read_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size);
int att_write_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size);
int gamepadBtInit(const uint8_t *usbHidDescriptor, size_t usbHidDescriptorSize, void (*suspCb)(int s));
void requestReportTransmission();
#endif
/*************************
* server_common.c
***********************/
/**
* Copyright (c) 2023 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdio.h>
#include "btstack.h"
#include "bt_nes_gamepad.h"
#include "server_common.h"
// this is borrowed from Btstack's HIDS but adapted to our specific needs.
// It contains a number of status variables.
typedef struct{
// connection handle to remember which device we're currently connected to.
uint16_t con_handle;
const uint8_t * hid_descriptor;// -> USB report descriptor
uint16_t hid_descriptor_size;
// handles are identifiers Btstack assigns to the GATT entries.
// this one maps Report IDs to specific reports to establish links between the USB HID descriptor
// and the items in the GATT.
uint16_t hid_report_map_handle;
uint16_t hid_gamepad_input_value_handle;// we take the value from a global variable, it's not part of the device status.
uint16_t hid_gamepad_input_client_configuration_handle;// -> used to enable notifications
uint16_t hid_gamepad_input_client_configuration_value;// -> characteristic's current value (so we can identify changes)
// the control point is used to suspend our device and wake it up.
uint16_t hid_control_point_value_handle;
uint8_t hid_control_point_suspend;
} gamepad_device_t;
// -> see Core Spec Supplement A, 1.3 (LE General Discoverable, no BR/EDR support)
#define APP_AD_FLAGS 0x06
// advertisement data which is regularly broadcast so other devices can see our gamepad
static uint8_t adv_data[] = {
// Flags general discoverable
0x02, BLUETOOTH_DATA_TYPE_FLAGS, APP_AD_FLAGS,
// Name
0x10, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, 'N', 'E', 'S', 'b', 'l', 'u', 'e', ' ', 'G', 'a', 'm', 'e', 'p', 'a', 'd',
// Bluetooth HID service
0x03, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, 0x12, 0x18,
// HID appearance (HID gamepad)
0x03, BLUETOOTH_DATA_TYPE_APPEARANCE, 0xC4, 0x03,
};
static const uint8_t adv_data_len = sizeof(adv_data);
int le_notification_enabled;
static gamepad_device_t gpstatus;
static att_service_handler_t hid_service;
static btstack_context_callback_registration_t notificationContext;
// called when suspend status changes
static void (*suspendCallback) (int suspended);
void canSendNotification(void *context);
void suspendDummy(int suspended);
void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(size);
UNUSED(channel);
bd_addr_t local_addr;
if (packet_type != HCI_EVENT_PACKET) return;
uint8_t event_type = hci_event_packet_get_type(packet);
// printf("HCI event: %02x\n", event_type);
switch(event_type){
case BTSTACK_EVENT_STATE:
if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING) return;
gap_local_bd_addr(local_addr);
printf("BTstack up and running on %s.\n", bd_addr_to_str(local_addr));
// setup advertisements
uint16_t adv_int_min = 800;
uint16_t adv_int_max = 800;
uint8_t adv_type = 0;
bd_addr_t null_addr;
memset(null_addr, 0, 6);
gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
assert(adv_data_len <= 31); // BLE basic advertisements are limited to 31 bytes
gap_advertisements_set_data(adv_data_len, (uint8_t*) adv_data);
gap_advertisements_enable(1);
// and now we wait for things to come.
break;
case SM_EVENT_JUST_WORKS_REQUEST:
printf("Just Works requested\n");
sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
break;
case HCI_EVENT_DISCONNECTION_COMPLETE:
le_notification_enabled = 0;
gpstatus.con_handle = HCI_CON_HANDLE_INVALID;
// Btstack automatically starts sending advertisements again.
break;
default:
break;
}
}
int gamepadBtInit(const uint8_t *usbHidDescriptor, size_t usbHidDescriptorSize, void (*suspCb)(int s)) {
// instead of looking at the header file generated from bt_nes_gamepad.h, we can query
// the GATT server to get the handle values we need.
uint16_t start_handle = 0;
uint16_t end_handle = 0xffff;
int service_found = gatt_server_get_handle_range_for_service_with_uuid16(ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE, &start_handle, &end_handle);
gpstatus.con_handle = HCI_CON_HANDLE_INVALID;
gpstatus.hid_descriptor = usbHidDescriptor;
gpstatus.hid_descriptor_size = usbHidDescriptorSize;
if(suspCb == NULL) {
suspendCallback = suspendDummy;
} else {
suspendCallback = suspCb;
}
// is there an HID service? If not, fail.
btstack_assert(service_found != 0);
UNUSED(service_found);
// otherwise, find all the handles we need to identify which item of data is accessed.
// The order in which we query the handles doesn't have to match the order they're defined.
gpstatus.hid_report_map_handle = gatt_server_get_value_handle_for_characteristic_with_uuid16(start_handle, end_handle, ORG_BLUETOOTH_CHARACTERISTIC_REPORT_MAP);
gpstatus.hid_control_point_value_handle = gatt_server_get_value_handle_for_characteristic_with_uuid16(start_handle, end_handle, ORG_BLUETOOTH_CHARACTERISTIC_HID_CONTROL_POINT);
gpstatus.hid_control_point_suspend = 0;
gpstatus.hid_gamepad_input_value_handle = gatt_server_get_value_handle_for_characteristic_with_uuid16(start_handle, end_handle, ORG_BLUETOOTH_CHARACTERISTIC_REPORT);
gpstatus.hid_gamepad_input_client_configuration_handle = gatt_server_get_client_configuration_handle_for_characteristic_with_uuid16(start_handle, end_handle, ORG_BLUETOOTH_CHARACTERISTIC_REPORT);
gpstatus.hid_gamepad_input_client_configuration_value = 0;
printf("report map handle: %04x\n", gpstatus.hid_report_map_handle);
printf("input report handle: %04x\n", gpstatus.hid_gamepad_input_value_handle);
printf("input report client configuration handle: %04x\n", gpstatus.hid_gamepad_input_client_configuration_handle);
printf("control point handle: %04x\n", gpstatus.hid_control_point_value_handle);
// now we have to configure our callback handler and register it with the ATT Server
hid_service.start_handle = start_handle;
hid_service.end_handle = end_handle;
hid_service.read_callback = &att_read_callback;
hid_service.write_callback = &att_write_callback;
att_server_register_service_handler(&hid_service);
notificationContext.callback = &canSendNotification;
return 0;
}
// returns the number of bytes sent
uint16_t att_read_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size) {
UNUSED(connection_handle);
// using the ATT handle, we determine which characteristic the client wants to read - and what data we have to return.
if (att_handle == gpstatus.hid_report_map_handle){
printf("Read report map\n");
// byte arrays are blobs
return att_read_callback_handle_blob(gpstatus.hid_descriptor, gpstatus.hid_descriptor_size, offset, buffer, buffer_size);
}
if (att_handle == gpstatus.hid_control_point_value_handle){
// check whether buffer is != NULL and at least one byte in size
if (buffer && (buffer_size >= 1u)){
buffer[0] = gpstatus.hid_control_point_suspend;
}
return 1;
}
if (att_handle == gpstatus.hid_gamepad_input_value_handle){
return att_read_callback_handle_blob((const uint8_t *)report, reportSize, offset, buffer, buffer_size);
}
if (att_handle == gpstatus.hid_gamepad_input_client_configuration_handle){
return att_read_callback_handle_little_endian_16(gpstatus.hid_gamepad_input_client_configuration_value, offset, buffer, buffer_size);
}
return 0;
}
int att_write_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size) {
UNUSED(transaction_mode);
UNUSED(offset);
UNUSED(buffer_size);
if (att_handle == gpstatus.hid_control_point_value_handle){
// check whether buffer has at least one byte
if (buffer_size < 1u){
return ATT_ERROR_INVALID_OFFSET;
}
gpstatus.hid_control_point_suspend = buffer[0];
gpstatus.con_handle = connection_handle;
// 0 means enter suspend, 1 means exit suspend (see BT HID spec, section 2.11.2).
suspendCallback(gpstatus.hid_control_point_suspend == 0 ? 1 : 0);
return 1;
}
if (att_handle == gpstatus.hid_gamepad_input_client_configuration_handle){
// writing to the client configuration can enable or disable notifications.
le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION;
printf("Notifications, new value: %d\n", le_notification_enabled);
// store connection handle and send the first notification if they were enabled.
if (le_notification_enabled) {
gpstatus.con_handle = connection_handle;
//att_server_request_can_send_now_event(gpstatus.con_handle);
requestReportTransmission();
} else {
gpstatus.con_handle = HCI_CON_HANDLE_INVALID;
}
}
return 0;
}
void canSendNotification(void *context) {
// tell the ATT server we want to transmit a notification
att_server_notify(gpstatus.con_handle, gpstatus.hid_gamepad_input_value_handle, report, reportSize);
}
inline void requestReportTransmission() {
if(le_notification_enabled) {
att_server_request_to_send_notification(&notificationContext, gpstatus.con_handle);
}
}
void suspendDummy(int suspended) {
}
/*********************************
* CMakeLists.txt
*************************/
#target_sources(bt_gamepad PRIVATE bt_gamepad.c nes_gamepad.c)
# enable USB stdio
pico_enable_stdio_usb(bt_gamepad 1)
target_link_libraries(bt_gamepad
pico_stdlib
# for Bluetooth
pico_btstack_ble
pico_btstack_cyw43
pico_cyw43_arch_none
# for PIO
hardware_resets
hardware_irq
hardware_pio
# hardware_adc
)
target_include_directories(bt_gamepad PRIVATE
${CMAKE_CURRENT_LIST_DIR} # to add the folder "generated" created by Btstack into include path
)
pico_btstack_make_gatt_header(bt_gamepad PRIVATE "${CMAKE_CURRENT_LIST_DIR}/bt_nes_gamepad.gatt")
pico_add_extra_outputs(bt_gamepad)
#example_auto_set_url(bt_gamepad)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment