Maker.io main logo

Arduino-Based Connected TODO Box

2024-10-22 | By Maker.io Staff

License: General Public License 3D Printing Switches Thermal Imaging Arduino

box_1

Keeping track of daily tasks and chores can be challenging, especially when new ones appear ‎unexpectedly. The device I created offers a solution. Its web-based interface and built-in thermal ‎printer allow you to organize daily chores and generate portable lists—perfect for creating a ‎shopping list or remembering chores on the go.‎

Project Components

Building this project requires only a few electronic components, as the connected MCU board ‎handles most tasks through software. This project uses an Arduino Nano RP2040 Connect, but it ‎is possible to substitute any similar device, like an ESP8266, if you need to meet specific ‎requirements. The project further utilizes a thermal printer to generate physical copies of the ‎currently open tasks. A physical button starts the print. Finally, a custom 3D-printed enclosure ‎houses all the components.‎

Quantity Part

Before starting this build, you should familiarize yourself with the basics of thermal printers and ‎how to make an Arduino communicate with standard serial printers. After understanding thermal ‎printers and verifying that the printer works as expected, you must connect the components to ‎the Arduino, as shown in the following circuit diagram:‎

schemeit_2

This diagram illustrates how to connect the RP2040 to the thermal printer, the push button, and ‎the LED. Scheme-It link

Note that the push button uses the Arduino’s built-in pull-up resistor instead of an external one, ‎which helps reduce the number of components in the build. However, the LED still needs an ‎external current-limiting resistor. Connecting the printer to the Arduino requires only two serial ‎communication wires.‎

Understanding the Firmware’s Tasks

The Arduino has to perform several tasks in this project. It must connect to a wireless network ‎and listen for incoming client requests. Whenever it receives a request, the MCU must interpret it ‎and determine the task the client wants to perform.‎

Clients can request a website that lists the open tasks, which they can use to instruct the Arduino ‎to store a new to-do entry and remove existing ones. For that purpose, the Arduino must ‎understand standard HTTP requests, which typically consist of three blocks. The first contains ‎the request method, the path with optional parameters, and the protocol version. The second ‎block includes a list of request headers for transmitting additional details to the server. The ‎request’s final block contains the payload. While the first line is mandatory, the last two blocks ‎may be empty. A single whitespace character separates the individual parts of a line, and each ‎line must contain exactly one entry. The requests and responses each end with a single empty ‎line.‎

table_3

This image illustrates the composition of HTTP requests and HTTP responses.

The request parsing logic is abstracted in a custom library that can be downloaded from GitHub. ‎The library code stores the client’s request in a character buffer, converts each line to Arduino ‎strings, and then tries to interpret the strings by looking for specific characters that characterize ‎one of the three blocks that can be in an HTTP request.‎

In addition to managing HTTP requests, the firmware must also use the LED to indicate that ‎tasks are open. Finally, the Arduino must monitor the connected push button state and print out ‎the currently open tasks when pressed.‎

Installing the Required Libraries

This project’s firmware relies on external libraries that greatly help reduce the boilerplate code for ‎handling basic tasks like button debouncing. An additional library handles communication with ‎the thermal printer via the RP2040’s hardware serial port. Thus, the following libraries are ‎required:‎

  • WiFiNINA

  • Bounce2‎

  • ButtonSuite

  • Adafruit Thermal Printer Library

Establishing a Network Connection and Starting the Server

Once all the libraries are installed, they can be added to the project’s firmware sketch before ‎defining multiple variables:‎

Copy Code
#include <WiFiNINA.h>
#include <PushEventButton.h>
#include <Adafruit_Thermal.h>
#include "HttpParser.h"
#include "WiFiSecrets.h"
#include "PageContents.h"

#define LED_PIN 2
#define BTN_PIN 4
#define LED_FLASH_DELAY 1500

struct TodoTask {
  String id;
  String description;
  TodoTask(String id, String description) : id(id), description(description) {}
};

vector<TodoTask> openTasks;
unsigned int addedTasks = 0;
bool printStarted = false;
unsigned long previousMillis = 0UL;

WiFiServer server(PORT);
Adafruit_Thermal printer(&Serial1);
PushEventButton printButton(BTN_PIN, PushEventButton::CAPTUREPUSH);

Next to the external libraries, the program includes three custom header files that must be ‎present in the same directory as the sketch. The first consists of the previously discussed HTTP ‎parsing library; the second header file contains the WiFi network’s credentials and the port to use; ‎and the third file contains the HTML code templates the Arduino serves clients. The subsequent ‎three statements define the pins that connect to the push button and the LED and how long the ‎LED flash delay should be. ‎

The Arduino stores user-created to-do tasks in a custom data structure defined after the include-‎statements. Each task has an ID and a description. The program keeps track of the entries by ‎placing them in the openTasks vector and storing the count. The other two variables manage the ‎application state and prevent users from starting multiple print processes simultaneously.‎

The program then creates new objects representing the WiFiServer, the printer, and the button. ‎The WiFiServer uses the port defined in the WiFiSecrets header file. The printer uses a ‎reference to the RP2040’s first hardware serial port. The print button requires passing the button ‎pin and the type of event it should detect. In this case, the object should only react to button ‎press events, not when the button is released. ‎

The program’s setup method then makes use of these objects to establish a WiFi connection, ‎start the WiFiServer, initialize the serial port and printer, and set up the LED pin:‎

Copy Code
void setup() {
  int status = WL_IDLE_STATUS;
  do {
    status = WiFi.begin(SSID, PASSWORD);
    delay(2000);
  }
  while (WiFi.status() != WL_CONNECTED);

  server.begin(); // Start the server

  Serial1.begin(19200); // Initialize the serial port
  printer.begin();      // Initialize the printer

  pinMode(LED_PIN, OUTPUT);
}

Responding to Client Requests

‎The sketch’s main loop performs three tasks: It first determines whether a client is available and ‎wants to communicate with the server and uses the HttpParser library to dissect the incoming ‎HTTP request if there is one. Secondly, the method checks whether a user pressed the push ‎button, and if they did, the program calls a helper function to print the to-do entries. Finally, the ‎loop function also flashes the LED if there are any tasks in the openTasks vector, and it turns off ‎the LED if the vector is empty:‎

Copy Code
void loop() {
  WiFiClient client = server.available();
  if (client && client.connected()) {
    handleClientRequest(client);
  }
  if (printButton.pushed()) {
    printOpenTasks();
  }
  if (openTasks.size() > 0) {
    toggleLED();
  } else {
    digitalWrite(LED_PIN, LOW);
  }
}

Separate helper methods perform each task to make the code more organized and easier to ‎read. The first helper function handles incoming client requests:‎

Copy Code
void handleClientRequest(WiFiClient client) {
  HttpParser clientParser(client, 1024);
    if (clientParser.receive()) {
      if(clientParser.getMethod() == "GET") {
        if (clientParser.getPath() == "/") {
          String pageToServe = String(BASIC_PAGE);
          String openTasksTableData = fillTasksTemplate(OPEN_TASK_TABLE_ROW_TEMPLATE, &openTasks);
          pageToServe.replace("%%%_OPEN_TASK_TABLE_ROWS_%%%", openTasksTableData);
          clientParser.transmit("text/html", pageToServe);
        } else if (clientParser.getPath() == "/deleteTask.js") {
          clientParser.transmit("application/javascript", DELETE_TASK_FUNCTION);
        } else if (clientParser.getPath() == "/addTask.js") {
          clientParser.transmit("application/javascript", ADD_TASK_FUNCTION);
        } else {
          clientParser.transmit(404, "Not Found");
        }
      } else if (clientParser.getMethod() == "POST") {
        openTasks.push_back(TodoTask(String(addedTasks++), String(clientParser.getData())));
        clientParser.transmit(201, "Created");
      } else if (clientParser.getMethod() == "DELETE") {
        deleteOpenTask(clientParser.getHeader("ID"));
        clientParser.transmit(200, "OK");
      } else {
        clientParser.transmit(405, "Method Not Allowed");
      }
    }
    client.stop();
    clientParser.end();
}

In this function, the server inspects the method defined in the client’s HTTP request. If it is a GET ‎request, the server checks the path. The server knows three valid paths: the main web interface ‎and two helper JavaScript (JS) files. The HTML and JS code are stored in the PageContents ‎header file. The server responds with a 404 error should a client request an unknown path.‎

If the client sends a POST request to any path, the server extracts the data supplied with the ‎request and passes it to the TodoTask struct to store it in the vector of open to-do entries. The ‎program assumes that the request data contains only the task’s description, and it assigns each ‎task a unique ID corresponding to the number of previously added tasks.‎

