From 5fbd45874904627323c22d4b62833877b6f99293 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 8 Apr 2021 15:09:55 +0100 Subject: [PATCH] Cheerlights example for Pico Wireless --- examples/pico_wireless/CMakeLists.txt | 3 +- examples/pico_wireless/cheerlights.cmake | 14 ++ examples/pico_wireless/cheerlights.cpp | 258 +++++++++++++++++++++++ 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 examples/pico_wireless/cheerlights.cmake create mode 100644 examples/pico_wireless/cheerlights.cpp diff --git a/examples/pico_wireless/CMakeLists.txt b/examples/pico_wireless/CMakeLists.txt index 860c8552..ecdfcc2f 100644 --- a/examples/pico_wireless/CMakeLists.txt +++ b/examples/pico_wireless/CMakeLists.txt @@ -1,2 +1,3 @@ include("${CMAKE_CURRENT_LIST_DIR}/demo.cmake") -include("${CMAKE_CURRENT_LIST_DIR}/rgb_http.cmake") \ No newline at end of file +include("${CMAKE_CURRENT_LIST_DIR}/rgb_http.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/cheerlights.cmake") \ No newline at end of file diff --git a/examples/pico_wireless/cheerlights.cmake b/examples/pico_wireless/cheerlights.cmake new file mode 100644 index 00000000..08e16595 --- /dev/null +++ b/examples/pico_wireless/cheerlights.cmake @@ -0,0 +1,14 @@ +add_executable( + wireless_cheerlights + cheerlights.cpp +) + +# enable usb output, disable uart output +pico_enable_stdio_usb(wireless_cheerlights 1) +pico_enable_stdio_uart(wireless_cheerlights 1) + +# Pull in pico libraries that we need +target_link_libraries(wireless_cheerlights pico_stdlib pico_wireless) + +# create map/bin/hex file etc. +pico_add_extra_outputs(wireless_cheerlights) diff --git a/examples/pico_wireless/cheerlights.cpp b/examples/pico_wireless/cheerlights.cpp new file mode 100644 index 00000000..e11886e1 --- /dev/null +++ b/examples/pico_wireless/cheerlights.cpp @@ -0,0 +1,258 @@ +#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 + +#define HTTP_REQUEST_DELAY 30 // Seconds between requests +#define HTTP_REQUEST_HOST "api.thingspeak.com" +#define HTTP_REQUEST_PATH "/channels/1417/field/2/last.txt" +#define HTTP_RESPONSE_BUF_SIZE 1024 + +using namespace pimoroni; + +PicoWireless wireless; +uint8_t r, g, b; +uint8_t response_buf[HTTP_RESPONSE_BUF_SIZE]; +typedef void(*http_handler)(unsigned int status_code, std::vector response_head, std::vector esponse_body); + +enum HTTP_REQUEST_STATUS { + HTTP_REQUEST_OK = 0, + HTTP_REQUEST_TIMEOUT, + HTTP_REQUEST_RESPONSE_INVALID, + HTTP_REQUEST_RESPONSE_UNHANDLED, + HTTP_REQUEST_CONNECTION_FAILED, + HTTP_REQUEST_RESPONSE_OVERFLOW, + HTTP_REQUEST_NO_RESPONSE +}; + +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; +} + +/* Basic function to connect to a client IP:PORT and poll for an established connection */ +bool connect(IPAddress host_address, uint16_t port, uint8_t client_sock, uint32_t timeout = 1000) { + wireless.start_client(host_address, port, client_sock, TCP_MODE); + + uint32_t t_start = millis(); + + while(millis() - t_start < timeout) { + uint8_t state = wireless.get_client_state(client_sock); + if(state == ESTABLISHED) return true; + sleep_ms(100); + } + + return false; +} + +/* Basic DNS lookup */ +IPAddress dns_lookup(std::string request_host) { + IPAddress host_address(0, 0, 0, 0); + printf("DNS lookup for: %s\n", request_host.c_str()); + if(!wireless.get_host_by_name(request_host.c_str(), host_address)) { + printf("DNS lookup failed!\n"); + } + return host_address; +} + +/* This is pretty much the simplest HTTP request function I could get away with. + It accepts a client socket ID, IP address, hostname, request path and handler function, + and calls the handler with the status code (only 200 & 404 at the moment), head + and body as `std::vector` into the underlying buffer. + */ +HTTP_REQUEST_STATUS http_request(uint8_t client_sock, IPAddress host_address, uint16_t port, std::string request_host, std::string request_path, http_handler handler, uint32_t timeout = 1000) { + if(!connect(host_address, port, client_sock)) { + printf("Connection failed!\n"); + return HTTP_REQUEST_CONNECTION_FAILED; + } + + // HTTP request to grab our API endpoint + const std::string http_request = "GET " + request_path + " HTTP/1.1\r\n\ +Host: " + request_host + "\r\n\ +Connection: close\r\n\r\n"; + + // Clear the response buffer + memset(response_buf, 0, HTTP_RESPONSE_BUF_SIZE); + + wireless.send_data(client_sock, (const uint8_t *)http_request.data(), http_request.length()); + + uint16_t response_length = 0; + uint16_t avail_length = 0; + uint32_t t_start = millis(); + + // Keep receiving data until our designated timeout + // There's no guarantee that `wireless.avail_data` will have the *whole* response in one shot + // and I *really* don't want to parse the HTTP response for a `Content-Length` header. + while(millis() - t_start < timeout) { + sleep_ms(50); + avail_length = wireless.avail_data(client_sock); + if(avail_length > 0) break; + } + + // 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(response_length < avail_length) { + uint16_t read_length = avail_length; // Request the full buffer + wireless.get_data_buf(client_sock, response_buf + response_length, &read_length); + response_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; + } + + // Explicitly stop our client, and don't leave it dangling! + wireless.stop_client(client_sock); + + // Bail if we timed out. + if(millis() - t_start >= timeout) return HTTP_REQUEST_TIMEOUT; + + if(response_length > 0) { + std::vector response = split(std::string_view((char *)response_buf, response_length)); + std::vector response_body; + uint32_t status_code = 0; + + // Bail early on an invalid HTTP request + if(response[0].compare(0, 8, "HTTP/1.1") != 0) return HTTP_REQUEST_RESPONSE_INVALID; + + // Scan for the blank line indicating content start + auto body_start = std::find(response.begin(), response.end(), ""); + + // Split the body from the head (ow!) + if(body_start != response.end()) { + response_body = std::vector(body_start + 1, response.end()); + response = std::vector(response.begin(), body_start); + } + + // Parse out the HTTP status code + status_code = std::stoul(std::string(response[0].substr(9, 12)), nullptr); + + if(status_code != 0) { + handler(status_code, response, response_body); + return HTTP_REQUEST_OK; + } + + return HTTP_REQUEST_RESPONSE_UNHANDLED; + } + + return HTTP_REQUEST_NO_RESPONSE; +} + +/* As above, but does DNS resolving for us, probably don't use this... */ +int http_request(uint8_t client_sock, std::string request_host, uint16_t port, std::string request_path, http_handler handler, uint32_t timeout = 1000) { + IPAddress host_address = dns_lookup(request_host); + return http_request(client_sock, host_address, port, request_host, request_path, handler, timeout); +} + +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; + } + + g = 255; + wireless.set_led(r, g, b); + + // Get a free client socket + uint8_t client_sock = wireless.get_socket(); + + // Be a good DNS citizen and cache our lookup + IPAddress host_address = dns_lookup(HTTP_REQUEST_HOST); + + while(1) { + printf("Requesting: %s\n", HTTP_REQUEST_PATH); + + HTTP_REQUEST_STATUS status = http_request(client_sock, host_address, HTTP_PORT, HTTP_REQUEST_HOST, HTTP_REQUEST_PATH, []( + unsigned int status_code, + std::vector response_head, + std::vector response_body) { + // Check for valid status + if(status_code != 200) return; + // Check for empty body + if(response_body.size() == 0) return; + // Check for our 7 chars "#000000" + if(response_body[0].length() != 7) return; + // Check for at least a *hopefully* valid hex colour + if(response_body[0].compare(0, 1, "#") != 0) return; + + // Convert the hex colour to an unsigned int + uint32_t rgb = std::stoul(std::string(response_body[0].substr(1)), nullptr, 16); + + // Unpack to RGB + r = (rgb >> 16) & 0xff; + g = (rgb >> 8) & 0xff; + b = (rgb >> 0) & 0xff; + printf("RGB: %i %i %i\n", r, g, b); + wireless.set_led(r, g, b); + }); + + if(status == HTTP_REQUEST_NO_RESPONSE) { + printf("No response :(\n"); + } + + if(status == HTTP_REQUEST_TIMEOUT) { + printf("Request timed out :(\n"); + } + + if(status == HTTP_REQUEST_RESPONSE_UNHANDLED) { + // Something unexpected happened! + } + + sleep_ms(HTTP_REQUEST_DELAY * 1000); // Sensible delay + } + + return 0; +} \ No newline at end of file