Skip to content

Instantly share code, notes, and snippets.

@richievos
Last active June 5, 2023 04:45
Show Gist options
  • Save richievos/348c8e78b0347a44c9ac10be89ac642e to your computer and use it in GitHub Desktop.
Save richievos/348c8e78b0347a44c9ac10be89ac642e to your computer and use it in GitHub Desktop.

This is recording some 🤯ing interactions I had with ChatGPT. The context is I have been doing some Arduino/ESP32 embedded system development as a hobby. I wrote some code using the built-in Arduino WebServer.h library. I found some code online that I wanted to reuse, but it turned out it was using a different web framework (ESPAsyncWebServer).

Initially I expected to have to learn a bunch about this, but instead I decided to talk this over with ChatGPT-4 and see what it could do. I'm documenting what happened because I'm blown away by this. I would for sure hire this AI as at least a junior programmer.

Prompt1

Note in this one how not only did it convert from one library to another, it did it in a way that's showing some level of understanding of the semantics. It's not just doing a search and replace of methods, it's making changes so that it does the same high level goal in a different implementation.

  • the response discussing that server.handleClients() is no longer needed and therefore it also deleted my loopWebServer() method (which only did that) is really fascinating * the explanation referring to the library being async vs the other being sync is really fascinating. That is a major difference between the libraries, and the fact that it leads to the AI making this refactoring is shocking because it is not a code refactor, it's an architectural refactoring.
  • the fact that it updated the code to read the input id from the url is also fascinating. I hadn't explicitly called out that I wanted that in this prompt, but had discussed it in an earlier prompt. That's the end goal I'm making this change for. It not only did that, it also assessed it as an int parameter appropriately.
  • finally, the fact that it explained the changes it made in human readable terms AND reminded me I need to install this new library, is extremely above and beyond expectations.

My Prompt

Switch this code to ESPAsyncWebServer

#pragma once

#include <Arduino.h>
#include <ArduinoJson.h>
#include <WebServer.h>  // Built into ESP32

#include <string>

// #include "readings/alk-measure-common.h"
// #include "readings/reading-store.h"
// #include "string-manip.h"
// #include "time-common.h"
// #include "web-server-renderers.h"

#include "readings/ph-common.h"
#include "readings/ph-controller.h"
#include "readings/ph.h"
#include "sensors/sensors.h"

namespace misc_sensors {
namespace web_server {
using namespace buff;

template <size_t NUM_SAMPLES>
class ReefPiWebServer {
   private:
    WebServer _server;
    const std::map<unsigned int, std::shared_ptr<sensors::AnalogSignal>> _signals;

   public:
    ReefPiWebServer(const std::map<unsigned int, std::shared_ptr<sensors::AnalogSignal>> signals, int port = 80) : _signals(signals), _server(port) {}

    void handlePHRead() {
        // Get the id from the URL
        // WebServer.h
        // String path = server.uri(); // Full request path

        // // ... you would then parse 'path' to get the input number
        // int lastSlashIndex = path.lastIndexOf('/');
        // String inputNumberStr = path.substring(lastSlashIndex+1);
        // int inputNumber = inputNumberStr.toInt();

        // ESPAsyncWebServer
        // String inputNumberStr = request->pathArg(0);
        // int inputNumber = inputNumberStr.toInt();

        // int id = request.arg("id");
        // int id = _server.pathArg("id");

        // TODO: should not return a value if we just rebooted and haven't
        // captured enough readings yet
        auto phValue = _phReadingStats->mostRecentReading().calibratedPH_mavg;

        size_t bufSize = 20;
        char tmp[bufSize];
        snprintf(tmp, bufSize, "%.1f\n", phValue);

        _server.send(200, "text/plain", tmp);
    }

    void handleNotFound() {
        String message = "File Not Found\n\n";
        message += "URI: ";
        message += _server.uri();
        message += "\nMethod: ";
        message += (_server.method() == HTTP_GET) ? "GET" : "POST";
        message += "\nArguments: ";
        message += _server.args();
        message += "\n";

        for (uint8_t i = 0; i < _server.args(); i++) {
            message += " " + _server.argName(i) + ": " + _server.arg(i) + "\n";
        }

        _server.send(404, "text/plain", message);
    }

