diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cda6d9f5..96be603dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for `atomvm:posix_clock_settime/2` - Added support for creations of binaries with unaligned strings - Added `-h` and `-v` flags to generic_unix AtomVM command -- Added support initial for Pico-W with the on-board LED - Removed support to ESP32 NVS from network module in order to make it generic. See also [UPDATING.md]. +- Added initial support for Pico-W: on-board LED, connection to wifi network. ### Changed - Changed offset of atomvmlib and of program on Pico. See also [UPDATING.md]. diff --git a/examples/erlang/rp2040/CMakeLists.txt b/examples/erlang/rp2040/CMakeLists.txt index 30ccd5a1b..1213dfaf8 100644 --- a/examples/erlang/rp2040/CMakeLists.txt +++ b/examples/erlang/rp2040/CMakeLists.txt @@ -26,3 +26,4 @@ pack_uf2(hello_pico hello_pico) pack_uf2(pico_blink pico_blink) pack_uf2(pico_rtc pico_rtc) pack_uf2(picow_blink picow_blink) +pack_uf2(picow_wifi_sta picow_wifi_sta) diff --git a/examples/erlang/rp2040/picow_wifi_sta.erl b/examples/erlang/rp2040/picow_wifi_sta.erl new file mode 100644 index 000000000..48363faac --- /dev/null +++ b/examples/erlang/rp2040/picow_wifi_sta.erl @@ -0,0 +1,49 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(picow_wifi_sta). + +-export([start/0]). + +start() -> + Config = [ + {sta, [ + {ssid, <<"myssid">>}, + {psk, <<"mypsk">>}, + {connected, fun connected/0}, + {got_ip, fun got_ip/1}, + {disconnected, fun disconnected/0} + ]} + ], + case network:start(Config) of + {ok, _Pid} -> + timer:sleep(infinity); + Error -> + erlang:display(Error) + end. + +connected() -> + io:format("Connected to access point.\n"). + +got_ip({IPv4, Netmask, Gateway}) -> + io:format("Got IP: ip=~p, netmask=~p, gateway=~p.\n", [IPv4, Netmask, Gateway]). + +disconnected() -> + io:format("Disconnected from access point.\n"). diff --git a/src/platforms/rp2040/src/lib/CMakeLists.txt b/src/platforms/rp2040/src/lib/CMakeLists.txt index 265c13d47..9928f4e5f 100644 --- a/src/platforms/rp2040/src/lib/CMakeLists.txt +++ b/src/platforms/rp2040/src/lib/CMakeLists.txt @@ -29,6 +29,7 @@ set(HEADER_FILES set(SOURCE_FILES gpiodriver.c + networkdriver.c platform_defaultatoms.c platform_nifs.c smp.c @@ -66,6 +67,7 @@ endif() if (PICO_CYW43_SUPPORTED) target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC pico_cyw43_arch_lwip_threadsafe_background) + target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,networkregister_port_driver") endif() -target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC -Wl,-u -Wl,gpio_nif) +target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif") diff --git a/src/platforms/rp2040/src/lib/networkdriver.c b/src/platforms/rp2040/src/lib/networkdriver.c new file mode 100644 index 000000000..41dae95c9 --- /dev/null +++ b/src/platforms/rp2040/src/lib/networkdriver.c @@ -0,0 +1,336 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2023 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include + +#include "rp2040_sys.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" + +#include +#include + +#pragma GCC diagnostic pop + +#ifdef LIB_PICO_CYW43_ARCH + +#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + REF_SIZE) + +static const char *const ap_atom = ATOM_STR("\x2", "ap"); +static const char *const psk_atom = ATOM_STR("\x3", "psk"); +static const char *const ssid_atom = ATOM_STR("\x4", "ssid"); +static const char *const sta_atom = ATOM_STR("\x3", "sta"); +static const char *const sta_connected_atom = ATOM_STR("\xD", "sta_connected"); +static const char *const sta_disconnected_atom = ATOM_STR("\x10", "sta_disconnected"); +static const char *const sta_got_ip_atom = ATOM_STR("\xA", "sta_got_ip"); + +enum network_cmd +{ + NetworkInvalidCmd = 0, + // TODO add support for scan, ifconfig + NetworkStartCmd +}; + +static const AtomStringIntPair cmd_table[] = { + { ATOM_STR("\x5", "start"), NetworkStartCmd }, + SELECT_INT_DEFAULT(NetworkInvalidCmd) +}; + +struct NetworkDriverData +{ + GlobalContext *global; + uint32_t owner_process_id; + uint64_t ref_ticks; + int link_status; +}; + +// Callbacks do not allow for user data +// netif->state is actually pointing to &cyw43_state +static struct NetworkDriverData *driver_data; + +static void network_driver_netif_status_cb(struct netif *netif); + +static term tuple_from_addr(Heap *heap, uint32_t addr) +{ + term terms[4]; + terms[0] = term_from_int32((addr >> 24) & 0xFF); + terms[1] = term_from_int32((addr >> 16) & 0xFF); + terms[2] = term_from_int32((addr >> 8) & 0xFF); + terms[3] = term_from_int32(addr & 0xFF); + + return port_heap_create_tuple_n(heap, 4, terms); +} + +static void send_term(Heap *heap, term t) +{ + term ref = term_from_ref_ticks(driver_data->ref_ticks, heap); + term msg = term_alloc_tuple(2, heap); + term_put_tuple_element(msg, 0, ref); + term_put_tuple_element(msg, 1, t); + + // Pid ! {Ref, T} + port_send_message(driver_data->global, term_from_local_process_id(driver_data->owner_process_id), msg); +} + +static void send_sta_connected() +{ + // {Ref, sta_connected} + BEGIN_WITH_STACK_HEAP(PORT_REPLY_SIZE, heap); + { + send_term(&heap, globalcontext_make_atom(driver_data->global, sta_connected_atom)); + } + END_WITH_STACK_HEAP(heap, driver_data->global); +} + +static void send_sta_disconnected() +{ + // {Ref, sta_disconnected} + BEGIN_WITH_STACK_HEAP(PORT_REPLY_SIZE, heap); + { + send_term(&heap, globalcontext_make_atom(driver_data->global, sta_disconnected_atom)); + } + END_WITH_STACK_HEAP(heap, driver_data->global); +} + +static void send_got_ip(struct netif *netif) +{ + // {Ref, {sta_got_ip, {{192, 168, 1, 2}, {255, 255, 255, 0}, {192, 168, 1, 1}}}} + BEGIN_WITH_STACK_HEAP(PORT_REPLY_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(3) + TUPLE_SIZE(4) * 3, heap); + { + term ip = tuple_from_addr(&heap, ntohl(ip4_addr_get_u32(netif_ip4_addr(netif)))); + term netmask = tuple_from_addr(&heap, ntohl(ip4_addr_get_u32(netif_ip4_netmask(netif)))); + term gw = tuple_from_addr(&heap, ntohl(ip4_addr_get_u32(netif_ip4_gw(netif)))); + + term ip_info = port_heap_create_tuple3(&heap, ip, netmask, gw); + term reply = port_heap_create_tuple2(&heap, globalcontext_make_atom(driver_data->global, sta_got_ip_atom), ip_info); + send_term(&heap, reply); + } + END_WITH_STACK_HEAP(heap, driver_data->global); +} + +static term start_sta(term sta_config, GlobalContext *global) +{ + term ssid_term = interop_kv_get_value(sta_config, ssid_atom, global); + term pass_term = interop_kv_get_value(sta_config, psk_atom, global); + + // + // Check parameters + // + if (term_is_invalid_term(ssid_term)) { + return BADARG_ATOM; + } + int ok = 0; + char *ssid = interop_term_to_string(ssid_term, &ok); + if (!ok || IS_NULL_PTR(ssid)) { + return BADARG_ATOM; + } + char *psk = NULL; + if (!term_is_invalid_term(pass_term)) { + psk = interop_term_to_string(pass_term, &ok); + if (!ok) { + free(ssid); + return BADARG_ATOM; + } + } + + cyw43_arch_enable_sta_mode(); + uint32_t auth = (psk == NULL) ? CYW43_AUTH_OPEN : CYW43_AUTH_WPA2_MIXED_PSK; + int result = cyw43_arch_wifi_connect_async(ssid, psk, auth); + // We need to set the callback after calling connect async because it's + // erased by cyw43_arch_wifi_connect_async. + // There could be a race condition here. + netif_set_status_callback(&cyw43_state.netif[CYW43_ITF_STA], network_driver_netif_status_cb); + network_driver_netif_status_cb(&cyw43_state.netif[CYW43_ITF_STA]); + free(ssid); + free(psk); + if (result != 0) { + return BADARG_ATOM; + } + + return OK_ATOM; +} + +static term start_ap(term ap_config, GlobalContext *glb) +{ + UNUSED(ap_config); + UNUSED(glb); + return BADARG_ATOM; +} + +static void network_driver_netif_status_cb(struct netif *netif) +{ + if (netif == &cyw43_state.netif[CYW43_ITF_STA]) { + // We don't really need to lock to call cyw43_tcpip_link_status + // However, we take advantage of this lock to protect driver_data->link_status. + cyw43_arch_lwip_begin(); + int link_status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + int previous_link_status = driver_data->link_status; + driver_data->link_status = link_status; + cyw43_arch_lwip_end(); + if (link_status != previous_link_status) { + if (link_status == CYW43_LINK_DOWN) { + send_sta_disconnected(); + } else if (link_status == CYW43_LINK_JOIN) { + send_sta_connected(); + } else if (link_status == CYW43_LINK_UP) { + send_got_ip(netif); + } + } + } +} + +static void start_network(Context *ctx, term pid, term ref, term config) +{ + // {Ref, ok | {error, atom() | integer()}} + size_t heap_size = PORT_REPLY_SIZE + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { + return; + } + + if (driver_data == NULL) { + driver_data = malloc(sizeof(struct NetworkDriverData)); + } + driver_data->global = ctx->global; + driver_data->owner_process_id = term_to_local_process_id(pid); + driver_data->ref_ticks = term_to_ref_ticks(ref); + driver_data->link_status = CYW43_LINK_DOWN; + + // + // Get the STA and AP config, if set + // + term sta_config = interop_kv_get_value_default(config, sta_atom, term_invalid_term(), ctx->global); + term ap_config = interop_kv_get_value_default(config, ap_atom, term_invalid_term(), ctx->global); + if (UNLIKELY(term_is_invalid_term(sta_config) && term_is_invalid_term(ap_config))) { + term error = port_create_error_tuple(ctx, BADARG_ATOM); + port_send_reply(ctx, pid, ref, error); + return; + } + + if (sta_config) { + term result_atom = start_sta(sta_config, ctx->global); + if (result_atom != OK_ATOM) { + term error = port_create_error_tuple(ctx, result_atom); + port_send_reply(ctx, pid, ref, error); + return; + } + } else { + cyw43_arch_disable_sta_mode(); + } + + if (ap_config) { + term result_atom = start_ap(ap_config, ctx->global); + if (result_atom != OK_ATOM) { + term error = port_create_error_tuple(ctx, result_atom); + port_send_reply(ctx, pid, ref, error); + return; + } + } else { + cyw43_arch_disable_ap_mode(); + } + + // + // Done -- send an ok so the FSM can proceed + // + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static NativeHandlerResult consume_mailbox(Context *ctx) +{ + Message *message = mailbox_first(&ctx->mailbox); + term msg = message->message; + + if (UNLIKELY(!term_is_tuple(msg) || term_get_tuple_arity(msg) != 3)) { + return NativeContinue; + } + + term pid = term_get_tuple_element(msg, 0); + term ref = term_get_tuple_element(msg, 1); + term cmd = term_get_tuple_element(msg, 2); + + if (term_is_tuple(cmd) && term_get_tuple_arity(cmd) == 2) { + + term cmd_term = term_get_tuple_element(cmd, 0); + term config = term_get_tuple_element(cmd, 1); + + enum network_cmd cmd = interop_atom_term_select_int(cmd_table, cmd_term, ctx->global); + switch (cmd) { + case NetworkStartCmd: + start_network(ctx, pid, ref, config); + break; + + default: { + // {Ref, {error, badarg}} + size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { + return NativeContinue; + } + port_send_reply(ctx, pid, ref, port_create_error_tuple(ctx, BADARG_ATOM)); + } + } + } else { + // {Ref, {error, badarg}} + size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { + return NativeContinue; + } + port_send_reply(ctx, pid, ref, port_create_error_tuple(ctx, BADARG_ATOM)); + } + + mailbox_remove_message(&ctx->mailbox, &ctx->heap); + + return NativeContinue; +} + +// +// Entrypoints +// + +void network_driver_init(GlobalContext *global) +{ + UNUSED(global); + + driver_data = NULL; +} + +void network_driver_destroy(GlobalContext *global) +{ + UNUSED(global); + + free(driver_data); + driver_data = NULL; +} + +Context *network_driver_create_port(GlobalContext *global, term opts) +{ + UNUSED(opts); + + Context *ctx = context_new(global); + ctx->native_handler = consume_mailbox; + ctx->platform_data = NULL; + return ctx; +} + +REGISTER_PORT_DRIVER(network, network_driver_init, network_driver_destroy, network_driver_create_port) + +#endif diff --git a/src/platforms/rp2040/src/lib/rp2040_sys.h b/src/platforms/rp2040/src/lib/rp2040_sys.h index 948ec5bed..4ca5476ff 100644 --- a/src/platforms/rp2040/src/lib/rp2040_sys.h +++ b/src/platforms/rp2040/src/lib/rp2040_sys.h @@ -25,8 +25,28 @@ #include +#include "sys.h" + // Because pico sdk uses -gc-section, it is required to add -Wl,-u NAME_nif -// to make sure a given module nif is linked in. +// to make sure a given module nif or port are linked in. + +#define REGISTER_PORT_DRIVER(NAME, INIT_CB, DESTROY_CB, CREATE_CB) \ + struct PortDriverDef NAME##_port_driver_def = { \ + .port_driver_name = #NAME, \ + .port_driver_init_cb = INIT_CB, \ + .port_driver_destroy_cb = DESTROY_CB, \ + .port_driver_create_port_cb = CREATE_CB \ + }; \ + \ + struct PortDriverDefListItem NAME##_port_driver_def_list_item = { \ + .def = &NAME##_port_driver_def \ + }; \ + \ + __attribute__((constructor)) void NAME##register_port_driver() \ + { \ + NAME##_port_driver_def_list_item.next = port_driver_list; \ + port_driver_list = &NAME##_port_driver_def_list_item; \ + } #define REGISTER_NIF_COLLECTION(NAME, INIT_CB, DESTROY_CB, RESOLVE_NIF_CB) \ struct NifCollectionDef NAME##_nif_collection_def = { \ @@ -51,6 +71,24 @@ struct RP2040PlatformData cond_t event_poll_cond; }; +typedef void (*port_driver_init_t)(GlobalContext *global); +typedef void (*port_driver_destroy_t)(GlobalContext *global); +typedef Context *(*port_driver_create_port_t)(GlobalContext *global, term opts); + +struct PortDriverDef +{ + const char *port_driver_name; + const port_driver_init_t port_driver_init_cb; + const port_driver_destroy_t port_driver_destroy_cb; + const port_driver_create_port_t port_driver_create_port_cb; +}; + +struct PortDriverDefListItem +{ + struct PortDriverDefListItem *next; + const struct PortDriverDef *const def; +}; + typedef void (*nif_collection_init_t)(GlobalContext *global); typedef void (*nif_collection_destroy_t)(GlobalContext *global); typedef const struct Nif *(*nif_collection_resolve_nif_t)(const char *name); @@ -68,8 +106,11 @@ struct NifCollectionDefListItem const struct NifCollectionDef *const def; }; +extern struct PortDriverDefListItem *port_driver_list; extern struct NifCollectionDefListItem *nif_collection_list; +void port_driver_init_all(GlobalContext *global); +void port_driver_destroy_all(GlobalContext *global); void nif_collection_init_all(GlobalContext *global); void nif_collection_destroy_all(GlobalContext *global); const struct Nif *nif_collection_resolve_nif(const char *name); diff --git a/src/platforms/rp2040/src/lib/sys.c b/src/platforms/rp2040/src/lib/sys.c index eb459abe0..b4f9c9e20 100644 --- a/src/platforms/rp2040/src/lib/sys.c +++ b/src/platforms/rp2040/src/lib/sys.c @@ -45,6 +45,7 @@ #include "rp2040_sys.h" +struct PortDriverDefListItem *port_driver_list; struct NifCollectionDefListItem *nif_collection_list; void sys_init_platform(GlobalContext *glb) @@ -181,11 +182,13 @@ Module *sys_load_module(GlobalContext *global, const char *module_name) return new_module; } -Context *sys_create_port(GlobalContext *glb, const char *driver_name, term opts) +Context *sys_create_port(GlobalContext *glb, const char *port_name, term opts) { - UNUSED(glb); - UNUSED(driver_name); - UNUSED(opts); + for (struct PortDriverDefListItem *item = port_driver_list; item != NULL; item = item->next) { + if (strcmp(port_name, item->def->port_driver_name) == 0) { + return item->def->port_driver_create_port_cb(glb, opts); + } + } return NULL; } @@ -197,6 +200,24 @@ term sys_get_info(Context *ctx, term key) return UNDEFINED_ATOM; } +void port_driver_init_all(GlobalContext *global) +{ + for (struct PortDriverDefListItem *item = port_driver_list; item != NULL; item = item->next) { + if (item->def->port_driver_init_cb) { + item->def->port_driver_init_cb(global); + } + } +} + +void port_driver_destroy_all(GlobalContext *global) +{ + for (struct PortDriverDefListItem *item = port_driver_list; item != NULL; item = item->next) { + if (item->def->port_driver_destroy_cb) { + item->def->port_driver_destroy_cb(global); + } + } +} + void nif_collection_init_all(GlobalContext *global) { for (struct NifCollectionDefListItem *item = nif_collection_list; item != NULL; item = item->next) {