From ff7731491e420781712822d10befe95e0c0fb9ca Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 21 Sep 2022 15:11:52 -0500 Subject: [PATCH] Implement enough of socketpool to do ntp and non-https requests --- .gitmodules | 6 + ports/raspberrypi/Makefile | 41 +- .../common-hal/socketpool/Socket.c | 912 +++++++++++++++++- .../common-hal/socketpool/SocketPool.c | 19 +- .../common-hal/socketpool/SocketPool.h | 2 + ports/raspberrypi/lib/cyw43-driver | 1 + ports/raspberrypi/lib/lwip | 1 + ports/raspberrypi/pioasm/CMakeLists.txt | 8 + shared-bindings/digitalio/DigitalInOut.c | 10 +- 9 files changed, 937 insertions(+), 63 deletions(-) create mode 160000 ports/raspberrypi/lib/cyw43-driver create mode 160000 ports/raspberrypi/lib/lwip create mode 100644 ports/raspberrypi/pioasm/CMakeLists.txt diff --git a/.gitmodules b/.gitmodules index e0d0a262ca..875b03dab8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -310,3 +310,9 @@ [submodule "ports/espressif/esp32-camera"] path = ports/espressif/esp32-camera url = https://github.com/adafruit/esp32-camera/ +[submodule "ports/raspberrypi/lib/cyw43-driver"] + path = ports/raspberrypi/lib/cyw43-driver + url = https://github.com/georgerobotics/cyw43-driver.git +[submodule "ports/raspberrypi/lib/lwip"] + path = ports/raspberrypi/lib/lwip + url = https://github.com/lwip-tcpip/lwip.git diff --git a/ports/raspberrypi/Makefile b/ports/raspberrypi/Makefile index 28659e9cb2..cf3967fee4 100644 --- a/ports/raspberrypi/Makefile +++ b/ports/raspberrypi/Makefile @@ -59,18 +59,14 @@ HAL_DIR=hal/$(MCU_SERIES) ifeq ($(CIRCUITPY_CYW43),1) INC_CYW43 := \ - -isystem sdk/lib/cyw43-driver/firmware \ - -isystem sdk/lib/cyw43-driver/src \ - -isystem sdk/lib/lwip/src/include \ + -isystem lib/cyw43-driver/firmware \ + -isystem lib/cyw43-driver/src \ + -isystem lib/lwip/src/include \ -isystem sdk/src/rp2_common/pico_cyw43_arch/include/ \ -isystem sdk/src/rp2_common/pico_lwip/include/ \ -CFLAGS_CYW43 := -DCYW43_LWIP=1 -DPICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1 -DCYW43_USE_SPI -DIGNORE_GPIO25 +CFLAGS_CYW43 := -DCYW43_LWIP=1 -DPICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1 -DCYW43_USE_SPI -DIGNORE_GPIO25 -DCYW43_LOGIC_DEBUG=0 SRC_SDK_CYW43 := \ - lib/cyw43-driver/src/cyw43_ctrl.c \ - lib/cyw43-driver/src/cyw43_ll.c \ - lib/cyw43-driver/src/cyw43_lwip.c \ - lib/cyw43-driver/src/cyw43_stats.c \ src/common/pico_sync/sem.c \ src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.c \ src/rp2_common/pico_cyw43_arch/cyw43_arch.c \ @@ -80,19 +76,24 @@ SRC_SDK_CYW43 := \ SRC_LWIP := \ shared/netutils/netutils.c \ - $(wildcard sdk/lib/lwip/src/core/*.c) \ - $(wildcard sdk/lib/lwip/src/core/ipv4/*.c) \ - sdk/lib/lwip/src/netif/ethernet.c \ + $(wildcard lib/lwip/src/core/*.c) \ + $(wildcard lib/lwip/src/core/ipv4/*.c) \ + lib/lwip/src/netif/ethernet.c \ $(wildcard lwip_src/*.c) \ -SRC_CYW43 := $(wildcard bindings/cyw43/*.c) +SRC_CYW43 := \ + $(wildcard bindings/cyw43/*.c) \ + lib/cyw43-driver/src/cyw43_stats.c \ + lib/cyw43-driver/src/cyw43_ctrl.c \ + lib/cyw43-driver/src/cyw43_ll.c \ + lib/cyw43-driver/src/cyw43_lwip.c \ -PIOASM = $(BUILD)/sdk/pioasm/pioasm +PIOASM = $(BUILD)/pioasm/pioasm/pioasm .PHONY: PioasmBuild PioasmBuild: $(PIOASM) $(PIOASM): - $(Q)cmake -S sdk -B $(BUILD)/sdk - $(Q)make -C $(BUILD)/sdk PioasmBuild + $(Q)cmake -S pioasm -B $(BUILD)/pioasm + $(Q)$(MAKE) -C $(BUILD)/pioasm PioasmBuild $(BUILD)/cyw43_bus_pio_spi.pio.h: sdk/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.pio $(PIOASM) $(Q)$(PIOASM) -o c-sdk $< $@ @@ -100,13 +101,13 @@ $(BUILD)/sdk/src/rp2_common/cyw43_driver/cyw43_bus_pio_spi.o: $(BUILD)/cyw43_bus CYW43_FIRMWARE_BIN = 43439A0-7.95.49.00.combined -$(BUILD)/cyw43_resource.o: sdk/lib/cyw43-driver/firmware/$(CYW43_FIRMWARE_BIN) +$(BUILD)/cyw43_resource.o: lib/cyw43-driver/firmware/$(CYW43_FIRMWARE_BIN) $(Q)$(OBJCOPY) -I binary -O elf32-littlearm -B arm \ --readonly-text \ --rename-section .data=.big_const,contents,alloc,load,readonly,data \ - --redefine-sym _binary_sdk_lib_cyw43_driver_firmware_43439A0_7_95_49_00_combined_start=fw_43439A0_7_95_49_00_start \ - --redefine-sym _binary_sdk_lib_cyw43_driver_firmware_43439A0_7_95_49_00_combined_size=fw_43439A0_7_95_49_00_size \ - --redefine-sym _binary_sdk_lib_cyw43_driver_firmware_43439A0_7_95_49_00_combined_end=fw_43439A0_7_95_49_00_end \ + --redefine-sym _binary_lib_cyw43_driver_firmware_43439A0_7_95_49_00_combined_start=fw_43439A0_7_95_49_00_start \ + --redefine-sym _binary_lib_cyw43_driver_firmware_43439A0_7_95_49_00_combined_size=fw_43439A0_7_95_49_00_size \ + --redefine-sym _binary_lib_cyw43_driver_firmware_43439A0_7_95_49_00_combined_end=fw_43439A0_7_95_49_00_end \ $< $@ OBJ_CYW43 := $(BUILD)/cyw43_resource.o # need to do the equivalent of this in cmake @@ -270,7 +271,7 @@ SRC_SDK := \ $(SRC_SDK_CYW43) \ SRC_SDK := $(addprefix sdk/, $(SRC_SDK)) -$(patsubst %.c,$(BUILD)/%.o,$(SRC_SDK)): CFLAGS += -Wno-missing-prototypes -Wno-undef +$(patsubst %.c,$(BUILD)/%.o,$(SRC_SDK) $(SRC_CYW43)): CFLAGS += -Wno-missing-prototypes -Wno-undef SRC_C += \ boards/$(BOARD)/board.c \ diff --git a/ports/raspberrypi/common-hal/socketpool/Socket.c b/ports/raspberrypi/common-hal/socketpool/Socket.c index a1b77533a2..05c8212d69 100644 --- a/ports/raspberrypi/common-hal/socketpool/Socket.c +++ b/ports/raspberrypi/common-hal/socketpool/Socket.c @@ -3,7 +3,11 @@ * * The MIT License (MIT) * + * Copyright (c) 2013-2019 Damien P. George + * Copyright (c) 2015 Galen Hazelwood + * Copyright (c) 2015-2017 Paul Sokolovsky * Copyright (c) 2020 Lucian Copeland for Adafruit Industries + * Copyright (c) 2022 Jeff Epler for Adafruit Industries * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,27 +30,582 @@ #include "shared-bindings/socketpool/Socket.h" -#include "shared/runtime/interrupt_char.h" +#include "py/gc.h" #include "py/mperrno.h" +#include "py/mphal.h" #include "py/runtime.h" +#include "py/stream.h" #include "shared-bindings/socketpool/SocketPool.h" +#include "shared/runtime/interrupt_char.h" #include "supervisor/port.h" #include "supervisor/shared/tick.h" #include "supervisor/workflow.h" +#include "lwip/dns.h" #include "lwip/err.h" -#include "lwip/sys.h" +#include "lwip/igmp.h" +#include "lwip/init.h" #include "lwip/netdb.h" +#include "lwip/priv/tcp_priv.h" +#include "lwip/raw.h" +#include "lwip/sys.h" +#include "lwip/tcp.h" +#include "lwip/timeouts.h" +#include "lwip/udp.h" + +#define MICROPY_PY_LWIP_SOCK_RAW (1) + +#if 0 // print debugging info +#define DEBUG_printf DEBUG_printf +#else // don't print debugging info +#define DEBUG_printf(...) (void)0 +#endif + +// Timeout between closing a TCP socket and doing a tcp_abort on that +// socket, if the connection isn't closed cleanly in that time. +#define MICROPY_PY_LWIP_TCP_CLOSE_TIMEOUT_MS (10000) + +// All socket options should be globally distinct, +// because we ignore option levels for efficiency. +#define IP_ADD_MEMBERSHIP 0x400 +#define IP_DROP_MEMBERSHIP 0x401 + +/******************************************************************************/ +// Table to convert lwIP err_t codes to socket errno codes, from the lwIP +// socket API. + +// Extension to lwIP error codes +// Matches lwIP 2.0.3 +#undef _ERR_BADF +#define _ERR_BADF -17 +static const int error_lookup_table[] = { + 0, /* ERR_OK 0 No error, everything OK */ + MP_ENOMEM, /* ERR_MEM -1 Out of memory error */ + MP_ENOBUFS, /* ERR_BUF -2 Buffer error */ + MP_EWOULDBLOCK, /* ERR_TIMEOUT -3 Timeout */ + MP_EHOSTUNREACH, /* ERR_RTE -4 Routing problem */ + MP_EINPROGRESS, /* ERR_INPROGRESS -5 Operation in progress */ + MP_EINVAL, /* ERR_VAL -6 Illegal value */ + MP_EWOULDBLOCK, /* ERR_WOULDBLOCK -7 Operation would block */ + MP_EADDRINUSE, /* ERR_USE -8 Address in use */ + MP_EALREADY, /* ERR_ALREADY -9 Already connecting */ + MP_EALREADY, /* ERR_ISCONN -10 Conn already established */ + MP_ENOTCONN, /* ERR_CONN -11 Not connected */ + -1, /* ERR_IF -12 Low-level netif error */ + MP_ECONNABORTED, /* ERR_ABRT -13 Connection aborted */ + MP_ECONNRESET, /* ERR_RST -14 Connection reset */ + MP_ENOTCONN, /* ERR_CLSD -15 Connection closed */ + MP_EIO, /* ERR_ARG -16 Illegal argument. */ + MP_EBADF, /* _ERR_BADF -17 Closed socket (null pcb) */ +}; + +/*******************************************************************************/ +// The socket object provided by lwip.socket. + +#define MOD_NETWORK_AF_INET (SOCKETPOOL_AF_INET) +#define MOD_NETWORK_AF_INET6 (SOCKETPOOL_AF_INET6) + +#define MOD_NETWORK_SOCK_STREAM (SOCKETPOOL_SOCK_STREAM) +#define MOD_NETWORK_SOCK_DGRAM (SOCKETPOOL_SOCK_DGRAM) +#define MOD_NETWORK_SOCK_RAW (SOCKETPOOL_SOCK_RAW) #define MAX_SOCKETS (8) -STATIC mp_obj_t open_socket_objs[MAX_SOCKETS]; +static inline void poll_sockets(void) { + #ifdef MICROPY_EVENT_POLL_HOOK + MICROPY_EVENT_POLL_HOOK; + #else + mp_hal_delay_ms(1); + #endif +} + +STATIC struct tcp_pcb *volatile *lwip_socket_incoming_array(socketpool_socket_obj_t *socket) { + if (socket->incoming.connection.alloc == 0) { + return &socket->incoming.connection.tcp.item; + } else { + return &socket->incoming.connection.tcp.array[0]; + } +} + +STATIC void lwip_socket_free_incoming(socketpool_socket_obj_t *socket) { + bool socket_is_listener = + socket->type == MOD_NETWORK_SOCK_STREAM + && socket->pcb.tcp->state == LISTEN; + + if (!socket_is_listener) { + if (socket->incoming.pbuf != NULL) { + pbuf_free(socket->incoming.pbuf); + socket->incoming.pbuf = NULL; + } + } else { + uint8_t alloc = socket->incoming.connection.alloc; + struct tcp_pcb *volatile *tcp_array = lwip_socket_incoming_array(socket); + for (uint8_t i = 0; i < alloc; ++i) { + // Deregister callback and abort + if (tcp_array[i] != NULL) { + tcp_poll(tcp_array[i], NULL, 0); + tcp_abort(tcp_array[i]); + tcp_array[i] = NULL; + } + } + } +} + +/*******************************************************************************/ +// Callback functions for the lwIP raw API. + +static inline void exec_user_callback(socketpool_socket_obj_t *socket) { + #if 0 + if (socket->callback != MP_OBJ_NULL) { + // Schedule the user callback to execute outside the lwIP context + mp_sched_schedule(socket->callback, MP_OBJ_FROM_PTR(socket)); + } + #endif +} + +#if MICROPY_PY_LWIP_SOCK_RAW +// Callback for incoming raw packets. +#if LWIP_VERSION_MAJOR < 2 +STATIC u8_t _lwip_raw_incoming(void *arg, struct raw_pcb *pcb, struct pbuf *p, ip_addr_t *addr) +#else +STATIC u8_t _lwip_raw_incoming(void *arg, struct raw_pcb *pcb, struct pbuf *p, const ip_addr_t *addr) +#endif +{ + socketpool_socket_obj_t *socket = (socketpool_socket_obj_t *)arg; + + if (socket->incoming.pbuf != NULL) { + pbuf_free(p); + } else { + socket->incoming.pbuf = p; + memcpy(&socket->peer, addr, sizeof(socket->peer)); + } + return 1; // we ate the packet +} +#endif + +// Callback for incoming UDP packets. We simply stash the packet and the source address, +// in case we need it for recvfrom. +#if LWIP_VERSION_MAJOR < 2 +STATIC void _lwip_udp_incoming(void *arg, struct udp_pcb *upcb, struct pbuf *p, ip_addr_t *addr, u16_t port) +#else +STATIC void _lwip_udp_incoming(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) +#endif +{ + socketpool_socket_obj_t *socket = (socketpool_socket_obj_t *)arg; + + if (socket->incoming.pbuf != NULL) { + // That's why they call it "unreliable". No room in the inn, drop the packet. + pbuf_free(p); + } else { + socket->incoming.pbuf = p; + socket->peer_port = (mp_uint_t)port; + memcpy(&socket->peer, addr, sizeof(socket->peer)); + } +} + +// Callback for general tcp errors. +STATIC void _lwip_tcp_error(void *arg, err_t err) { + socketpool_socket_obj_t *socket = (socketpool_socket_obj_t *)arg; + + // Free any incoming buffers or connections that are stored + lwip_socket_free_incoming(socket); + // Pass the error code back via the connection variable. + socket->state = err; + // If we got here, the lwIP stack either has deallocated or will deallocate the pcb. + socket->pcb.tcp = NULL; +} + +// Callback for tcp connection requests. Error code err is unused. (See tcp.h) +STATIC err_t _lwip_tcp_connected(void *arg, struct tcp_pcb *tpcb, err_t err) { + socketpool_socket_obj_t *socket = (socketpool_socket_obj_t *)arg; + + socket->state = STATE_CONNECTED; + return ERR_OK; +} + +// Handle errors (eg connection aborted) on TCP PCBs that have been put on the +// accept queue but are not yet actually accepted. +STATIC void _lwip_tcp_err_unaccepted(void *arg, err_t err) { + struct tcp_pcb *pcb = (struct tcp_pcb *)arg; + + // The ->connected entry is repurposed to store the parent socket; this is safe + // because it's only ever used by lwIP if tcp_connect is called on the TCP PCB. + socketpool_socket_obj_t *socket = (socketpool_socket_obj_t *)pcb->connected; + + // Array is not volatile because thiss callback is executed within the lwIP context + uint8_t alloc = socket->incoming.connection.alloc; + struct tcp_pcb **tcp_array = (struct tcp_pcb **)lwip_socket_incoming_array(socket); + + // Search for PCB on the accept queue of the parent socket + struct tcp_pcb **shift_down = NULL; + uint8_t i = socket->incoming.connection.iget; + do { + if (shift_down == NULL) { + if (tcp_array[i] == pcb) { + shift_down = &tcp_array[i]; + } + } else { + *shift_down = tcp_array[i]; + shift_down = &tcp_array[i]; + } + if (++i >= alloc) { + i = 0; + } + } while (i != socket->incoming.connection.iput); + + // PCB found in queue, remove it + if (shift_down != NULL) { + *shift_down = NULL; + socket->incoming.connection.iput = shift_down - tcp_array; + } +} + +// By default, a child socket of listen socket is created with recv +// handler which discards incoming pbuf's. We don't want to do that, +// so set this handler which requests lwIP to keep pbuf's and deliver +// them later. We cannot cache pbufs in child socket on Python side, +// until it is created in accept(). +STATIC err_t _lwip_tcp_recv_unaccepted(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { + return ERR_BUF; +} + +// Callback for incoming tcp connections. +STATIC err_t _lwip_tcp_accept(void *arg, struct tcp_pcb *newpcb, err_t err) { + // err can be ERR_MEM to notify us that there was no memory for an incoming connection + if (err != ERR_OK) { + return ERR_OK; + } + + socketpool_socket_obj_t *socket = (socketpool_socket_obj_t *)arg; + tcp_recv(newpcb, _lwip_tcp_recv_unaccepted); + + // Search for an empty slot to store the new connection + struct tcp_pcb *volatile *slot = &lwip_socket_incoming_array(socket)[socket->incoming.connection.iput]; + if (*slot == NULL) { + // Have an empty slot to store waiting connection + *slot = newpcb; + if (++socket->incoming.connection.iput >= socket->incoming.connection.alloc) { + socket->incoming.connection.iput = 0; + } + + // Schedule user accept callback + exec_user_callback(socket); + + // Set the error callback to handle the case of a dropped connection before we + // have a chance to take it off the accept queue. + // The ->connected entry is repurposed to store the parent socket; this is safe + // because it's only ever used by lwIP if tcp_connect is called on the TCP PCB. + newpcb->connected = (void *)socket; + tcp_arg(newpcb, newpcb); + tcp_err(newpcb, _lwip_tcp_err_unaccepted); + + return ERR_OK; + } + + DEBUG_printf("_lwip_tcp_accept: No room to queue pcb waiting for accept\n"); + return ERR_BUF; +} + +// Callback for inbound tcp packets. +STATIC err_t _lwip_tcp_recv(void *arg, struct tcp_pcb *tcpb, struct pbuf *p, err_t err) { + socketpool_socket_obj_t *socket = (socketpool_socket_obj_t *)arg; + + if (p == NULL) { + // Other side has closed connection. + DEBUG_printf("_lwip_tcp_recv[%p]: other side closed connection\n", socket); + socket->state = STATE_PEER_CLOSED; + exec_user_callback(socket); + return ERR_OK; + } + + if (socket->incoming.pbuf == NULL) { + socket->incoming.pbuf = p; + } else { + #ifdef SOCKET_SINGLE_PBUF + return ERR_BUF; + #else + pbuf_cat(socket->incoming.pbuf, p); + #endif + } + + exec_user_callback(socket); + + return ERR_OK; +} + +/*******************************************************************************/ +// Functions for socket send/receive operations. Socket send/recv and friends call +// these to do the work. + +// Helper function for send/sendto to handle raw/UDP packets. +STATIC mp_uint_t lwip_raw_udp_send(socketpool_socket_obj_t *socket, const byte *buf, mp_uint_t len, ip_addr_t *dest, uint32_t port, int *_errno) { + if (len > 0xffff) { + // Any packet that big is probably going to fail the pbuf_alloc anyway, but may as well try + len = 0xffff; + } + + MICROPY_PY_LWIP_ENTER + + // FIXME: maybe PBUF_ROM? + struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); + if (p == NULL) { + MICROPY_PY_LWIP_EXIT + *_errno = MP_ENOMEM; + return -1; + } + + memcpy(p->payload, buf, len); + + err_t err; + if (dest == NULL) { + #if MICROPY_PY_LWIP_SOCK_RAW + if (socket->type == MOD_NETWORK_SOCK_RAW) { + err = raw_send(socket->pcb.raw, p); + } else + #endif + { + err = udp_send(socket->pcb.udp, p); + } + } else { + #if MICROPY_PY_LWIP_SOCK_RAW + if (socket->type == MOD_NETWORK_SOCK_RAW) { + err = raw_sendto(socket->pcb.raw, p, dest); + } else + #endif + { + err = udp_sendto(socket->pcb.udp, p, dest, port); + } + } + + pbuf_free(p); + + MICROPY_PY_LWIP_EXIT + + // udp_sendto can return 1 on occasion for ESP8266 port. It's not known why + // but it seems that the send actually goes through without error in this case. + // So we treat such cases as a success until further investigation. + if (err != ERR_OK && err != 1) { + *_errno = error_lookup_table[-err]; + return -1; + } + + return len; +} + +// Helper function for recv/recvfrom to handle raw/UDP packets +STATIC mp_uint_t lwip_raw_udp_receive(socketpool_socket_obj_t *socket, byte *buf, mp_uint_t len, byte *ip, uint32_t *port, int *_errno) { + + if (socket->incoming.pbuf == NULL) { + if (socket->timeout == 0) { + // Non-blocking socket. + *_errno = MP_EAGAIN; + return -1; + } + + // Wait for data to arrive on UDP socket. + mp_uint_t start = mp_hal_ticks_ms(); + while (socket->incoming.pbuf == NULL) { + if (socket->timeout != -1 && mp_hal_ticks_ms() - start > socket->timeout) { + *_errno = MP_ETIMEDOUT; + return -1; + } + poll_sockets(); + } + } + + if (ip != NULL) { + memcpy(ip, &socket->peer, sizeof(socket->peer)); + *port = socket->peer_port; + } + + struct pbuf *p = socket->incoming.pbuf; + + MICROPY_PY_LWIP_ENTER + + u16_t result = pbuf_copy_partial(p, buf, ((p->tot_len > len) ? len : p->tot_len), 0); + pbuf_free(p); + socket->incoming.pbuf = NULL; + + MICROPY_PY_LWIP_EXIT + + return (mp_uint_t)result; +} + +// For use in stream virtual methods +#define STREAM_ERROR_CHECK(socket) \ + if (socket->state < 0) { \ + *_errno = error_lookup_table[-socket->state]; \ + return MP_STREAM_ERROR; \ + } \ + assert(socket->pcb.tcp); + +// Version of above for use when lock is held +#define STREAM_ERROR_CHECK_WITH_LOCK(socket) \ + if (socket->state < 0) { \ + *_errno = error_lookup_table[-socket->state]; \ + MICROPY_PY_LWIP_EXIT \ + return MP_STREAM_ERROR; \ + } \ + assert(socket->pcb.tcp); + + +// Helper function for send/sendto to handle TCP packets +STATIC mp_uint_t lwip_tcp_send(socketpool_socket_obj_t *socket, const byte *buf, mp_uint_t len, int *_errno) { + // Check for any pending errors + STREAM_ERROR_CHECK(socket); + + MICROPY_PY_LWIP_ENTER + + u16_t available = tcp_sndbuf(socket->pcb.tcp); + + if (available == 0) { + // Non-blocking socket + if (socket->timeout == 0) { + MICROPY_PY_LWIP_EXIT + *_errno = MP_EAGAIN; + return MP_STREAM_ERROR; + } + + mp_uint_t start = mp_hal_ticks_ms(); + // Assume that STATE_PEER_CLOSED may mean half-closed connection, where peer closed it + // sending direction, but not receiving. Consequently, check for both STATE_CONNECTED + // and STATE_PEER_CLOSED as normal conditions and still waiting for buffers to be sent. + // If peer fully closed socket, we would have socket->state set to ERR_RST (connection + // reset) by error callback. + // Avoid sending too small packets, so wait until at least 16 bytes available + while (socket->state >= STATE_CONNECTED && (available = tcp_sndbuf(socket->pcb.tcp)) < 16) { + MICROPY_PY_LWIP_EXIT + if (socket->timeout != -1 && mp_hal_ticks_ms() - start > socket->timeout) { + *_errno = MP_ETIMEDOUT; + return MP_STREAM_ERROR; + } + poll_sockets(); + MICROPY_PY_LWIP_REENTER + } + + // While we waited, something could happen + STREAM_ERROR_CHECK_WITH_LOCK(socket); + } + + u16_t write_len = MIN(available, len); + + // If tcp_write returns ERR_MEM then there's currently not enough memory to + // queue the write, so wait and keep trying until it succeeds (with 10s limit). + // Note: if the socket is non-blocking then this code will actually block until + // there's enough memory to do the write, but by this stage we have already + // committed to being able to write the data. + err_t err; + for (int i = 0; i < 200; ++i) { + err = tcp_write(socket->pcb.tcp, buf, write_len, TCP_WRITE_FLAG_COPY); + if (err != ERR_MEM) { + break; + } + err = tcp_output(socket->pcb.tcp); + if (err != ERR_OK) { + break; + } + MICROPY_PY_LWIP_EXIT + mp_hal_delay_ms(50); + MICROPY_PY_LWIP_REENTER + } + + // If the output buffer is getting full then send the data to the lower layers + if (err == ERR_OK && tcp_sndbuf(socket->pcb.tcp) < TCP_SND_BUF / 4) { + err = tcp_output(socket->pcb.tcp); + } + + MICROPY_PY_LWIP_EXIT + + if (err != ERR_OK) { + *_errno = error_lookup_table[-err]; + return MP_STREAM_ERROR; + } + + return write_len; +} + +// Helper function for recv/recvfrom to handle TCP packets +STATIC mp_uint_t lwip_tcp_receive(socketpool_socket_obj_t *socket, byte *buf, mp_uint_t len, int *_errno) { + // Check for any pending errors + STREAM_ERROR_CHECK(socket); + + if (socket->incoming.pbuf == NULL) { + + // Non-blocking socket + if (socket->timeout == 0) { + if (socket->state == STATE_PEER_CLOSED) { + return 0; + } + *_errno = MP_EAGAIN; + return -1; + } + + mp_uint_t start = mp_hal_ticks_ms(); + while (socket->state == STATE_CONNECTED && socket->incoming.pbuf == NULL) { + if (socket->timeout != -1 && mp_hal_ticks_ms() - start > socket->timeout) { + *_errno = MP_ETIMEDOUT; + return -1; + } + poll_sockets(); + } + + if (socket->state == STATE_PEER_CLOSED) { + if (socket->incoming.pbuf == NULL) { + // socket closed and no data left in buffer + return 0; + } + } else if (socket->state != STATE_CONNECTED) { + if (socket->state >= STATE_NEW) { + *_errno = MP_ENOTCONN; + } else { + *_errno = error_lookup_table[-socket->state]; + } + return -1; + } + } + + MICROPY_PY_LWIP_ENTER + + assert(socket->pcb.tcp != NULL); + + struct pbuf *p = socket->incoming.pbuf; + + mp_uint_t remaining = p->len - socket->recv_offset; + if (len > remaining) { + len = remaining; + } + + memcpy(buf, (byte *)p->payload + socket->recv_offset, len); + + remaining -= len; + if (remaining == 0) { + socket->incoming.pbuf = p->next; + // If we don't ref here, free() will free the entire chain, + // if we ref, it does what we need: frees 1st buf, and decrements + // next buf's refcount back to 1. + pbuf_ref(p->next); + pbuf_free(p); + socket->recv_offset = 0; + } else { + socket->recv_offset += len; + } + tcp_recved(socket->pcb.tcp, len); + + MICROPY_PY_LWIP_EXIT + + return len; +} + + +STATIC socketpool_socket_obj_t *open_socket_objs[MAX_SOCKETS]; STATIC bool user_socket[MAX_SOCKETS]; void socket_user_reset(void) { for (size_t i = 0; i < MP_ARRAY_SIZE(open_socket_objs); i++) { if (open_socket_objs[i] && user_socket[i]) { - // shut 'er down + socketpool_socket_close(open_socket_objs[i]); + open_socket_objs[i] = NULL; + user_socket[i] = false; } } } @@ -54,48 +613,131 @@ void socket_user_reset(void) { // The writes below send an event to the socket select task so that it redoes the // select with the new open socket set. -STATIC bool register_open_socket(mp_obj_t obj) { +STATIC bool register_open_socket(socketpool_socket_obj_t *obj) { for (size_t i = 0; i < MP_ARRAY_SIZE(open_socket_objs); i++) { if (!open_socket_objs[i]) { open_socket_objs[i] = obj; + DEBUG_printf("register_open_socket(%p) -> %d\n", obj, i); user_socket[i] = false; return true; } } + DEBUG_printf("register_open_socket(%p) fails due to full table\n", obj); return false; } -STATIC void unregister_open_socket(mp_obj_t obj) { +STATIC void unregister_open_socket(socketpool_socket_obj_t *obj) { for (size_t i = 0; i < MP_ARRAY_SIZE(open_socket_objs); i++) { if (open_socket_objs[i] == obj) { + DEBUG_printf("unregister_open_socket(%p) clears %d\n", obj, i); open_socket_objs[i] = NULL; user_socket[i] = false; + return; } } + DEBUG_printf("unregister_open_socket(%p) fails due to missing entry\n", obj); } -STATIC void mark_user_socket(mp_obj_t obj) { +STATIC void mark_user_socket(socketpool_socket_obj_t *obj) { for (size_t i = 0; i < MP_ARRAY_SIZE(open_socket_objs); i++) { if (open_socket_objs[i] == obj) { + DEBUG_printf("mark_user_socket(%p) -> %d\n", obj, i); user_socket[i] = true; return; } } + DEBUG_printf("mark_user_socket(%p) fails due to missing entry\n", obj); } bool socketpool_socket(socketpool_socketpool_obj_t *self, socketpool_socketpool_addressfamily_t family, socketpool_socketpool_sock_t type, - socketpool_socket_obj_t *sock) { - mp_raise_NotImplementedError(NULL); + socketpool_socket_obj_t *socket) { + + if (!register_open_socket(socket)) { + DEBUG_printf("collecting garbage to open socket\n"); + gc_collect(); + if (!register_open_socket(socket)) { + return false; + } + } + + socket->timeout = -1; + socket->recv_offset = 0; + socket->domain = SOCKETPOOL_AF_INET; + socket->type = type; + socket->callback = MP_OBJ_NULL; + socket->state = STATE_NEW; + + switch (socket->type) { + case SOCKETPOOL_SOCK_STREAM: + socket->pcb.tcp = tcp_new(); + socket->incoming.connection.alloc = 0; + socket->incoming.connection.tcp.item = NULL; + break; + case SOCKETPOOL_SOCK_DGRAM: + socket->pcb.udp = udp_new(); + socket->incoming.pbuf = NULL; + break; + #if MICROPY_PY_LWIP_SOCK_RAW + case SOCKETPOOL_SOCK_RAW: { + socket->pcb.raw = raw_new(0); + break; + } + #endif + default: + return false; + } + + if (socket->pcb.tcp == NULL) { + return false; + } + + switch (socket->type) { + case MOD_NETWORK_SOCK_STREAM: { + // Register the socket object as our callback argument. + tcp_arg(socket->pcb.tcp, (void *)socket); + // Register our error callback. + tcp_err(socket->pcb.tcp, _lwip_tcp_error); + break; + } + case MOD_NETWORK_SOCK_DGRAM: { + socket->state = STATE_ACTIVE_UDP; + // Register our receive callback now. Since UDP sockets don't require binding or connection + // before use, there's no other good time to do it. + udp_recv(socket->pcb.udp, _lwip_udp_incoming, (void *)socket); + break; + } + #if MICROPY_PY_LWIP_SOCK_RAW + case MOD_NETWORK_SOCK_RAW: { + // Register our receive callback now. Since raw sockets don't require binding or connection + // before use, there's no other good time to do it. + raw_recv(socket->pcb.raw, _lwip_raw_incoming, (void *)socket); + break; + } + #endif + } + return true; } socketpool_socket_obj_t *common_hal_socketpool_socket(socketpool_socketpool_obj_t *self, socketpool_socketpool_addressfamily_t family, socketpool_socketpool_sock_t type) { - mp_raise_NotImplementedError(NULL); + if (family != SOCKETPOOL_AF_INET) { + mp_raise_NotImplementedError(translate("Only IPv4 sockets supported")); + } + + // we must allocate sockets long-lived because we depend on their object-identity + socketpool_socket_obj_t *socket = m_new_ll_obj_with_finaliser(socketpool_socket_obj_t); + socket->base.type = &socketpool_socket_type; + + if (!socketpool_socket(self, family, type, socket)) { + mp_raise_RuntimeError(translate("Out of sockets")); + } + mark_user_socket(socket); + return socket; } int socketpool_socket_accept(socketpool_socket_obj_t *self, uint8_t *ip, uint32_t *port) { - mp_raise_NotImplementedError(NULL); + return -MP_EBADF; } socketpool_socket_obj_t *common_hal_socketpool_socket_accept(socketpool_socket_obj_t *self, @@ -108,58 +750,264 @@ bool common_hal_socketpool_socket_bind(socketpool_socket_obj_t *self, mp_raise_NotImplementedError(NULL); } -void socketpool_socket_close(socketpool_socket_obj_t *self) { - mp_raise_NotImplementedError(NULL); +STATIC err_t _lwip_tcp_close_poll(void *arg, struct tcp_pcb *pcb) { + // Connection has not been cleanly closed so just abort it to free up memory + tcp_poll(pcb, NULL, 0); + tcp_abort(pcb); + return ERR_OK; } -void common_hal_socketpool_socket_close(socketpool_socket_obj_t *self) { - mp_raise_NotImplementedError(NULL); +void socketpool_socket_close(socketpool_socket_obj_t *socket) { + unregister_open_socket(socket); + MICROPY_PY_LWIP_ENTER + if (socket->pcb.tcp == NULL) { // already closed + MICROPY_PY_LWIP_EXIT + return; + } + lwip_socket_free_incoming(socket); + switch (socket->type) { + case SOCKETPOOL_SOCK_STREAM: { + // Deregister callback (pcb.tcp is set to NULL below so must deregister now) + tcp_arg(socket->pcb.tcp, NULL); + tcp_err(socket->pcb.tcp, NULL); + tcp_recv(socket->pcb.tcp, NULL); + + if (socket->pcb.tcp->state != LISTEN) { + // Schedule a callback to abort the connection if it's not cleanly closed after + // the given timeout. The callback must be set before calling tcp_close since + // the latter may free the pcb; if it doesn't then the callback will be active. + tcp_poll(socket->pcb.tcp, _lwip_tcp_close_poll, MICROPY_PY_LWIP_TCP_CLOSE_TIMEOUT_MS / 500); + } + if (tcp_close(socket->pcb.tcp) != ERR_OK) { + DEBUG_printf("lwip_close: had to call tcp_abort()\n"); + tcp_abort(socket->pcb.tcp); + } + break; + } + case SOCKETPOOL_SOCK_DGRAM: + udp_recv(socket->pcb.udp, NULL, NULL); + udp_remove(socket->pcb.udp); + break; + #if MICROPY_PY_LWIP_SOCK_RAW + case SOCKETPOOL_SOCK_RAW: + raw_recv(socket->pcb.raw, NULL, NULL); + raw_remove(socket->pcb.raw); + break; + #endif + } + + socket->pcb.tcp = NULL; + socket->state = _ERR_BADF; + MICROPY_PY_LWIP_EXIT } -void common_hal_socketpool_socket_connect(socketpool_socket_obj_t *self, +void common_hal_socketpool_socket_close(socketpool_socket_obj_t *socket) { + socketpool_socket_close(socket); +} + +void common_hal_socketpool_socket_connect(socketpool_socket_obj_t *socket, const char *host, size_t hostlen, uint32_t port) { - mp_raise_NotImplementedError(NULL); + + if (socket->pcb.tcp == NULL) { + mp_raise_OSError(MP_EBADF); + } + + // get address + ip_addr_t dest; + int error = socketpool_resolve_host(socket->pool, host, &dest); + if (error != 0) { + mp_raise_OSError(EHOSTUNREACH); + } + + err_t err = ERR_ARG; + switch (socket->type) { + case MOD_NETWORK_SOCK_STREAM: { + if (socket->state != STATE_NEW) { + if (socket->state == STATE_CONNECTED) { + mp_raise_OSError(MP_EISCONN); + } else { + mp_raise_OSError(MP_EALREADY); + } + } + + // Register our receive callback. + MICROPY_PY_LWIP_ENTER + tcp_recv(socket->pcb.tcp, _lwip_tcp_recv); + socket->state = STATE_CONNECTING; + err = tcp_connect(socket->pcb.tcp, &dest, port, _lwip_tcp_connected); + if (err != ERR_OK) { + MICROPY_PY_LWIP_EXIT + socket->state = STATE_NEW; + mp_raise_OSError(error_lookup_table[-err]); + } + socket->peer_port = (mp_uint_t)port; + memcpy(socket->peer, &dest, sizeof(socket->peer)); + MICROPY_PY_LWIP_EXIT + + // And now we wait... + if (socket->timeout != -1) { + for (mp_uint_t retries = socket->timeout / 100; retries--;) { + mp_hal_delay_ms(100); + if (socket->state != STATE_CONNECTING) { + break; + } + } + if (socket->state == STATE_CONNECTING) { + mp_raise_OSError(MP_EINPROGRESS); + } + } else { + while (socket->state == STATE_CONNECTING) { + poll_sockets(); + } + } + if (socket->state == STATE_CONNECTED) { + err = ERR_OK; + } else { + err = socket->state; + } + break; + } + case MOD_NETWORK_SOCK_DGRAM: { + err = udp_connect(socket->pcb.udp, &dest, port); + break; + } + #if MICROPY_PY_LWIP_SOCK_RAW + case MOD_NETWORK_SOCK_RAW: { + err = raw_connect(socket->pcb.raw, &dest); + break; + } + #endif + } + + if (err != ERR_OK) { + mp_raise_OSError(error_lookup_table[-err]); + } } -bool common_hal_socketpool_socket_get_closed(socketpool_socket_obj_t *self) { - mp_raise_NotImplementedError(NULL); +bool common_hal_socketpool_socket_get_closed(socketpool_socket_obj_t *socket) { + return !socket->pcb.tcp; } -bool common_hal_socketpool_socket_get_connected(socketpool_socket_obj_t *self) { - mp_raise_NotImplementedError(NULL); +bool common_hal_socketpool_socket_get_connected(socketpool_socket_obj_t *socket) { + return socket->state == STATE_CONNECTED; } bool common_hal_socketpool_socket_listen(socketpool_socket_obj_t *self, int backlog) { mp_raise_NotImplementedError(NULL); } -mp_uint_t common_hal_socketpool_socket_recvfrom_into(socketpool_socket_obj_t *self, +mp_uint_t common_hal_socketpool_socket_recvfrom_into(socketpool_socket_obj_t *socket, uint8_t *buf, uint32_t len, uint8_t *ip, uint32_t *port) { - mp_raise_NotImplementedError(NULL); + int _errno; + + mp_uint_t ret = 0; + switch (socket->type) { + case SOCKETPOOL_SOCK_STREAM: { + memcpy(ip, &socket->peer, sizeof(socket->peer)); + *port = (mp_uint_t)socket->peer_port; + ret = lwip_tcp_receive(socket, (byte *)buf, len, &_errno); + break; + } + case SOCKETPOOL_SOCK_DGRAM: + #if MICROPY_PY_LWIP_SOCK_RAW + case SOCKETPOOL_SOCK_RAW: + #endif + ret = lwip_raw_udp_receive(socket, (byte *)buf, len, ip, port, &_errno); + break; + } + if (ret == -1) { + mp_raise_OSError(_errno); + } + + return ret; } -int socketpool_socket_recv_into(socketpool_socket_obj_t *self, +int socketpool_socket_recv_into(socketpool_socket_obj_t *socket, const uint8_t *buf, uint32_t len) { - mp_raise_NotImplementedError(NULL); + mp_uint_t ret = 0; + int _errno = 0; + switch (socket->type) { + case SOCKETPOOL_SOCK_STREAM: { + ret = lwip_tcp_receive(socket, (byte *)buf, len, &_errno); + break; + } + case SOCKETPOOL_SOCK_DGRAM: + #if MICROPY_PY_LWIP_SOCK_RAW + case SOCKETPOOL_SOCK_RAW: + #endif + ret = lwip_raw_udp_receive(socket, (byte *)buf, len, NULL, NULL, &_errno); + break; + } + return ret; } mp_uint_t common_hal_socketpool_socket_recv_into(socketpool_socket_obj_t *self, const uint8_t *buf, uint32_t len) { - mp_raise_NotImplementedError(NULL); + int received = socketpool_socket_recv_into(self, buf, len); + if (received < 0) { + mp_raise_OSError(received); + } + return received; } -int socketpool_socket_send(socketpool_socket_obj_t *self, const uint8_t *buf, uint32_t len) { - mp_raise_NotImplementedError(NULL); +int socketpool_socket_send(socketpool_socket_obj_t *socket, const uint8_t *buf, uint32_t len) { + mp_uint_t ret = 0; + int _errno = 0; + switch (socket->type) { + case SOCKETPOOL_SOCK_STREAM: { + ret = lwip_tcp_send(socket, buf, len, &_errno); + break; + } + case SOCKETPOOL_SOCK_DGRAM: + #if MICROPY_PY_LWIP_SOCK_RAW + case SOCKETPOOL_SOCK_RAW: + #endif + ret = lwip_raw_udp_send(socket, buf, len, NULL, 0, &_errno); + break; + } + if (ret == -1) { + return -_errno; + } + return ret; } mp_uint_t common_hal_socketpool_socket_send(socketpool_socket_obj_t *self, const uint8_t *buf, uint32_t len) { - mp_raise_NotImplementedError(NULL); + int sent = socketpool_socket_send(self, buf, len); + + if (sent < 0) { + mp_raise_OSError(-sent); + } + return sent; } -mp_uint_t common_hal_socketpool_socket_sendto(socketpool_socket_obj_t *self, +mp_uint_t common_hal_socketpool_socket_sendto(socketpool_socket_obj_t *socket, const char *host, size_t hostlen, uint32_t port, const uint8_t *buf, uint32_t len) { - mp_raise_NotImplementedError(NULL); + int _errno; + ip_addr_t ip; + int error = socketpool_resolve_host(socket->pool, host, &ip); + if (error != 0) { + mp_raise_OSError(EHOSTUNREACH); + } + + mp_uint_t ret = 0; + switch (socket->type) { + case SOCKETPOOL_SOCK_STREAM: { + ret = lwip_tcp_send(socket, buf, len, &_errno); + break; + } + case SOCKETPOOL_SOCK_DGRAM: + #if MICROPY_PY_LWIP_SOCK_RAW + case SOCKETPOOL_SOCK_RAW: + #endif + ret = lwip_raw_udp_send(socket, buf, len, &ip, port, &_errno); + break; + } + if (ret == -1) { + mp_raise_OSError(_errno); + } + + return ret; } void common_hal_socketpool_socket_settimeout(socketpool_socket_obj_t *self, uint32_t timeout_ms) { - mp_raise_NotImplementedError(NULL); + self->timeout = timeout_ms; } diff --git a/ports/raspberrypi/common-hal/socketpool/SocketPool.c b/ports/raspberrypi/common-hal/socketpool/SocketPool.c index 723b753064..d2f403c852 100644 --- a/ports/raspberrypi/common-hal/socketpool/SocketPool.c +++ b/ports/raspberrypi/common-hal/socketpool/SocketPool.c @@ -57,8 +57,7 @@ STATIC void lwip_getaddrinfo_cb(const char *name, const ip_addr_t *ipaddr, void } } -mp_obj_t common_hal_socketpool_socketpool_gethostbyname(socketpool_socketpool_obj_t *self, - const char *host) { +int socketpool_resolve_host(socketpool_socketpool_obj_t *self, const char *host, ip_addr_t *addr) { getaddrinfo_state_t state; state.status = 0; @@ -85,13 +84,27 @@ mp_obj_t common_hal_socketpool_socketpool_gethostbyname(socketpool_socketpool_ob } if (state.status < 0) { + return state.status; // TODO: CPython raises gaierror, we raise with native lwIP negative error // values, to differentiate from normal errno's at least in such way. mp_raise_OSError(state.status); } + *addr = state.ipaddr; + return 0; +} + +mp_obj_t common_hal_socketpool_socketpool_gethostbyname(socketpool_socketpool_obj_t *self, + const char *host) { + + ip_addr_t addr; + int result = socketpool_resolve_host(self, host, &addr); + if (result < 0) { + mp_raise_OSError(-result); + } + char ip_str[IP4ADDR_STRLEN_MAX]; - inet_ntoa_r(state.ipaddr, ip_str, IP4ADDR_STRLEN_MAX); + inet_ntoa_r(addr, ip_str, IP4ADDR_STRLEN_MAX); mp_obj_t ip_obj = mp_obj_new_str(ip_str, strlen(ip_str)); return ip_obj; } diff --git a/ports/raspberrypi/common-hal/socketpool/SocketPool.h b/ports/raspberrypi/common-hal/socketpool/SocketPool.h index ea0679b5eb..3d498d9a49 100644 --- a/ports/raspberrypi/common-hal/socketpool/SocketPool.h +++ b/ports/raspberrypi/common-hal/socketpool/SocketPool.h @@ -31,3 +31,5 @@ typedef struct { mp_obj_base_t base; } socketpool_socketpool_obj_t; + +int socketpool_resolve_host(socketpool_socketpool_obj_t *self, const char *host, ip_addr_t *addr); diff --git a/ports/raspberrypi/lib/cyw43-driver b/ports/raspberrypi/lib/cyw43-driver new file mode 160000 index 0000000000..195dfcc10b --- /dev/null +++ b/ports/raspberrypi/lib/cyw43-driver @@ -0,0 +1 @@ +Subproject commit 195dfcc10bb6f379e3dea45147590db2203d3c7b diff --git a/ports/raspberrypi/lib/lwip b/ports/raspberrypi/lib/lwip new file mode 160000 index 0000000000..239918ccc1 --- /dev/null +++ b/ports/raspberrypi/lib/lwip @@ -0,0 +1 @@ +Subproject commit 239918ccc173cb2c2a62f41a40fd893f57faf1d6 diff --git a/ports/raspberrypi/pioasm/CMakeLists.txt b/ports/raspberrypi/pioasm/CMakeLists.txt new file mode 100644 index 0000000000..1923068bae --- /dev/null +++ b/ports/raspberrypi/pioasm/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.12) +project(pioasm) +set(PICO_TINYUSB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib/tinyusb) +include(../sdk/pico_sdk_init.cmake) +pico_sdk_init() + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PICO_SDK_PATH}/tools) +find_package(Pioasm REQUIRED) diff --git a/shared-bindings/digitalio/DigitalInOut.c b/shared-bindings/digitalio/DigitalInOut.c index c4a1f853b5..8ede649cd7 100644 --- a/shared-bindings/digitalio/DigitalInOut.c +++ b/shared-bindings/digitalio/DigitalInOut.c @@ -158,10 +158,7 @@ STATIC mp_obj_t digitalio_digitalinout_switch_to_output(size_t n_args, const mp_ drive_mode = DRIVE_MODE_OPEN_DRAIN; } // do the transfer - digitalinout_result_t result = common_hal_digitalio_digitalinout_switch_to_output(self, args[ARG_value].u_bool, drive_mode); - if (result == DIGITALINOUT_INPUT_ONLY) { - mp_raise_NotImplementedError(translate("Pin is input only")); - } + check_result(common_hal_digitalio_digitalinout_switch_to_output(self, args[ARG_value].u_bool, drive_mode)); return mp_const_none; } MP_DEFINE_CONST_FUN_OBJ_KW(digitalio_digitalinout_switch_to_output_obj, 1, digitalio_digitalinout_switch_to_output); @@ -229,10 +226,7 @@ STATIC mp_obj_t digitalio_digitalinout_obj_set_direction(mp_obj_t self_in, mp_ob if (value == MP_ROM_PTR(&digitalio_direction_input_obj)) { check_result(common_hal_digitalio_digitalinout_switch_to_input(self, PULL_NONE)); } else if (value == MP_ROM_PTR(&digitalio_direction_output_obj)) { - digitalinout_result_t result = common_hal_digitalio_digitalinout_switch_to_output(self, false, DRIVE_MODE_PUSH_PULL); - if (result == DIGITALINOUT_INPUT_ONLY) { - mp_raise_NotImplementedError(translate("Pin is input only")); - } + check_result(common_hal_digitalio_digitalinout_switch_to_output(self, false, DRIVE_MODE_PUSH_PULL)); } else { mp_arg_error_invalid(MP_QSTR_direction); }