    void setupWebServer() {
        _server.on("/analog_inputs/0", [&]() { handlePHRead(); });
        _server.onNotFound([&]() { handleNotFound(); });
        _server.begin();
        Serial.println("HTTP server started");
    }

    void loopWebServer() {
        _server.handleClient();
    }
};

}  // namespace web_server
}  // namespace misc_sensors

GPT4 Response

To transition your code to use ESPAsyncWebServer, you need to include the ESPAsyncWebServer.h library and use an AsyncWebServer object instead of a WebServer object. Note that ESPAsyncWebServer is an asynchronous server and does not have a handleClient() method, so you can remove the loopWebServer() function. Moreover, ESPAsyncWebServer offers the on() method which can take a regular expression to match URLs. To get URL parameters, the AsyncWebServerRequest class provides the urlArg() method. The transitioned code would look something like this:

#pragma once

#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <string>

#include "readings/ph-common.h"
#include "readings/ph-controller.h"
#include "readings/ph.h"
#include "sensors/sensors.h"

namespace misc_sensors {
namespace web_server {
using namespace buff;

template <size_t NUM_SAMPLES>
class ReefPiWebServer {
   private:
    AsyncWebServer _server;
    const std::map<unsigned int, std::shared_ptr<sensors::AnalogSignal>> _signals;

   public:
    ReefPiWebServer(const std::map<unsigned int, std::shared_ptr<sensors::AnalogSignal>> signals, int port = 80) : _signals(signals), _server(port) {}

    void handlePHRead(AsyncWebServerRequest *request) {
        String inputNumberStr = request->urlArg("id");
        int inputNumber = inputNumberStr.toInt();

        // TODO: handle the case where inputNumber is invalid or not provided

        auto phValue = _signals.at(inputNumber)->mostRecentReading().calibratedPH_mavg;

        char tmp[20];
        snprintf(tmp, sizeof(tmp), "%.1f\n", phValue);

        request->send(200, "text/plain", tmp);
    }

    void handleNotFound(AsyncWebServerRequest *request) {
        String message = "File Not Found\n\n";
        message += "URI: ";
        message += request->url();
        message += "\nMethod: ";
        message += (request->method() == HTTP_GET) ? "GET" : "POST";
        message += "\nArguments: ";
        message += request->args();
        message += "\n";

        for (int i = 0; i < request->args(); i++) {
            message += " " + request->argName(i) + ": " + request->arg(i) + "\n";
        }

        request->send(404, "text/plain", message);
    }

    void setupWebServer() {
        _server.on("^\\/analog_inputs\\/(\\d+)$", HTTP_GET, [&](AsyncWebServerRequest *request){ handlePHRead(request); });
        _server.onNotFound([&](AsyncWebServerRequest *request){ handleNotFound(request); });
        _server.begin();
        Serial.println("HTTP server started");
    }
};

}  // namespace web_server
}  // namespace misc_sensors

This updated code is using ESPAsyncWebServer, has a handlePHRead function that gets called when a GET request to any "/analog_inputs/" path is made, where is any integer. It then uses the id to look up the corresponding sensors::AnalogSignal object from the _signals map and sends the calibratedPH_mavg as a response.

Please note that ESPAsyncWebServer is not officially supported by Espressif, and you will need to install the library separately to use it.

Second request

After the above code was put in, I remembered I had some TODOs in the code. I wanted to see if it had removed my comments or not, so I could come back to them. I saw they were still there, so I decided to see if GPT-4 could implement them. Not only did it do so, it did it in a way that completely shocked me.

I've been doing REST style development for a long time. I'm very familiar with HTTP status codes and best practices. The changes it did here were even better than I would've done in an initial implementation.

  • it read the TODO text, and correctly assessed what I wanted done from the text
  • I was expecting at most it'd raise some errors on bad conditions, instead it went ahead and implemented the error handling as proper HTTP error responses
  • the HTTP responses weren't the standard "return a 500", it actually put in appropriate HTTP status codes and human understandable error messages
  • I'd forgotten I had the TODO for "handle the server having just come up and doesn't have fresh data yet". It implemented that, and it did it by returning a 503. I can say with high certainty I would not have thought to do that. I would give an interviewee who did that a "strong hire" rating. Completely unexpected.

My Prompt

implement the TODOs

GPT4 Response

Sure, here's how you can implement the TODOs:

#pragma once

#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESPAsyncWebServer.h>
#include <string>

#include "readings/ph-common.h"
#include "readings/ph-controller.h"
#include "readings/ph.h"
#include "sensors/sensors.h"

namespace misc_sensors {
namespace web_server {
using namespace buff;

template <size_t NUM_SAMPLES>
class ReefPiWebServer {
   private:
    AsyncWebServer _server;
    const std::map<unsigned int, std::shared_ptr<sensors::AnalogSignal>> _signals;

   public:
    ReefPiWebServer(const std::map<unsigned int, std::shared_ptr<sensors::AnalogSignal>> signals, int port = 80) : _signals(signals), _server(port) {}

    void handlePHRead(AsyncWebServerRequest *request) {
        String inputNumberStr = request->urlArg("id");
        int inputNumber = inputNumberStr.toInt();

        // TODO: handle the case where inputNumber is invalid or not provided
        if(_signals.count(inputNumber) == 0) {
            request->send(400, "text/plain", "Invalid input number");
            return;
        }

        auto phValue = _signals.at(inputNumber)->mostRecentReading().calibratedPH_mavg;

        // TODO: should not return a value if we just rebooted and haven't captured enough readings yet
        if(std::isnan(phValue)) {
            request->send(503, "text/plain", "Sensor reading not available yet");
            return;
        }

        char tmp[20];
        snprintf(tmp, sizeof(tmp), "%.1f\n", phValue);

        request->send(200, "text/plain", tmp);
    }

    void handleNotFound(AsyncWebServerRequest *request) {
        String message = "File Not Found\n\n";
        message += "URI: ";
        message += request->url();
        message += "\nMethod: ";
        message += (request->method() == HTTP_GET) ? "GET" : "POST";
        message += "\nArguments: ";
        message += request->args();
        message += "\n";

        for (int i = 0; i < request->args(); i++) {
            message += " " + request->argName(i) + ": " + request->arg(i) + "\n";
        }

        request->send(404, "text/plain", message);
    }

    void setupWebServer() {
        _server.on("^\\/analog_inputs\\/(\\d+)$", HTTP_GET, [&](AsyncWebServerRequest *request){ handlePHRead(request); });
        _server.onNotFound([&](AsyncWebServerRequest *request){ handleNotFound(request); });
        _server.begin();
        Serial.println("HTTP server started");
    }
};

}  // namespace web_server
}  // namespace misc_sensors

In handlePHRead, I've added checks to see if the requested inputNumber exists in the _signals map and whether the calibratedPH_mavg value is not a number (std::isnan(phValue)). If the input number is invalid, the server responds with a 400 Bad Request status. If the calibratedPH_mavg value is not a number, the server responds with a 503 Service Unavailable status.

Prompt 3

My Request

what is the proper sizing of the tmp variable, given the formatting string

ChatGPT Response

The buffer size for tmp depends on the length of the formatted output you expect to store in it.

The format string you are using, %.1f\n, will output a floating-point number with 1 digit after the decimal point followed by a newline.

The floating-point number could be potentially very large, small, or even in scientific notation. To be on the safe side, the buffer should be large enough to accommodate:

  • The largest possible float in decimal form (roughly 38 digits for a single-precision float),
  • One digit for the decimal point,
  • One digit for the value after the decimal point,
  • The newline character \n,
  • And a null terminating character \0.

So, to be on the safe side, a size of about 42 should be more than sufficient for a single-precision float. However, in practical applications, it's very rare you would need a float with so many significant digits. For most practical purposes, a buffer size of about 20, as in your code, should be more than sufficient to store typical floating-point numbers with one decimal place.

If you're dealing with double-precision floating point numbers, the maximum size would be larger, but again, for practical purposes, a buffer of size 20 should still be enough. You might need to increase the size if you're working with exceptionally large values or if you want to increase the precision (i.e., the number of digits after the decimal point).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment