Skip to content

Instantly share code, notes, and snippets.

@danasf
Last active April 23, 2023 06:38
Show Gist options
  • Save danasf/764f096a7d15d5f3c8b5e5b38a7f7464 to your computer and use it in GitHub Desktop.
Save danasf/764f096a7d15d5f3c8b5e5b38a7f7464 to your computer and use it in GitHub Desktop.
ESP32 Receipt Printer Storybot
/*
* StoryBot 1.0
* Copyright 2022 Dana Sniezko, GPLv3
*
* A simple ESP32 robot that tells stories.
* Connects to Wifi, uses GPT-3 to generate stories,
* and a thermal printer to print them out!
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* <https://www.gnu.org/licenses/gpl-3.0.html>
*
*/
#include "Adafruit_Thermal.h"
#include <ArduinoOTA.h>
#include <ArduinoJson.h>
#include <ESPmDNS.h>
#include <HTTPClient.h>
#include <WiFi.h>
// Setup pins for thermal printer serial and push button
#define RXD2 16
#define TXD2 17
#define BUTTON_PIN 21
unsigned long debounceDelay = 5000; // 5 seconds
// Settings for OpenAI API
#define AI_URI "https://api.openai.com/v1/completions"
#define AI_KEY "Bearer sk-xyzxxxx"
#define USER_AGENT "Storybot 1.0 (https://storybot.lol/about)"
#define STORY_SIZE 150
#define DEFAULT_PROMPT "Write a funny story about the anarchist hackerspace:"
#define AI_TIMEOUT 30000
#define RETRIES 4
// Device name for OTA updates
#define DEVICE_NAME "storybot10"
#define DEVICE_PASS "storybot"
// Web App - set to 0 if you don't want to use a web app
// This allows you to save stories to a database and post to mastodon
#define USE_WEB_APP 0
#define STORYBOT_KEY "ASDFQWERTY"
// if you want to post stories to Mastodon
#define MASTO_KEY "Bearer xyzzzzzzz"
#define MASTO_URI "https://botsin.space/api/v1/statuses"
// If you have multiple storybots, you can change the device name to differentiate them
#define DEVICE_ID 100
// Be REALLY verbose in Serial output
#define DEBUG 1
Adafruit_Thermal printer(&Serial2); // Pass addr tbo printer constructor
// Include various Wifi networks you want to connect to here, it'll cycle through them
#define NUM_NETWORKS 3
const char* wifi[][2] = {
{"Noisebridge Cap",""},
{"Noisebridge","noisebridge"}
};
unsigned long lastTime = 0;
int counter = 0;
void setup() {
Serial.begin(115200);
Serial2.begin(19200, SERIAL_8N1, RXD2, TXD2);
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Set up wifi
tryWifiNetworks();
OTASetup();
printer.begin();
printer.setDefault();
if(USE_WEB_APP) { phoneHome(DEVICE_ID); }
}
void loop() {
// handle OTA if needed
ArduinoOTA.handle();
counter = 0;
// reconnect to wifi if disconnected
checkWifi();
// Check if button is pressed and debounce, wait 5 seconds before allowing printing again
if (digitalRead(BUTTON_PIN) == LOW && millis() - lastTime > debounceDelay) {
lastTime = millis();
Serial.println("Button pressed");
// Get the story from the API
Serial.println("Generating and parsing story...");
String res = getStory(DEFAULT_PROMPT,STORY_SIZE);
// If there's a valid story print it out
if(res.length() > 5) {
// Print the story
Serial.println("Printing story...");
printStory(res);
}
}
}
// API-post request story
String getStory(String prompt, int length) {
counter++;
// HTTPClient setup stuff
HTTPClient http;
http.setUserAgent(USER_AGENT);
// set this timeout much higher
http.setTimeout(AI_TIMEOUT);
int responseCode = 0;
http.begin(AI_URI);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", AI_KEY);
http.addHeader("User-Agent", USER_AGENT);
String payload = "{\"model\":\"text-davinci-003\",\"prompt\":\""+prompt+"\",\"max_tokens\":"+String(length)+",\"temperature\":0.9,\"top_p\":1,\"frequency_penalty\":0.2,\"presence_penalty\":0.1, \"stop\":\"\"}";
responseCode = http.POST(payload);
if(DEBUG) {
Serial.println("Response code: " + String(responseCode));
}
if(responseCode == 200) {
// Parse response
Serial.println("Parsing JSON...");
StaticJsonDocument<400> filter;
filter["choices"][0]["text"] = true;
DynamicJsonDocument doc(2048);
deserializeJson(doc, http.getStream(),DeserializationOption::Filter(filter));
if(DEBUG) {
Serial.println("What the JSON body looks like");
serializeJsonPretty(doc, Serial);
}
http.end();
// Let's save the story to a remote database and post to mastodon
if(USE_WEB_APP) {
Serial.println("Saving story to database...");
// deal with mastodon, len 500 chars
sendToMastodon(doc["choices"][0]["text"],"0");
sendStoryToDB(doc["choices"][0]["text"]);
}
return doc["choices"][0]["text"].as<String>();
} else {
Serial.println("Story generation failed.");
Serial.println("Response body: " + http.getString());
}
http.end();
// if something fails with our api call, retry it up to the retry limit
if(counter < RETRIES) {
Serial.println("Something failed. Waiting a few seconds and trying again...");
delay(5000);
return getStory(prompt,length);
} else {
return String("");
}
}
// sends to your mastodon instance
void sendToMastodon(String story,String replyId) {
HTTPClient http;
http.setUserAgent(USER_AGENT);
int responseCode = 0;
http.begin(MASTO_URI);
http.addHeader("Authorization", MASTO_KEY);
String payload = "status="+story.substring(0,499);
if(replyId.length() > 10) {
payload = "in_reply_to_id="+String(replyId)+"&status="+story.substring(0,499);
}
responseCode = http.POST(payload);
DynamicJsonDocument doc(1024);
deserializeJson(doc, http.getStream());
if(DEBUG) {
Serial.println("Response code: " + String(responseCode));
Serial.println("Payload: " + payload);
//Serial.println(http.getString());
serializeJsonPretty(doc, Serial);
}
if (responseCode == 200) {
// recursive call, there's still more to the body so we need to create additional posts
if(story.length() > 499) {
delay(1000);
sendToMastodon(story.substring(499),doc["id"]);
}
}
http.end();
}
void sendStoryToDB(String story) {
HTTPClient http;
http.setUserAgent(USER_AGENT);
int responseCode = 0;
http.begin("https://storybot.lol/save_story");
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "abc123");
responseCode = http.POST("{\"key\":\""+String(STORYBOT_KEY)+"\"\"story\":\""+story+"\"}");
if(DEBUG) {
Serial.println("Response code: " + String(responseCode));
Serial.println("Response body: " + http.getString());
}
http.end();
}
void phoneHome(int id) {
HTTPClient http;
http.setUserAgent(USER_AGENT);
int responseCode = 0;
http.begin("https://storybot.lol/checkin");
http.addHeader("Content-Type", "application/json");
String payload = "{\"message\":\"hello\",\"id\":"+String(id)+"}";
responseCode = http.POST(payload);
http.end();
// To Do: get a prompt from your web app.
}
// Print story to thermal printer
void printStory(String story) {
// formatting is a PAIN
printer.setDefault();
printer.wake();
printer.setSize('M');
printer.boldOn();
printer.println("*** HACKERSPACE STORIES ***");
printer.doubleHeightOff();
printer.boldOff();
printer.setSize('S');
printer.println("Written by AI. storybot.lol");
printer.print(DEFAULT_PROMPT);
printer.println(story);
printer.feed(2);
printer.sleep();
if(DEBUG) { Serial.println("Printing..."+story); }
}
void OTASetup(){
// Hostname defaults to esp3232-[MAC]
ArduinoOTA.setHostname(DEVICE_NAME);
// No authentication by default
ArduinoOTA.setPassword(DEVICE_PASS);
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH)
type = "sketch";
else // U_SPIFFS
type = "filesystem";
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
})
.onEnd([]() {
Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.begin();
}
void tryWifiNetworks() {
for (int i = 0; i < NUM_NETWORKS; i++) {
int counter = 0;
Serial.print("Trying to connect to WiFi network: ");
Serial.println(wifi[i][0]);
WiFi.begin(wifi[i][0], wifi[i][1]);
while (WiFi.status() != WL_CONNECTED && counter < 30) {
delay(500);
Serial.print(".");
counter++;
}
if(WiFi.status() == WL_CONNECTED) {
// we found a network so let's be done with this.
Serial.print("Connected to WiFi network: ");
Serial.println(wifi[i][0]);
//Serial.println("IP address: "+WiFi.localIP());
break;
}
}
// if we still aren't connected for some reason, let's try again...
if(WiFi.status() != WL_CONNECTED) {
Serial.println("No wifi connection established. Waiting 10 seconds and trying again.");
delay(10000);
tryWifiNetworks();
}
}
void checkWifi() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Reconnecting to WiFi...");
WiFi.disconnect();
WiFi.reconnect();
delay(100);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment