Skip to content

Commit

Permalink
Merge pull request #4126 from FiriaCTO/webusb
Browse files Browse the repository at this point in the history
WebUSB serial support (compile-time option, currently defaulted to OFF)
  • Loading branch information
dhalbert authored Feb 5, 2021
2 parents f6e881e + 1352938 commit 0a55cfb
Show file tree
Hide file tree
Showing 12 changed files with 407 additions and 10 deletions.
94 changes: 94 additions & 0 deletions WEBUSB_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!--
SPDX-FileCopyrightText: 2014 MicroPython & CircuitPython contributors (https://github.com/adafruit/circuitpython/graphs/contributors)
SPDX-License-Identifier: MIT
-->

# WebUSB Serial Support

To date, this has only been tested on one port (esp32s2), on one board (espressif_kaluga_1).

## What it does

If you have ever used CircuitPython on a platform with a graphical LCD display, you have probably
already seen multiple "consoles" in use (although the LCD console is "output only").

New compile-time option CIRCUITPY_USB_VENDOR enables an additional "console" that can be used in
parallel with the original (CDC) serial console.

Web pages that support the WebUSB standard can connect to the "vendor" interface and activate
this WebUSB serial console at any time.

You can type into either console, and CircuitPython output is sent to all active consoles.

One example of a web page you can use to test drive this feature can be found at:

https://adafruit.github.io/Adafruit_TinyUSB_Arduino/examples/webusb-serial/index.html

## How to enable

Update your platform's mpconfigboard.mk file to enable and disable specific types of USB interfaces.

CIRCUITPY_USB_HID = xxx
CIRCUITPY_USB_MIDI = xxx
CIRCUITPY_USB_VENDOR = xxx

On at least some of the hardware platforms, the maximum number of USB endpoints is fixed.
For example, on the ESP32S2, you must pick only one of the above 3 interfaces to be enabled.

Original espressif_kaluga_1 mpconfigboard.mk settings:

CIRCUITPY_USB_HID = 1
CIRCUITPY_USB_MIDI = 0
CIRCUITPY_USB_VENDOR = 0

Settings to enable WebUSB instead:

CIRCUITPY_USB_HID = 0
CIRCUITPY_USB_MIDI = 0
CIRCUITPY_USB_VENDOR = 1

Notice that to enable VENDOR on ESP32-S2, we had to give up HID. There may be platforms that can have both, or even all three.

## Implementation Notes

CircuitPython uses the tinyusb library.

The tinyusb library already has support for WebUSB serial.
The tinyusb examples already include a "WebUSB serial" example.

Sidenote - The use of the term "vendor" instead of "WebUSB" was done to match tinyusb.

Basically, this feature was ported into CircuitPython by pulling code snippets out of the
tinyusb example, and putting them where they best belonged in the CircuitPython codebase.

There was one complication:

tinyusb uses C preprocessor macros to define things like USB descriptors.

CircuitPython uses a Python program (tools/gen_usb_descriptor.py) to create USB descriptors (etc.)
using "helper objects" from another repo (adafruit_usb_descriptor). This means some of the example
code had to be adapted to the new programing model, and gen_usb_descriptor gained new command-line
options to control the generated code.

The generated files go into the "build" directory, look for autogen_usb_descriptor.c and
genhdr/autogen_usb_descriptor.h.


Also worth pointing out - the re-use of the CDC connect/disconnect mechanism is not actually part
of the WebUSB standard, it's more of "common idiom". We make use of it here because we need to know
when we should be paying attention to the WebUSB serial interface, and when we should ignore it..

## Possible future work areas

The current code uses the existing Python infrastructure to create the Interface descriptor, but
simply outputs the code snippets from the original tinyusb demo code to create the WEBUSB_URL,
BOS, and MS_OS_20 descriptors. I suppose additional work could be done to add these to the
adafruit_usb_descriptor project, and then gen_usb_descriptor.py could be modified to make use
of them.

Program gen_usb_descriptor.py creates objects for most interface types, regardless of whether or
not they are actually enabled. This increases the size of a generated string table. I made the
new vendor-interface-related code not do this (because some of the ARM platforms would no longer
build), but I did not go back and do this for the other interface types (CDC, MIDI, HID, etc.)
Some FLASH savings are probably possible if this is done.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Full Table of Contents
../BUILDING
../CODE_OF_CONDUCT
../license.rst
../WEBUSB_README

Indices and tables
==================
Expand Down
4 changes: 3 additions & 1 deletion ports/esp32s2/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ LIBS += -lm
endif

# TinyUSB defines
CFLAGS += -DCFG_TUSB_MCU=OPT_MCU_ESP32S2 -DCFG_TUSB_OS=OPT_OS_FREERTOS -DCFG_TUD_CDC_RX_BUFSIZE=1024 -DCFG_TUD_CDC_TX_BUFSIZE=1024 -DCFG_TUD_MSC_BUFSIZE=4096 -DCFG_TUD_MIDI_RX_BUFSIZE=128 -DCFG_TUD_MIDI_TX_BUFSIZE=128
CFLAGS += -DCFG_TUSB_MCU=OPT_MCU_ESP32S2 -DCFG_TUSB_OS=OPT_OS_FREERTOS -DCFG_TUD_CDC_RX_BUFSIZE=1024 -DCFG_TUD_CDC_TX_BUFSIZE=1024
CFLAGS += -DCFG_TUD_MSC_BUFSIZE=4096 -DCFG_TUD_MIDI_RX_BUFSIZE=128 -DCFG_TUD_MIDI_TX_BUFSIZE=128
CFLAGS += -DCFG_TUD_VENDOR_RX_BUFSIZE=128 -DCFG_TUD_VENDOR_TX_BUFSIZE=128


######################################
Expand Down
6 changes: 6 additions & 0 deletions ports/esp32s2/boards/espressif_kaluga_1/mpconfigboard.mk
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ CIRCUITPY_ESP_FLASH_MODE=dio
CIRCUITPY_ESP_FLASH_FREQ=80m
CIRCUITPY_ESP_FLASH_SIZE=4MB

# We only have enough endpoints available in hardware to
# enable ONE of these at a time.
CIRCUITPY_USB_MIDI = 1
CIRCUITPY_USB_HID = 0
CIRCUITPY_USB_VENDOR = 0

CIRCUITPY_MODULE=wrover
5 changes: 4 additions & 1 deletion ports/esp32s2/mpconfigport.mk
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ CIRCUITPY_I2CPERIPHERAL = 0
CIRCUITPY_ROTARYIO = 1
CIRCUITPY_NVM = 1
# We don't have enough endpoints to include MIDI.
CIRCUITPY_USB_MIDI = 0
CIRCUITPY_USB_MIDI ?= 0
CIRCUITPY_USB_HID ?= 1
# We have borrowed the VENDOR nomenclature from tinyusb. VENDOR AKA WEBUSB
CIRCUITPY_USB_VENDOR ?= 0
CIRCUITPY_WIFI = 1
CIRCUITPY_WATCHDOG ?= 1
CIRCUITPY_ESPIDF = 1
Expand Down
3 changes: 3 additions & 0 deletions py/circuitpy_defns.mk
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ endif
ifeq ($(CIRCUITPY_USB_MIDI),1)
SRC_PATTERNS += usb_midi/%
endif
ifeq ($(CIRCUITPY_USB_VENDOR),1)
SRC_PATTERNS += usb_vendor/%
endif
ifeq ($(CIRCUITPY_USTACK),1)
SRC_PATTERNS += ustack/%
endif
Expand Down
6 changes: 6 additions & 0 deletions py/circuitpy_mpconfig.mk
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ CFLAGS += -DCIRCUITPY_USB_HID=$(CIRCUITPY_USB_HID)
CIRCUITPY_USB_MIDI ?= 1
CFLAGS += -DCIRCUITPY_USB_MIDI=$(CIRCUITPY_USB_MIDI)

# Defaulting this to OFF initially because it has only been tested on a
# limited number of platforms, and the other platforms do not have this
# setting in their mpconfigport.mk and/or mpconfigboard.mk files yet.
CIRCUITPY_USB_VENDOR ?= 0
CFLAGS += -DCIRCUITPY_USB_VENDOR=$(CIRCUITPY_USB_VENDOR)

CIRCUITPY_PEW ?= 0
CFLAGS += -DCIRCUITPY_PEW=$(CIRCUITPY_PEW)

Expand Down
30 changes: 30 additions & 0 deletions supervisor/shared/serial.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ busio_uart_obj_t debug_uart;
byte buf_array[64];
#endif

#if CIRCUITPY_USB_VENDOR
bool tud_vendor_connected(void);
#endif

void serial_early_init(void) {
#if defined(DEBUG_UART_TX) && defined(DEBUG_UART_RX)
debug_uart.base.type = &busio_uart_type;
Expand All @@ -66,6 +70,12 @@ void serial_init(void) {
}

bool serial_connected(void) {
#if CIRCUITPY_USB_VENDOR
if (tud_vendor_connected()) {
return true;
}
#endif

#if defined(DEBUG_UART_TX) && defined(DEBUG_UART_RX)
return true;
#else
Expand All @@ -74,6 +84,14 @@ bool serial_connected(void) {
}

char serial_read(void) {
#if CIRCUITPY_USB_VENDOR
if (tud_vendor_connected() && tud_vendor_available() > 0) {
char tiny_buffer;
tud_vendor_read(&tiny_buffer, 1);
return tiny_buffer;
}
#endif

#if defined(DEBUG_UART_TX) && defined(DEBUG_UART_RX)
if (tud_cdc_connected() && tud_cdc_available() > 0) {
return (char) tud_cdc_read_char();
Expand All @@ -88,6 +106,12 @@ char serial_read(void) {
}

bool serial_bytes_available(void) {
#if CIRCUITPY_USB_VENDOR
if (tud_vendor_connected() && tud_vendor_available() > 0) {
return true;
}
#endif

#if defined(DEBUG_UART_TX) && defined(DEBUG_UART_RX)
return common_hal_busio_uart_rx_characters_available(&debug_uart) || (tud_cdc_available() > 0);
#else
Expand All @@ -104,6 +128,12 @@ void serial_write_substring(const char* text, uint32_t length) {
common_hal_terminalio_terminal_write(&supervisor_terminal, (const uint8_t*) text, length, &errcode);
#endif

#if CIRCUITPY_USB_VENDOR
if (tud_vendor_connected()) {
tud_vendor_write(text, length);
}
#endif

uint32_t count = 0;
while (count < length && tud_cdc_connected()) {
count += tud_cdc_write(text + count, length - count);
Expand Down
1 change: 1 addition & 0 deletions supervisor/shared/usb/tusb_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
#define CFG_TUD_MSC 1
#define CFG_TUD_HID CIRCUITPY_USB_HID
#define CFG_TUD_MIDI CIRCUITPY_USB_MIDI
#define CFG_TUD_VENDOR CIRCUITPY_USB_VENDOR
#define CFG_TUD_CUSTOM_CLASS 0

/*------------------------------------------------------------------*/
Expand Down
69 changes: 69 additions & 0 deletions supervisor/shared/usb/usb.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@

#include "tusb.h"

#if CIRCUITPY_USB_VENDOR
#include "genhdr/autogen_usb_descriptor.h"

// The WebUSB support being conditionally added to this file is based on the
// tinyusb demo examples/device/webusb_serial.

extern const tusb_desc_webusb_url_t desc_webusb_url;

static bool web_serial_connected = false;
#endif



// Serial number as hex characters. This writes directly to the USB
// descriptor.
extern uint16_t usb_serial_number[1 + COMMON_HAL_MCU_PROCESSOR_UID_LENGTH * 2];
Expand Down Expand Up @@ -145,6 +158,62 @@ void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) {
}
}

#if CIRCUITPY_USB_VENDOR
//--------------------------------------------------------------------+
// WebUSB use vendor class
//--------------------------------------------------------------------+

bool tud_vendor_connected(void)
{
return web_serial_connected;
}

// Invoked when a control transfer occurred on an interface of this class
// Driver response accordingly to the request and the transfer stage (setup/data/ack)
// return false to stall control endpoint (e.g unsupported request)
bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const * request)
{
// nothing to with DATA & ACK stage
if (stage != CONTROL_STAGE_SETUP ) return true;

switch (request->bRequest)
{
case VENDOR_REQUEST_WEBUSB:
// match vendor request in BOS descriptor
// Get landing page url
return tud_control_xfer(rhport, request, (void*) &desc_webusb_url, desc_webusb_url.bLength);

case VENDOR_REQUEST_MICROSOFT:
if ( request->wIndex == 7 )
{
// Get Microsoft OS 2.0 compatible descriptor
uint16_t total_len;
memcpy(&total_len, desc_ms_os_20+8, 2);

return tud_control_xfer(rhport, request, (void*) desc_ms_os_20, total_len);
} else
{
return false;
}

case 0x22:
// Webserial simulate the CDC_REQUEST_SET_CONTROL_LINE_STATE (0x22) to
// connect and disconnect.
web_serial_connected = (request->wValue != 0);

// response with status OK
return tud_control_status(rhport, request);

default:
// stall unknown request
return false;
}

return true;
}
#endif CIRCUITPY_USB_VENDOR


#if MICROPY_KBD_EXCEPTION

/**
Expand Down
20 changes: 20 additions & 0 deletions supervisor/supervisor.mk
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ else
shared-module/usb_midi/PortOut.c
endif

ifeq ($(CIRCUITPY_USB_VENDOR), 1)
SRC_SUPERVISOR += \
lib/tinyusb/src/class/vendor/vendor_device.c
endif

CFLAGS += -DUSB_AVAILABLE
endif

Expand All @@ -119,13 +124,22 @@ ifndef USB_INTERFACE_NAME
USB_INTERFACE_NAME = "CircuitPython"
endif

# In the following URL, don't include the https:// prefix.
# It gets added automatically.
ifndef USB_WEBUSB_URL
USB_WEBUSB_URL = "circuitpython.org"
endif

USB_DEVICES_COMPUTED := CDC,MSC
ifeq ($(CIRCUITPY_USB_MIDI),1)
USB_DEVICES_COMPUTED := $(USB_DEVICES_COMPUTED),AUDIO
endif
ifeq ($(CIRCUITPY_USB_HID),1)
USB_DEVICES_COMPUTED := $(USB_DEVICES_COMPUTED),HID
endif
ifeq ($(CIRCUITPY_USB_VENDOR),1)
USB_DEVICES_COMPUTED := $(USB_DEVICES_COMPUTED),VENDOR
endif
USB_DEVICES ?= "$(USB_DEVICES_COMPUTED)"

ifndef USB_HID_DEVICES
Expand Down Expand Up @@ -198,6 +212,12 @@ USB_DESCRIPTOR_ARGS = \
--output_c_file $(BUILD)/autogen_usb_descriptor.c\
--output_h_file $(BUILD)/genhdr/autogen_usb_descriptor.h

ifeq ($(CIRCUITPY_USB_VENDOR), 1)
USB_DESCRIPTOR_ARGS += \
--vendor_ep_num_out 0 --vendor_ep_num_in 0 \
--webusb_url $(USB_WEBUSB_URL)
endif

ifeq ($(USB_RENUMBER_ENDPOINTS), 0)
USB_DESCRIPTOR_ARGS += --no-renumber_endpoints
endif
Expand Down
Loading

0 comments on commit 0a55cfb

Please sign in to comment.