Browse Source

Zephyr port: WebUSB vendor class + status_led pin fix

Implement the WebUSB config-panel pipe as a Zephyr custom vendor class
(usb_webusb_class.cpp): a bulk OUT/IN interface bridged to byte rings that the
Adafruit_USBD_WebUSB shim's read/write/available/connected use. Register a BOS
WebUSB platform capability + landing-page URL (vendor request) so a browser can
discover and claim the interface.

Also fix the SuperMini blue LED (P0.15) absolute-pin override for the
status_led under the GPIO shim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jari 4 months ago
parent
commit
9c5e3cfbb1

+ 7 - 0
OpenPuckZephyr/CMakeLists.txt

@@ -16,6 +16,12 @@ target_include_directories(app PRIVATE
   ${CMAKE_CURRENT_SOURCE_DIR}/src
   ${CMAKE_CURRENT_SOURCE_DIR}/src/compat)
 
+# status_led uses Arduino logical pin numbers; under the absolute-nRF-pin
+# digitalWrite shim, the SuperMini's P0.15 blue LED is absolute pin 15 (the
+# header's default 24 is the Adafruit logical pin). LED_BUILTIN (P1.15 = 47) is
+# set in the Arduino.h shim.
+target_compile_definitions(app PRIVATE WAKE_LED_PIN_B=15)
+
 target_sources(app PRIVATE
   src/main.cpp
 
@@ -25,6 +31,7 @@ target_sources(app PRIVATE
   src/compat/serial_cdc_glue.cpp
   src/compat/usb_glue.cpp           # Zephyr USBD backing for the TinyUSB shim
   src/compat/usb_device_setup.cpp   # usbd_context (HID pool + CDC) bring-up
+  src/compat/usb_webusb_class.cpp   # WebUSB vendor class (config panel pipe)
 
   # ported firmware modules
   src/config.cpp

+ 67 - 0
OpenPuckZephyr/src/compat/usb_device_setup.cpp

@@ -7,6 +7,9 @@
 // through the shim (g_opk_usb_id) are applied here just before enable.
 #include <zephyr/usb/usbd.h>
 #include <zephyr/usb/usb_device.h>
+#include <zephyr/usb/bos.h>
+#include <zephyr/sys/byteorder.h>
+#include <zephyr/net_buf.h>
 #include <zephyr/device.h>
 #include <zephyr/devicetree.h>
 #include <zephyr/logging/log.h>
@@ -28,6 +31,64 @@ USBD_DESC_SERIAL_NUMBER_DEFINE(opk_sn);
 
 USBD_DESC_CONFIG_DEFINE(opk_fs_cfg_desc, "OpenPuck FS");
 
+// ---- WebUSB platform capability (BOS) + landing-page URL ----
+// Lets a browser discover the vendor interface (usb_webusb_class.cpp). The URL
+// is served via the WebUSB vendor request.
+#define OPK_WEBUSB_VENDOR_CODE 0x01U
+#define OPK_WEBUSB_LANDING 0x01U
+#define WEBUSB_REQ_GET_URL 0x02U
+#define WEBUSB_DESC_TYPE_URL 0x03U
+
+struct opk_bos_webusb {
+	struct usb_bos_platform_descriptor platform;
+	struct usb_bos_capability_webusb cap;
+} __packed;
+
+static const struct opk_bos_webusb opk_bos_webusb = {
+	.platform = {
+		.bLength = sizeof(struct usb_bos_platform_descriptor) +
+			   sizeof(struct usb_bos_capability_webusb),
+		.bDescriptorType = USB_DESC_DEVICE_CAPABILITY,
+		.bDevCapabilityType = USB_BOS_CAPABILITY_PLATFORM,
+		.bReserved = 0,
+		.PlatformCapabilityUUID = {
+			0x38, 0xB6, 0x08, 0x34, 0xA9, 0x09, 0xA0, 0x47,
+			0x8B, 0xFD, 0xA0, 0x76, 0x88, 0x15, 0xB6, 0x65,
+		},
+	},
+	.cap = {
+		.bcdVersion = sys_cpu_to_le16(0x0100),
+		.bVendorCode = OPK_WEBUSB_VENDOR_CODE,
+		.iLandingPage = OPK_WEBUSB_LANDING,
+	},
+};
+
+// Landing page (the browser config panel). UTF-8, scheme byte 0x01 = https.
+static const uint8_t opk_webusb_url[] = {
+	0x16, WEBUSB_DESC_TYPE_URL, 0x01,
+	'o', 'p', 'e', 'n', 'p', 'u', 'c', 'k', '.', 'l', 'o', 'c', 'a', 'l',
+	'/', 'p', 'a', 'n', 'e', 'l',
+};
+
+static int webusb_to_host(const struct usbd_context *const ctx,
+			  const struct usb_setup_packet *const setup,
+			  struct net_buf *const buf)
+{
+	(void)ctx;
+	if (setup->wIndex == WEBUSB_REQ_GET_URL &&
+	    USB_GET_DESCRIPTOR_INDEX(setup->wValue) == OPK_WEBUSB_LANDING) {
+		net_buf_add_mem(buf, opk_webusb_url,
+				MIN(net_buf_tailroom(buf),
+				    sizeof(opk_webusb_url)));
+		return 0;
+	}
+	return -ENOTSUP;
+}
+
+USBD_DESC_BOS_VREQ_DEFINE(opk_bos_vreq_webusb, sizeof(opk_bos_webusb),
+			  &opk_bos_webusb, OPK_WEBUSB_VENDOR_CODE, webusb_to_host,
+			  NULL);
+
 // Bus-powered + remote-wakeup (the firmware always arms remote wakeup).
 static const uint8_t opk_attr = USB_SCD_REMOTE_WAKEUP;
 USBD_CONFIGURATION_DEFINE(opk_fs_config, opk_attr, 250, &opk_fs_cfg_desc);
@@ -70,6 +131,12 @@ static int do_setup(void)
 	}
 
 	usbd_device_set_code_triple(&opk_usbd, USBD_SPEED_FS, 0, 0, 0);
+
+	// WebUSB BOS (browser discovers the vendor interface); best-effort.
+	(void)usbd_device_set_bcd_usb(&opk_usbd, USBD_SPEED_FS, 0x0210);
+	if (usbd_add_descriptor(&opk_usbd, &opk_bos_vreq_webusb))
+		LOG_WRN("WebUSB BOS add failed");
+
 	usbd_msg_register_cb(&opk_usbd, msg_cb);
 	s_setup_done = true;
 	return 0;

+ 13 - 8
OpenPuckZephyr/src/compat/usb_glue.cpp

@@ -328,9 +328,13 @@ extern "C" void opk_usb_msg(const enum usbd_msg_type type)
 }
 
 // ===================== WebUSB (vendor) =====================
