I'm looking for a simple example to exchange JSON ? data between a WebBrowser (USB Host) and an ESP-32-S3 from Lilygo. ChatGPT didn't help :-)
- HID Keyboard is very common library but there is no communication from JS to ESP-32
- WebHID can send "report" but no sample and very limited.
- WebSerial and WebUSB are available on Chrome but no working sample
No, I don't want to setup a webserver on the ESP-32. I really want to send data through USB to a webpage or a chrome extension.
The only working code I get is :
Vendor.onEvent(usbEventCallback);
Vendor.onRequest(vendorRequestCallback);
Vendor.begin();
USB.onEvent(usbEventCallback);
USB.webUSB(true);
USB.webUSBURL("http://localhost/webusb");
USB.begin();
and the implementation :
#include "USB.h"
#include "USBVendor.h"
USBVendor Vendor;
static void usbEventCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (event_base == ARDUINO_USB_EVENTS) {
arduino_usb_event_data_t * data = (arduino_usb_event_data_t*)event_data;
switch (event_id) {
case ARDUINO_USB_STARTED_EVENT:
Serial.println("USB PLUGGED");
break;
case ARDUINO_USB_STOPPED_EVENT:
Serial.println("USB UNPLUGGED");
break;
case ARDUINO_USB_SUSPEND_EVENT:
Serial.printf("USB SUSPENDED: remote_wakeup_en: %u\n", data->suspend.remote_wakeup_en);
break;
case ARDUINO_USB_RESUME_EVENT:
Serial.println("USB RESUMED");
break;
default:
break;
}
} else if (event_base == ARDUINO_USB_VENDOR_EVENTS) {
arduino_usb_vendor_event_data_t * data = (arduino_usb_vendor_event_data_t*)event_data;
switch (event_id) {
case ARDUINO_USB_VENDOR_DATA_EVENT:
Serial.printf("Vendor RX: len:%u\n", data->data.len);
for (uint16_t i = 0; i < data->data.len; i++) {
Serial.write(Vendor.read());
}
Serial.println();
break;
default:
break;
}
}
}
// CDC Control Requests
#define REQUEST_SET_LINE_CODING 0x20
#define REQUEST_GET_LINE_CODING 0x21
#define REQUEST_SET_CONTROL_LINE_STATE 0x22
//CDC Line Coding Control Request Structure
typedef struct __attribute__ ((packed)) {
uint32_t bit_rate;
uint8_t stop_bits; //0: 1 stop bit, 1: 1.5 stop bits, 2: 2 stop bits
uint8_t parity; //0: None, 1: Odd, 2: Even, 3: Mark, 4: Space
uint8_t data_bits; //5, 6, 7, 8 or 16
} request_line_coding_t;
static request_line_coding_t vendor_line_coding = {9600, 0, 0, 8};
// Bit 0: DTR (Data Terminal Ready), Bit 1: RTS (Request to Send)
static uint8_t vendor_line_state = 0;
static const char * strRequestDirections[] = {"OUT", "IN"};
static const char * strRequestTypes[] = {"STANDARD", "CLASS", "VENDOR", "INVALID"};
static const char * strRequestRecipients[] = {"DEVICE", "INTERFACE", "ENDPOINT", "OTHER"};
static const char * strRequestStages[] = {"SETUP", "DATA", "ACK"};
uint8_t outBuffer[64];
bool vendorRequestCallback(uint8_t rhport, uint8_t requestStage, arduino_usb_control_request_t const * request) {
Serial.printf("Vendor Request: Stage: %5s, Direction: %3s, Type: %8s, Recipient: %9s, bRequest: 0x%02x, wValue: 0x%04x, wIndex: %u, wLength: %u\n",
strRequestStages[requestStage],
strRequestDirections[request->bmRequestDirection],
strRequestTypes[request->bmRequestType],
strRequestRecipients[request->bmRequestRecipient],
request->bRequest, request->wValue, request->wIndex, request->wLength);
bool result = false;
if (request->bmRequestDirection == REQUEST_DIRECTION_OUT &&
request->bmRequestType == REQUEST_TYPE_STANDARD &&
request->bmRequestRecipient == REQUEST_RECIPIENT_INTERFACE &&
request->bRequest == 0x0b
) {
if (requestStage == REQUEST_STAGE_SETUP) {
// response with status OK
result = Vendor.sendResponse(rhport, request);
} else if (requestStage == REQUEST_STAGE_DATA) {
// This is where the actual data is received
size_t len = Vendor.read(outBuffer, sizeof(outBuffer));
outBuffer[len] = 0; // Null-terminate the string
Serial.println((char*)outBuffer);
} else {
result = true;
}
} else
//Implement CDC Control Requests
if (request->bmRequestType == REQUEST_TYPE_CLASS && request->bmRequestRecipient == REQUEST_RECIPIENT_DEVICE) {
switch (request->bRequest) {
case REQUEST_SET_LINE_CODING: //0x20
// Accept only direction OUT with data size 7
if (request->wLength != sizeof(request_line_coding_t) || request->bmRequestDirection != REQUEST_DIRECTION_OUT) {
break;
}
if (requestStage == REQUEST_STAGE_SETUP) {
//Send the response in setup stage (it will write the data to vendor_line_coding in the DATA stage)
result = Vendor.sendResponse(rhport, request, (void*) &vendor_line_coding, sizeof(request_line_coding_t));
} else if (requestStage == REQUEST_STAGE_ACK) {
//In the ACK stage the response is complete
Serial.printf("Vendor Line Coding: bit_rate: %u, data_bits: %u, stop_bits: %u, parity: %u\n", vendor_line_coding.bit_rate, vendor_line_coding.data_bits, vendor_line_coding.stop_bits, vendor_line_coding.parity);
}
result = true;
break;
case REQUEST_GET_LINE_CODING: //0x21
// Accept only direction IN with data size 7
if (request->wLength != sizeof(request_line_coding_t) || request->bmRequestDirection != REQUEST_DIRECTION_IN) {
break;
}
if (requestStage == REQUEST_STAGE_SETUP) {
//Send the response in setup stage (it will write the data to vendor_line_coding in the DATA stage)
result = Vendor.sendResponse(rhport, request, (void*) &vendor_line_coding, sizeof(request_line_coding_t));
}
result = true;
break;
case REQUEST_SET_CONTROL_LINE_STATE: //0x22
// Accept only direction OUT with data size 0
if (request->wLength != 0 || request->bmRequestDirection != REQUEST_DIRECTION_OUT) {
break;
}
if (requestStage == REQUEST_STAGE_SETUP) {
//Send the response in setup stage
vendor_line_state = request->wValue;
result = Vendor.sendResponse(rhport, request);
} else if (requestStage == REQUEST_STAGE_ACK) {
//In the ACK stage the response is complete
bool dtr = (vendor_line_state & 1) != 0;
bool rts = (vendor_line_state & 2) != 0;
Serial.printf("Vendor Line State: dtr: %u, rts: %u\n", dtr, rts);
}
result = true;
break;
default:
// stall unknown request
break;
}
}
return result;
}
On the webpage side :
<html>
<body>
Hello
<button id="connectButton">Connect</button>
<script>
// Debug in Chrome: chrome://usb-internals/
let device;
const connectUSB = async () => {
device = await navigator.usb.requestDevice({
filters: [{ }]
});
await device.open(); // ouvre le périphérique
await device.selectConfiguration(1); // Sélectionne la configuration du périphérique
await device.releaseInterface(1);
await device.claimInterface(1); // Réclame l'interface du périphérique
let encoder = new TextEncoder();
let data = encoder.encode("Hello ESP32"); // Convert string to bytes
device.transferOut(2, data); // Send data to endpoint 1
listen();
};
const listen = async () => {
const result = await device.transferIn(3, 64);
const decoder = new TextDecoder();
const message = decoder.decode(result.data);
console.log(message);
listen();
};
document.getElementById("connectButton").onclick = connectUSB;
</script>
</body>
</html>
The Chrome browser see the ESP-32 in it's debug interface chrome://usb-internals/ and there is 2 interfaces : HID and Special.
- device.claimInterface(0) is refused because Chrome protect HID device
- device.claimInterface(1) hang forever
So I don't know how to move forward. There is also a TinyUSB library but no working samples.