diff --git a/.gitignore b/.gitignore index d48c759..8bc50b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea -.vscode \ No newline at end of file +.vscode + +CMakeLists.txt \ No newline at end of file diff --git a/examples/client/client.ino b/examples/client/client.ino new file mode 100644 index 0000000..7fccb58 --- /dev/null +++ b/examples/client/client.ino @@ -0,0 +1,52 @@ +#include + +BridgeTCPClient<> client(Bridge); + +void setup() { + + if (!Bridge.begin()) { + while (true) {} + } + + if (!Monitor.begin()) { + while (true) {} + } + +} + +void loop() { + + Monitor.println("\nStarting connection to server..."); + /* if you get a connection, report back via serial: */ + if (client.connect("arduino.tips", 80) < 0) { + Monitor.println("unable to connect to server"); + return; + } + + Monitor.println("connected to server"); + /* Make an HTTP request: */ + size_t w = client.println("GET /asciilogo.txt HTTP/1.1"); + w += client.println("Host: arduino.tips"); + w += client.println("User-Agent: Arduino"); + w += client.println("Connection: close"); + w += client.println(); + + /* if there are incoming bytes available from the server, + * read them and print them: + */ + while (client.connected()) { + size_t len = client.available(); + if (len) { + uint8_t buff[len]; + client.read(buff, len); + Monitor.write(buff, len); + } + delay(0); + } + + /* if the server's disconnected, stop the client: */ + Monitor.println(); + Monitor.println("disconnecting from server."); + client.stop(); + delay(1000); +} diff --git a/examples/clientSSL/clientSSL.ino b/examples/clientSSL/clientSSL.ino new file mode 100644 index 0000000..9e8c269 --- /dev/null +++ b/examples/clientSSL/clientSSL.ino @@ -0,0 +1,68 @@ +#include + +static const char ca_cert[] = { +"-----BEGIN CERTIFICATE-----\n" +"MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD\n" +"VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG\n" +"A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw\n" +"WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz\n" +"IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n" +"AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi\n" +"QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR\n" +"HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" +"BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D\n" +"9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8\n" +"p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD\n" +"-----END CERTIFICATE-----\n" +}; + +BridgeTCPClient<> client(Bridge); + +void setup() { + + if (!Bridge.begin()) { + while (true) {} + } + + if (!Monitor.begin()) { + while (true) {} + } + +} + +void loop() { + Monitor.println("\nStarting connection to server..."); + + /* if you get a connection, report back via monitor: */ + if (client.connectSSL("arduino.tips", 443, ca_cert) < 0) { + Monitor.println("unable to connect to server"); + return; + } + + Monitor.println("connected to server"); + /* Make aHTTP request: */ + size_t w = client.println("GET /asciilogo.txt HTTP/1.1"); + w += client.println("Host: arduino.tips"); + w += client.println("User-Agent: Arduino"); + w += client.println("Connection: close"); + w += client.println(); + + /* if there are incoming bytes available from the server, + * read them and print them: + */ + while (client.connected()) { + size_t len = client.available(); + if (len) { + uint8_t buff[len]; + client.read(buff, len); + Monitor.write(buff, len); + } + delay(0); + } + + /* if the server's disconnected, stop the client: */ + Monitor.println(); + Monitor.println("disconnecting from server."); + client.stop(); + delay(1000); +} diff --git a/examples/server/python/main.py b/examples/server/python/main.py new file mode 100644 index 0000000..2a9148f --- /dev/null +++ b/examples/server/python/main.py @@ -0,0 +1,27 @@ +# A python sketch that uses RPC Bridge to test the server.ino + +import time +from arduino.app_utils import * + + +def log(msg): + with open("./log.log", "a") as f: + f.write(str(msg) + "\n") + +def main(): + res = Bridge.call("tcp/connect", "127.0.0.1", 5678) + log(f"Connection attempt id: {res}") + + written = Bridge.call("tcp/write", res, "Hello friend") + log(f"Written msg of len: {written}") + + time.sleep(1) + + ok = Bridge.call("tcp/close", res) + log(f"Closed connection: {ok}") + +if __name__ == "__main__": + + while True: + main() + time.sleep(1) diff --git a/examples/server/server.ino b/examples/server/server.ino new file mode 100644 index 0000000..b141034 --- /dev/null +++ b/examples/server/server.ino @@ -0,0 +1,46 @@ +#include + +IPAddress localhost(127, 0, 0, 1); +BridgeTCPServer<> server(Bridge, localhost, 5678); + +void setup() { + + if (!Bridge.begin()) { + while (true) {} + } + + if (!Monitor.begin()) { + while (true) {} + } + + server.begin(); + +} + +void loop() { + + BridgeTCPClient<> client = server.accept(); + + if (client.connected() == 1){ + Monitor.print("client "); + Monitor.print(client.getId()); + Monitor.println(" connected"); + } + + if (client) { + Monitor.println("A client established a connection"); + } + + while (client.connected()) { + size_t len = client.available(); + if (len) { + Monitor.println("Message received from client"); + uint8_t buff[len]; + client.read(buff, len); + Monitor.write(buff, len); + } + } + + server.disconnect(); // Disconnects the client server-side + +} diff --git a/src/Arduino_RouterBridge.h b/src/Arduino_RouterBridge.h index 26f6b51..3174d0f 100644 --- a/src/Arduino_RouterBridge.h +++ b/src/Arduino_RouterBridge.h @@ -15,5 +15,7 @@ #include "Arduino.h" #include "bridge.h" #include "monitor.h" +#include "tcp_client.h" +#include "tcp_server.h" #endif //ARDUINO_ROUTER_BRIDGE_H diff --git a/src/tcp_client.h b/src/tcp_client.h new file mode 100644 index 0000000..8874314 --- /dev/null +++ b/src/tcp_client.h @@ -0,0 +1,219 @@ +/* + This file is part of the Arduino_RouterBridge library. + + Copyright (c) 2025 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +*/ + +#pragma once + +#ifndef BRIDGE_TCP_CLIENT_H +#define BRIDGE_TCP_CLIENT_H + +#define TCP_CONNECT_METHOD "tcp/connect" +#define TCP_CONNECT_SSL_METHOD "tcp/connectSSL" +#define TCP_CLOSE_METHOD "tcp/close" +#define TCP_WRITE_METHOD "tcp/write" +#define TCP_READ_METHOD "tcp/read" + +#include +#include +#include "bridge.h" + +#define DEFAULT_TCP_CLIENT_BUF_SIZE 512 + + +template +class BridgeTCPClient : public Client { + + BridgeClass* bridge; + uint32_t connection_id{}; + RingBufferN temp_buffer; + struct k_mutex client_mutex{}; + bool _connected = false; + +public: + explicit BridgeTCPClient(BridgeClass& bridge): bridge(&bridge) {} + + BridgeTCPClient(BridgeClass& bridge, uint32_t connection_id, bool connected=true): bridge(&bridge), connection_id(connection_id), _connected {connected} {} + + bool begin() { + k_mutex_init(&client_mutex); + if (!(*bridge)) { + return bridge->begin(); + } + return true; + } + + int connect(IPAddress ip, uint16_t port) override { + return connect(ip.toString().c_str(), port); + } + + int connect(const char *host, uint16_t port) override { + + if (_connected) return 0; + + String hostname = host; + + k_mutex_lock(&client_mutex, K_FOREVER); + + const bool resp = bridge->call(TCP_CONNECT_METHOD, connection_id, hostname, port); + + if (!resp) { + _connected = false; + k_mutex_unlock(&client_mutex); + return -1; + } + _connected = true; + + k_mutex_unlock(&client_mutex); + + return 0; + } + + int connectSSL(const char *host, uint16_t port, const char *ca_cert) { + + if (_connected) return 0; + + String hostname = host; + String ca_cert_str = ca_cert; + + k_mutex_lock(&client_mutex, K_FOREVER); + + const bool resp = bridge->call(TCP_CONNECT_SSL_METHOD, connection_id, hostname, port, ca_cert_str); + + if (!resp) { + _connected = false; + k_mutex_unlock(&client_mutex); + return -1; + } + _connected = true; + + k_mutex_unlock(&client_mutex); + return 0; + } + + uint32_t getId() const { + return connection_id; + } + + size_t write(uint8_t c) override { + return write(&c, 1); + } + + size_t write(const uint8_t *buf, size_t size) override { + + if (!_connected) return 0; + + MsgPack::arr_t payload; + + for (size_t i = 0; i < size; ++i) { + payload.push_back(buf[i]); + } + + size_t written; + const bool ret = bridge->call(TCP_WRITE_METHOD, written, connection_id, payload); + if (ret) { + return written; + } + + return 0; + } + + int available() override { + k_mutex_lock(&client_mutex, K_FOREVER); + const int size = temp_buffer.availableForStore(); + if (size > 0) _read(size); + const int _available = temp_buffer.available(); + k_mutex_unlock(&client_mutex); + return _available; + } + + int read() override { + uint8_t c; + read(&c, 1); + return c; + } + + int read(uint8_t *buf, size_t size) override { + k_mutex_lock(&client_mutex, K_FOREVER); + int i = 0; + while (temp_buffer.available() && i < size) { + buf[i++] = temp_buffer.read_char(); + } + k_mutex_unlock(&client_mutex); + return i; + } + + int peek() override { + k_mutex_lock(&client_mutex, K_FOREVER); + if (temp_buffer.available()) { + k_mutex_unlock(&client_mutex); + return temp_buffer.peek(); + } + k_mutex_unlock(&client_mutex); + return -1; + } + + void flush() override { + // No-op: flush is implemented for Client subclasses using an output buffer + } + + void close() { + stop(); + } + + void stop() override { + k_mutex_lock(&client_mutex, K_FOREVER); + String msg; + const bool resp = bridge->call(TCP_CLOSE_METHOD, msg, connection_id); + if (resp) { + _connected = false; + } + k_mutex_unlock(&client_mutex); + } + + uint8_t connected() override { + if (_connected) return 1; + return 0; + } + + operator bool() override { + return available() || connected(); + } + + //friend class BridgeTCPServer; + + using Print::write; + +private: + void _read(size_t size) { + + if (size == 0 || !_connected) return; + + k_mutex_lock(&client_mutex, K_FOREVER); + + MsgPack::arr_t message; + const bool ret = bridge->call(TCP_READ_METHOD, message, connection_id, size); + + if (ret) { + for (size_t i = 0; i < message.size(); ++i) { + temp_buffer.store_char(static_cast(message[i])); + } + } + + if (bridge->get_last_client_error().code > NO_ERR) { + _connected = false; + } + + k_mutex_unlock(&client_mutex); + } + +}; + + +#endif //BRIDGE_TCP_CLIENT_H diff --git a/src/tcp_server.h b/src/tcp_server.h new file mode 100644 index 0000000..ad4c0a5 --- /dev/null +++ b/src/tcp_server.h @@ -0,0 +1,139 @@ +/* + This file is part of the Arduino_RouterBridge library. + + Copyright (c) 2025 Arduino SA + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +*/ + +#pragma once + +#ifndef BRIDGE_TCP_SERVER_H +#define BRIDGE_TCP_SERVER_H + +#define TCP_LISTEN_METHOD "tcp/listen" +#define TCP_ACCEPT_METHOD "tcp/accept" +#define TCP_CLOSE_LISTENER_METHOD "tcp/closeListener" + + +#include +#include "bridge.h" +#include "tcp_client.h" + +#define DEFAULT_TCP_SERVER_BUF_SIZE 512 + + +template +class BridgeTCPServer final: public Server { + BridgeClass* bridge; + IPAddress _addr{}; + uint16_t _port; + bool _listening = false; + uint32_t listener_id = 0; + uint32_t connection_id = 0; + bool _connected = false; + struct k_mutex server_mutex{}; + +public: + explicit BridgeTCPServer(BridgeClass& bridge, const IPAddress& addr, uint16_t port): bridge(&bridge), _addr(addr), _port(port) {} + + // explicit BridgeTCPServer(BridgeClass& bridge, uint16_t port): bridge(&bridge), _addr(INADDR_NONE), _port(port) {} + + void begin() override { + k_mutex_init(&server_mutex); + + if (!(*bridge)) { + while (!bridge->begin()); + } + + k_mutex_lock(&server_mutex, K_FOREVER); + + String hostname = _addr.toString(); + _listening = bridge->call(TCP_LISTEN_METHOD, listener_id, hostname, _port); + + k_mutex_unlock(&server_mutex); + + } + + BridgeTCPClient accept() { + + if (!_listening) { + return BridgeTCPClient(*bridge, 0, false); + } + + if (_connected) { + return BridgeTCPClient(*bridge, connection_id); + } + + k_mutex_lock(&server_mutex, K_FOREVER); + + const bool ret = bridge->call(TCP_ACCEPT_METHOD, connection_id, listener_id); + + k_mutex_unlock(&server_mutex); + + if (ret) { // connection_id 0 marks an invalid connection + _connected = true; + return BridgeTCPClient(*bridge, connection_id); + } else { + _connected = false; + // Return invalid client + return BridgeTCPClient(*bridge, 0, false); + } + + } + + size_t write(uint8_t c) override { + return write(&c, 1); + } + + size_t write(const uint8_t *buf, size_t size) override { + + BridgeTCPClient client = accept(); + + if (client && _connected) { + return client.write(buf, size); + } + + return 0; + } + + void close() { + k_mutex_lock(&server_mutex, K_FOREVER); + + String msg; + const bool ret = bridge->call(TCP_CLOSE_LISTENER_METHOD, msg, listener_id); + + if (ret) { + _listening = false; + } + + k_mutex_unlock(&server_mutex); + } + + void disconnect() { + k_mutex_lock(&server_mutex, K_FOREVER); + _connected = false; + connection_id = 0; + k_mutex_unlock(&server_mutex); + } + + bool is_listening() const { + return _listening; + } + + uint16_t getPort() const { + return _port; + } + + operator bool() const { + return _listening; + } + + using Print::write; + +}; + +#endif //BRIDGE_TCP_SERVER_H \ No newline at end of file