diff --git a/.gitignore b/.gitignore index 1bbff61..3b85665 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ build/ .vscode .ipynb_checkpoints/ - +.pio/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/ArduinoCore-Linux/cores/arduino/Arduino.h b/ArduinoCore-Linux/cores/arduino/Arduino.h index 343fc1a..eadb69a 100644 --- a/ArduinoCore-Linux/cores/arduino/Arduino.h +++ b/ArduinoCore-Linux/cores/arduino/Arduino.h @@ -18,6 +18,8 @@ */ #pragma once +#include "Platform.h" + /// do not use exithandler atexit #ifndef HOST # define HOST diff --git a/ArduinoCore-Linux/cores/arduino/DesktopSocket.h b/ArduinoCore-Linux/cores/arduino/DesktopSocket.h new file mode 100644 index 0000000..1f3f538 --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/DesktopSocket.h @@ -0,0 +1,8 @@ +#pragma once +#include "Platform.h" + +#if ARDUINO_EMULATOR_WINDOWS +#include "DesktopSocketWindows.h" +#else +#include "DesktopSocketPosix.h" +#endif diff --git a/ArduinoCore-Linux/cores/arduino/DesktopSocketPosix.h b/ArduinoCore-Linux/cores/arduino/DesktopSocketPosix.h new file mode 100644 index 0000000..a42ccce --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/DesktopSocketPosix.h @@ -0,0 +1,47 @@ +#pragma once + +#include +// Collide with existing definitions +#undef INADDR_NONE + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace arduino { + +using SocketHandle = int; +constexpr SocketHandle INVALID_SOCKET_HANDLE = -1; +using SocketLength = socklen_t; + +class SocketRuntime { + public: + SocketRuntime() = default; + ~SocketRuntime() = default; +}; + +inline void ensureSocketRuntime() { + static SocketRuntime runtime; + (void)runtime; +} + +inline void closeSocket(SocketHandle socket) { ::close(socket); } +inline int socketLastError() { return errno; } + +inline bool socketWouldBlock(int error) { + return error == EWOULDBLOCK || error == EAGAIN; +} +inline bool socketConnectionReset(int error) { return error == ECONNRESET; } + +inline bool setSocketNonBlocking(SocketHandle socket, bool enabled = true) { + int flags = fcntl(socket, F_GETFL, 0); + return flags >= 0 && fcntl(socket, F_SETFL, enabled ? flags | O_NONBLOCK + : flags & ~O_NONBLOCK) == 0; +} + +} // namespace arduino diff --git a/ArduinoCore-Linux/cores/arduino/DesktopSocketWindows.h b/ArduinoCore-Linux/cores/arduino/DesktopSocketWindows.h new file mode 100644 index 0000000..987a6fa --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/DesktopSocketWindows.h @@ -0,0 +1,46 @@ +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#define INPUT ARDUINO_WINDOWS_INPUT +#include +#include +#undef INPUT +#undef INADDR_NONE + +#ifndef SHUT_RDWR +#define SHUT_RDWR SD_BOTH +#endif + +namespace arduino { + +using SocketHandle = SOCKET; +constexpr SocketHandle INVALID_SOCKET_HANDLE = INVALID_SOCKET; +using SocketLength = int; + +class SocketRuntime { + public: + SocketRuntime() { + WSADATA data; + WSAStartup(MAKEWORD(2, 2), &data); + } + ~SocketRuntime() { WSACleanup(); } +}; + +inline void ensureSocketRuntime() { + static SocketRuntime runtime; + (void)runtime; +} + +inline void closeSocket(SocketHandle socket) { closesocket(socket); } +inline int socketLastError() { return WSAGetLastError(); } +inline bool socketWouldBlock(int error) { return error == WSAEWOULDBLOCK; } +inline bool socketConnectionReset(int error) { return error == WSAECONNRESET; } + +inline bool setSocketNonBlocking(SocketHandle socket, bool enabled = true) { + u_long mode = enabled ? 1UL : 0UL; + return ioctlsocket(socket, FIONBIO, &mode) == 0; +} + +} // namespace arduino diff --git a/ArduinoCore-Linux/cores/arduino/Ethernet.h b/ArduinoCore-Linux/cores/arduino/Ethernet.h index 9e4e02a..a8e0c3c 100644 --- a/ArduinoCore-Linux/cores/arduino/Ethernet.h +++ b/ArduinoCore-Linux/cores/arduino/Ethernet.h @@ -19,9 +19,8 @@ */ #pragma once -#include // for inet_pton -#include // for gethostbyname, struct hostent -#include // for close +#include "DesktopSocket.h" +#include #include // This is the include you need #include "ArduinoLogger.h" @@ -351,12 +350,17 @@ class EthernetClient : public Client { serv_addr4.sin_port = htons(port); if (::inet_pton(AF_INET, address, &serv_addr4.sin_addr) <= 0) { // Not an IP, try to resolve hostname - struct hostent* he = ::gethostbyname(address); - if (he == nullptr || he->h_addr_list[0] == nullptr) { + addrinfo hints{}; + hints.ai_family = AF_INET; + addrinfo* info = nullptr; + int rc = ::getaddrinfo(address, nullptr, &hints, &info); + if (rc != 0 || info == nullptr) { Logger.error(WIFICLIENT, "Hostname resolution failed"); serv_addr4.sin_addr.s_addr = 0; } else { - memcpy(&serv_addr4.sin_addr, he->h_addr_list[0], he->h_length); + auto* resolved = reinterpret_cast(info->ai_addr); + memcpy(&serv_addr4.sin_addr, &resolved->sin_addr, sizeof(serv_addr4.sin_addr)); + freeaddrinfo(info); } } return IPAddress(serv_addr4.sin_addr.s_addr); diff --git a/ArduinoCore-Linux/cores/arduino/EthernetServer.h b/ArduinoCore-Linux/cores/arduino/EthernetServer.h index 7e4d974..5a52335 100644 --- a/ArduinoCore-Linux/cores/arduino/EthernetServer.h +++ b/ArduinoCore-Linux/cores/arduino/EthernetServer.h @@ -19,194 +19,10 @@ */ #pragma once -#include -#include +#include "Platform.h" -#include "Ethernet.h" -#include "api/Server.h" -#include "SignalHandler.h" - -namespace arduino { - -/** - * A minimal ethernet server - */ - -class EthernetServer : public Server { - private: - uint16_t _port; - int server_fd = 0; - struct sockaddr_in server_addr; - int _status = wl_status_t::WL_DISCONNECTED; - bool is_blocking = false; - bool _noDelay = false; - - static std::vector& active_servers() { - static std::vector servers; - return servers; - } - static void cleanupAll(int sig) { - for (auto* server : active_servers()) { - if (server && server->server_fd > 0) { - shutdown(server->server_fd, SHUT_RDWR); - close(server->server_fd); - server->server_fd = 0; - } - } - } - - public: - EthernetServer(int port = 80) { - _port = port; - // Register signal handler only once - static bool signal_registered = false; - if (!signal_registered) { - SignalHandler::registerHandler(SIGINT, cleanupAll); - SignalHandler::registerHandler(SIGTERM, cleanupAll); - signal_registered = true; - } - } - - ~EthernetServer() { - stop(); - // Remove from active servers list - auto& servers = active_servers(); - auto it = std::find(servers.begin(), servers.end(), this); - if (it != servers.end()) { - servers.erase(it); - } - } - void begin() { begin(_port); } - void begin(int port) { begin_(port); } - - void stop() { - if (server_fd > 0) { - // Set SO_LINGER to force immediate close - struct linger linger_opt = {1, 0}; - setsockopt(server_fd, SOL_SOCKET, SO_LINGER, &linger_opt, - sizeof(linger_opt)); - - shutdown(server_fd, SHUT_RDWR); - close(server_fd); - } - server_fd = 0; - _status = wl_status_t::WL_DISCONNECTED; - } - WiFiClient accept() { return available_(); } - WiFiClient available(uint8_t* status = NULL) { return available_(); } - virtual size_t write(uint8_t ch) { return write(&ch, 1); } - virtual size_t write(const uint8_t* buf, size_t size) { - int rc = ::write(server_fd, buf, size); - if (rc < 0) { - rc = 0; - } - return rc; - } - int status() { return _status; } - - // The following are for compatibility with ESP32 WiFiServer - // get/setNoDelay maintain the state variable, but otherwise - // have no effect on the code. - void setNoDelay(bool nodelay) { _noDelay = nodelay; } - bool getNoDelay() { return _noDelay; } - bool hasClient() { - if (server_fd <= 0) return false; - struct pollfd pfd; - pfd.fd = server_fd; - pfd.events = POLLIN; - return ::poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); - } - - using Print::write; - - protected: - - bool begin_(int port = 0) { - if (port > 0) _port = port; - _status = wl_status_t::WL_DISCONNECTED; - - // create server socket - if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { - // error("socket failed"); - _status = wl_status_t::WL_CONNECT_FAILED; - return false; - } - - // Reuse address after restart - int iSetOption = 1; - setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&iSetOption, - sizeof(iSetOption)); - - // Set SO_REUSEPORT for better port reuse - setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, (char*)&iSetOption, - sizeof(iSetOption)); - - // config socket - server_addr.sin_family = AF_INET; - server_addr.sin_addr.s_addr = INADDR_ANY; - server_addr.sin_port = htons(_port); - - // bind socket to port - while (::bind(server_fd, (struct sockaddr*)&server_addr, - sizeof(server_addr)) < 0) { - // error("bind failed"); - //_status = wl_status_t::WL_CONNECT_FAILED; - Logger.error("bind failed"); - // return false; - delay(1000); - } - - // listen for connections - if (::listen(server_fd, 10) < 0) { - // error("listen failed"); - _status = wl_status_t::WL_CONNECT_FAILED; - Logger.error("listen failed"); - return false; - } - - _noDelay = false; - - // Add to active servers list for signal handling - active_servers().push_back(this); - _status = wl_status_t::WL_CONNECTED; - return true; - } - - void setBlocking(bool flag) { is_blocking = flag; } - - EthernetClient available_() { - struct sockaddr_in client_addr; - socklen_t client_addr_len = sizeof(client_addr); - int client_fd; - - if (_status == wl_status_t::WL_CONNECT_FAILED) { - begin(_port); - } - - struct pollfd pfd; - pfd.fd = server_fd; - pfd.events = POLLIN; - int poll_rc = ::poll(&pfd, 1, 200); - - // non blocking check if we have any request to accept - if (!is_blocking) { - if (poll_rc <= 0 || !(pfd.revents & POLLIN)) { - EthernetClient result(nullptr); - return result; - } - } - - // accept client connection (blocking call) - if ((client_fd = ::accept(server_fd, (struct sockaddr*)&client_addr, - &client_addr_len)) < 0) { - EthernetClient result(nullptr); - Logger.error("accept failed"); - return result; - } - std::shared_ptr sock_impl = std::make_shared(client_fd, (struct sockaddr_in*)&client_addr); - EthernetClient result{sock_impl}; - return result; - } -}; - -} // namespace arduino +#if ARDUINO_EMULATOR_WINDOWS +#include "EthernetServerWindows.h" +#else +#include "EthernetServerPosix.h" +#endif diff --git a/ArduinoCore-Linux/cores/arduino/EthernetServerPosix.h b/ArduinoCore-Linux/cores/arduino/EthernetServerPosix.h new file mode 100644 index 0000000..4e11a4c --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/EthernetServerPosix.h @@ -0,0 +1,212 @@ +/* + EthernetServer.h + Copyright (c) 2025 Phil Schatzmann. All right reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#pragma once + +#include "DesktopSocket.h" + +#include "Ethernet.h" +#include "api/Server.h" +#include "SignalHandler.h" + +namespace arduino { + +/** + * A minimal ethernet server + */ + +class EthernetServer : public Server { + private: + uint16_t _port; + int server_fd = 0; + struct sockaddr_in server_addr; + int _status = wl_status_t::WL_DISCONNECTED; + bool is_blocking = false; + bool _noDelay = false; + + static std::vector& active_servers() { + static std::vector servers; + return servers; + } + static void cleanupAll(int sig) { + for (auto* server : active_servers()) { + if (server && server->server_fd > 0) { + shutdown(server->server_fd, SHUT_RDWR); + close(server->server_fd); + server->server_fd = 0; + } + } + } + + public: + EthernetServer(int port = 80) { + _port = port; + // Register signal handler only once + static bool signal_registered = false; + if (!signal_registered) { + SignalHandler::registerHandler(SIGINT, cleanupAll); + SignalHandler::registerHandler(SIGTERM, cleanupAll); + signal_registered = true; + } + } + + ~EthernetServer() { + stop(); + // Remove from active servers list + auto& servers = active_servers(); + auto it = std::find(servers.begin(), servers.end(), this); + if (it != servers.end()) { + servers.erase(it); + } + } + void begin() { begin(_port); } + void begin(int port) { begin_(port); } + + void stop() { + if (server_fd > 0) { + // Set SO_LINGER to force immediate close + struct linger linger_opt = {1, 0}; + setsockopt(server_fd, SOL_SOCKET, SO_LINGER, &linger_opt, + sizeof(linger_opt)); + + shutdown(server_fd, SHUT_RDWR); + close(server_fd); + } + server_fd = 0; + _status = wl_status_t::WL_DISCONNECTED; + } + WiFiClient accept() { return available_(); } + WiFiClient available(uint8_t* status = NULL) { return available_(); } + virtual size_t write(uint8_t ch) { return write(&ch, 1); } + virtual size_t write(const uint8_t* buf, size_t size) { + int rc = ::write(server_fd, buf, size); + if (rc < 0) { + rc = 0; + } + return rc; + } + int status() { return _status; } + + // The following are for compatibility with ESP32 WiFiServer + // get/setNoDelay maintain the state variable, but otherwise + // have no effect on the code. + void setNoDelay(bool nodelay) { _noDelay = nodelay; } + bool getNoDelay() { return _noDelay; } + bool hasClient() { + if (server_fd <= 0) return false; + struct pollfd pfd; + pfd.fd = server_fd; + pfd.events = POLLIN; + return ::poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); + } + + using Print::write; + + protected: + + bool begin_(int port = 0) { + if (port > 0) _port = port; + _status = wl_status_t::WL_DISCONNECTED; + + // create server socket + ensureSocketRuntime(); + if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + // error("socket failed"); + _status = wl_status_t::WL_CONNECT_FAILED; + return false; + } + + // Reuse address after restart + int iSetOption = 1; + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&iSetOption, + sizeof(iSetOption)); + + // Set SO_REUSEPORT for better port reuse + setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, (char*)&iSetOption, + sizeof(iSetOption)); + + // config socket + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = INADDR_ANY; + server_addr.sin_port = htons(_port); + + // bind socket to port + while (::bind(server_fd, (struct sockaddr*)&server_addr, + sizeof(server_addr)) < 0) { + // error("bind failed"); + //_status = wl_status_t::WL_CONNECT_FAILED; + Logger.error("bind failed"); + // return false; + delay(1000); + } + + // listen for connections + if (::listen(server_fd, 10) < 0) { + // error("listen failed"); + _status = wl_status_t::WL_CONNECT_FAILED; + Logger.error("listen failed"); + return false; + } + + _noDelay = false; + + // Add to active servers list for signal handling + active_servers().push_back(this); + _status = wl_status_t::WL_CONNECTED; + return true; + } + + void setBlocking(bool flag) { is_blocking = flag; } + + EthernetClient available_() { + struct sockaddr_in client_addr; + socklen_t client_addr_len = sizeof(client_addr); + int client_fd; + + if (_status == wl_status_t::WL_CONNECT_FAILED) { + begin(_port); + } + + struct pollfd pfd; + pfd.fd = server_fd; + pfd.events = POLLIN; + int poll_rc = ::poll(&pfd, 1, 200); + + // non blocking check if we have any request to accept + if (!is_blocking) { + if (poll_rc <= 0 || !(pfd.revents & POLLIN)) { + EthernetClient result(nullptr); + return result; + } + } + + // accept client connection (blocking call) + if ((client_fd = ::accept(server_fd, (struct sockaddr*)&client_addr, + &client_addr_len)) < 0) { + EthernetClient result(nullptr); + Logger.error("accept failed"); + return result; + } + setSocketNonBlocking(client_fd); + std::shared_ptr sock_impl = std::make_shared(client_fd, (struct sockaddr_in*)&client_addr); + EthernetClient result{sock_impl}; + return result; + } +}; + +} // namespace arduino diff --git a/ArduinoCore-Linux/cores/arduino/EthernetServerWindows.h b/ArduinoCore-Linux/cores/arduino/EthernetServerWindows.h new file mode 100644 index 0000000..c589e87 --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/EthernetServerWindows.h @@ -0,0 +1,134 @@ +#pragma once + +#include "DesktopSocket.h" +#include "Ethernet.h" +#include "api/Server.h" +#include "SignalHandler.h" + +namespace arduino { + +class EthernetServer : public Server { + private: + uint16_t _port; + SocketHandle server_fd = INVALID_SOCKET_HANDLE; + struct sockaddr_in server_addr; + int _status = wl_status_t::WL_DISCONNECTED; + bool is_blocking = false; + bool _noDelay = false; + + static std::vector& active_servers() { + static std::vector servers; + return servers; + } + static void cleanupAll(int) { + for (auto* server : active_servers()) { + if (server && server->server_fd != INVALID_SOCKET_HANDLE) { + shutdown(server->server_fd, SHUT_RDWR); + closeSocket(server->server_fd); + server->server_fd = INVALID_SOCKET_HANDLE; + } + } + } + + public: + EthernetServer(int port = 80) : _port(port) { + static bool signal_registered = false; + if (!signal_registered) { + SignalHandler::registerHandler(SIGINT, cleanupAll); + SignalHandler::registerHandler(SIGTERM, cleanupAll); + signal_registered = true; + } + } + ~EthernetServer() { + stop(); + auto& servers = active_servers(); + auto it = std::find(servers.begin(), servers.end(), this); + if (it != servers.end()) servers.erase(it); + } + + void begin() { begin(_port); } + void begin(int port) { begin_(port); } + void stop() { + if (server_fd != INVALID_SOCKET_HANDLE) { + struct linger linger_opt = {1, 0}; + setsockopt(server_fd, SOL_SOCKET, SO_LINGER, + reinterpret_cast(&linger_opt), sizeof(linger_opt)); + shutdown(server_fd, SHUT_RDWR); + closeSocket(server_fd); + } + server_fd = INVALID_SOCKET_HANDLE; + _status = wl_status_t::WL_DISCONNECTED; + } + WiFiClient accept() { return available_(); } + WiFiClient available(uint8_t* = nullptr) { return available_(); } + size_t write(uint8_t ch) override { return write(&ch, 1); } + size_t write(const uint8_t* buf, size_t size) override { + int rc = ::send(server_fd, reinterpret_cast(buf), static_cast(size), 0); + return rc < 0 ? 0 : rc; + } + int status() { return _status; } + void setNoDelay(bool nodelay) { _noDelay = nodelay; } + bool getNoDelay() { return _noDelay; } + bool hasClient() { + if (server_fd == INVALID_SOCKET_HANDLE) return false; + WSAPOLLFD pfd{}; + pfd.fd = server_fd; + pfd.events = POLLRDNORM; + return WSAPoll(&pfd, 1, 0) > 0 && (pfd.revents & POLLRDNORM); + } + using Print::write; + + protected: + bool begin_(int port = 0) { + if (port > 0) _port = port; + _status = wl_status_t::WL_DISCONNECTED; + ensureSocketRuntime(); + if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET_HANDLE) { + _status = wl_status_t::WL_CONNECT_FAILED; + return false; + } + int option = 1; + setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&option), sizeof(option)); + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = INADDR_ANY; + server_addr.sin_port = htons(_port); + while (::bind(server_fd, reinterpret_cast(&server_addr), + sizeof(server_addr)) < 0) { + Logger.error("bind failed"); + delay(1000); + } + if (::listen(server_fd, 10) < 0) { + _status = wl_status_t::WL_CONNECT_FAILED; + Logger.error("listen failed"); + return false; + } + _noDelay = false; + active_servers().push_back(this); + _status = wl_status_t::WL_CONNECTED; + return true; + } + void setBlocking(bool flag) { is_blocking = flag; } + + EthernetClient available_() { + struct sockaddr_in client_addr; + SocketLength client_addr_len = sizeof(client_addr); + SocketHandle client_fd; + if (_status == wl_status_t::WL_CONNECT_FAILED) begin(_port); + WSAPOLLFD pfd{}; + pfd.fd = server_fd; + pfd.events = POLLRDNORM; + int poll_rc = WSAPoll(&pfd, 1, 200); + if (!is_blocking && (poll_rc <= 0 || !(pfd.revents & POLLRDNORM))) + return EthernetClient(nullptr); + if ((client_fd = ::accept(server_fd, reinterpret_cast(&client_addr), + &client_addr_len)) == INVALID_SOCKET_HANDLE) { + Logger.error("accept failed"); + return EthernetClient(nullptr); + } + setSocketNonBlocking(client_fd); + return EthernetClient(std::make_shared(client_fd, &client_addr)); + } +}; + +} // namespace arduino diff --git a/ArduinoCore-Linux/cores/arduino/FileStream.h b/ArduinoCore-Linux/cores/arduino/FileStream.h index f90f4ea..0c5c074 100644 --- a/ArduinoCore-Linux/cores/arduino/FileStream.h +++ b/ArduinoCore-Linux/cores/arduino/FileStream.h @@ -19,6 +19,7 @@ #pragma once #include #include +#include "Platform.h" #include "api/Stream.h" namespace arduino { @@ -110,6 +111,12 @@ class FileStream : public Stream { * Serial1 object. Example: Serial1.begin(9600); Serial1.println("Hello from * Serial1"); */ +#if ARDUINO_EMULATOR_WINDOWS +// No physical COM port is assumed on Windows. Use SerialImpl and pass the +// selected device explicitly when hardware serial is required. +static FileStream Serial1(nullptr, nullptr); +#else static FileStream Serial1("/dev/ttyACM0"); +#endif } // namespace arduino diff --git a/ArduinoCore-Linux/cores/arduino/HardwareSetup.h b/ArduinoCore-Linux/cores/arduino/HardwareSetup.h index dff530e..b4f5a4d 100644 --- a/ArduinoCore-Linux/cores/arduino/HardwareSetup.h +++ b/ArduinoCore-Linux/cores/arduino/HardwareSetup.h @@ -28,3 +28,5 @@ #if defined(USE_FIRMATA) # include "../firmata/HardwareSetupFIR.h" #endif + +void hardwareSetup(); diff --git a/ArduinoCore-Linux/cores/arduino/HardwareSetupRemote.h b/ArduinoCore-Linux/cores/arduino/HardwareSetupRemote.h index 5c33e9d..22dc623 100644 --- a/ArduinoCore-Linux/cores/arduino/HardwareSetupRemote.h +++ b/ArduinoCore-Linux/cores/arduino/HardwareSetupRemote.h @@ -27,6 +27,7 @@ #include "RemoteSPI.h" #include "SPIWrapper.h" #include "UDPStream.h" +#include namespace arduino { diff --git a/ArduinoCore-Linux/cores/arduino/Main.cpp b/ArduinoCore-Linux/cores/arduino/Main.cpp index 0caa200..db3094e 100644 --- a/ArduinoCore-Linux/cores/arduino/Main.cpp +++ b/ArduinoCore-Linux/cores/arduino/Main.cpp @@ -22,8 +22,14 @@ #include "HardwareSetup.h" -__attribute__((weak)) void setup() {} -__attribute__((weak)) void loop() {} +#if defined(__GNUC__) +#define ARDUINO_EMULATOR_WEAK __attribute__((weak)) +#else +#define ARDUINO_EMULATOR_WEAK +#endif + +ARDUINO_EMULATOR_WEAK void setup() {} +ARDUINO_EMULATOR_WEAK void loop() {} void hardwareSetup(){ #if !defined(SKIP_HARDWARE_SETUP) @@ -37,7 +43,7 @@ void hardwareSetup(){ #endif } -__attribute__((weak)) int main () { +ARDUINO_EMULATOR_WEAK int main () { hardwareSetup(); setup(); while(true){ diff --git a/ArduinoCore-Linux/cores/arduino/Platform.h b/ArduinoCore-Linux/cores/arduino/Platform.h new file mode 100644 index 0000000..5f391ee --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/Platform.h @@ -0,0 +1,13 @@ +#pragma once + +#if defined(_WIN32) +#define ARDUINO_EMULATOR_WINDOWS 1 +#else +#define ARDUINO_EMULATOR_WINDOWS 0 +#endif + +#if defined(__linux__) +#define ARDUINO_EMULATOR_LINUX 1 +#else +#define ARDUINO_EMULATOR_LINUX 0 +#endif diff --git a/ArduinoCore-Linux/cores/arduino/RemoteI2C.h b/ArduinoCore-Linux/cores/arduino/RemoteI2C.h index de9536c..1140027 100644 --- a/ArduinoCore-Linux/cores/arduino/RemoteI2C.h +++ b/ArduinoCore-Linux/cores/arduino/RemoteI2C.h @@ -1,5 +1,5 @@ #pragma once -#include "Stream.h" +#include "api/Stream.h" #include "api/HardwareI2C.h" #include "HardwareService.h" diff --git a/ArduinoCore-Linux/cores/arduino/RingBufferExt.h b/ArduinoCore-Linux/cores/arduino/RingBufferExt.h index 03ebc3f..c4a2cb1 100644 --- a/ArduinoCore-Linux/cores/arduino/RingBufferExt.h +++ b/ArduinoCore-Linux/cores/arduino/RingBufferExt.h @@ -62,11 +62,12 @@ class RingBufferExt { int read(uint8_t* str, int len) { for (int j = 0; j < len; j++) { - int current = read(); - if (current <= 0) { + const int current = read(); + if (current < 0) { return j; } - str[j] = current; + // A byte value of 0x00 is valid payload data; only -1 means empty. + str[j] = static_cast(current); } return len; } diff --git a/ArduinoCore-Linux/cores/arduino/Serial.h b/ArduinoCore-Linux/cores/arduino/Serial.h index da96543..41dfb82 100644 --- a/ArduinoCore-Linux/cores/arduino/Serial.h +++ b/ArduinoCore-Linux/cores/arduino/Serial.h @@ -33,7 +33,11 @@ namespace arduino { class SerialImpl : public HardwareSerial { public: - SerialImpl(const char* device = "/dev/ttyACM0") { this->device = device; } + SerialImpl(const char* device = nullptr) { this->device = device; } + + // Physical serial is opt-in: the host cannot safely guess a usable COM + // port. Examples can use SerialImpl serial; serial.setDevice("COM5"). + void setDevice(const char* device) { this->device = device; } virtual void begin(unsigned long baudrate) { open(baudrate); } @@ -87,8 +91,14 @@ class SerialImpl : public HardwareSerial { long timeout = 1000; virtual void open(unsigned long baudrate) { - if (!serial.openDevice(device, baudrate)) { + if (device == nullptr) { + is_open = false; + return; + } + if (serial.openDevice(device, baudrate) <= 0) { + is_open = false; Logger.error("SerialImpl", "could not open", device); + return; } is_open = true; } @@ -96,4 +106,4 @@ class SerialImpl : public HardwareSerial { } // namespace arduino -#endif \ No newline at end of file +#endif diff --git a/ArduinoCore-Linux/cores/arduino/SignalHandler.h b/ArduinoCore-Linux/cores/arduino/SignalHandler.h index 3b5a6e2..56bf7b5 100644 --- a/ArduinoCore-Linux/cores/arduino/SignalHandler.h +++ b/ArduinoCore-Linux/cores/arduino/SignalHandler.h @@ -26,7 +26,14 @@ #include #include #include +#include "Platform.h" +#if !ARDUINO_EMULATOR_WINDOWS #include +#else +#include +#include +#include +#endif #undef INADDR_NONE @@ -78,13 +85,24 @@ class SignalHandler { return handlers; } +#if ARDUINO_EMULATOR_WINDOWS + static std::atomic& pendingSignal() { static std::atomic value{0}; return value; } + static std::condition_variable& condition() { static std::condition_variable value; return value; } + static std::mutex& conditionMutex() { static std::mutex value; return value; } +#endif + // Async-signal-safe: writes one byte and returns. No map access, no // std::function calls, no exit() - all of that is deferred to the // reaper thread, well outside signal-handler context. static void dispatch(int signum) { +#if ARDUINO_EMULATOR_WINDOWS + pendingSignal() = signum; + condition().notify_one(); +#else char sig = (char)signum; ssize_t n = write(pipeWriteFd(), &sig, 1); (void)n; // nothing safe to do with a failed write() from a handler +#endif } static int& pipeWriteFd() { @@ -101,6 +119,19 @@ class SignalHandler { static void ensureReaperThread() { static std::once_flag started; std::call_once(started, [] { +#if ARDUINO_EMULATOR_WINDOWS + std::thread([] { + std::unique_lock lock(conditionMutex()); + condition().wait(lock, [] { return pendingSignal() != 0; }); + int signum = pendingSignal(); + auto& handlers = getHandlers(); + auto it = handlers.find(signum); + if (it != handlers.end()) { + for (auto& func : it->second) func(signum); + } + std::_Exit(0); + }).detach(); +#else int fds[2]; pipe(fds); pipeReadFd() = fds[0]; @@ -121,6 +152,7 @@ class SignalHandler { _exit(0); } }).detach(); +#endif }); } }; diff --git a/ArduinoCore-Linux/cores/arduino/SocketImpl.cpp b/ArduinoCore-Linux/cores/arduino/SocketImpl.cpp index ea40427..53f7b06 100644 --- a/ArduinoCore-Linux/cores/arduino/SocketImpl.cpp +++ b/ArduinoCore-Linux/cores/arduino/SocketImpl.cpp @@ -20,6 +20,8 @@ #include "SocketImpl.h" +#if !ARDUINO_EMULATOR_WINDOWS + #include #include #include @@ -268,3 +270,5 @@ const char *SocketImpl::getIPAddress(const char *validEntries[]) { } } // namespace arduino + +#endif // !ARDUINO_EMULATOR_WINDOWS diff --git a/ArduinoCore-Linux/cores/arduino/SocketImpl.h b/ArduinoCore-Linux/cores/arduino/SocketImpl.h index 1fab20d..bb95690 100644 --- a/ArduinoCore-Linux/cores/arduino/SocketImpl.h +++ b/ArduinoCore-Linux/cores/arduino/SocketImpl.h @@ -18,26 +18,30 @@ */ #pragma once +#include "DesktopSocket.h" +#include +#if !ARDUINO_EMULATOR_WINDOWS #include -#include +#endif +#include namespace arduino { class SocketImpl { public: SocketImpl() = default; - SocketImpl(int socket) { + SocketImpl(SocketHandle socket) { sock = socket; is_connected = true; memset(&serv_addr, 0, sizeof(serv_addr)); }; - SocketImpl(int socket, struct sockaddr_in* address) { + SocketImpl(SocketHandle socket, struct sockaddr_in* address) { sock = socket; is_connected = true; serv_addr = *address; }; virtual ~SocketImpl() { - if (sock != -1) { + if (sock != INVALID_SOCKET_HANDLE) { close(); } } @@ -63,11 +67,12 @@ class SocketImpl { virtual void setCACert(const char* cert) {} virtual void setInsecure() {} - int fd() { return sock; } + SocketHandle fd() { return sock; } protected: bool is_connected = false; - int sock = -1, valread; + SocketHandle sock = INVALID_SOCKET_HANDLE; + int valread = 0; struct sockaddr_in serv_addr; }; diff --git a/ArduinoCore-Linux/cores/arduino/SocketImplWindows.cpp b/ArduinoCore-Linux/cores/arduino/SocketImplWindows.cpp new file mode 100644 index 0000000..ed55b34 --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/SocketImplWindows.cpp @@ -0,0 +1,173 @@ +/* + SocketImplWindows.cpp + Copyright (c) 2025 Phil Schatzmann. All right reserved. + + Windows/Winsock implementation of SocketImpl. The original POSIX + implementation remains in SocketImpl.cpp. +*/ + +#include "SocketImpl.h" + +#if ARDUINO_EMULATOR_WINDOWS + +#include +#include +#include + +#include + +#include "ArduinoLogger.h" + +namespace arduino { + +const char *SOCKET_IMPL = "SocketImpl"; + +uint8_t SocketImpl::connected() { + ensureSocketRuntime(); + if (sock == INVALID_SOCKET_HANDLE) return false; + + char buffer[1]; + int result = ::recv(sock, buffer, 1, MSG_PEEK); + if (result > 0) return is_connected = true; + if (result == 0) return is_connected = false; + if (socketWouldBlock(socketLastError())) return is_connected; + return is_connected = false; +} + +int SocketImpl::connect(const char *address, uint16_t port) { + ensureSocketRuntime(); + + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + addrinfo *resolved = nullptr; + char service[8]; + snprintf(service, sizeof(service), "%u", port); + + if (::getaddrinfo(address, service, &hints, &resolved) != 0 || !resolved) { + Logger.error(SOCKET_IMPL, "invalid address"); + return -2; + } + + sock = ::socket(resolved->ai_family, resolved->ai_socktype, + resolved->ai_protocol); + if (sock == INVALID_SOCKET_HANDLE) { + freeaddrinfo(resolved); + Logger.error(SOCKET_IMPL, "could not create socket"); + return -1; + } + + std::memcpy(&serv_addr, resolved->ai_addr, + std::min(sizeof(serv_addr), + static_cast(resolved->ai_addrlen))); + int result = ::connect(sock, resolved->ai_addr, + static_cast(resolved->ai_addrlen)); + freeaddrinfo(resolved); + + if (result < 0) { + close(); + Logger.error(SOCKET_IMPL, "could not connect"); + return -3; + } + + setSocketNonBlocking(sock); + is_connected = true; + Logger.info(SOCKET_IMPL, "connected!"); + return 1; +} + +size_t SocketImpl::write(const uint8_t *data, size_t length) { + ensureSocketRuntime(); + return ::send(sock, reinterpret_cast(data), + static_cast(length), 0); +} + +size_t SocketImpl::available() { + ensureSocketRuntime(); + u_long bytes = 0; + ioctlsocket(sock, FIONREAD, &bytes); + return bytes; +} + +size_t SocketImpl::read(uint8_t *buffer, size_t length) { + ensureSocketRuntime(); + int result = ::recv(sock, reinterpret_cast(buffer), + static_cast(length), 0); + if (result < 0 && socketWouldBlock(socketLastError())) return 0; + return result > 0 ? static_cast(result) : 0; +} + +int SocketImpl::peek() { + ensureSocketRuntime(); + char buffer[1]; + int result = ::recv(sock, buffer, 1, MSG_PEEK); + return result > 0 ? static_cast(buffer[0]) : -1; +} + +void SocketImpl::close() { + Logger.info(SOCKET_IMPL, "close"); + if (sock != INVALID_SOCKET_HANDLE) closeSocket(sock); + sock = INVALID_SOCKET_HANDLE; + is_connected = false; +} + +const char *SocketImpl::getIPAddress() { + return getIPAddress(nullptr); +} + +const char *SocketImpl::getIPAddress(const char *validEntries[]) { + static char address[INET_ADDRSTRLEN] = "127.0.0.1"; + ensureSocketRuntime(); + + ULONG buffer_length = 0; + if (GetAdaptersAddresses(AF_INET, GAA_FLAG_INCLUDE_PREFIX, nullptr, nullptr, + &buffer_length) != ERROR_BUFFER_OVERFLOW) { + return address; + } + + std::vector buffer(buffer_length); + auto *adapters = reinterpret_cast(buffer.data()); + if (GetAdaptersAddresses(AF_INET, GAA_FLAG_INCLUDE_PREFIX, nullptr, adapters, + &buffer_length) != NO_ERROR) { + return address; + } + + for (auto *adapter = adapters; adapter != nullptr; adapter = adapter->Next) { + if (adapter->OperStatus != IfOperStatusUp) continue; + + bool is_valid = validEntries == nullptr; + for (size_t i = 0; !is_valid && validEntries[i] != nullptr; ++i) { + if (std::strcmp(adapter->AdapterName, validEntries[i]) == 0) { + is_valid = true; + break; + } + + int length = WideCharToMultiByte(CP_UTF8, 0, adapter->FriendlyName, -1, + nullptr, 0, nullptr, nullptr); + if (length <= 0) continue; + std::vector friendly_name(length); + WideCharToMultiByte(CP_UTF8, 0, adapter->FriendlyName, -1, + friendly_name.data(), length, nullptr, nullptr); + if (std::strcmp(friendly_name.data(), validEntries[i]) == 0) { + is_valid = true; + break; + } + } + if (!is_valid) continue; + + for (auto *entry = adapter->FirstUnicastAddress; entry != nullptr; + entry = entry->Next) { + if (entry->Address.lpSockaddr->sa_family == AF_INET) { + auto *ipv4 = reinterpret_cast(entry->Address.lpSockaddr); + if (inet_ntop(AF_INET, &ipv4->sin_addr, address, sizeof(address))) { + return address; + } + } + } + } + return address; +} + +} // namespace arduino + +#endif // ARDUINO_EMULATOR_WINDOWS diff --git a/ArduinoCore-Linux/cores/arduino/StdioDevice.h b/ArduinoCore-Linux/cores/arduino/StdioDevice.h index de43dbd..b7f21da 100644 --- a/ArduinoCore-Linux/cores/arduino/StdioDevice.h +++ b/ArduinoCore-Linux/cores/arduino/StdioDevice.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include #include #if defined(__unix__) || defined(__APPLE__) diff --git a/ArduinoCore-Linux/cores/arduino/Stream.h b/ArduinoCore-Linux/cores/arduino/Stream.h new file mode 100644 index 0000000..e53bcbc --- /dev/null +++ b/ArduinoCore-Linux/cores/arduino/Stream.h @@ -0,0 +1,4 @@ +#pragma once + +// Compatibility include for libraries that expect the classic Arduino header +#include "api/Stream.h" diff --git a/ArduinoCore-Linux/cores/arduino/UDP.cpp b/ArduinoCore-Linux/cores/arduino/UDP.cpp index f15d883..4695150 100644 --- a/ArduinoCore-Linux/cores/arduino/UDP.cpp +++ b/ArduinoCore-Linux/cores/arduino/UDP.cpp @@ -17,19 +17,14 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "EthernetUdp.h" -#include -#include -#include -#include -#include - +#include "DesktopSocket.h" #undef write #undef read namespace arduino { EthernetUDP::EthernetUDP() - : udp_server(-1), + : udp_server(INVALID_SOCKET_HANDLE), server_port(0), remote_port(0), tx_buffer(0), @@ -41,6 +36,7 @@ EthernetUDP::EthernetUDP() EthernetUDP::~EthernetUDP() { stop(); + ensureSocketRuntime(); auto &udp_list = active_udp(); auto it = std::find(udp_list.begin(), udp_list.end(), this); @@ -51,23 +47,24 @@ EthernetUDP::~EthernetUDP() { uint8_t EthernetUDP::begin(IPAddress address, uint16_t port) { stop(); + ensureSocketRuntime(); server_port = port; tx_buffer = new char[1460]; if (!tx_buffer) { - log_e("EthernetUDP: could not create tx buffer: %d", errno); + log_e("EthernetUDP: could not create tx buffer: %d", socketLastError()); return 0; } - if ((udp_server = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { - log_e("EthernetUDP: could not create socket: %d", errno); + if ((udp_server = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET_HANDLE) { + log_e("EthernetUDP: could not create socket: %d", socketLastError()); return 0; } int yes = 1; - if (setsockopt(udp_server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { - log_e("EthernetUDP: could not set socket option: %d", errno); + if (setsockopt(udp_server, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes)) < 0) { + log_e("EthernetUDP: could not set socket option: %d", socketLastError()); stop(); return 0; } @@ -76,27 +73,27 @@ uint8_t EthernetUDP::begin(IPAddress address, uint16_t port) { memset((char *)&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(server_port); - addr.sin_addr.s_addr = (in_addr_t)address; + addr.sin_addr.s_addr = (uint32_t)address; if (bind(udp_server, (struct sockaddr *)&addr, sizeof(addr)) == -1) { - log_e("EthernetUDP: could not bind socket: %d", errno); + log_e("EthernetUDP: could not bind socket: %d", socketLastError()); stop(); return 0; } - fcntl(udp_server, F_SETFL, O_NONBLOCK); + setSocketNonBlocking(udp_server); return 1; } -uint8_t EthernetUDP::begin(uint16_t p) { return begin(IPAddress(INADDR_ANY), p); } +uint8_t EthernetUDP::begin(uint16_t p) { return begin(IPAddress((uint32_t)INADDR_ANY), p); } uint8_t EthernetUDP::beginMulticast(IPAddress a, uint16_t p) { - if (begin(IPAddress(INADDR_ANY), p)) { + if (begin(IPAddress((uint32_t)INADDR_ANY), p)) { if ((uint32_t)a != 0) { struct ip_mreq mreq; - mreq.imr_multiaddr.s_addr = (in_addr_t)a; + mreq.imr_multiaddr.s_addr = (uint32_t)a; mreq.imr_interface.s_addr = INADDR_ANY; - if (setsockopt(udp_server, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, + if (setsockopt(udp_server, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char*)&mreq, sizeof(mreq)) < 0) { - log_e("EthernetUDP: could not join igmp: %d", errno); + log_e("EthernetUDP: could not join igmp: %d", socketLastError()); stop(); return 0; } @@ -118,20 +115,20 @@ void EthernetUDP::stop() { rx_buffer = NULL; delete b; } - if (udp_server == -1) return; + if (udp_server == INVALID_SOCKET_HANDLE) return; if ((uint32_t)multicast_ip != 0) { struct ip_mreq mreq; - mreq.imr_multiaddr.s_addr = (in_addr_t)multicast_ip; - mreq.imr_interface.s_addr = (in_addr_t)0; - setsockopt(udp_server, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)); - multicast_ip = IPAddress(INADDR_ANY); + mreq.imr_multiaddr.s_addr = (uint32_t)multicast_ip; + mreq.imr_interface.s_addr = 0; + setsockopt(udp_server, IPPROTO_IP, IP_DROP_MEMBERSHIP, (const char*)&mreq, sizeof(mreq)); + multicast_ip = IPAddress((uint32_t)INADDR_ANY); } - close(udp_server); - udp_server = -1; + closeSocket(udp_server); + udp_server = INVALID_SOCKET_HANDLE; } int EthernetUDP::beginMulticastPacket() { - if (!server_port || multicast_ip == IPAddress(INADDR_ANY)) return 0; + if (!server_port || multicast_ip == IPAddress((uint32_t)INADDR_ANY)) return 0; remote_ip = multicast_ip; remote_port = server_port; return beginPacket(); @@ -140,11 +137,13 @@ int EthernetUDP::beginMulticastPacket() { int EthernetUDP::beginPacket() { if (!remote_port) return 0; + ensureSocketRuntime(); + // allocate tx_buffer if is necessary if (!tx_buffer) { tx_buffer = new char[1460]; if (!tx_buffer) { - log_e("EthernetUDP: could not create tx buffer: %d", errno); + log_e("EthernetUDP: could not create tx buffer: %d", socketLastError()); return 0; } } @@ -152,14 +151,14 @@ int EthernetUDP::beginPacket() { tx_buffer_len = 0; // check whereas socket is already open - if (udp_server != -1) return 1; + if (udp_server != INVALID_SOCKET_HANDLE) return 1; - if ((udp_server = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { - log_e("EthernetUDP: could not create socket: %d", errno); + if ((udp_server = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET_HANDLE) { + log_e("EthernetUDP: could not create socket: %d", socketLastError()); return 0; } - fcntl(udp_server, F_SETFL, O_NONBLOCK); + setSocketNonBlocking(udp_server); return 1; } @@ -171,14 +170,17 @@ int EthernetUDP::beginPacket(IPAddress ip, uint16_t port) { } int EthernetUDP::beginPacket(const char *host, uint16_t port) { - struct hostent *server; - server = gethostbyname(host); - if (server == NULL) { - log_e("EthernetUDP: could not get host from dns: %d", errno); + addrinfo hints{}; + hints.ai_family = AF_INET; + addrinfo* info = nullptr; + if (getaddrinfo(host, nullptr, &hints, &info) != 0 || info == nullptr) { + log_e("EthernetUDP: could not get host from dns: %d", socketLastError()); return 0; } - return beginPacket(IPAddress((const uint8_t *)(server->h_addr_list[0])), - port); + auto* address = reinterpret_cast(info->ai_addr); + IPAddress ip(address->sin_addr.s_addr); + freeaddrinfo(info); + return beginPacket(ip, port); } int EthernetUDP::endPacket() { @@ -189,7 +191,7 @@ int EthernetUDP::endPacket() { int sent = sendto(udp_server, tx_buffer, tx_buffer_len, 0, (struct sockaddr *)&recipient, sizeof(recipient)); if (sent < 0) { - log_e("EthernetUDP: could not send data: %d", errno); + log_e("EthernetUDP: could not send data: %d", socketLastError()); return 0; } return 1; @@ -218,14 +220,14 @@ int EthernetUDP::parsePacket() { if (!buf) { return 0; } - if ((len = recvfrom(udp_server, buf, 1460, MSG_DONTWAIT, - (struct sockaddr *)&si_other, (socklen_t *)&slen)) == - -1) { + if ((len = recvfrom(udp_server, buf, 1460, 0, + (struct sockaddr *)&si_other, (SocketLength *)&slen)) < 0) { delete[] buf; - if (errno == EWOULDBLOCK) { + const int error = socketLastError(); + if (socketWouldBlock(error) || socketConnectionReset(error)) { return 0; } - log_e("EthernetUDP: could not receive data: %d", errno); + log_e("EthernetUDP: could not receive data: %d", error); return 0; } remote_ip = IPAddress(si_other.sin_addr.s_addr); @@ -292,4 +294,4 @@ void EthernetUDP::log_e(const char *msg, int errorNo) { Logger.error(errorStr); } -} // namespace arduino \ No newline at end of file +} // namespace arduino diff --git a/ArduinoCore-Linux/cores/arduino/UDP.h b/ArduinoCore-Linux/cores/arduino/UDP.h index 0547491..f9d2991 100644 --- a/ArduinoCore-Linux/cores/arduino/UDP.h +++ b/ArduinoCore-Linux/cores/arduino/UDP.h @@ -18,6 +18,7 @@ */ #pragma once +#include "DesktopSocket.h" #include "api/IPAddress.h" #include "api/Udp.h" #include @@ -29,7 +30,7 @@ namespace arduino { class EthernetUDP : public UDP { private: - int udp_server; + SocketHandle udp_server; IPAddress multicast_ip; IPAddress remote_ip; uint16_t server_port; diff --git a/ArduinoCore-Linux/cores/arduino/cbuf.h b/ArduinoCore-Linux/cores/arduino/cbuf.h index 490352e..a771339 100644 --- a/ArduinoCore-Linux/cores/arduino/cbuf.h +++ b/ArduinoCore-Linux/cores/arduino/cbuf.h @@ -23,7 +23,7 @@ #include #include -#include +#include class cbuf { diff --git a/ArduinoCore-Linux/cores/arduino/serialib.cpp b/ArduinoCore-Linux/cores/arduino/serialib.cpp index 8eeb987..f411954 100644 --- a/ArduinoCore-Linux/cores/arduino/serialib.cpp +++ b/ArduinoCore-Linux/cores/arduino/serialib.cpp @@ -14,6 +14,7 @@ This is a licence-free software, it can be used by anyone who try to build a bet #if PROVIDE_SERIALLIB #include "serialib.h" +#include //_____________________________________ @@ -93,7 +94,14 @@ char serialib::openDevice(const char *Device,const unsigned int Bauds) { #if defined (_WIN32) || defined( _WIN64) // Open serial port - hSerial = CreateFileA(Device,GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0); + // Win32 requires the \\\.\\ prefix for COM ports 10 and above. + char extendedDevice[64]; + const char *deviceName = Device; + if (Device != NULL && strncmp(Device, "COM", 3) == 0 && atoi(Device + 3) >= 10) { + snprintf(extendedDevice, sizeof(extendedDevice), "\\\\.\\%s", Device); + deviceName = extendedDevice; + } + hSerial = CreateFileA(deviceName,GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0); if(hSerial==INVALID_HANDLE_VALUE) { if(GetLastError()==ERROR_FILE_NOT_FOUND) return -1; // Device not found @@ -951,4 +959,4 @@ unsigned long int timeOut::elapsedTime_ms() return sec*1000+usec/1000; } -#endif \ No newline at end of file +#endif diff --git a/ArduinoCore-Linux/libraries/SD.h b/ArduinoCore-Linux/libraries/SD.h index 2efafd0..68bf150 100644 --- a/ArduinoCore-Linux/libraries/SD.h +++ b/ArduinoCore-Linux/libraries/SD.h @@ -21,6 +21,9 @@ #include #include #include +#if defined(_WIN32) +#include +#endif #include #include @@ -251,7 +254,13 @@ class SdFat { } bool remove(const char *name) { return std::remove(name) == 0; } - bool mkdir(const char *name) { return ::mkdir(name, 0777) == 0; } + bool mkdir(const char *name) { +#if defined(_WIN32) + return ::_mkdir(name) == 0; +#else + return ::mkdir(name, 0777) == 0; +#endif + } bool rmdir(const char *path) { int rc = ::rmdir(path); return rc == 0; diff --git a/CMakeLists.txt b/CMakeLists.txt index 41b0e60..595bf06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.11...3.19) # Project name and a few useful settings. Other commands can pick up the results -project(arduino_emulator VERSION 0.1 DESCRIPTION "Arduino Emulator for Linux" LANGUAGES C CXX) +project(arduino_emulator VERSION 0.1 DESCRIPTION "Desktop Arduino Emulator" LANGUAGES C CXX) # Https takes quite some time and is not always neede option(USE_HTTPS "Https Support" OFF) @@ -11,6 +11,7 @@ option(USE_REMOTE "Remote API Support" OFF) option(USE_FIRMATA "Firmata GPIO Support" OFF) option(BUILD_SHARED_LIBS "Build with shared libraries" OFF) option(BUILD_EXAMPLES "Build with examples" OFF) +option(USE_FTDI "FTDI hardware support" OFF) # Https support: Build with wolfssl if(USE_HTTPS) @@ -40,10 +41,20 @@ add_definitions(-DHOST) file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-API/api/*.h") file(GLOB SRC_LIST CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-API/api/*.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/arduino/*.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/rasperry_pi/*.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/ftdi/*.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/firmata/*.cpp") + "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/arduino/*.cpp") + +if(USE_RPI) + file(GLOB RPI_SRC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/rasperry_pi/*.cpp") + list(APPEND SRC_LIST ${RPI_SRC}) +endif() +if(USE_FTDI) + file(GLOB FTDI_SRC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/ftdi/*.cpp") + list(APPEND SRC_LIST ${FTDI_SRC}) +endif() +if(USE_FIRMATA) + file(GLOB FIRMATA_SRC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/firmata/*.cpp") + list(APPEND SRC_LIST ${FIRMATA_SRC}) +endif() # Make an dynamic library add_library(arduino_emulator ${SRC_LIST} ${HEADER_LIST}) @@ -74,6 +85,11 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/ArduinoCore-Linux/cores/arduino/" # All users of this library will need at least C++17 target_compile_features(arduino_emulator PUBLIC cxx_std_17) +if(WIN32) + target_compile_definitions(arduino_emulator PUBLIC ARDUINO_EMULATOR_WINDOWS=1) + target_link_libraries(arduino_emulator PUBLIC ws2_32) +endif() + # Include Arduino library functions include(${CMAKE_CURRENT_SOURCE_DIR}/Arduino.cmake) diff --git a/examples/platformio-native/src/entry.cpp b/examples/platformio-native/src/entry.cpp new file mode 100644 index 0000000..5a1191c --- /dev/null +++ b/examples/platformio-native/src/entry.cpp @@ -0,0 +1,19 @@ +// Keep the host entry point in the same translation unit as the sketch. The +// forward declaration makes this application's entry point strong even though +// the compatibility API declares a weak fallback main(). +int main(); +#include +#include "main.cpp" + +// MinGW's native PlatformIO runner uses the Windows subsystem startup object. +// This entry point keeps the example independent of the API's weak main(). +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, + int nShowCmd) { + (void)hInstance; + (void)hPrevInstance; + (void)lpCmdLine; + (void)nShowCmd; + hardwareSetup(); + setup(); + for (;;) loop(); +} diff --git a/examples/platformio-native/src/main.cpp b/examples/platformio-native/src/main.cpp new file mode 100644 index 0000000..b87156a --- /dev/null +++ b/examples/platformio-native/src/main.cpp @@ -0,0 +1,11 @@ +#include + +void setup() { + Serial.println("Arduino Emulator native platform"); + pinMode(13, OUTPUT); +} + +void loop() { + digitalWrite(13, !digitalRead(13)); + delay(10); +} diff --git a/library.json b/library.json index 41d3b83..474ea5c 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Arduino-Emulator", - "version": "0.1.0", + "version": "0.1.8", "description": "Desktop Arduino compatibility runtime", "keywords": ["arduino", "emulator", "native"], "frameworks": "*", @@ -10,7 +10,6 @@ "includeDir": ".", "flags": [ "-I ArduinoCore-API", - "-I ArduinoCore-API/api", "-I ArduinoCore-Linux/cores/arduino", "-I ArduinoCore-Linux/libraries" ], diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..c82eecc --- /dev/null +++ b/platformio.ini @@ -0,0 +1,14 @@ +[platformio] +src_dir = examples/platformio-native/src +test_dir = test + +[env:native] +platform = native +test_framework = unity +build_flags = + -std=gnu++17 + -DARDUINO_EMULATOR +lib_deps = + file://. +extra_scripts = pre:platformio_extra.py +build_src_filter = +<*> - diff --git a/platformio_extra.py b/platformio_extra.py new file mode 100644 index 0000000..8e0e66c --- /dev/null +++ b/platformio_extra.py @@ -0,0 +1,7 @@ +Import("env") +import sys + +# Winsock is a system library on MinGW and is not needed on POSIX hosts. +if sys.platform == "win32": + env.Append(LIBS=["ws2_32", "iphlpapi"]) + env.Append(LINKFLAGS=["-mconsole"]) diff --git a/test/native/test_native.cpp b/test/native/test_native.cpp new file mode 100644 index 0000000..dc8e350 --- /dev/null +++ b/test/native/test_native.cpp @@ -0,0 +1,249 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +using namespace arduino; + +namespace { + +class MockGPIO final : public HardwareGPIO { + public: + std::array modes{}; + std::array digital{}; + std::array analog{}; + uint8_t reference = 0; + int pwm_pin = -1; + int pwm_value = -1; + uint8_t resolution = 0; + uint32_t frequency = 0; + + void pinMode(pin_size_t pin, PinMode mode) override { modes.at(pin) = mode; } + void digitalWrite(pin_size_t pin, PinStatus status) override { digital.at(pin) = status; } + PinStatus digitalRead(pin_size_t pin) override { return digital.at(pin); } + int analogRead(pin_size_t pin) override { return analog.at(pin); } + void analogReference(uint8_t mode) override { reference = mode; } + void analogWrite(pin_size_t pin, int value) override { + pwm_pin = pin; + pwm_value = value; + } + void tone(uint8_t, unsigned int, unsigned long) override {} + void noTone(uint8_t) override {} + unsigned long pulseIn(uint8_t, uint8_t, unsigned long) override { return 123; } + unsigned long pulseInLong(uint8_t, uint8_t, unsigned long) override { return 456; } + void analogWriteFrequency(pin_size_t, uint32_t value) override { frequency = value; } + void analogWriteResolution(uint8_t bits) override { resolution = bits; } +}; + +void test_timing() { + const unsigned long before = millis(); + delay(15); + TEST_ASSERT_TRUE(millis() - before >= 10); + + const unsigned long micros_before = micros(); + delayMicroseconds(1000); + TEST_ASSERT_TRUE(micros() - micros_before >= 500); +} + +void test_gpio_delegation() { + MockGPIO mock; + GPIO.setGPIO(&mock); + + pinMode(3, OUTPUT); + digitalWrite(3, HIGH); + analogWrite(4, 127); + analogReference(2); + analogWriteResolution(10); + analogWriteFrequency(4, 1000); + + TEST_ASSERT_EQUAL(OUTPUT, mock.modes[3]); + TEST_ASSERT_EQUAL(HIGH, digitalRead(3)); + TEST_ASSERT_EQUAL(4, mock.pwm_pin); + TEST_ASSERT_EQUAL(127, mock.pwm_value); + TEST_ASSERT_EQUAL_UINT8(2, mock.reference); + TEST_ASSERT_EQUAL_UINT8(10, mock.resolution); + TEST_ASSERT_EQUAL_UINT32(1000, mock.frequency); + TEST_ASSERT_EQUAL_UINT32(123, pulseIn(3, HIGH, 100)); + TEST_ASSERT_EQUAL_UINT32(456, pulseInLong(3, HIGH, 100)); + + GPIO.setGPIO(nullptr); +} + +void test_arduino_types() { + String value = String("Arduino") + " Emulator"; + TEST_ASSERT_EQUAL_UINT16(16, value.length()); + TEST_ASSERT_TRUE(value.startsWith("Arduino")); + TEST_ASSERT_TRUE(value.endsWith("Emulator")); + + IPAddress address(192, 168, 1, 42); + TEST_ASSERT_EQUAL_UINT8(192, address[0]); + TEST_ASSERT_EQUAL_UINT8(42, address[3]); + TEST_ASSERT_EQUAL_STRING("192.168.1.42", address.toString().c_str()); +} + +void test_ethernet_configuration() { + uint8_t mac[] = {0, 1, 2, 3, 4, 5}; + const IPAddress local(10, 0, 0, 20); + Ethernet.begin(mac, local, IPAddress(1, 1, 1, 1), + IPAddress(10, 0, 0, 1), IPAddress(255, 255, 255, 0)); + + uint8_t returned_mac[6] = {}; + Ethernet.MACAddress(returned_mac); + TEST_ASSERT_EQUAL_UINT8_ARRAY(mac, returned_mac, 6); + TEST_ASSERT_EQUAL(local, Ethernet.localIP()); + TEST_ASSERT_EQUAL(1, Ethernet.hardwareStatus()); + TEST_ASSERT_EQUAL(2, Ethernet.linkStatus()); +} + +void test_socket_error_mapping() { +#if defined(_WIN32) + TEST_ASSERT_TRUE(socketWouldBlock(WSAEWOULDBLOCK)); + TEST_ASSERT_TRUE(socketConnectionReset(WSAECONNRESET)); +#else + TEST_ASSERT_TRUE(socketWouldBlock(EAGAIN)); + TEST_ASSERT_TRUE(socketConnectionReset(ECONNRESET)); +#endif +} + +void test_sd_filesystem() { + const std::filesystem::path directory = + std::filesystem::current_path() / "sd_unit_test_directory"; + const std::filesystem::path filename = directory / "record.txt"; + SD.remove(filename.string().c_str()); + SD.rmdir(directory.string().c_str()); + + TEST_ASSERT_TRUE(SD.begin()); + TEST_ASSERT_TRUE(SD.mkdir(directory.string().c_str())); + TEST_ASSERT_TRUE(SD.exists(directory.string().c_str())); + + File output = SD.open(filename.string().c_str(), O_WRITE); + TEST_ASSERT_TRUE(output); + const char content[] = "sd-emulator"; + TEST_ASSERT_EQUAL(sizeof(content) - 1, + output.write(reinterpret_cast(content), + sizeof(content) - 1)); + TEST_ASSERT_TRUE(output.close()); + + File input = SD.open(filename.string().c_str(), O_READ); + TEST_ASSERT_TRUE(input); + char readback[sizeof(content)] = {}; + TEST_ASSERT_EQUAL(sizeof(content) - 1, + input.readBytes(readback, sizeof(readback) - 1)); + TEST_ASSERT_EQUAL_STRING(content, readback); + TEST_ASSERT_TRUE(input.close()); + + TEST_ASSERT_TRUE(SD.remove(filename.string().c_str())); + TEST_ASSERT_TRUE(SD.rmdir(directory.string().c_str())); + TEST_ASSERT_FALSE(SD.exists(filename.string().c_str())); +} + +void test_udp_loopback() { + constexpr uint16_t port = 45871; + EthernetUDP receiver; + EthernetUDP sender; + TEST_ASSERT_EQUAL_UINT8(1, receiver.begin(port)); + TEST_ASSERT_EQUAL_UINT8(1, sender.begin(port + 1)); + + const char payload[] = "udp-loopback"; + TEST_ASSERT_EQUAL(1, sender.beginPacket(IPAddress(127, 0, 0, 1), port)); + TEST_ASSERT_EQUAL(sizeof(payload) - 1, sender.write(reinterpret_cast(payload), sizeof(payload) - 1)); + TEST_ASSERT_EQUAL(1, sender.endPacket()); + + TEST_ASSERT_TRUE(receiver.parsePacket() > 0); + char received[sizeof(payload)] = {}; + TEST_ASSERT_EQUAL(sizeof(payload) - 1, receiver.read(received, sizeof(received) - 1)); + TEST_ASSERT_EQUAL_STRING(payload, received); + TEST_ASSERT_EQUAL_UINT16(port + 1, receiver.remotePort()); + receiver.stop(); + sender.stop(); +} + +void test_udp_send_then_receive_on_same_socket() { + constexpr uint16_t simulator_port = 45873; + constexpr uint16_t backend_port = 45874; + EthernetUDP simulator; + EthernetUDP backend; + TEST_ASSERT_EQUAL_UINT8(1, simulator.begin(simulator_port)); + TEST_ASSERT_EQUAL_UINT8(1, backend.begin(backend_port)); + + const char telemetry[] = "telemetry"; + TEST_ASSERT_EQUAL(1, simulator.beginPacket(IPAddress(127, 0, 0, 1), backend_port)); + TEST_ASSERT_EQUAL(sizeof(telemetry) - 1, + simulator.write(reinterpret_cast(telemetry), sizeof(telemetry) - 1)); + TEST_ASSERT_EQUAL(1, simulator.endPacket()); + TEST_ASSERT_TRUE(backend.parsePacket() > 0); + backend.flush(); + + const uint8_t command[] = {'c', 0x00, 'm', 'm', 'a', 'n', 'd'}; + TEST_ASSERT_EQUAL(1, backend.beginPacket(IPAddress(127, 0, 0, 1), simulator_port)); + TEST_ASSERT_EQUAL(sizeof(command), backend.write(command, sizeof(command))); + TEST_ASSERT_EQUAL(1, backend.endPacket()); + TEST_ASSERT_TRUE(simulator.parsePacket() > 0); + char received[sizeof(command)] = {}; + TEST_ASSERT_EQUAL(sizeof(command), simulator.read(received, sizeof(received))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(command, reinterpret_cast(received), sizeof(command)); + TEST_ASSERT_EQUAL_UINT16(backend_port, simulator.remotePort()); + simulator.stop(); + backend.stop(); +} + +void test_tcp_loopback() { + constexpr uint16_t port = 45872; + EthernetServer server(port); + server.begin(); + TEST_ASSERT_EQUAL(WL_CONNECTED, server.status()); + + EthernetClient client; + TEST_ASSERT_EQUAL(1, client.connect(IPAddress(127, 0, 0, 1), port)); + EthernetClient accepted = server.available(); + TEST_ASSERT_TRUE(accepted); + + const char payload[] = "tcp-loopback"; + TEST_ASSERT_EQUAL(sizeof(payload) - 1, client.write(reinterpret_cast(payload), sizeof(payload) - 1)); + char received[sizeof(payload)] = {}; + TEST_ASSERT_EQUAL(sizeof(payload) - 1, accepted.readBytes(received, sizeof(received) - 1)); + TEST_ASSERT_EQUAL_STRING(payload, received); + + accepted.stop(); + client.stop(); + server.stop(); +} + +} // namespace + +void setUp() {} +void tearDown() {} + +void setup_test() { + UNITY_BEGIN(); + RUN_TEST(test_timing); + RUN_TEST(test_gpio_delegation); + RUN_TEST(test_arduino_types); + RUN_TEST(test_ethernet_configuration); + RUN_TEST(test_socket_error_mapping); + RUN_TEST(test_sd_filesystem); + RUN_TEST(test_udp_loopback); + RUN_TEST(test_udp_send_then_receive_on_same_socket); + RUN_TEST(test_tcp_loopback); + UNITY_END(); +} + +static int run_tests() { + setup_test(); + return 0; +} + +#if defined(_WIN32) +int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { return run_tests(); } +#else +int main() { return run_tests(); } +#endif diff --git a/test/test_native/test_native.cpp b/test/test_native/test_native.cpp new file mode 100644 index 0000000..26f4d17 --- /dev/null +++ b/test/test_native/test_native.cpp @@ -0,0 +1,3 @@ +// Keep the implementation in the native test source while using PlatformIO's +// standard test_* suite layout for Unity configuration and discovery. +#include "../native/test_native.cpp"