-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
shared/tinyusb: Add support for USB Network (NCM) interface. #16459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #16459 +/- ##
=======================================
Coverage 98.57% 98.57%
=======================================
Files 169 169
Lines 21968 21968
=======================================
Hits 21654 21654
Misses 314 314 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Code size report:
|
f98bf4b
to
d08d6fc
Compare
Interesting, is the assumption that the MCU is network capable, and will act as a bridge/router? |
With this feature any MCU with TinyUSB based USB connection becomes network capable. I hadn't thought of routing or bridging to other interfaces, though that's probably possible. My initial use case at Planet Innovation is for larger systems where we have a Linux or windows host PC with an embedded module running micropython attached. We wanted an option to use standard networking protocols to communicate between the application on the host computer and the device, however not every system warrants an MCU with an Ethernet connection, or the host computer Ethernet is used for external connection and we want the internal connection on a separate interface etc. With this feature the connection between host and device is still point to point like any USB connection, but you can easily run multiple services like a http server, mqtt client, webrepl etc. Currently I don't think you can monitor or query for the IP address/s handed out by the micropython dhcp server to active clients, I might want to add that if I'm correct so that any client library could easily find / talk to services running on the host. |
In that respect, who is server, who is client?
|
There's no hard and fast rule here, both can run servers and be clients. This is just a network connection. Both the micropython device and the PC have their own network stacks. The USB network connection kind of acts the same as if the micropython board has an Ethernet port that is connected to a PC Ethernet port via a crossover cable. So both ends have their own IP address that can be manually or automatically assigned but there's no router or hub in between. Currently the micropython end is hardcoded to 192.168.7.1 though I'd plan to make that configurable eventually. I have enabled the built-in dhcp server on micropython on the interface (so mpy end is a server from that perspective) and the PC end seems to get provisioned as 192.168.7.16 by default, though this isn't necessarily guaranteed. If the PC happens to be running a web server then a micropython requests library will be able to access it on that 192.168.7.16 address. Conversely if you run microdot or similar on the micropython board the PC will be able to access it on 192.168.7.1 I'm planning on often having a Linux host running mosquito mqtt broker which both the micropython board and the host PC can both publish/subscribe to. I've also looked briefly at a pure python mqtt broker that I might try running on the device instead though! My point is server/client relationship is generally on a per-service basis and can work both ways. |
f8a4a5a
to
af4cac6
Compare
89f246a
to
c4afbae
Compare
6383771
to
9f831e0
Compare
This has been updated to make The ip addresses used by default are now in the link-local range which feels appropriate for this kind of isolated point-to-point link and should reduce the chance of ip collisions with other networks on the host. Once the host is provided with an ip from the dhcp server in micropython, the host ip address is set as the gateway address on the micropython side of the interface which makes it easy for the micropython application to find/use the host ip address. I've written up a quick demo of host to use mqtt between the device and host pc here, it works really well! https://github.com/andrewleech/mpy-usb-net-mqtt-demo/ |
Could you please introduce an example that forwards WLAN to RNDIS using this? It'd be a welcome thing in the usb examples folder. EDIT: I was unable to build the branch (for rp2 ofc), I get this error:
EDIT: SOLVED: I forgot to properly specify my board variant and build variant. EDIT: Nope, I don't get it, I've been trying for a while an am unable to build it for the Raspberry Pi Pico 2 W. I tried with:
In case anyone feels like helping out a newbie. Thx. |
On most ports, features are generally enabled like:
That plug gap alignment error is a strange one, I actually got it yesterday on a RPI build on an unrelated branch. I think it's related to the compilation of picotool more than the firmware issue, don't know where it came. I haven't used this change on a board with wifi, though it probably should work fine. Not sure how to proxy interfaces with lwip / micropython but I'm guessing there will probably be some standard patterns for this. |
Thanks a lot, I'm new to Python and really stumbled upon this out of sheer luck! EDIT: It seems to be built but not linked, none of the ./firmware.* files contain the string USBNET. NOTE: I hope this makes it into master as a default built option |
Thanks for your testing information, I'll have to look into making it easier to include on-demand with the make command. At the moment I'm not expecting it to be built in by default as it adds a reasonable amount of flash/ram use; I've got a build variant of the original Pico added as an example but that's not really helping with your board. |
SOLVED: I ported the commit to the RPI_PICO2_W, it works. I'm not sure how one should forward an interface to another (but that's unrelated to this PR) |
Great to hear @xplshn
|
@andrewleech do you think an example of RNDIS usage could be provided? I want to finish my wifi2usb.py program, I'd appreciate help, perhaps this could become the RNDIS usage example :) import network
import rp2
import time
import sys
import machine
from machine import Pin
import select
import socket
pico_led = Pin("LED", Pin.OUT)
SCAN_TIMEOUT = 5000
class WiFi2USB:
def __init__(self):
self.usb_net = network.USB_NET()
self.usb_net.active(True)
self.wlan = network.WLAN(network.STA_IF)
self.wlan.active(True)
self.networks = []
self.current_ssid = ""
self.current_password = ""
self.connected = False
def scan_networks(self):
"""Scan for available WiFi networks"""
pico_led.on()
print("Scanning for networks...")
self.networks = self.wlan.scan()
pico_led.off()
return self.networks
def connect_wifi(self, ssid, password):
"""Connect to a WiFi network with given SSID and password"""
pico_led.off()
print(f"Connecting to {ssid}...")
self.wlan.connect(ssid, password)
start_time = time.ticks_ms()
while not self.wlan.isconnected():
# Check for cancel with BOOTSEL button
if rp2.bootsel_button() == 1:
print("\nConnection attempt cancelled")
return False
if time.ticks_diff(time.ticks_ms(), start_time) > SCAN_TIMEOUT:
print("\nConnection timeout - please try again")
return False
pico_led.on()
time.sleep(0.1)
pico_led.off()
time.sleep(0.1)
ip = self.wlan.ifconfig()[0]
print(f'Connected on {ip}')
pico_led.on()
self.connected = True
self.current_ssid = ssid
self.current_password = password
return True
def disconnect_wifi(self):
"""Disconnect from WiFi network"""
if self.connected:
self.wlan.disconnect()
self.connected = False
pico_led.off()
print("Disconnected from WiFi")
def get_network_info(self):
"""Return current network configuration"""
if self.connected:
return {
'ssid': self.current_ssid,
'ip': self.wlan.ifconfig()[0],
'subnet': self.wlan.ifconfig()[1],
'gateway': self.wlan.ifconfig()[2],
'dns': self.wlan.ifconfig()[3],
'status': 'Connected'
}
else:
return {'status': 'Not connected'}
def test_connectivity(self):
"""Test network connectivity"""
if not self.connected:
print("Not connected to any network")
return False
try:
ip = self.wlan.ifconfig()[0]
print(f"Connected with IP: {ip}")
print("Gateway: " + self.wlan.ifconfig()[2])
# TODO: ping a reliable site here
print("Connectivity test passed")
return True
except:
print("Connectivity test failed")
return False
def get_usb_status(self):
"""Return USB RNDIS status"""
return {
'active': self.usb_net.active(),
'ifconfig': self.usb_net.ifconfig()
}
def forward_traffic(self):
"""Forward traffic between WiFi and USB interfaces"""
try:
wlan_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
usb_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
wlan_sock.bind((self.wlan.ifconfig()[0], 0))
usb_sock.bind((self.usb_net.ifconfig()[0], 0))
poller = select.poll()
poller.register(wlan_sock, select.POLLIN)
poller.register(usb_sock, select.POLLIN)
print("Forwarding traffic between WiFi and USB interfaces...")
while True:
events = poller.poll(1000)
for fd, event in events:
if fd == wlan_sock.fileno():
data, addr = wlan_sock.recvfrom(1500)
usb_sock.sendto(data, (self.usb_net.ifconfig()[0], 0))
elif fd == usb_sock.fileno():
data, addr = usb_sock.recvfrom(1500)
wlan_sock.sendto(data, (self.wlan.ifconfig()[0], 0))
except OSError as e:
print(f"Socket error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
class CLI:
def __init__(self, wifi2usb):
self.wifi2usb = wifi2usb
self.current_menu = self.main_menu
def start(self):
"""Start the CLI interface"""
print("\n" + "=" * 50)
print("WiFi2USB Dongle - CLI Interface")
print("=" * 50)
while True:
try:
self.current_menu()
except KeyboardInterrupt:
print("\nExiting CLI Interface")
break
def main_menu(self):
"""Display the main menu"""
print("\nMAIN MENU:")
print("1. Reconfigure network")
print("2. See network config")
print("3. Test network connectivity")
print("4. Enter microPython REPL")
print("0. Exit")
choice = input("\nEnter your choice: ")
if choice == "1":
self.current_menu = self.network_selection_menu
elif choice == "2":
self.network_config_menu()
elif choice == "3":
self.connectivity_test_menu()
elif choice == "4":
self.enter_repl()
elif choice == "0":
print("Exiting...")
sys.exit()
else:
print("Invalid choice, please try again")
def network_selection_menu(self):
"""Display network selection menu"""
print("\nScanning for networks...")
networks = self.wifi2usb.scan_networks()
print("\nAvailable Networks:")
print(" Network Name [#Channel] |Other info|")
print("-" * 60)
for i, network in enumerate(networks, 1):
try:
ssid = network[0].decode('utf-8')
bssid = network[1] # MAC address in bytes
channel = network[2]
rssi = network[3]
authmode = network[4]
# Format MAC address
mac = ':'.join('{:02x}'.format(b) for b in bssid)
# Safe access to security types with fallback
security_types = ["Open", "WEP", "WPA-PSK", "WPA2-PSK", "WPA/WPA2-PSK", "WPA3"]
if 0 <= authmode < len(security_types):
security = security_types[authmode]
else:
security = f"Unknown({authmode})"
print(f"{i}. {ssid:<25} [#{channel}] |mac:{mac}, db:{rssi}, {security}|")
except Exception as e:
print(f"{i}. [Error reading network info: {e}]")
print("-" * 60)
print("0: go back to last menu")
try:
choice = input("\nSelect network: ")
if choice == "0":
self.current_menu = self.main_menu
return
idx = int(choice) - 1
if 0 <= idx < len(networks):
selected_ssid = networks[idx][0].decode('utf-8')
password = input(f"Enter password for {selected_ssid}: ")
if self.wifi2usb.connect_wifi(selected_ssid, password):
print(f"Successfully connected to {selected_ssid}")
self.wifi2usb.forward_traffic()
else:
print(f"Failed to connect to {selected_ssid}")
# Return to main menu after connection attempt
self.current_menu = self.main_menu
else:
print("Invalid selection")
except ValueError:
print("Please enter a valid number")
def network_config_menu(self):
"""Display current network configuration"""
print("\nCurrent Network Configuration:")
print("-" * 50)
# WiFi status
wifi_info = self.wifi2usb.get_network_info()
print("WiFi Status:", wifi_info['status'])
if wifi_info['status'] == 'Connected':
print(f"SSID: {wifi_info['ssid']}")
print(f"IP Address: {wifi_info['ip']}")
print(f"Subnet Mask: {wifi_info['subnet']}")
print(f"Gateway: {wifi_info['gateway']}")
print(f"DNS Server: {wifi_info['dns']}")
# USB RNDIS status
usb_status = self.wifi2usb.get_usb_status()
print("\nUSB Network Interface:")
print(f"Active: {usb_status['active']}")
print(f"IP Configuration: {usb_status['ifconfig']}")
print("-" * 50)
input("Press Enter to return to main menu...")
def connectivity_test_menu(self):
"""Test network connectivity"""
print("\nTesting Network Connectivity:")
print("-" * 50)
self.wifi2usb.test_connectivity()
print("-" * 50)
input("Press Enter to return to main menu...")
def enter_repl(self):
"""Enter MicroPython REPL"""
print("\nEntering MicroPython REPL...")
print('Type "exit()" to return to CLI')
print("-" * 50)
# TODO: Replace this, it sucks
while True:
try:
code = input(">>> ")
if code.strip() == "exit()":
break
try:
result = eval(code)
if result is not None:
print(result)
except SyntaxError:
exec(code)
except Exception as e:
print(f"Error: {e}")
print("-" * 50)
print("Returned from REPL")
def main():
wifi2usb = WiFi2USB()
cli = CLI(wifi2usb)
cli.start()
if __name__ == "__main__":
main() |
@xplshn I really don't know anything about how packet forwarding is supposed to work. Does your application there seem to work at all? FWIW I've got an example usage of this published at https://github.com/andrewleech/mpy-usb-net-mqtt-demo/ ps. it's not RNDIS, that's the older standard protocol for usb network adapters. NCM is the newer one, it's a different protocol / driver behind the scenes. They should basically work the same as far as users are concerned though. |
My code works, it stablishes a WLAN connection, and an NCM connection (as you said), but packet forwarding isn't handled at all, because I do not know how, if someone can shed some light, I'd appreciate it
Thank you! didn't know about these two |
92993ab
to
25e7069
Compare
I've been working on this PR on stm32 in conjunction with stm32/usb: Add support for using TinyUSB stack. #15592 and it's looking quite stable now. I found and fixed a data corruption issue (hathach/tinyusb#3131) and have improved the speed of the interface significantly in the process. I'm also finding ipv6 to be highly beneficial for this use case as I don't need to worry about clashing ip addresses from multiple devices being attached to a PC. For windows testing I've got some powershell snippets that automatically find the correct ip address to communicate with for a given USB |
5baf248
to
cf9e19f
Compare
bb3d97a
to
e59d8f2
Compare
This sets the DHCP server's own IP address as the default DNS server when no explicit DNS is configured, providing a more complete network configuration for DHCP clients. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Provides atoi() function implementation required by lwIP's netif_find() function when using string-based network interface lookup. Based on (MIT) implementation from https://github.com/littlekernel/lk Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Adds TinyUSB configuration and descriptor support for USB Network Control Model (NCM) interface. This provides the low-level USB infrastructure needed for USB networking. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Implements a complete USB Network Control Model (NCM) driver that provides ethernet-over-USB functionality integrated with lwIP stack. Features: - Link-local IP address generation from MAC using CRC32 - DHCP server integration with connect callbacks - Full lwIP network interface with IPv6 support - USB NCM protocol handling via TinyUSB - Python network.USB_NET class interface Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Adds USB Network support to the RP2 port through a build variant of the RPI_PICO board. The USB_NET variant enables lwIP networking and USB Network Control Model (NCM) interface. This allows the Raspberry Pi Pico to function as a USB network adapter providing ethernet-over-USB connectivity to a host computer. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Integrates USB Network Control Model (NCM) support into the STM32 port, enabling ethernet-over-USB functionality. Includes lwIP configuration updates and network interface registration. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Enables USB Network Control Model (NCM) interface in the MIMXRT port for ethernet-over-USB functionality. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Consolidates TinyUSB source includes across multiple ports (alif, mimxrt, renesas-ra, samd) to use a common approach for TinyUSB integration. This cleanup ensures consistent TinyUSB usage patterns across ports and simplifies maintenance. Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
This change allows USB class drivers (CDC, MSC, NCM) to be individually enabled/disabled at runtime without requiring TinyUSB submodule changes. Key features: - Classes are always compiled in but only enabled drivers appear in USB descriptors - When MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE disabled: all compiled classes auto-enabled (backward compatibility) - When MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE enabled: only CDC enabled by default, others require explicit activation - New Python API: usb.enable_cdc(), usb.enable_msc(), usb.enable_ncm() - Dynamic descriptor generation based on enabled classes - Proper cleanup of disabled drivers (e.g. NCM networking) Usage: usb = machine.USBDevice() usb.active(False) usb.enable_msc(True) # Enable mass storage usb.enable_ncm(True) # Enable USB networking usb.active(True) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Automatically enable MICROPY_HW_NETWORK_USBNET when MICROPY_PY_LWIP is enabled, following the same pattern as WEBSOCKET and WEBREPL. This makes USB networking available by default on boards that support both USB device mode and LWIP networking, while still allowing boards to explicitly disable it by setting MICROPY_HW_NETWORK_USBNET=0. Affects: STM32, RP2, MIMXRT, and Renesas-RA ports. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Summary
This PR provides a network interface over the USB connection which is connected to the existing LWIP network stack.
It includes integration into only rp2 port so far via a new RPI_PICO/USB_NET variant build.
dmesg
after plugging in pico with RPI_PICO/USB_NET variant build.The regular usb serial port is still provided for mpremote. Use that to enable the network interface:

DHCP server is being provided by the pico over the usb network interface, so the PC can be provided an IP address automatically:

After than, sockets / service can be used as per any normal network connection:
Any port based on TinyUSB will also be able to use this with minimal effort, with caveats on needing LWIP integration as well and the board / chip will need to be large enough to support the ~20KB ram needs of LWIP.
Building
This can be enabled on a cmake base port like so:
CFLAGS="-DMICROPY_HW_NETWORK_USBNET=1" make -C ports/rp2 BOARD=RPI_PICO2_W
On a make based port:
make -j -C ports/mimxrt BOARD=SEEED_ARCH_MIX CFLAGS_EXTRA="-DMICROPY_HW_NETWORK_USBNET=1 -DLWIP_IPV6=1"
Testing
This has had minimal testing so far on the PICO as per shown above and more extensive tesing on a STM32F765 running microdot.
The mimx build above does not appear to be working, once flashed it appears as COM and NCM devices but both are locked up / not usable.
Trade-offs and Alternatives
As mentioned above LWIP has overheads to include in a build, so for smaller parts using USB with a custom protocol for communication between device and pc can be smaller. However providing a network connection over the USB can open up use of many different existing servers and services for communicating with a device.
TODO
Test is this coexists with dynamic usb devices, ensure it can be kept active when creating dynamic devices and ensure if doesn't crash / is handled cleanly if dynamic setup disables the builtins.
This work was funded by Planet Innovation.