From 30e5b7d1627128185c399038a23d3b8fe860bb68 Mon Sep 17 00:00:00 2001 From: williamckha Date: Tue, 16 Jul 2024 11:37:43 +0200 Subject: [PATCH] Merge #3235 - Specify network interface in thunderscope --- docs/getting-started.md | 20 +- src/proto/parameters.proto | 15 + .../embedded/services/network/network.cpp | 19 +- .../embedded/services/network/network.h | 4 +- src/software/embedded/thunderloop.cpp | 10 +- src/software/logger/network_sink.cpp | 10 +- src/software/logger/plotjuggler_sink.cpp | 11 +- src/software/logger/plotjuggler_sink.h | 7 +- src/software/network_log_listener_main.cpp | 9 +- src/software/networking/udp/BUILD | 52 +++ src/software/networking/udp/network_utils.cpp | 44 +++ src/software/networking/udp/network_utils.h | 30 ++ .../networking/udp/network_utils_test.cpp | 43 +++ .../networking/udp/proto_udp_listener.hpp | 126 ++++++-- .../udp/threaded_proto_udp_listener.hpp | 33 +- .../udp/threaded_proto_udp_listener_test.cpp | 30 ++ .../udp/threaded_proto_udp_sender.hpp | 14 +- .../udp/threaded_proto_udp_sender_test.cpp | 21 ++ .../networking/udp/threaded_udp_sender.cpp | 6 +- .../networking/udp/threaded_udp_sender.h | 10 +- src/software/networking/udp/udp_sender.cpp | 43 ++- src/software/networking/udp/udp_sender.h | 22 +- src/software/python_bindings.cpp | 33 +- src/software/thunderscope/BUILD | 1 + .../game_controller.py | 15 +- src/software/thunderscope/common/BUILD | 3 + .../common/proto_configuration_widget.py | 9 +- .../common/proto_parameter_tree_util.py | 39 ++- src/software/thunderscope/requirements.txt | 2 + .../thunderscope/robot_communication.py | 300 ++++++++++++++---- .../thunderscope/thunderscope_main.py | 26 +- .../thunderscope/widget_setup_functions.py | 3 + 32 files changed, 859 insertions(+), 151 deletions(-) create mode 100644 src/software/networking/udp/network_utils.cpp create mode 100644 src/software/networking/udp/network_utils.h create mode 100644 src/software/networking/udp/network_utils_test.cpp create mode 100644 src/software/networking/udp/threaded_proto_udp_listener_test.cpp create mode 100644 src/software/networking/udp/threaded_proto_udp_sender_test.cpp diff --git a/docs/getting-started.md b/docs/getting-started.md index 74940a54ac..ef46628921 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -224,13 +224,11 @@ Now that you're setup, if you can run it on the command line, you can run it in - If we want to run it with real robots: - Open your terminal, `cd` into `Software/src` and run `ifconfig`. - - Pick the network interface you would like to use: - 1. If you are running things locally, you can pick any interface that is not `lo` - 2. If you would like to communicate with robots on the network, make sure to select the interface that is connected to the same network as the robots. + - Pick the network interface you would like to use. If you would like to communicate with robots on the network, make sure to select the interface that is connected to the same network as the robots. - For example, on a sample machine, the output may look like this: ``` - enp0s5: flags=4163 mtu 1500 + wlp3s0: flags=4163 mtu 1500 ... [omitted] ... @@ -241,22 +239,30 @@ Now that you're setup, if you can run it on the command line, you can run it in ... ``` - - An appropriate interface we could choose is `enp0s5` + - An appropriate interface we could choose is `wlp3s0` + - Hint: If you are using a wired connection, the interface will likely start with `e-`. If you are using a WiFi connection, the interface will likely start with `w-`. - If we are running the AI as "blue": `./tbots.py run thunderscope_main --interface=[interface_here] --run_blue` - If we are running the AI as "yellow": `./tbots.py run thunderscope_main --interface=[interface_here] --run_yellow` - `[interface_here]` corresponds to the `ifconfig` interfaces seen in the previous step - - For instance, a call to run the AI as blue on wifi could be: `./tbots.py run thunderscope_main --interface=enp0s5 --run_blue` + - For instance, a call to run the AI as blue on WiFi could be: `./tbots.py run thunderscope_main --interface=wlp3s0 --run_blue`. This will start Thunderscope and set up communication with robots over the wifi interface. It will also listen for referee and vision messages on the same interface. + - **Note: You do not need to include the `--interface=[interface_here]` argument!** You can run Thunderscope without it and use the dynamic configuration widget to set the interfaces for communication to send and receive robot, vision and referee messages. + - If you choose to include `--interface=[interface_here]` argument, Thunderscope will listen for and send robot messages on this port as well as receive vision and referee messages. + - Using the dynamic configuration widget is recommended at Robocup. To reduce latencies, it is recommended to connect the robot router to the AI computer via ethernet and use a separate ethernet connection to receive vision and referee messages. In this configuration, Thunderscope will need to bind to two different interfaces, each likely starting with a "e-". + - If you have specified `--run_blue` or `--run_yellow`, navigate to the "Parameters" widget. In "ai_config" > "ai_control_config" > "network_config", you can set the appropriate interface using the dropdowns for robot, vision and referee message communication. - This command will set up robot communication and the Unix full system binary context manager. The Unix full system context manager hooks up our AI, Backend and SensorFusion 2. Run AI along with Robot Diagnostics: - The Mechanical and Electrical sub-teams use Robot Diagnostics to test specific parts of the Robot. - If we want to run with one AI and Diagnostics - - `./tbots.py run thunderscope_main [--run_blue | --run_yellow] --run_diagnostics` will start Thunderscope + - `./tbots.py run thunderscope_main [--run_blue | --run_yellow] --run_diagnostics --interface=[interface_here]` will start Thunderscope - `[--run_blue | --run_yellow]` indicate which FullSystem to run - `--run_diagnostics` indicates if diagnostics should be loaded as well - Initially, the robots are all connected to the AI and only receive input from it - To change the input source for the robot, use the drop-down menu of that robot to change it between None, AI, and Manual - None means the robots are receiving no commands - More info about Manual control below + - `--interface=[interface_here]` corresponds to the `ifconfig` interfaces seen in the previous step + - For instance, a call to run the AI as blue on WiFi could be: `./tbots.py run thunderscope_main --interface=wlp3s0 --run_blue --run_diagnostics` + - The `--interface` flag is optional. If you do not include it, you can set the interface in the dynamic configuration widget. See above for how to set the interface in the dynamic configuration widget. 3. Run only Diagnostics - To run just Diagnostics - `./tbots.py run thunderscope --run_diagnostics --interface ` diff --git a/src/proto/parameters.proto b/src/proto/parameters.proto index c4551bd1ed..2ced406294 100644 --- a/src/proto/parameters.proto +++ b/src/proto/parameters.proto @@ -56,6 +56,9 @@ message AiControlConfig // Override the existing play with the Play enum provided required PlayName override_ai_play = 2 [default = UseAiSelection]; + + // Interfaces for various network listeners + required NetworkConfig network_config = 3; } message AiParameterConfig @@ -719,6 +722,18 @@ message PossessionTrackerConfig ]; } +message NetworkConfig +{ + // The robot communication interface + required string robot_communication_interface = 1 [default = "lo"]; + + // The referee interface + required string referee_interface = 2 [default = "lo"]; + + // The vision interface + required string vision_interface = 3 [default = "lo"]; +} + message CreaseDefenderConfig { // The additional buffer length for each side of the goal diff --git a/src/software/embedded/services/network/network.cpp b/src/software/embedded/services/network/network.cpp index 2dfe4ae8bb..707cde7a04 100644 --- a/src/software/embedded/services/network/network.cpp +++ b/src/software/embedded/services/network/network.cpp @@ -2,16 +2,27 @@ NetworkService::NetworkService(const std::string& ip_address, unsigned short primitive_listener_port, - unsigned short robot_status_sender_port, bool multicast) + unsigned short robot_status_sender_port, + const std::string& interface, bool multicast) : primitive_tracker(ProtoTracker("primitive set")) { + std::optional error; sender = std::make_unique>( - ip_address, robot_status_sender_port, multicast); + ip_address, robot_status_sender_port, interface, multicast, error); + if (error) + { + LOG(FATAL) << *error; + } udp_listener_primitive_set = std::make_unique>( - ip_address, primitive_listener_port, - boost::bind(&NetworkService::primitiveSetCallback, this, _1), multicast); + ip_address, primitive_listener_port, interface, + boost::bind(&NetworkService::primitiveSetCallback, this, _1), multicast, + error); + if (error) + { + LOG(FATAL) << *error; + } radio_listener_primitive_set = std::make_unique>( diff --git a/src/software/embedded/services/network/network.h b/src/software/embedded/services/network/network.h index b136382757..295fae8477 100644 --- a/src/software/embedded/services/network/network.h +++ b/src/software/embedded/services/network/network.h @@ -24,11 +24,13 @@ class NetworkService * @param ip_address The IP Address the service should connect to * @param primitive_listener_port The port to listen for primitive protos * @param robot_status_sender_port The port to send robot status + * @param interface the interface to listen and send on * @param multicast If true, then the provided IP address is a multicast address and * we should join the group */ NetworkService(const std::string& ip_address, unsigned short primitive_listener_port, - unsigned short robot_status_sender_port, bool multicast); + unsigned short robot_status_sender_port, const std::string& interface, + bool multicast); /** * When the network service is polled, it sends the robot_status and returns diff --git a/src/software/embedded/thunderloop.cpp b/src/software/embedded/thunderloop.cpp index 1516486cca..f7111d2942 100644 --- a/src/software/embedded/thunderloop.cpp +++ b/src/software/embedded/thunderloop.cpp @@ -57,10 +57,10 @@ extern "C" crash_msg.set_exit_signal(g3::signalToStr(signal_num)); *(crash_msg.mutable_status()) = *robot_status; + std::optional error; auto sender = std::make_unique>( - std::string(ROBOT_MULTICAST_CHANNELS.at(channel_id)) + "%" + - network_interface, - ROBOT_CRASH_PORT, true); + std::string(ROBOT_MULTICAST_CHANNELS.at(channel_id)), ROBOT_CRASH_PORT, + network_interface, true, error); sender->sendProto(crash_msg); std::cerr << "Broadcasting robot crash msg"; @@ -108,8 +108,8 @@ Thunderloop::Thunderloop(const RobotConstants_t& robot_constants, bool enable_lo << "THUNDERLOOP: Network Logger initialized! Next initializing Network Service"; network_service_ = std::make_unique( - std::string(ROBOT_MULTICAST_CHANNELS.at(channel_id_)) + "%" + network_interface_, - PRIMITIVE_PORT, ROBOT_STATUS_PORT, true); + std::string(ROBOT_MULTICAST_CHANNELS.at(channel_id_)), PRIMITIVE_PORT, + ROBOT_STATUS_PORT, network_interface, true); LOG(INFO) << "THUNDERLOOP: Network Service initialized! Next initializing Power Service"; diff --git a/src/software/logger/network_sink.cpp b/src/software/logger/network_sink.cpp index 1b72097201..742c92dc13 100644 --- a/src/software/logger/network_sink.cpp +++ b/src/software/logger/network_sink.cpp @@ -11,9 +11,15 @@ NetworkSink::NetworkSink(unsigned int channel, const std::string& interface, int bool enable_log_merging) : robot_id(robot_id), log_merger(LogMerger(enable_log_merging)) { + std::optional error; log_output.reset(new ThreadedProtoUdpSender( - std::string(ROBOT_MULTICAST_CHANNELS.at(channel)) + "%" + interface, - ROBOT_LOGS_PORT, true)); + std::string(ROBOT_MULTICAST_CHANNELS.at(channel)), ROBOT_LOGS_PORT, interface, + true, error)); + if (error) + { + std::cerr << error.value() << std::endl; + std::terminate(); + } } void NetworkSink::sendToNetwork(g3::LogMessageMover log_entry) diff --git a/src/software/logger/plotjuggler_sink.cpp b/src/software/logger/plotjuggler_sink.cpp index bd1099e94c..ea0c6f7e9e 100644 --- a/src/software/logger/plotjuggler_sink.cpp +++ b/src/software/logger/plotjuggler_sink.cpp @@ -1,13 +1,18 @@ - #include "software/logger/plotjuggler_sink.h" #include #include "shared/constants.h" -PlotJugglerSink::PlotJugglerSink() - : udp_sender(PLOTJUGGLER_GUI_DEFAULT_HOST, PLOTJUGGLER_GUI_DEFAULT_PORT, false) +PlotJugglerSink::PlotJugglerSink(const std::string& interface) + : udp_sender(PLOTJUGGLER_GUI_DEFAULT_HOST, PLOTJUGGLER_GUI_DEFAULT_PORT, interface, + false, error) { + if (error.has_value()) + { + std::cerr << "Error setting up UDP sender for PlotJugglerSink: " << error.value(); + std::terminate(); + } } void PlotJugglerSink::sendToPlotJuggler(g3::LogMessageMover log_entry) diff --git a/src/software/logger/plotjuggler_sink.h b/src/software/logger/plotjuggler_sink.h index b4ab754fed..ff40a4be47 100644 --- a/src/software/logger/plotjuggler_sink.h +++ b/src/software/logger/plotjuggler_sink.h @@ -16,8 +16,10 @@ class PlotJugglerSink public: /** * Creates a PlotJugglerSink that sends udp packets to the PlotJuggler server + * + * @param interface The interface to send Plotjuggler UDP packets on */ - PlotJugglerSink(); + PlotJugglerSink(const std::string& interface = "lo"); ~PlotJugglerSink() = default; @@ -30,6 +32,9 @@ class PlotJugglerSink void sendToPlotJuggler(g3::LogMessageMover log_entry); private: + // Any error that occurs during the creation of the UDP sender will be stored here + std::optional error; + ThreadedUdpSender udp_sender; }; diff --git a/src/software/network_log_listener_main.cpp b/src/software/network_log_listener_main.cpp index b762e63ef1..55edb5e92e 100644 --- a/src/software/network_log_listener_main.cpp +++ b/src/software/network_log_listener_main.cpp @@ -93,9 +93,14 @@ int main(int argc, char **argv) logFromNetworking(log); }; + std::optional error; auto log_input = std::make_unique>( - std::string(ROBOT_MULTICAST_CHANNELS.at(args.channel)) + "%" + args.interface, - ROBOT_LOGS_PORT, robot_log_callback, true); + std::string(ROBOT_MULTICAST_CHANNELS.at(args.channel)), ROBOT_LOGS_PORT, + args.interface, robot_log_callback, true, error); + if (error) + { + LOG(FATAL) << *error; + } LOG(INFO) << "Network logger listening on channel " diff --git a/src/software/networking/udp/BUILD b/src/software/networking/udp/BUILD index e84149b344..37513cdc2e 100644 --- a/src/software/networking/udp/BUILD +++ b/src/software/networking/udp/BUILD @@ -1,5 +1,26 @@ package(default_visibility = ["//visibility:public"]) +cc_library( + name = "network_utils", + srcs = [ + "network_utils.cpp", + ], + hdrs = [ + "network_utils.h", + ], +) + +cc_test( + name = "network_utils_test", + srcs = [ + "network_utils_test.cpp", + ], + deps = [ + ":network_utils", + "//shared/test_util:tbots_gtest_main", + ], +) + cc_library( name = "proto_udp_listener", hdrs = [ @@ -7,6 +28,7 @@ cc_library( ], visibility = ["//visibility:private"], deps = [ + ":network_utils", "//software/logger", "//software/util/typename", ], @@ -22,6 +44,7 @@ cc_library( ], visibility = ["//visibility:private"], deps = [ + ":network_utils", "@boost//:asio", ], ) @@ -37,6 +60,17 @@ cc_library( ], ) +cc_test( + name = "threaded_proto_udp_listener_test", + srcs = [ + "threaded_proto_udp_listener_test.cpp", + ], + deps = [ + ":threaded_proto_udp_listener", + "//shared/test_util:tbots_gtest_main", + ], +) + cc_library( name = "threaded_proto_udp_sender", hdrs = [ @@ -47,6 +81,17 @@ cc_library( ], ) +cc_test( + name = "threaded_proto_udp_sender_test", + srcs = [ + "threaded_proto_udp_sender_test.cpp", + ], + deps = [ + ":threaded_proto_udp_sender", + "//shared/test_util:tbots_gtest_main", + ], +) + cc_library( name = "threaded_udp_sender", srcs = [ @@ -59,3 +104,10 @@ cc_library( ":udp_sender", ], ) + +cc_library( + name = "udp_network_factory", + hdrs = [ + "udp_network_factory.hpp", + ], +) diff --git a/src/software/networking/udp/network_utils.cpp b/src/software/networking/udp/network_utils.cpp new file mode 100644 index 0000000000..88a47e7772 --- /dev/null +++ b/src/software/networking/udp/network_utils.cpp @@ -0,0 +1,44 @@ +#include "software/networking/udp/network_utils.h" + +#include + +bool getLocalIp(const std::string& interface, std::string& ip_address, bool ipv4) +{ + struct ifaddrs* ifAddrStruct = nullptr; + struct ifaddrs* ifa = nullptr; + + getifaddrs(&ifAddrStruct); + + for (ifa = ifAddrStruct; ifa != nullptr; ifa = ifa->ifa_next) + { + if (ifa->ifa_name == interface) + { + if (ipv4 && ifa->ifa_addr->sa_family == AF_INET) + { + char addressBuffer[INET_ADDRSTRLEN]; + struct sockaddr_in* sa = (struct sockaddr_in*)ifa->ifa_addr; + inet_ntop(AF_INET, &sa->sin_addr, addressBuffer, INET_ADDRSTRLEN); + freeifaddrs(ifAddrStruct); + ip_address = addressBuffer; + return true; + } + else if (!ipv4 && ifa->ifa_addr->sa_family == AF_INET6) + { + char addressBuffer[INET6_ADDRSTRLEN]; + struct sockaddr_in6* sa = (struct sockaddr_in6*)ifa->ifa_addr; + inet_ntop(AF_INET6, &sa->sin6_addr, addressBuffer, INET6_ADDRSTRLEN); + freeifaddrs(ifAddrStruct); + ip_address = addressBuffer; + return true; + } + } + } + + return false; +} + +bool isIpv6(const std::string& ip_address) +{ + struct sockaddr_in6 sa; + return inet_pton(AF_INET6, ip_address.c_str(), &(sa.sin6_addr)) != 0; +} diff --git a/src/software/networking/udp/network_utils.h b/src/software/networking/udp/network_utils.h new file mode 100644 index 0000000000..2d6744b70a --- /dev/null +++ b/src/software/networking/udp/network_utils.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include + +/** + * Given an interface, get the IP address associated with that interface + * + * The modified ip_address is valid only if the function returns true + * + * @param interface The interface to get the IP address from + * @param ip_address A reference to the std::string that will store the IP address if + * found + * @param ipv4 If true, get the IPv4 address, otherwise get the IPv6 address + * + * @return true if the IP address was found, false otherwise + */ +bool getLocalIp(const std::string& interface, std::string& ip_address, bool ipv4 = true); + +/** + * Check if the given string follows the IPv6 address format + * + * Addresses that are actually an "embedded IPv4 address" are still considered as an IPv6 + * address since it follows the IPv6 address format + * + * @param ip_address The string to check + * @return true if the string is a valid IPv6 address, false otherwise + */ +bool isIpv6(const std::string& ip_address); diff --git a/src/software/networking/udp/network_utils_test.cpp b/src/software/networking/udp/network_utils_test.cpp new file mode 100644 index 0000000000..dff9fc2ad2 --- /dev/null +++ b/src/software/networking/udp/network_utils_test.cpp @@ -0,0 +1,43 @@ +#include "software/networking/udp/network_utils.h" + +#include + +TEST(NetworkUtilsTest, getLocalIpValidInterface) +{ + std::string interface = "lo"; + std::string ip_address; + EXPECT_TRUE(getLocalIp(interface, ip_address, true)); + EXPECT_EQ(ip_address, "127.0.0.1"); +} + +TEST(NetworkUtilsTest, getLocalIpInvalidInterface) +{ + std::string interface = "interfaceymcinterfaceface"; + std::string ip_address; + EXPECT_FALSE(getLocalIp(interface, ip_address, true)); +} + +TEST(NetworkUtilsTest, isIpv6Valid) +{ + std::string ip_address = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + EXPECT_TRUE(isIpv6(ip_address)); +} + +TEST(NetworkUtilsTest, isIpv6ForIpv4) +{ + std::string ip_address = "127.0.0.1"; + EXPECT_FALSE(isIpv6(ip_address)); +} + +TEST(NetworkUtilsTest, isIpv6ForIpv4Mapped) +{ + // This is actually an IPv4 address mapped to an IPv6 address + std::string ip_address = "::ffff:0:0"; + EXPECT_TRUE(isIpv6(ip_address)); +} + +TEST(NetworkUtilsTest, isIpv6ForLoopback) +{ + std::string ip_address = "::1"; + EXPECT_TRUE(isIpv6(ip_address)); +} diff --git a/src/software/networking/udp/proto_udp_listener.hpp b/src/software/networking/udp/proto_udp_listener.hpp index 7c3b9b11c5..391d31e7b6 100644 --- a/src/software/networking/udp/proto_udp_listener.hpp +++ b/src/software/networking/udp/proto_udp_listener.hpp @@ -8,6 +8,7 @@ #include #include "software/logger/logger.h" +#include "software/networking/udp/network_utils.h" #include "software/networking/udp/proto_udp_listener.hpp" #include "software/util/typename/typename.h" @@ -21,19 +22,23 @@ class ProtoUdpListener * ReceiveProtoT packet received, the receive_callback will be called to perform any * operations desired by the caller * + * The caller must check that the error is not set before using the listener + * * @param io_service The io_service to use to service incoming ReceiveProtoT data * @param ip_address The ip address of on which to listen for the given ReceiveProtoT * packets (IPv4 in dotted decimal or IPv6 in hex string) example IPv4: 192.168.0.2 - * example IPv6: ff02::c3d0:42d2:bb8%wlp4s0 (the interface is specified after %) + * example IPv6: ff02::c3d0:42d2:bb8 * @param port The port on which to listen for ReceiveProtoT packets + * @param listen_interface The interface to listen on * @param receive_callback The function to run for every ReceiveProtoT packet received * from the network * @param multicast If true, joins the multicast group of given ip_address + * @param error A user-provided optional string to store any error messages */ ProtoUdpListener(boost::asio::io_service& io_service, const std::string& ip_address, - unsigned short port, - std::function receive_callback, - bool multicast); + unsigned short port, const std::string& listen_interface, + std::function receive_callback, bool multicast, + std::optional& error); /** * Creates an ProtoUdpListener that will listen for ReceiveProtoT packets from @@ -41,13 +46,17 @@ class ProtoUdpListener * received, the receive_callback will be called to perform any operations desired by * the caller * + * The caller must check that the error is not set before using the listener + * * @param io_service The io_service to use to service incoming ReceiveProtoT data * @param port The port on which to listen for ReceiveProtoT packets * @param receive_callback The function to run for every ReceiveProtoT packet received * from the network + * @param error A user-provided optional string to store any error messages */ ProtoUdpListener(boost::asio::io_service& io_service, unsigned short port, - std::function receive_callback); + std::function receive_callback, + std::optional& error); /** * Closes the socket associated to the UDP listener */ @@ -66,6 +75,19 @@ class ProtoUdpListener void handleDataReception(const boost::system::error_code& error, size_t num_bytes_received); + /** + * Sets up multicast for the given ip_address and listen_interface + * + * Any errors during setup will be stored in the error string + * + * @param ip_address The ip address of the multicast group to join + * @param listen_interface The interface to listen on + * @param error A user-provided optional string to store any error messages + */ + void setupMulticast(const boost::asio::ip::address& ip_address, + const std::string& listen_interface, + std::optional& error); + /** * Start listening for data */ @@ -89,12 +111,17 @@ class ProtoUdpListener template ProtoUdpListener::ProtoUdpListener( boost::asio::io_service& io_service, const std::string& ip_address, - const unsigned short port, std::function receive_callback, - bool multicast) + const unsigned short port, const std::string& listen_interface, + std::function receive_callback, bool multicast, + std::optional& error) : socket_(io_service), receive_callback(receive_callback) { - boost::asio::ip::udp::endpoint listen_endpoint( - boost::asio::ip::make_address(ip_address), port); + boost::asio::ip::address boost_ip = boost::asio::ip::make_address(ip_address); + if (isIpv6(ip_address)) + { + boost_ip = boost::asio::ip::make_address(ip_address + "%" + listen_interface); + } + boost::asio::ip::udp::endpoint listen_endpoint(boost_ip, port); socket_.open(listen_endpoint.protocol()); socket_.set_option(boost::asio::socket_base::reuse_address(true)); try @@ -103,19 +130,24 @@ ProtoUdpListener::ProtoUdpListener( } catch (const boost::exception& ex) { - LOG(FATAL) << "UdpListener: There was an issue binding the socket to " - "the listen_endpoint when trying to connect to the " - "address. This may be due to another instance of the " - "UdpListener running and using the port already. " - "(ip = " - << ip_address << ", port = " << port << ")" << std::endl; + std::stringstream ss; + ss << "UdpListener: There was an issue binding the socket to " + "the listen_endpoint when trying to connect to the " + "address. This may be due to another instance of the " + "UdpListener running and using the port already. " + "(ip = " + << ip_address << ", port = " << port << ")"; + error = ss.str(); + return; } if (multicast) { - // Join the multicast group. - socket_.set_option(boost::asio::ip::multicast::join_group( - boost::asio::ip::address::from_string(ip_address))); + setupMulticast(boost_ip, listen_interface, error); + if (error) + { + return; + } } startListen(); @@ -124,7 +156,8 @@ ProtoUdpListener::ProtoUdpListener( template ProtoUdpListener::ProtoUdpListener( boost::asio::io_service& io_service, const unsigned short port, - std::function receive_callback) + std::function receive_callback, + std::optional& error) : socket_(io_service), receive_callback(receive_callback) { boost::asio::ip::udp::endpoint listen_endpoint(boost::asio::ip::udp::v6(), port); @@ -137,12 +170,15 @@ ProtoUdpListener::ProtoUdpListener( } catch (const boost::exception& ex) { - LOG(FATAL) << "UdpListener: There was an issue binding the socket to " - "the listen_endpoint when trying to connect to the " - "address. This may be due to another instance of the " - "UdpListener running and using the port already. " - "(port = " - << port << ")" << std::endl; + std::stringstream ss; + ss << "UdpListener: There was an issue binding the socket to " + "the listen_endpoint when trying to connect to the " + "address. This may be due to another instance of the " + "UdpListener running and using the port already. " + "(port = " + << port << ")"; + error = ss.str(); + return; } startListen(); @@ -181,13 +217,18 @@ void ProtoUdpListener::handleDataReception( } else { - // Start listening again to receive the next data - startListen(); - LOG(WARNING) << "An unknown network error occurred when attempting to receive " << TYPENAME(ReceiveProtoT) << " Data. The boost system error is: " << error.message() << std::endl; + + if (!running_) + { + return; + } + + // Start listening again to receive the next data + startListen(); } if (num_bytes_received > MAX_BUFFER_LENGTH) @@ -199,10 +240,33 @@ void ProtoUdpListener::handleDataReception( } } +template +void ProtoUdpListener::setupMulticast( + const boost::asio::ip::address& ip_address, const std::string& listen_interface, + std::optional& error) +{ + if (ip_address.is_v4()) + { + std::string interface_ip; + if (!getLocalIp(listen_interface, interface_ip)) + { + std::stringstream ss; + ss << "Could not find the local ip address for the given interface: " + << listen_interface << std::endl; + error = ss.str(); + return; + } + socket_.set_option(boost::asio::ip::multicast::join_group( + ip_address.to_v4(), + boost::asio::ip::address::from_string(interface_ip).to_v4())); + return; + } + socket_.set_option(boost::asio::ip::multicast::join_group(ip_address)); +} + template ProtoUdpListener::~ProtoUdpListener() { - close(); } template @@ -217,8 +281,8 @@ void ProtoUdpListener::close() { LOG(WARNING) << "An unknown network error occurred when attempting to shutdown UDP socket for " - << TYPENAME(ReceiveProtoT) - << ". The boost system error is: " << error_code.message() << std::endl; + << TYPENAME(ReceiveProtoT) << ". The boost system error is: " << error_code + << ": " << error_code.message() << std::endl; } socket_.close(error_code); diff --git a/src/software/networking/udp/threaded_proto_udp_listener.hpp b/src/software/networking/udp/threaded_proto_udp_listener.hpp index 674819ee0f..caecef4025 100644 --- a/src/software/networking/udp/threaded_proto_udp_listener.hpp +++ b/src/software/networking/udp/threaded_proto_udp_listener.hpp @@ -15,17 +15,24 @@ class ThreadedProtoUdpListener * ReceiveProtoT packet received, the receive_callback will be called to perform any * operations desired by the caller. * + * Any caller using this constructor should ensure that error is not set before using + * the listener. + * * @param ip_address The ip address on which to listen for the given ReceiveProtoT * packets (IPv4 in dotted decimal or IPv6 in hex string) example IPv4: 192.168.0.2 - * example IPv6: ff02::c3d0:42d2:bb8%wlp4s0 (the interface is specified after %) + * example IPv6: ff02::c3d0:42d2:bb8%wlp4s0 * @param port The port on which to listen for ReceiveProtoT packets + * @param interface The interface on which to listen for ReceiveProtoT packets * @param receive_callback The function to run for every ReceiveProtoT packet received * from the network * @param multicast If true, joins the multicast group of given ip_address + * @param error A user-provided optional string to store any error messages */ ThreadedProtoUdpListener(const std::string& ip_address, unsigned short port, + const std::string& interface, std::function receive_callback, - bool multicast); + bool multicast, + std::optional& error = std::nullopt); /** * Creates a ThreadedProtoUdpListener that will listen for ReceiveProtoT packets @@ -33,12 +40,18 @@ class ThreadedProtoUdpListener * packet received, the receive_callback will be called to perform any operations * desired by the caller. * + * Any caller using this constructor should ensure that error is not set before using + * the listener. + * * @param port The port on which to listen for ReceiveProtoT packets + * @param interface The interface on which to listen for ReceiveProtoT packets * @param receive_callback The function to run for every ReceiveProtoT packet received * from the network + * @param error A user-provided optional string to store any error messages */ - ThreadedProtoUdpListener(unsigned short port, - std::function receive_callback); + ThreadedProtoUdpListener(unsigned short port, const std::string& interface, + std::function receive_callback, + std::optional& error = std::nullopt); /** * Closes the socket and stops the IO service thread @@ -61,9 +74,11 @@ class ThreadedProtoUdpListener template ThreadedProtoUdpListener::ThreadedProtoUdpListener( const std::string& ip_address, const unsigned short port, - std::function receive_callback, bool multicast) + const std::string& interface, std::function receive_callback, + bool multicast, std::optional& error) : io_service(), - udp_listener(io_service, ip_address, port, receive_callback, multicast) + udp_listener(io_service, ip_address, port, interface, receive_callback, multicast, + error) { // start the thread to run the io_service in the background io_service_thread = std::thread([this]() { io_service.run(); }); @@ -71,8 +86,10 @@ ThreadedProtoUdpListener::ThreadedProtoUdpListener( template ThreadedProtoUdpListener::ThreadedProtoUdpListener( - const unsigned short port, std::function receive_callback) - : io_service(), udp_listener(io_service, port, receive_callback) + const unsigned short port, const std::string& interface, + std::function receive_callback, + std::optional& error) + : io_service(), udp_listener(io_service, port, interface, receive_callback, error) { // start the thread to run the io_service in the background io_service_thread = std::thread([this]() { io_service.run(); }); diff --git a/src/software/networking/udp/threaded_proto_udp_listener_test.cpp b/src/software/networking/udp/threaded_proto_udp_listener_test.cpp new file mode 100644 index 0000000000..68aa89921f --- /dev/null +++ b/src/software/networking/udp/threaded_proto_udp_listener_test.cpp @@ -0,0 +1,30 @@ +#include "software/networking/udp/threaded_proto_udp_listener.hpp" + +#include + +#include "google/protobuf/empty.pb.h" + +TEST(ThreadedProtoUdpListenerTest, error_finding_local_ip_address) +{ + std::optional error; + ThreadedProtoUdpListener( + "224.5.23.1", 40000, "interfacemcinterfaceface", [](const auto&) {}, true, error); + EXPECT_TRUE(error.has_value()); +} + +TEST(ThreadedProtoUdpListenerTest, error_creating_socket) +{ + std::optional error; + // This will always fail because it requires root privileges to open this port + ThreadedProtoUdpListener( + "224.5.23.1", 1023, "lo", [](const auto&) {}, true, error); + EXPECT_TRUE(error.has_value()); +} + +TEST(ThreadedProtoUdpListenerTest, no_error_creating_socket) +{ + std::optional error; + ThreadedProtoUdpListener( + "224.5.23.0", 40000, "lo", [](const auto&) {}, true, error); + EXPECT_FALSE(error.has_value()); +} diff --git a/src/software/networking/udp/threaded_proto_udp_sender.hpp b/src/software/networking/udp/threaded_proto_udp_sender.hpp index 477cdeabcd..dd67dd9d2d 100644 --- a/src/software/networking/udp/threaded_proto_udp_sender.hpp +++ b/src/software/networking/udp/threaded_proto_udp_sender.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "software/networking/udp/threaded_udp_sender.h" @@ -14,16 +15,23 @@ class ThreadedProtoUdpSender : private ThreadedUdpSender * Creates a UdpSender that sends the SendProto over the network on the * given address and port. * + * Any callers must check the error string to see if the initialization was successful + * before using the object. + * * @param ip_address The ip address to send data on * (IPv4 in dotted decimal or IPv6 in hex string) * example IPv4: 192.168.0.2 - * example IPv6: ff02::c3d0:42d2:bb8%wlp4s0 (the interface is specified after %) + * example IPv6: ff02::c3d0:42d2:bb8 * @param port The port to send SendProto data on + * @param interface The interface to send data on * @param multicast If true, joins the multicast group of given ip_address + * @param error An optional user-provided string that will be set to an error message + * if an error occurs */ ThreadedProtoUdpSender(const std::string& ip_address, unsigned short port, - bool multicast) - : ThreadedUdpSender(ip_address, port, multicast) + const std::string& interface, bool multicast, + std::optional& error) + : ThreadedUdpSender(ip_address, port, interface, multicast, error) { } diff --git a/src/software/networking/udp/threaded_proto_udp_sender_test.cpp b/src/software/networking/udp/threaded_proto_udp_sender_test.cpp new file mode 100644 index 0000000000..12dfc4ac7d --- /dev/null +++ b/src/software/networking/udp/threaded_proto_udp_sender_test.cpp @@ -0,0 +1,21 @@ +#include "software/networking/udp/threaded_proto_udp_sender.hpp" + +#include + +#include "google/protobuf/empty.pb.h" + +TEST(ThreadedProtoUdpSenderTest, error_finding_local_ip_address) +{ + std::optional error; + ThreadedProtoUdpSender( + "224.5.23.1", 40000, "interfacemcinterfaceface", true, error); + EXPECT_TRUE(error.has_value()); +} + +TEST(ThreadedProtoUdpSenderTest, no_error_creating_socket) +{ + std::optional error; + ThreadedProtoUdpSender("224.5.23.1", 40000, "lo", true, + error); + EXPECT_FALSE(error.has_value()); +} diff --git a/src/software/networking/udp/threaded_udp_sender.cpp b/src/software/networking/udp/threaded_udp_sender.cpp index 93e4d2d027..4441a9752a 100644 --- a/src/software/networking/udp/threaded_udp_sender.cpp +++ b/src/software/networking/udp/threaded_udp_sender.cpp @@ -1,9 +1,11 @@ #include "software/networking/udp/threaded_udp_sender.h" ThreadedUdpSender::ThreadedUdpSender(const std::string& ip_address, - const unsigned short port, bool multicast) + const unsigned short port, + const std::string& interface, bool multicast, + std::optional& error) : io_service(), - udp_sender(io_service, ip_address, port, multicast), + udp_sender(io_service, ip_address, port, interface, multicast, error), io_service_thread([this]() { io_service.run(); }) { } diff --git a/src/software/networking/udp/threaded_udp_sender.h b/src/software/networking/udp/threaded_udp_sender.h index 31c95981ec..8163a67055 100644 --- a/src/software/networking/udp/threaded_udp_sender.h +++ b/src/software/networking/udp/threaded_udp_sender.h @@ -13,14 +13,22 @@ class ThreadedUdpSender * Creates a UdpSender that sends the sendString over the network on the * given address and port. * + * All callers must check the error string to see if the initialization was successful + * before using the object. + * * @param ip_address The ip address to send data on * (IPv4 in dotted decimal or IPv6 in hex string) * example IPv4: 192.168.0.2 * example IPv6: ff02::c3d0:42d2:bb8%wlp4s0 (the interface is specified after %) * @param port The port to send sendString data on + * @param interface The interface to send data on * @param multicast If true, joins the multicast group of given ip_address + * @param error An optional user-provided string that will be set to an error message + * if an error occurs */ - ThreadedUdpSender(const std::string& ip_address, unsigned short port, bool multicast); + ThreadedUdpSender(const std::string& ip_address, unsigned short port, + const std::string& interface, bool multicast, + std::optional& error); ~ThreadedUdpSender(); diff --git a/src/software/networking/udp/udp_sender.cpp b/src/software/networking/udp/udp_sender.cpp index 83580ecd41..f8be80d5cf 100644 --- a/src/software/networking/udp/udp_sender.cpp +++ b/src/software/networking/udp/udp_sender.cpp @@ -2,20 +2,27 @@ #include +#include "software/networking/udp/network_utils.h" + UdpSender::UdpSender(boost::asio::io_service& io_service, const std::string& ip_address, - const unsigned short port, bool multicast) + const unsigned short port, const std::string& interface, + bool multicast, std::optional& error) : socket_(io_service) { - boost::asio::ip::address addr = boost::asio::ip::make_address(ip_address); + boost::asio::ip::address boost_ip = boost::asio::ip::make_address(ip_address); + if (isIpv6(ip_address)) + { + boost_ip = boost::asio::ip::make_address(ip_address + "%" + interface); + } // The receiver endpoint identifies where this UdpSender will send data to - receiver_endpoint = boost::asio::ip::udp::endpoint(addr, port); + receiver_endpoint = boost::asio::ip::udp::endpoint(boost_ip, port); socket_.open(receiver_endpoint.protocol()); if (multicast) { - socket_.set_option(boost::asio::ip::multicast::join_group(addr)); + setupMulticast(boost_ip, interface, error); } } @@ -24,6 +31,34 @@ void UdpSender::sendString(const std::string& message) socket_.send_to(boost::asio::buffer(message, message.length()), receiver_endpoint); } +void UdpSender::setupMulticast(const boost::asio::ip::address& ip_address, + const std::string& interface, + std::optional& error) +{ + if (ip_address.is_v4()) + { + std::string interface_ip; + if (!getLocalIp(interface, interface_ip)) + { + std::stringstream ss; + ss << "UdpSender: Could not get the local IP address for the interface " + "specified. (interface = " + << interface << ")"; + error = ss.str(); + + return; + } + + socket_.set_option(boost::asio::ip::multicast::join_group( + ip_address.to_v4(), + boost::asio::ip::address::from_string(interface_ip).to_v4())); + return; + } + + socket_.set_option(boost::asio::ip::multicast::join_group(ip_address)); +} + + UdpSender::~UdpSender() { socket_.close(); diff --git a/src/software/networking/udp/udp_sender.h b/src/software/networking/udp/udp_sender.h index 791765f651..b0a8c3abbc 100644 --- a/src/software/networking/udp/udp_sender.h +++ b/src/software/networking/udp/udp_sender.h @@ -11,22 +11,36 @@ class UdpSender * Creates a UdpSender that sends strings over the network to the * given address and port. * - * The UdpSender will be assigned a random available port on creation - * so it doesn't conflict with anything else on the system. + * Callers must ensure that error is not set before using this object. * * @param io_service The io_service to use to service outgoing SendString data * @param ip_address The ip address to send data on * (IPv4 in dotted decimal or IPv6 in hex string) * example IPv4: 192.168.0.2 - * example IPv6: ff02::c3d0:42d2:bb8%wlp4s0 (the interface is specified after %) + * example IPv6: ff02::c3d0:42d2:bb8 * @param port The port to send SendString data to + * @param interface The interface to send data on * @param multicast If true, joins the multicast group of given ip_address + * @param error A user-provided optional string to store any errors that occur */ UdpSender(boost::asio::io_service& io_service, const std::string& ip_address, - unsigned short port, bool multicast); + unsigned short port, const std::string& interface, bool multicast, + std::optional& error); ~UdpSender(); + /** + * Set up multicast for the given multicast ip address and interface + * + * Any errors during setup will be stored in the error string + * + * @param ip_address The multicast ip address to join + * @param interface The interface to join the multicast group on + * @param error A user-provided optional string to store any errors that occur + */ + void setupMulticast(const boost::asio::ip::address& ip_address, + const std::string& interface, std::optional& error); + /** * Sends a string message to the initialized ip address and port * This function returns after the message has been sent. diff --git a/src/software/python_bindings.cpp b/src/software/python_bindings.cpp index 4e38a665a1..c3ce0f2d9d 100644 --- a/src/software/python_bindings.cpp +++ b/src/software/python_bindings.cpp @@ -64,8 +64,22 @@ void declareThreadedProtoUdpSender(py::module& m, std::string name) std::string pyclass_name = name + "ProtoUdpSender"; py::class_>(m, pyclass_name.c_str(), py::buffer_protocol(), py::dynamic_attr()) - .def(py::init()) .def("send_proto", &Class::sendProto); + + std::string create_pyclass_name = "create" + pyclass_name; + m.def(create_pyclass_name.c_str(), + [](const std::string& ip_address, unsigned short port, + const std::string& interface, bool multicast) { + // Pybind doesn't bind references in some cases + // (https://pybind11.readthedocs.io/en/stable/faq.html#limitations-involving-reference-arguments) + std::optional error; + std::shared_ptr sender = + std::make_shared(ip_address, port, interface, multicast, error); + + // Return the sender and the error message to the Python side + // Use as: sender, error = create{name}ProtoUdpSender(...) + return std::make_tuple(sender, error); + }); } /** @@ -98,8 +112,23 @@ void declareThreadedProtoUdpListener(py::module& m, std::string name) std::string pyclass_name = name + "ProtoListener"; py::class_>(m, pyclass_name.c_str(), py::buffer_protocol(), py::dynamic_attr()) - .def(py::init&, bool>()) .def("close", &Class::close); + + std::string create_pyclass_name = "create" + pyclass_name; + m.def(create_pyclass_name.c_str(), + [](const std::string& ip_address, unsigned short port, + const std::string& interface, const std::function& callback, + bool multicast) { + // Pybind doesn't bind references in some cases + // (https://pybind11.readthedocs.io/en/stable/faq.html#limitations-involving-reference-arguments) + std::optional error; + std::shared_ptr listener = std::make_shared( + ip_address, port, interface, callback, multicast, error); + + // Return the listener and the error message to the Python side + // Use as: listener, error = create{name}ProtoListener(...) + return std::make_tuple(listener, error); + }); } template diff --git a/src/software/thunderscope/BUILD b/src/software/thunderscope/BUILD index 0b80295420..4b01818b75 100644 --- a/src/software/thunderscope/BUILD +++ b/src/software/thunderscope/BUILD @@ -156,6 +156,7 @@ py_library( ], deps = [ "//software/thunderscope:constants", + requirement("colorama"), ], ) diff --git a/src/software/thunderscope/binary_context_managers/game_controller.py b/src/software/thunderscope/binary_context_managers/game_controller.py index 0263aeae9c..35f50e7a11 100644 --- a/src/software/thunderscope/binary_context_managers/game_controller.py +++ b/src/software/thunderscope/binary_context_managers/game_controller.py @@ -20,6 +20,8 @@ from software.thunderscope.binary_context_managers.util import * from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer +logger = logging.getLogger(__name__) + class Gamecontroller(object): """Gamecontroller Context Manager""" @@ -192,10 +194,19 @@ def __send_referee_command(data: Referee) -> None: if autoref_proto_unix_io is not None: autoref_proto_unix_io.send_proto(Referee, data) - self.receive_referee_command = tbots_cpp.SSLRefereeProtoListener( - Gamecontroller.REFEREE_IP, self.referee_port, __send_referee_command, True, + self.receive_referee_command, error = tbots_cpp.createSSLRefereeProtoListener( + Gamecontroller.REFEREE_IP, + self.referee_port, + "lo", + __send_referee_command, + True, ) + if error: + logger.error( + "[Gamecontroller] Failed to bind to the referee port and listen to referee messages" + ) + blue_full_system_proto_unix_io.register_observer( ManualGCCommand, self.command_override_buffer ) diff --git a/src/software/thunderscope/common/BUILD b/src/software/thunderscope/common/BUILD index 20c32601f8..e0abd3776d 100644 --- a/src/software/thunderscope/common/BUILD +++ b/src/software/thunderscope/common/BUILD @@ -1,5 +1,7 @@ package(default_visibility = ["//visibility:public"]) +load("@thunderscope_deps//:requirements.bzl", "requirement") + py_library( name = "proto_configuration_widget", srcs = ["proto_configuration_widget.py"], @@ -26,6 +28,7 @@ py_library( srcs = ["proto_parameter_tree_util.py"], deps = [ ":common_widgets", + requirement("netifaces"), ], ) diff --git a/src/software/thunderscope/common/proto_configuration_widget.py b/src/software/thunderscope/common/proto_configuration_widget.py index 236dcb6086..98a53029f8 100644 --- a/src/software/thunderscope/common/proto_configuration_widget.py +++ b/src/software/thunderscope/common/proto_configuration_widget.py @@ -19,6 +19,9 @@ class ProtoConfigurationWidget(QWidget): """ + DELAYED_CONFIGURATION_TIMEOUT_S = 5 + """How long to wait after startup to send the first configuration to our AI""" + def __init__( self, on_change_callback: Callable[[Any, Any, ThunderbotsConfig], None], @@ -76,7 +79,10 @@ def __init__( layout.addWidget(self.search_query) layout.addWidget(self.param_tree) - self.run_onetime_async(3, self.send_proto_to_fullsystem) + self.run_onetime_async( + ProtoConfigurationWidget.DELAYED_CONFIGURATION_TIMEOUT_S, + self.send_proto_to_fullsystem, + ) def run_onetime_async(self, time_in_seconds: float, func: Callable): """ @@ -303,7 +309,6 @@ def config_proto_to_param_dict(self, message, search_term=None): :param search_term: The search filter """ - field_list = proto_parameter_tree_util.config_proto_to_field_list( message, search_term=search_term, diff --git a/src/software/thunderscope/common/proto_parameter_tree_util.py b/src/software/thunderscope/common/proto_parameter_tree_util.py index 16576ef7c1..d07e0e1f6a 100644 --- a/src/software/thunderscope/common/proto_parameter_tree_util.py +++ b/src/software/thunderscope/common/proto_parameter_tree_util.py @@ -2,6 +2,23 @@ from pyqtgraph import parametertree from google.protobuf.json_format import MessageToDict from thefuzz import fuzz +import netifaces + + +""" +Instead of the using the generic parameter parsing, this constant can be used to define custom handlers for specific +fields in the proto. + +To define a custom handler: + 1. The key is the name of the field in the proto + 2. The value is the function that will be called to parse the field. Make sure the function is callable from this + file +""" +CUSTOM_PARAMETERS_OVERRIDE = { + "robot_communication_interface": "__create_network_enum", + "referee_interface": "__create_network_enum", + "vision_interface": "__create_network_enum", +} def __create_int_parameter_writable(key, value, descriptor): @@ -124,6 +141,20 @@ def __create_parameter_read_only(key, value, descriptor): return {"name": key, "type": "str", "value": value, "readonly": True} +def __create_network_enum(key, value, _): + """ + Lists all the network interfaces available on the system as enum options for the given parameter field. + + :param key: The name of the parameter + :param value: The default value + """ + network_interfaces = netifaces.interfaces() + + return parametertree.parameterTypes.ListParameter( + name=key, default=None, value=value, limits=network_interfaces + ) + + def get_string_val(descriptor, value): """ Converts the given value to a string depending on the descriptor type @@ -170,7 +201,13 @@ def config_proto_to_field_list( if fuzz.partial_ratio(search_term, key) < search_filter_threshold: continue - if descriptor.type == descriptor.TYPE_MESSAGE: + if descriptor.name in CUSTOM_PARAMETERS_OVERRIDE.keys(): + field_list.append( + eval(CUSTOM_PARAMETERS_OVERRIDE[descriptor.name])( + key, value, descriptor + ) + ) + elif descriptor.type == descriptor.TYPE_MESSAGE: field_list.append( { "name": key, diff --git a/src/software/thunderscope/requirements.txt b/src/software/thunderscope/requirements.txt index fef73d9322..99f50fc355 100644 --- a/src/software/thunderscope/requirements.txt +++ b/src/software/thunderscope/requirements.txt @@ -1,2 +1,4 @@ +colorama==0.4.6 +netifaces==0.11.0 numpy==1.24.4 pyqtgraph==0.13.3 diff --git a/src/software/thunderscope/robot_communication.py b/src/software/thunderscope/robot_communication.py index aa2310c0c2..7a2188322f 100644 --- a/src/software/thunderscope/robot_communication.py +++ b/src/software/thunderscope/robot_communication.py @@ -7,23 +7,29 @@ from proto.import_all_protos import * from pyqtgraph.Qt import QtCore from software.thunderscope.proto_unix_io import ProtoUnixIO -from typing import Type +from colorama import Fore, Style +import logging +from typing import Any, Callable, Tuple, Type import threading import time import os from google.protobuf.message import Message +DISCONNECTED = "DISCONNECTED" +"""A constant to represent a disconnected interface""" + +logger = logging.getLogger(__name__) -class RobotCommunication(object): +class RobotCommunication(object): """ Communicate with the robots """ def __init__( self, current_proto_unix_io: ProtoUnixIO, multicast_channel: str, - interface: str, estop_mode: EstopMode, + interface: str = None, estop_path: os.PathLike = None, estop_baudrate: int = 115200, enable_radio: bool = False, @@ -33,23 +39,29 @@ def __init__( :param current_proto_unix_io: the current proto unix io object :param multicast_channel: The multicast channel to use - :param interface: The interface to use :param estop_mode: what estop mode we are running right now, of type EstopMode + :param interface: The interface to use for communication with the robots :param estop_path: The path to the estop :param estop_baudrate: The baudrate of the estop :param enable_radio: Whether to use radio to send primitives to robots :param referee_port: the referee port that we are using. If this is None, the default port is used """ + self.is_setup_for_fullsystem = False self.referee_port = referee_port self.receive_ssl_referee_proto = None self.receive_ssl_wrapper = None + + self.receive_robot_status = None + self.receive_robot_log = None + self.receive_robot_crash = None + self.send_primitive_set = None + self.sequence_number = 0 self.last_time = time.time() self.current_proto_unix_io = current_proto_unix_io self.multicast_channel = str(multicast_channel) - self.interface = interface self.estop_mode = estop_mode self.estop_path = estop_path @@ -79,6 +91,23 @@ def __init__( PowerControl, self.power_control_diagnostics_buffer ) + # Whether to accept the next configuration update. We will be provided a proto configuration from the + # ProtoConfigurationWidget. If the user provides an interface, we will accept it as the first network + # configuration and ignore the provided one from the widget. If not, we will wait for this first configuration + self.accept_next_network_config = True + self.current_network_config = NetworkConfig( + robot_communication_interface=DISCONNECTED, + referee_interface=DISCONNECTED, + vision_interface=DISCONNECTED, + ) + if interface: + self.accept_next_network_config = False + self.__setup_for_robot_communication(interface) + self.network_config_buffer = ThreadSafeBuffer(1, NetworkConfig) + self.current_proto_unix_io.register_observer( + NetworkConfig, self.network_config_buffer + ) + self.send_estop_state_thread = threading.Thread( target=self.__send_estop_state, daemon=True ) @@ -104,34 +133,151 @@ def __init__( except Exception: raise Exception(f"Invalid Estop found at location {self.estop_path}") - def setup_for_fullsystem(self) -> None: + self.print_current_network_config() + + def setup_for_fullsystem( + self, referee_interface: str, vision_interface: str, + ) -> None: """ - Sets up a listener for SSL vision and referee data, and connects all robots to fullsystem as default + Sets up a listener for SSL vision and referee data + + :param referee_interface: the interface to listen for referee data + :param vision_interface: the interface to listen for vision data """ - self.receive_ssl_wrapper = tbots_cpp.SSLWrapperPacketProtoListener( - SSL_VISION_ADDRESS, - SSL_VISION_PORT, - lambda data: self.__forward_to_proto_unix_io(SSL_WrapperPacket, data), - True, + # Check cache to see if we're already connected + change_referee_interface = ( + referee_interface != self.current_network_config.referee_interface + ) and (referee_interface != DISCONNECTED) + change_vision_interface = ( + vision_interface != self.current_network_config.vision_interface + ) and (vision_interface != DISCONNECTED) + + if change_vision_interface: + ( + self.receive_ssl_wrapper, + error, + ) = tbots_cpp.createSSLWrapperPacketProtoListener( + SSL_VISION_ADDRESS, + SSL_VISION_PORT, + vision_interface, + lambda data: self.__forward_to_proto_unix_io(SSL_WrapperPacket, data), + True, + ) + + if error: + logger.error(f"Error setting up vision interface:\n{error}") + + self.current_network_config.vision_interface = ( + vision_interface if not error else DISCONNECTED + ) + + if change_referee_interface: + ( + self.receive_ssl_referee_proto, + error, + ) = tbots_cpp.createSSLRefereeProtoListener( + SSL_REFEREE_ADDRESS, + self.referee_port, + referee_interface, + lambda data: self.__forward_to_proto_unix_io(Referee, data), + True, + ) + + if error: + logger.error(f"Error setting up referee interface:\n{error}") + + self.current_network_config.referee_interface = ( + referee_interface if not error else DISCONNECTED + ) + + if not self.is_setup_for_fullsystem: + self.robots_connected_to_fullsystem = { + robot_id for robot_id in range(MAX_ROBOT_IDS_PER_SIDE) + } + + self.is_setup_for_fullsystem = True + + def __setup_for_robot_communication( + self, robot_communication_interface: str + ) -> None: + """ + Set up senders and listeners for communicating with the robots + + :param robot_communication_interface: the interface to listen/send for robot status data. Ignored for sending + primitives if using radio + """ + if ( + robot_communication_interface + == self.current_network_config.robot_communication_interface + ) or (robot_communication_interface == DISCONNECTED): + return + + is_listener_setup_successfully = True + + def setup_listener(listener_creator: Callable[[], Tuple[Any, str]]) -> Any: + """ + Sets up a listener with the given creator function. Logs any errors that occur. + + :param listener_creator: the function to create the listener. It must return a type of + (listener object, error) + """ + listener, error = listener_creator() + if error: + logger.error(f"Error setting up robot status interface:\n{error}") + + return listener + + # Create the multicast listeners + self.receive_robot_status = setup_listener( + lambda: tbots_cpp.createRobotStatusProtoListener( + self.multicast_channel, + ROBOT_STATUS_PORT, + robot_communication_interface, + self.__receive_robot_status, + True, + ) ) - self.receive_ssl_referee_proto = tbots_cpp.SSLRefereeProtoListener( - SSL_REFEREE_ADDRESS, - self.referee_port, - lambda data: self.current_proto_unix_io.send_proto(Referee, data), - True, + self.receive_robot_log = setup_listener( + lambda: tbots_cpp.createRobotLogProtoListener( + self.multicast_channel, + ROBOT_LOGS_PORT, + robot_communication_interface, + lambda data: self.__forward_to_proto_unix_io(RobotLog, data), + True, + ) ) - self.robots_connected_to_fullsystem = { - robot_id for robot_id in range(MAX_ROBOT_IDS_PER_SIDE) - } + self.receive_robot_crash = setup_listener( + lambda: tbots_cpp.createRobotCrashProtoListener( + self.multicast_channel, + ROBOT_CRASH_PORT, + robot_communication_interface, + lambda data: self.current_proto_unix_io.send_proto(RobotCrash, data), + True, + ) + ) + + # Create multicast senders + if self.enable_radio: + self.send_primitive_set = tbots_cpp.PrimitiveSetProtoRadioSender() + else: + self.send_primitive_set, error = tbots_cpp.createPrimitiveSetProtoUdpSender( + self.multicast_channel, + PRIMITIVE_PORT, + robot_communication_interface, + True, + ) - def close_for_fullsystem(self) -> None: - if self.receive_ssl_wrapper: - self.receive_ssl_wrapper.close() + if error: + is_listener_setup_successfully = False + logger.error(f"Error setting up primitive set sender:\n{error}") - if self.receive_ssl_referee_proto: - self.receive_ssl_referee_proto.close() + self.current_network_config.robot_communication_interface = ( + robot_communication_interface + if is_listener_setup_successfully + else DISCONNECTED + ) def toggle_keyboard_estop(self) -> None: """ @@ -225,7 +371,37 @@ def __run_primitive_set(self) -> None: is useful to dip in and out of robot diagnostics. """ + network_config = self.network_config_buffer.get( + block=True if self.accept_next_network_config else False, + return_cached=False, + ) while self.running: + if network_config is not None and self.accept_next_network_config: + logging.info(f"[RobotCommunication] Received new NetworkConfig") + + if self.is_setup_for_fullsystem: + self.setup_for_fullsystem( + referee_interface=network_config.referee_interface, + vision_interface=network_config.vision_interface, + ) + self.__setup_for_robot_communication( + robot_communication_interface=network_config.robot_communication_interface + ) + self.print_current_network_config() + elif network_config is not None: + logger.warning( + "[RobotCommunication] We received a proto configuration update with a newer network" + " configuration. We will ignore this update, likely because the interface was provided at startup but" + " the next update will be accepted." + ) + self.accept_next_network_config = True + self.print_current_network_config() + + # Set up network on the next tick + network_config = self.network_config_buffer.get( + block=False, return_cached=False + ) + # total primitives for all robots robot_primitives = {} @@ -298,36 +474,6 @@ def __enter__(self) -> "self": for RobotStatus, RobotLogs, and RobotCrash msgs, and multicast sender for PrimitiveSet """ - # Create the multicast listeners - self.receive_robot_status = tbots_cpp.RobotStatusProtoListener( - self.multicast_channel + "%" + self.interface, - ROBOT_STATUS_PORT, - self.__receive_robot_status, - True, - ) - - self.receive_robot_log = tbots_cpp.RobotLogProtoListener( - self.multicast_channel + "%" + self.interface, - ROBOT_LOGS_PORT, - lambda data: self.__forward_to_proto_unix_io(RobotLog, data), - True, - ) - - self.receive_robot_crash = tbots_cpp.RobotCrashProtoListener( - self.multicast_channel + "%" + self.interface, - ROBOT_CRASH_PORT, - lambda data: self.current_proto_unix_io.send_proto(RobotCrash, data), - True, - ) - - # Create multicast senders - if self.enable_radio: - self.send_primitive_set = tbots_cpp.PrimitiveSetProtoRadioSender() - else: - self.send_primitive_set = tbots_cpp.PrimitiveSetProtoUdpSender( - self.multicast_channel + "%" + self.interface, PRIMITIVE_PORT, True - ) - self.running = True self.send_estop_state_thread.start() @@ -357,8 +503,44 @@ def __exit__(self, type, value, traceback) -> None: """ self.running = False - self.close_for_fullsystem() - - self.receive_robot_log.close() - self.receive_robot_status.close() self.run_primitive_set_thread.join() + + def print_current_network_config(self) -> None: + """ + Prints the current network configuration to the console + """ + + def output_string(comm_name: str, status: str) -> str: + """ + Returns a formatted string with the communication name and status + + Any status other than DISCONNECTED will be coloured green, otherwise red + + :param comm_name: the name of the communication + :param status: the status of the communication + """ + colour = Fore.RED if status == DISCONNECTED else Fore.GREEN + return f"{comm_name} {colour}{status} {Style.RESET_ALL}" + + logging.info( + output_string( + "Robot Status\t", + self.current_network_config.robot_communication_interface, + ) + ) + logging.info( + output_string( + "Vision\t\t", + self.current_network_config.vision_interface + if self.is_setup_for_fullsystem + else DISCONNECTED, + ) + ) + logging.info( + output_string( + "Referee\t\t", + self.current_network_config.referee_interface + if self.is_setup_for_fullsystem + else DISCONNECTED, + ) + ) diff --git a/src/software/thunderscope/thunderscope_main.py b/src/software/thunderscope/thunderscope_main.py index a22e42329a..88c4e7de0f 100644 --- a/src/software/thunderscope/thunderscope_main.py +++ b/src/software/thunderscope/thunderscope_main.py @@ -4,6 +4,7 @@ import os import sys import threading +from robot_communication import DISCONNECTED from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer from software.thunderscope.thunderscope import Thunderscope from software.thunderscope.binary_context_managers import * @@ -228,12 +229,18 @@ # we only have --launch_gc parameter but not args.run_yellow and args.run_blue if not args.run_blue and not args.run_yellow and args.launch_gc: - parser.error("--launch_gc has to be ran with --run_blue argument") + parser.error( + "--launch_gc has to be ran with --run_blue or --run_yellow argument" + ) - # Sanity check that an interface was provided - if args.run_blue or args.run_yellow: - if args.interface is None: - parser.error("Must specify interface") + # Sanity check that an interface was provided if we are running diagnostics since it will not load the network + # configuration widget + if ( + not (args.run_blue or args.run_yellow) + and args.run_diagnostics + and args.interface is None + ): + parser.error("Must specify interface") ########################################################################### # Visualize CPP Tests # @@ -354,7 +361,13 @@ ) if args.run_blue or args.run_yellow: - robot_communication.setup_for_fullsystem() + robot_communication.setup_for_fullsystem( + referee_interface=args.interface + if args.interface + else DISCONNECTED, + vision_interface=args.interface if args.interface else DISCONNECTED, + ) + robot_communication.print_current_network_config() full_system_runtime_dir = ( args.blue_full_system_runtime_dir if args.run_blue @@ -461,7 +474,6 @@ def __ticker(tick_rate_ms: int) -> None: if args.enable_autoref else contextlib.nullcontext() ) as autoref: - tscope.register_refresh_function(gamecontroller.refresh) autoref_proto_unix_io = ProtoUnixIO() diff --git a/src/software/thunderscope/widget_setup_functions.py b/src/software/thunderscope/widget_setup_functions.py index 91f77ed091..f5cde2f5ac 100644 --- a/src/software/thunderscope/widget_setup_functions.py +++ b/src/software/thunderscope/widget_setup_functions.py @@ -214,6 +214,9 @@ def on_change_callback( attr: Any, value: Any, updated_proto: ThunderbotsConfig ) -> None: proto_unix_io.send_proto(ThunderbotsConfig, updated_proto) + proto_unix_io.send_proto( + NetworkConfig, updated_proto.ai_config.ai_control_config.network_config + ) return ProtoConfigurationWidget( on_change_callback, is_yellow=friendly_colour_yellow