Finally, clients can also delete previously added tasks by sending a DELETE request with the ID ‎of the task to delete in the request header. The server uses the following helper method to find ‎and delete a previously created task:‎

Copy Code
void deleteOpenTask(String id) {
  int toRemove = -1;
  for (int i = 0; i < openTasks.size(); i++) {
    TodoTask current = openTasks.at(i);
    if (current.id == id) {
      toRemove = i;
      break;
    }
  }
  if (toRemove != -1) {
    openTasks.erase(openTasks.begin() + toRemove);
  }
}

This method iterates over all openTasks and finds the one with the matching ID if it exists. The ‎function only deletes an entry if it finds a match.‎

Printing the To-Do List

‎The second helper method concerns itself with sending the open tasks to the thermal printer to ‎create a physical list that users can take with them when running errands:‎

Copy Code
void printOpenTasks(void) {
  if (!printStarted) {
    printStarted = true;
    printer.println("Today\'s Tasks");
    printer.println("----------------");
    for (int i = 0; i < openTasks.size(); i++) {
      TodoTask current = openTasks.at(i);
      printer.print("[] ");
      printer.println(current.description);
    }
    printer.println("----------------");
    printer.println("Have a nice day!");
    printer.feed(1);
    resetTasks();
    printStarted = false;
  }
}

This method first determines whether a print is ongoing to prevent multiple simultaneous ‎processes. If no print is active, the helper sets the flag and sends a few lines of hard-coded text ‎to the printer. It then iterates over the open tasks vector and sends each entry to the thermal ‎printer. It concludes by outputting some additional lines of text before deleting the open tasks ‎from the vector and resetting the printStarted flag. The helper method that resets the tasks ‎vector looks as follows:‎

Copy Code
void resetTasks(void) {
  openTasks.clear();
  addedTasks = 0;
}

Toggling the Status LED

‎A third helper function toggles the device’s status LED, and it looks like this:‎

Copy Code
void toggleLED(void) {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= LED_FLASH_DELAY) {
    previousMillis = currentMillis;
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  }
}

It compares the flash delay, defined at the program's start, to the elapsed time since it last ‎changed the LED state. If the elapsed time exceeds the delay value, the program toggles the ‎LED and sets a new previousMillis value.‎

Building and Serving HTML Responses

When clients request a file from the server, the loop method discussed above must return a valid ‎HTML website wrapped within a valid HTTP response. In this example, clients can request the ‎website and two JavaScript files, all stored as Arduino strings in the PageContents.h file. The ‎website contains a single table that lists the open tasks, and each entry has a corresponding ‎delete button for removing the task. Beneath the table, users can enter a new task description ‎and send it to the server:‎

opentasks_4

This image shows the web interface hosted on the Arduino.‎

The Arduino must dynamically inject and remove data from the HTML page. Therefore, the ‎HTML code contains placeholder values that start and end with three percentage characters. For ‎example, the HTML table includes a placeholder for a template table row that represents a task. ‎The row template itself contains three placeholder values, and it looks as follows:‎

Copy Code
String OPEN_TASK_TABLE_ROW_TEMPLATE =
"<tr>\n"
"  <td>%%%_TASK_ID_%%%</td>\n"
"  <td>%%%_TASK_DESCRIPTION_%%%</td>\n"
"  <td>\n"
"    <button onclick=\"deleteTask(%%%_TASK_ID_%%%)\">Delete</button>\n"
"  </td>\n"
"</tr>\n";

The template represents a table row (tr) with three columns (td elements). The first column ‎contains the task ID, the second the description, and the third holds the delete button. This button ‎calls the deleteTask function, which is defined in one of the JavaScript files.‎

Whenever a client requests the HTML website, the server loads the templates from the header ‎file and replaces the placeholder values using the following helper function in the main sketch file:‎

Copy Code
String fillTasksTemplate(String templ, vector<TodoTask> *src) {
  String tableContents = "";
  for (int i = 0; i < src->size(); i++) {
    TodoTask current = src->at(i);
    String tmp = String(templ);
    tmp.replace("%%%_TASK_ID_%%%", String(current.id));
    tmp.replace("%%%_TASK_DESCRIPTION_%%%", current.description);
    tableContents = tableContents + tmp;
  }
  return tableContents;
}

This helper method loops over all entries in the vector and creates a copy of the template in each ‎iteration. It then utilizes the default string replace function to replace each placeholder value with ‎the actual data before the server sends the enriched HTML code to the client. Note that the ‎remainder of the HTML code was omitted for brevity but can be downloaded at the end of the ‎article.‎

