Skip to content

Commit

Permalink
Add support for PicoW networking
Browse files Browse the repository at this point in the history
Port OTP Socket code to lwIP raw API
Also fix networkdriver code to avoid sending messages from ISR
Also add socket:recv/2,3 and socket:recvfrom/2,3

Signed-off-by: Paul Guyot <[email protected]>
  • Loading branch information
pguyot committed Oct 29, 2023
1 parent ed578f2 commit 41b784d
Show file tree
Hide file tree
Showing 23 changed files with 2,259 additions and 642 deletions.
30 changes: 13 additions & 17 deletions CMakeModules/DefineIfExists.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,19 @@ include(CheckSymbolExists)
include(CheckCSourceCompiles)

function(define_if_symbol_exists target symbol header scope macro)
if (NOT DEFINED ${macro})
check_symbol_exists(${symbol} ${header} ${macro})
if (${macro})
target_compile_definitions(${target} ${scope} ${macro})
endif(${macro})
endif()
check_symbol_exists(${symbol} ${header} ${macro})
if (${macro})
target_compile_definitions(${target} ${scope} ${macro})
endif(${macro})
endfunction()
function(define_if_function_exists target symbol header scope macro)
if (NOT DEFINED ${macro})
check_c_source_compiles("
#include <${header}>
int main(int argc)
{
return ((int*)(&${symbol}))[argc];
}" ${macro})
if (${macro})
target_compile_definitions(${target} ${scope} ${macro})
endif(${macro})
endif()
check_c_source_compiles("
#include <${header}>
int main(int argc)
{
return ((int*)(&${symbol}))[argc];
}" ${macro})
if (${macro})
target_compile_definitions(${target} ${scope} ${macro})
endif(${macro})
endfunction()
2 changes: 2 additions & 0 deletions examples/erlang/rp2040/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ pack_uf2(pico_rtc pico_rtc)
pack_uf2(picow_blink picow_blink)
pack_uf2(picow_wifi_sta picow_wifi_sta)
pack_uf2(picow_wifi_ap picow_wifi_ap)
pack_uf2(picow_udp_beacon picow_udp_beacon)
pack_uf2(picow_tcp_server picow_tcp_server)
89 changes: 89 additions & 0 deletions examples/erlang/rp2040/picow_tcp_server.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
%
% This file is part of AtomVM.
%
% Copyright 2023 Paul Guyot <[email protected]>
%
% 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_tcp_server).

-export([start/0]).

-spec start() -> no_return().
start() ->
Config = [
{sta, [
{ssid, <<"myssid">>},
{psk, <<"mypsk">>},
{got_ip, fun got_ip/1}
]}
],
case network:start(Config) of
{ok, _Pid} ->
timer:sleep(infinity);
Error ->
erlang:display(Error)
end.

got_ip({IPv4, Netmask, Gateway}) ->
io:format("Got IP: ip=~p, netmask=~p, gateway=~p.\n", [IPv4, Netmask, Gateway]),
setup().

setup() ->
{ok, ListeningSocket} = socket:open(inet, stream, tcp),

ok = socket:setopt(ListeningSocket, {socket, reuseaddr}, true),
ok = socket:setopt(ListeningSocket, {socket, linger}, #{onoff => true, linger => 0}),

ok = socket:bind(ListeningSocket, #{family => inet, addr => any, port => 44404}),
ok = socket:listen(ListeningSocket),
io:format("Listening on ~p.~n", [socket:sockname(ListeningSocket)]),

spawn(fun() -> accept(ListeningSocket) end).

accept(ListeningSocket) ->
io:format("Waiting to accept connection...~n"),
case socket:accept(ListeningSocket) of
{ok, ConnectedSocket} ->
io:format("Accepted connection. local: ~p peer: ~p~n", [
socket:sockname(ConnectedSocket), socket:peername(ConnectedSocket)
]),
spawn(fun() -> accept(ListeningSocket) end),
echo(ConnectedSocket);
{error, Reason} ->
io:format("An error occurred accepting connection: ~p~n", [Reason])
end.

-spec echo(ConnectedSocket :: socket:socket()) -> no_return().
echo(ConnectedSocket) ->
io:format("Waiting to receive data...~n"),
case socket:recv(ConnectedSocket) of
{ok, Data} ->
io:format("Received data ~p from ~p. Echoing back...~n", [
Data, socket:peername(ConnectedSocket)
]),
case socket:send(ConnectedSocket, Data) of
ok ->
io:format("All data was sent~n");
{ok, Rest} ->
io:format("Some data was sent. Remaining: ~p~n", [Rest]);
{error, Reason} ->
io:format("An error occurred sending data: ~p~n", [Reason])
end,
echo(ConnectedSocket);
{error, Reason} ->
io:format("An error occurred waiting on a connected socket: ~p~n", [Reason])
end.
58 changes: 58 additions & 0 deletions examples/erlang/rp2040/picow_udp_beacon.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
%
% This file is part of AtomVM.
%
% Copyright 2023 Paul Guyot <[email protected]>
%
% 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_udp_beacon).

-export([start/0]).

start() ->
Self = self(),
Config = [
{sta, [
{ssid, <<"myssid">>},
{psk, <<"mypsk">>},
{got_ip, fun(IPInfo) -> got_ip(Self, IPInfo) end}
]}
],
case network:start(Config) of
{ok, _Pid} ->
loop(disconnected);
Error ->
erlang:display(Error)
end.

got_ip(Parent, {IPv4, Netmask, Gateway}) ->
io:format("Got IP: ip=~p, netmask=~p, gateway=~p.\n", [IPv4, Netmask, Gateway]),
Parent ! connected.

loop(disconnected) ->
receive
connected ->
{ok, UDPSocket} = socket:open(inet, dgram, udp),
loop({UDPSocket, 0})
end;
loop({UDPSocket, N}) ->
Message = io_lib:format("AtomVM beacon #~B\n", [N]),
ok = socket:sendto(UDPSocket, Message, #{
family => inet, port => 4444, addr => {255, 255, 255, 255}
}),
io:format("Sent beacon #~B\n", [N]),
timer:sleep(1000),
loop({UDPSocket, N + 1}).
90 changes: 68 additions & 22 deletions libs/estdlib/src/socket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
sockname/1,
peername/1,
recv/1,
recv/2,
recv/3,
recvfrom/1,
recvfrom/2,
recvfrom/3,
send/2,
sendto/3,
setopt/3,
Expand All @@ -42,8 +46,8 @@
-export([
nif_select_read/3,
nif_accept/1,
nif_recv/1,
nif_recvfrom/1,
nif_recv/2,
nif_recvfrom/2,
nif_select_stop/1,
nif_send/2,
nif_sendto/3
Expand Down Expand Up @@ -248,6 +252,12 @@ accept(Socket, Timeout) ->
R
end;
{closed, Ref} ->
% socket was closed by another process
% TODO: we need to handle:
% (a) SELECT_STOP being scheduled
% (b) flush of messages as we can have both
% {closed, Ref} and {select, _, Ref, _} in the
% queue
{error, closed}
after Timeout ->
{error, timeout}
Expand All @@ -257,45 +267,58 @@ accept(Socket, Timeout) ->
end.

%%-----------------------------------------------------------------------------
%% @equiv socket:recv(Socket, infinity)
%% @equiv socket:recv(Socket, 0)
%% @end
%%-----------------------------------------------------------------------------
-spec recv(Socket :: socket()) -> {ok, Data :: binary()} | {error, Reason :: term()}.
recv(Socket) ->
recv(Socket, infinity).
recv(Socket, 0, infinity).

%%-----------------------------------------------------------------------------
%% @equiv socket:recv(Socket, Length, infinity)
%% @end
%%-----------------------------------------------------------------------------
-spec recv(Socket :: socket(), Length :: non_neg_integer()) ->
{ok, Data :: binary()} | {error, Reason :: term()}.
recv(Socket, Length) ->
recv(Socket, Length, infinity).

%%-----------------------------------------------------------------------------
%% @param Socket the socket
%% @param Length number of bytes to receive
%% @param Timeout timeout (in milliseconds)
%% @returns `{ok, Data}' if successful; `{error, Reason}', otherwise.
%% @doc Receive data on the specified socket.
%%
%% Note that this function will block until data is received
%% on the socket.
%% This function is equivalent to `recvfrom/3' except for the return type.
%%
%% Example:
%%
%% `{ok, Data} = socket:recv(ConnectedSocket)'
%% @end
%%-----------------------------------------------------------------------------
-spec recv(Socket :: socket(), Timeout :: timeout()) ->
-spec recv(Socket :: socket(), Length :: non_neg_integer(), Timeout :: timeout()) ->
{ok, Data :: binary()} | {error, Reason :: term()}.
recv(Socket, Timeout) ->
recv(Socket, Length, Timeout) ->
Self = self(),
Ref = erlang:make_ref(),
?TRACE("select read for recv. self=~p ref=~p~n", [Self, Ref]),
case ?MODULE:nif_select_read(Socket, Self, Ref) of
ok ->
receive
{select, _AcceptedSocket, Ref, ready_input} ->
case ?MODULE:nif_recv(Socket) of
{error, closed} = E ->
case ?MODULE:nif_recv(Socket, Length) of
{error, _} = E ->
?MODULE:nif_select_stop(Socket),
E;
R ->
R
% TODO: Assemble data to have more if Length > byte_size(Data)
% as long as timeout did not expire
{ok, Data} ->
{ok, Data}
end;
{closed, Ref} ->
% socket was closed by another process
% TODO: see above in accept/2
{error, closed}
after Timeout ->
{error, timeout}
Expand All @@ -305,16 +328,26 @@ recv(Socket, Timeout) ->
end.

%%-----------------------------------------------------------------------------
%% @equiv socket:recvfrom(Socket, infinity)
%% @equiv socket:recvfrom(Socket, 0)
%% @end
%%-----------------------------------------------------------------------------
-spec recvfrom(Socket :: socket()) ->
{ok, {Address :: sockaddr(), Data :: binary()}} | {error, Reason :: term()}.
recvfrom(Socket) ->
recvfrom(Socket, infinity).
recvfrom(Socket, 0).

%%-----------------------------------------------------------------------------
%% @equiv socket:recvfrom(Socket, Length, infinity)
%% @end
%%-----------------------------------------------------------------------------
-spec recvfrom(Socket :: socket(), Length :: non_neg_integer()) ->
{ok, {Address :: sockaddr(), Data :: binary()}} | {error, Reason :: term()}.
recvfrom(Socket, Length) ->
recvfrom(Socket, Length, infinity).

%%-----------------------------------------------------------------------------
%% @param Socket the socket
%% @param Length number of bytes to receive
%% @param Timeout timeout (in milliseconds)
%% @returns `{ok, {Address, Data}}' if successful; `{error, Reason}', otherwise.
%% @doc Receive data on the specified socket, returning the from address.
Expand All @@ -325,26 +358,39 @@ recvfrom(Socket) ->
%% Example:
%%
%% `{ok, {Address, Data}} = socket:recvfrom(ConnectedSocket)'
%%
%% If socket is UDP, the function retrieves the first available packet and
%% truncate it to Length bytes, unless Length is 0 in which case it returns
%% the whole packet ("all available").
%%
%% If socket is TCP and Length is 0, this function retrieves all available
%% data without waiting (using peek if the platform allows it).
%% If socket is TCP and Length is not 0, this function waits until Length
%% bytes are available and return these bytes.
%% @end
%%-----------------------------------------------------------------------------
-spec recvfrom(Socket :: socket(), Timeout :: timeout()) ->
-spec recvfrom(Socket :: socket(), Length :: non_neg_integer(), Timeout :: timeout()) ->
{ok, {Address :: sockaddr(), Data :: binary()}} | {error, Reason :: term()}.
recvfrom(Socket, Timeout) ->
recvfrom(Socket, Length, Timeout) ->
Self = self(),
Ref = erlang:make_ref(),
?TRACE("select read for recvfrom. self=~p ref=~p", [Self, Ref]),
case ?MODULE:nif_select_read(Socket, Self, Ref) of
ok ->
receive
{select, _AcceptedSocket, Ref, ready_input} ->
case ?MODULE:nif_recvfrom(Socket) of
{error, closed} = E ->
case ?MODULE:nif_recvfrom(Socket, Length) of
{error, _} = E ->
?MODULE:nif_select_stop(Socket),
E;
R ->
R
% TODO: Assemble data to have more if Length > byte_size(Data)
% as long as timeout did not expire
{ok, {Address, Data}} ->
{ok, {Address, Data}}
end;
{closed, Ref} ->
% socket was closed by another process
% TODO: see above in accept/2
{error, closed}
after Timeout ->
{error, timeout}
Expand Down Expand Up @@ -475,11 +521,11 @@ nif_accept(_Socket) ->
erlang:nif_error(undefined).

%% @private
nif_recv(_Socket) ->
nif_recv(_Socket, _Length) ->
erlang:nif_error(undefined).

%% @private
nif_recvfrom(_Socket) ->
nif_recvfrom(_Socket, _Length) ->
erlang:nif_error(undefined).

%% @private
Expand Down
2 changes: 2 additions & 0 deletions src/libAtomVM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ define_if_symbol_exists(libAtomVM O_SEARCH "fcntl.h" PRIVATE HAVE_O_SEARCH)
define_if_symbol_exists(libAtomVM O_TTY_INIT "fcntl.h" PRIVATE HAVE_O_TTY_INIT)
define_if_symbol_exists(libAtomVM clock_settime "time.h" PRIVATE HAVE_CLOCK_SETTIME)
define_if_symbol_exists(libAtomVM settimeofday "sys/time.h" PRIVATE HAVE_SETTIMEOFDAY)
define_if_symbol_exists(libAtomVM socket "sys/socket.h" PUBLIC HAVE_SOCKET)
define_if_symbol_exists(libAtomVM select "sys/select.h" PUBLIC HAVE_SELECT)

if (AVM_USE_32BIT_FLOAT)
target_compile_definitions(libAtomVM PUBLIC AVM_USE_SINGLE_PRECISION)
Expand Down
Loading

0 comments on commit 41b784d

Please sign in to comment.