-// Scaffolded inert: the WebUSB vendor pipe needs a Zephyr custom vendor class +
-// BOS/MS-OS-2.0 descriptors (the config panel won't connect until then). The
-// firmware tolerates connected()==false (it simply skips the panel).
+// Backed by the Zephyr custom vendor class in usb_webusb_class.cpp.
+extern "C" bool opk_webusb_connected(void);
+extern "C" int opk_webusb_available(void);
+extern "C" int opk_webusb_read(void);
+extern "C" uint32_t opk_webusb_write(const void *data, uint32_t n);
+extern "C" void opk_webusb_flush(void);
+
 Adafruit_USBD_WebUSB::Adafruit_USBD_WebUSB()
 {
 }
@@ -340,22 +344,23 @@ bool Adafruit_USBD_WebUSB::begin()
 }
 bool Adafruit_USBD_WebUSB::connected()
 {
-	return false;
+	return opk_webusb_connected();
 }
 int Adafruit_USBD_WebUSB::available()
 {
-	return 0;
+	return opk_webusb_available();
 }
 int Adafruit_USBD_WebUSB::read()
 {
-	return -1;
+	return opk_webusb_read();
 }
-uint32_t Adafruit_USBD_WebUSB::write(const void *, uint32_t n)
+uint32_t Adafruit_USBD_WebUSB::write(const void *data, uint32_t n)
 {
-	return n;
+	return opk_webusb_write(data, n);
 }
 void Adafruit_USBD_WebUSB::flush()
 {
+	opk_webusb_flush();
 }
 
 // ===================== DFU entry =====================

+ 211 - 0
OpenPuckZephyr/src/compat/usb_webusb_class.cpp

@@ -0,0 +1,211 @@
+// usb_webusb_class.cpp -- Zephyr custom vendor class backing the WebUSB shim.
+//
+// Implements the single WebUSB vendor interface (1 bulk OUT + 1 bulk IN, FS) and
+// bridges it to byte rings so the firmware's Adafruit_USBD_WebUSB API works:
+//   read()/available()  <- OUT endpoint  (browser -> device)
+//   write()/flush()      -> IN endpoint   (device -> browser)
+// The BOS WebUSB platform-capability + URL request and the MS OS 2.0 descriptor
+// (for WinUSB binding) are registered in usb_device_setup.cpp.
+//
+// Modeled on Zephyr's samples/subsys/usb/webusb. nRF52840 is full-speed only.
+#include <zephyr/usb/usbd.h>
+#include <zephyr/drivers/usb/udc.h>
+#include <zephyr/sys/byteorder.h>
+#include <zephyr/sys/ring_buffer.h>
+#include <zephyr/logging/log.h>
+#include <string.h>
+
+LOG_MODULE_REGISTER(opk_webusb, LOG_LEVEL_ERR);
+
+#define WUSB_ENABLED 0
+
+NET_BUF_POOL_FIXED_DEFINE(wusb_pool, 4, 64, sizeof(struct udc_buf_info), NULL);
+
+RING_BUF_DECLARE(wusb_rx_ring, 256);
+static atomic_t wusb_state;
+
+struct wusb_desc {
+	struct usb_if_descriptor if0;
+	struct usb_ep_descriptor if0_out_ep;
+	struct usb_ep_descriptor if0_in_ep;
+	struct usb_desc_header nil_desc;
+};
+
+struct wusb_data {
+	struct wusb_desc *const desc;
+	const struct usb_desc_header **const fs_desc;
+};
+
+static struct usbd_class_data *wusb_cd;
+
+static uint8_t out_ep(struct usbd_class_data *c)
+{
+	struct wusb_data *d = (struct wusb_data *)usbd_class_get_private(c);
+	return d->desc->if0_out_ep.bEndpointAddress;
+}
+static uint8_t in_ep(struct usbd_class_data *c)
+{
+	struct wusb_data *d = (struct wusb_data *)usbd_class_get_private(c);
+	return d->desc->if0_in_ep.bEndpointAddress;
+}
+
+static struct net_buf *alloc_buf(uint8_t ep)
+{
+	struct net_buf *buf = net_buf_alloc(&wusb_pool, K_NO_WAIT);
+	if (!buf)
+		return NULL;
+	struct udc_buf_info *bi = udc_get_buf_info(buf);
+	memset(bi, 0, sizeof(*bi));
+	bi->ep = ep;
+	return buf;
+}
+
+static void queue_out(struct usbd_class_data *c)
+{
+	struct net_buf *buf = alloc_buf(out_ep(c));
+	if (!buf)
+		return;
+	if (usbd_ep_enqueue(c, buf))
+		net_buf_unref(buf);
+}
+
+static int wusb_request(struct usbd_class_data *c, struct net_buf *buf, int err)
+{
+	struct usbd_context *ctx = usbd_class_get_ctx(c);
+	struct udc_buf_info *bi = (struct udc_buf_info *)net_buf_user_data(buf);
+
+	if (err == 0 && bi->ep == out_ep(c)) {
+		// browser -> device: stash payload, re-arm the OUT read
+		if (buf->len)
+			ring_buf_put(&wusb_rx_ring, buf->data,
+				     MIN(buf->len,
+					 ring_buf_space_get(&wusb_rx_ring)));
+		usbd_ep_buf_free(ctx, buf);
+		if (atomic_test_bit(&wusb_state, WUSB_ENABLED))
+			queue_out(c);
+		return 0;
+	}
+	usbd_ep_buf_free(ctx, buf); // IN done or error
+	return 0;
+}
+
+static void *wusb_get_desc(struct usbd_class_data *const c,
+			   const enum usbd_speed speed)
+{
+	struct wusb_data *d = (struct wusb_data *)usbd_class_get_private(c);
+	(void)speed;
+	return d->fs_desc;
+}
+
+static void wusb_enable(struct usbd_class_data *const c)
+{
+	wusb_cd = c;
+	if (!atomic_test_and_set_bit(&wusb_state, WUSB_ENABLED))
+		queue_out(c);
+}
+static void wusb_disable(struct usbd_class_data *const c)
+{
+	(void)c;
+	atomic_clear_bit(&wusb_state, WUSB_ENABLED);
+}
+static int wusb_init(struct usbd_class_data *c)
+{
+	wusb_cd = c;
+	return 0;
+}
+
+static struct usbd_class_api wusb_api = {
+	.request = wusb_request,
+	.enable = wusb_enable,
+	.disable = wusb_disable,
+	.init = wusb_init,
+	.get_desc = wusb_get_desc,
+};
+
+static struct wusb_desc wusb_desc_0 = {
+	.if0 = {
+		.bLength = sizeof(struct usb_if_descriptor),
+		.bDescriptorType = USB_DESC_INTERFACE,
+		.bInterfaceNumber = 0,
+		.bAlternateSetting = 0,
+		.bNumEndpoints = 2,
+		.bInterfaceClass = USB_BCC_VENDOR,
+		.bInterfaceSubClass = 0,
+		.bInterfaceProtocol = 0,
+		.iInterface = 0,
+	},
+	.if0_out_ep = {
+		.bLength = sizeof(struct usb_ep_descriptor),
+		.bDescriptorType = USB_DESC_ENDPOINT,
+		.bEndpointAddress = 0x02,
+		.bmAttributes = USB_EP_TYPE_BULK,
+		.wMaxPacketSize = sys_cpu_to_le16(64U),
+		.bInterval = 0x00,
+	},
+	.if0_in_ep = {
+		.bLength = sizeof(struct usb_ep_descriptor),
+		.bDescriptorType = USB_DESC_ENDPOINT,
+		.bEndpointAddress = 0x82,
+		.bmAttributes = USB_EP_TYPE_BULK,
+		.wMaxPacketSize = sys_cpu_to_le16(64U),
+		.bInterval = 0x00,
+	},
+	.nil_desc = {
+		.bLength = 0,
+		.bDescriptorType = 0,
+	},
+};
+
+const static struct usb_desc_header *wusb_fs_desc_0[] = {
+	(struct usb_desc_header *)&wusb_desc_0.if0,
+	(struct usb_desc_header *)&wusb_desc_0.if0_in_ep,
+	(struct usb_desc_header *)&wusb_desc_0.if0_out_ep,
+	(struct usb_desc_header *)&wusb_desc_0.nil_desc,
+};
+
+static struct wusb_data wusb_data_0 = {
+	.desc = &wusb_desc_0,
+	.fs_desc = wusb_fs_desc_0,
+};
+
+USBD_DEFINE_CLASS(opk_webusb_0, &wusb_api, &wusb_data_0, NULL);
+
+// ---- bridge to the Adafruit_USBD_WebUSB shim ----
+extern "C" bool opk_webusb_connected(void)
+{
+	return atomic_test_bit(&wusb_state, WUSB_ENABLED);
+}
+extern "C" int opk_webusb_available(void)
+{
+	return (int)ring_buf_size_get(&wusb_rx_ring);
+}
+extern "C" int opk_webusb_read(void)
+{
+	uint8_t b;
+	return ring_buf_get(&wusb_rx_ring, &b, 1) == 1 ? b : -1;
+}
+extern "C" uint32_t opk_webusb_write(const void *data, uint32_t n)
+{
+	if (!wusb_cd || !opk_webusb_connected())
+		return 0;
+	// Chunk into 64-byte IN transfers.
+	const uint8_t *p = (const uint8_t *)data;
+	uint32_t sent = 0;
+	while (sent < n) {
+		uint32_t chunk = MIN(n - sent, 64U);
+		struct net_buf *buf = alloc_buf(in_ep(wusb_cd));
+		if (!buf)
+			break;
+		net_buf_add_mem(buf, p + sent, chunk);
+		if (usbd_ep_enqueue(wusb_cd, buf)) {
+			net_buf_unref(buf);
+			break;
+		}
+		sent += chunk;
+	}
+	return sent;
+}
+extern "C" void opk_webusb_flush(void)
+{
+	// writes are enqueued immediately; nothing to flush
+}