Creating and Deleting Tasks

Clients must also be able to delete previously created entries by sending a DELETE request to ‎the server, which is handled by one of the website's helper JavaScript files:‎

Copy Code
String DELETE_TASK_FUNCTION = "function deleteTask(id) {\n"
"    fetch('/', {\n"
"        method: 'DELETE',\n"
"        headers: {\n"
"            'ID': id,\n"
"        },\n"
"    })\n"
"    .then(response => {\n"
"        if (response.status === 200) {\n"
"            window.location.reload();\n"
"        }\n"
"    });\n"
"}\n";

This script sends a DELETE request to the server with the task’s ID in a custom header. If the ‎deletion succeeds, the server responds with a status code of 200, and the script reloads the ‎HTML page displayed in the client’s browser.‎

The addTask function instructs the server to create a new task. It first reads the user-entered ‎value from the taskDescription input field. Then, it sends a POST request to the server with the ‎new task’s description in the request body. If the server returns HTTP code 201, the script ‎reloads the page.‎

Copy Code
String ADD_TASK_FUNCTION = "function addTask() {\n"
"    const inputField = document.querySelector('input[name=\"taskDescription\"]');\n"
"    const taskDescription = inputField.value.trim();\n"
"    if (taskDescription.length == 0) {\n"
"        return;\n"
"    }\n"
"    fetch('/', {\n"
"        method: 'POST',\n"
"        headers: {\n"
"            'Content-Type': 'text/plain',\n"
"        },\n"
"        body: taskDescription\n"
"    })\n"
"    .then(response => {\n"
"        if (response.status === 201) {\n"
"            window.location.reload();\n"
"        }\n"
"    });\n"
"}\n";

Assembling the Device

Assembly starts with downloading and printing the enclosure STL files on a 3D printer. The case ‎comprises two parts. The printer is mounted in the top case using the screws and clamps on the ‎thermal printer’s housing. This part also houses the push button, which snaps into place in the ‎bigger of the two holes. The smaller one is intended for the power cable. It needs to be inserted ‎and then secured, for example, by tying a knot in the insulated part to serve as a strain relief. ‎After inserting the parts into the case, they must be connected electrically, as shown in the circuit ‎diagram above.‎

enclosure_5

This image shows the thermal printer and button mounted in the top half of the enclosure. The ‎Arduino was removed for this image.‎

Finally, the code must be uploaded to the Arduino board before snapping the bottom piece of the ‎enclosure into the top half. The two pieces should fit together nicely by friction alone. However, a ‎bit of glue can create a more permanent bond.‎

Summary

project_6

This practical device lets you organize your daily to-do list using a simple web interface. You can ‎then create a physical copy of the list to take with them, for example, when going out for ‎groceries.‎

An Arduino Nano RP2040 Connect is the heart of this project. It opens a server, accepts ‎incoming client requests, parses them, and responds to clients with the requested resource. For ‎that purpose, the MCU must understand and create standard HTTP requests. The protocol is ‎straightforward, and well-formed requests and responses comprise three blocks. In HTTP ‎requests, the first block contains the method, path, and protocol version. In its responses, the ‎server includes the status code and a message in the first line. This line is followed by optional ‎headers containing further information about the request. The third block is also optional. When ‎included, it contains the request’s actual data.‎

Aside from handling client requests, the project’s firmware is also responsible for flashing the ‎device’s LED to indicate that users have unfinished tasks. It further monitors a push-button state, ‎which starts the thermal print process when pressed.‎

制造商零件编号 ACM12US05
AC/DC WALL MOUNT ADAPTER 5V 10W
XP Power
¥100.81
Details
制造商零件编号 LP1OA1AB
SWITCH PUSH SPST-NO 0.1A 30V
E-Switch
¥18.72
Details
制造商零件编号 CF14JT220R
RES 220 OHM 5% 1/4W AXIAL
Stackpole Electronics Inc
¥0.81
Details
制造商零件编号 1957
JUMPER WIRE M TO M 6" 28AWG
Adafruit Industries LLC
¥15.87
Details
制造商零件编号 RM-PL0225
POLYMAKER POLYLITE PLA FILAMENT
LulzBot
¥193.71
Details
制造商零件编号 ABX00052
ARDUINO NANO RP2040 CONNECT
Arduino
¥239.32
Details
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum