diff --git a/drivers/esp32spi/ip_address.cpp b/drivers/esp32spi/ip_address.cpp index 4ab06e08..ca8863d7 100644 --- a/drivers/esp32spi/ip_address.cpp +++ b/drivers/esp32spi/ip_address.cpp @@ -6,6 +6,12 @@ namespace pimoroni { addr.dword = 0; } + std::string IPAddress::to_string() { + char buf[16] = {0}; + snprintf(buf, 16, "%d.%d.%d.%d", addr.bytes[0], addr.bytes[1], addr.bytes[2], addr.bytes[3]); + return std::string(buf, 16); + } + IPAddress::IPAddress(uint8_t first_octet, uint8_t second_octet, uint8_t third_octet, uint8_t fourth_octet) { addr.bytes[0] = first_octet; addr.bytes[1] = second_octet; diff --git a/drivers/esp32spi/ip_address.hpp b/drivers/esp32spi/ip_address.hpp index 2fcdbc4e..31b29445 100644 --- a/drivers/esp32spi/ip_address.hpp +++ b/drivers/esp32spi/ip_address.hpp @@ -1,6 +1,7 @@ #pragma once #include "pico/stdlib.h" +#include namespace pimoroni { @@ -35,6 +36,7 @@ namespace pimoroni { uint8_t& operator[](int index); IPAddress& operator=(uint32_t address); operator uint32_t() const; + std::string to_string(); }; } \ No newline at end of file diff --git a/examples/pico_wireless/CMakeLists.txt b/examples/pico_wireless/CMakeLists.txt index 74f7786f..860c8552 100644 --- a/examples/pico_wireless/CMakeLists.txt +++ b/examples/pico_wireless/CMakeLists.txt @@ -1,14 +1,2 @@ -add_executable( - wireless_time - demo.cpp -) - -# enable usb output, disable uart output -pico_enable_stdio_usb(wireless_time 1) -pico_enable_stdio_uart(wireless_time 0) - -# Pull in pico libraries that we need -target_link_libraries(wireless_time pico_stdlib pico_wireless) - -# create map/bin/hex file etc. -pico_add_extra_outputs(wireless_time) +include("${CMAKE_CURRENT_LIST_DIR}/demo.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/rgb_http.cmake") \ No newline at end of file diff --git a/examples/pico_wireless/demo.cmake b/examples/pico_wireless/demo.cmake new file mode 100644 index 00000000..74f7786f --- /dev/null +++ b/examples/pico_wireless/demo.cmake @@ -0,0 +1,14 @@ +add_executable( + wireless_time + demo.cpp +) + +# enable usb output, disable uart output +pico_enable_stdio_usb(wireless_time 1) +pico_enable_stdio_uart(wireless_time 0) + +# Pull in pico libraries that we need +target_link_libraries(wireless_time pico_stdlib pico_wireless) + +# create map/bin/hex file etc. +pico_add_extra_outputs(wireless_time) diff --git a/examples/pico_wireless/rgb_http.cmake b/examples/pico_wireless/rgb_http.cmake new file mode 100644 index 00000000..80cb3e00 --- /dev/null +++ b/examples/pico_wireless/rgb_http.cmake @@ -0,0 +1,14 @@ +add_executable( + wireless_rgb_http + rgb_http.cpp +) + +# enable usb output, enable uart output +pico_enable_stdio_usb(wireless_rgb_http 1) +pico_enable_stdio_uart(wireless_rgb_http 1) + +# Pull in pico libraries that we need +target_link_libraries(wireless_rgb_http pico_stdlib pico_wireless) + +# create map/bin/hex file etc. +pico_add_extra_outputs(wireless_rgb_http) diff --git a/examples/pico_wireless/rgb_http.cpp b/examples/pico_wireless/rgb_http.cpp new file mode 100644 index 00000000..99050c40 --- /dev/null +++ b/examples/pico_wireless/rgb_http.cpp @@ -0,0 +1,244 @@ +#include +#include "pico/stdlib.h" +#include "hardware/uart.h" +#include "hardware/gpio.h" +#include "hardware/spi.h" +#include "pico_wireless.hpp" +#include "secrets.h" + +#include +#include + +#define HTTP_PORT 80 +#define HTTP_REQUEST_BUF_SIZE 2048 + +#define DNS_CLOUDFLARE IPAddress(1, 1, 1, 1) +#define DNS_GOOGLE IPAddress(8, 8, 8, 8) +#define USE_DNS DNS_CLOUDFLARE + +using namespace pimoroni; + +PicoWireless wireless; +uint8_t r, g, b; +uint8_t request_buf[HTTP_REQUEST_BUF_SIZE]; +const std::string response_501 = "HTTP/1.1 501 Not Implemented\nContent-Length: 19\n\n501 Not Implemented"; + +enum http_request_method_t { + GET, + POST +}; +typedef std::string_view(*http_request_handler)(http_request_method_t method, std::string_view path, std::vector request_head, std::vector request_body); + +std::vector split(std::string_view str, std::string delim="\r\n") { + std::vector result; + size_t offset = 0; + while (offset < str.size()) { + const auto pos = str.find_first_of(delim, offset); + // Emit an empty view even if two adjacent delimiters are found + // this ensurs the HTTP "blank line" start of content is found + result.emplace_back(str.substr(offset, pos - offset)); + if (pos == std::string_view::npos) break; + offset = pos + delim.length(); + } + return result; +} + +uint32_t millis() { + return to_us_since_boot(get_absolute_time()) / 1000; +} + +bool wifi_connect(std::string network, std::string password, IPAddress dns_server, uint32_t timeout=10000) { + printf("Connecting to %s...\n", network.c_str()); + wireless.wifi_set_passphrase(network, password); + + uint32_t t_start = millis(); + + while(millis() - t_start < timeout) { + if(wireless.get_connection_status() == WL_CONNECTED) { + printf("Connected!\n"); + wireless.set_dns(1, dns_server, 0); + return true; + } + wireless.set_led(255, 0, 0); + sleep_ms(500); + wireless.set_led(0, 0, 0); + sleep_ms(500); + printf("...\n"); + } + + return false; +} + +int socket_accept(int8_t server_sock) { + uint8_t new_sock = wireless.avail_server(server_sock); + if(new_sock != server_sock + && new_sock != 255) { + return new_sock; + } + return -1; +} + +uint8_t start_server(uint16_t http_port) { + printf("Starting server...\n", NETWORK); + // Get a socket for our server + uint8_t server_sock = wireless.get_socket(); + wireless.start_server(http_port, server_sock); + while(1){ + uint8_t state = wireless.get_server_state(server_sock); + if(state == LISTEN) break; + } + IPAddress ip; + wireless.get_ip_address(ip); + printf("Server listening on %s:%i\n", ip.to_string().c_str(), http_port); + return server_sock; +} + +bool poll_http_requests(uint8_t server_sock, http_request_handler request_handler, uint32_t timeout = 1000) { + uint16_t request_length = 0; + uint16_t avail_length = 0; + uint32_t t_start = millis(); + + // Clear the request buffer + memset(request_buf, 0, HTTP_REQUEST_BUF_SIZE); + + // Try to accept an incoming connection + int client_sock = socket_accept(server_sock); + if(client_sock == -1) return false; + + printf("Got client: %d\n", client_sock); + + // Any data? This *usually* seems to be available immediately, + // so we can dispense with any ongoing polling and hope for the best. + avail_length = wireless.avail_data(client_sock); + if(avail_length == 0) return false; + + printf("Got request: %i bytes\n", avail_length); + + // Read the full response + // Sometimes the bytes read is less than the bytes we request, so loop until we get the data we expect + while(request_length < avail_length) { + uint16_t read_length = avail_length; // Request the full buffer + wireless.get_data_buf(client_sock, request_buf + request_length, &read_length); + request_length += read_length; // Increment the response_length by the amount we actually read + + // Also check for timeouts here, too + if(millis() - t_start >= timeout) break; + } + + // Bail if we timed out. + if(millis() - t_start >= timeout) { + wireless.stop_client(client_sock); + }; + + if(request_length > 0) { + std::vector request = split(std::string_view((char *)request_buf, request_length)); + std::vector request_body; + std::vector request_detail = split(request[0], " "); + http_request_method_t method; + + // Bail early on an invalid HTTP request + if(request_detail[0].compare("POST") == 0) { + method = POST; + } else if (request_detail[0].compare("GET") == 0) { + method = GET; + } else { // PUT, DELETE, HEAD? + // Return a 501 unimplemented + wireless.send_data(client_sock, (const uint8_t *)response_501.data(), response_501.length()); + wireless.stop_client(client_sock); + return false; + } + + // Get the request path so we can handle routes + std::string_view path = request_detail[1]; + + // Scan for the blank line indicating content start for form post data + auto body_start = std::find(request.begin(), request.end(), ""); + + // Split the body from the head (ow!) + if(body_start != request.end()) { + request_body = std::vector(body_start + 1, request.end()); + request = std::vector(request.begin(), body_start); + } + + std::string_view response_body = request_handler(method, path, request, request_body); + std::string response_head = "HTTP/1.1 200 OK\nContent-Length: " + std::to_string(response_body.length()) + "\nContent-Type: text/html\n\n"; + wireless.send_data(client_sock, (const uint8_t *)response_head.data(), response_head.length()); + wireless.send_data(client_sock, (const uint8_t *)response_body.data(), response_body.length()); + wireless.stop_client(client_sock); + + return true; + } + + return false; +} + +int main() { + stdio_init_all(); + + wireless.init(); + sleep_ms(500); + + printf("Firmware version Nina %s\n", wireless.get_fw_version()); + + if(!wifi_connect(NETWORK, PASSWORD, USE_DNS)) { + return 0; + } + + uint8_t server_sock = start_server(HTTP_PORT); + + g = 255; + wireless.set_led(r, g, b); + + while(1) { + // Handle any incoming HTTP requests + poll_http_requests(server_sock, []( + http_request_method_t request_method, + std::string_view request_path, + std::vector request_head, + std::vector request_body) -> std::string_view { + + if(request_method == POST) { + // Split the URL-encoded POST data and parse the RGB values + std::vector post_data = split(request_body[0], "&"); + for(auto &data : post_data) { + // Must be at least 3 chars for "x=y" + if(data.length() < 3) continue; + // Must be in the form "x=y" + if(data[1] != '=') continue; + // "v" will be 0 on a parse failure, which isn't so bad. + int v = atoi((const char *)(data.data() + 2)); + switch(data[0]) { + case 'r': + r = v; + printf("Got R: %i\n", r); + break; + case 'g': + g = v; + printf("Got G: %i\n", g); + break; + case 'b': + b = v; + printf("Got B: %i\n", b); + break; + } + wireless.set_led(r, g, b); + } + } else { // GET + if(request_path.compare("/hello") == 0) { + return "Hello World!"; + } + } + + return "\ +
\ + \ + \ + \ + \ +
"; + }); + sleep_ms(10); + } + + return 0; +} \ No newline at end of file