ErikmeetsChatGPT/2025_ErikaCode
2025-01-18 16:20:15 +01:00

555 lines
No EOL
16 KiB
Text

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "utf8coder.hpp"
#include "ddr2unicode.h"
#include "unicode2ddr.h"
// hardware specific includes
#include <Adafruit_NeoPixel.h>
//#include "esp32wroom_board.h"
#include "M5StampS3_board.h" // in the EFS26 connector
#include "neopixel.hpp"
#define CHATAI 1
#define KOBOLD 2
#define OPENAI 3
#define HOST CHATAI
#include "aimodelconfig.h"
#include "credentials.h"
#define PC_BAUD 115200
class ErikaSchreibmaschine
{
private:
int _txPin, _rxPin, _rtsPin, _baud;
bool _wait = false;
#ifdef SWSERIAL
EspSoftwareSerial::UART serconn;
#else
#define serconn HWSERIAL
#endif
size_t num_spalten;
size_t spalte;
public:
ErikaSchreibmaschine(int rxPin, int txPin, int rtsPin, int baud)
#ifdef SWSERIAL
:spalte(0), num_spalten(75), serconn(_rxPin, _txPin) // RX, TX
#else
:spalte(0), num_spalten(75) // end of line "beep" here
#endif
{
_rtsPin=rtsPin;
_txPin=txPin;
_rxPin=rxPin;
_baud=baud;
}
bool Init()
{
pinMode(_rtsPin, INPUT_PULLUP);
#ifdef SWSERIAL
serconn.begin(_baud, SWSERIAL_8N1, _rxPin, _txPin, false);
#else
serconn.begin(_baud);
#endif
//serconn.setTimeout(0);
//serconn.begin(_baud);
//return ClearBuf();
return true;
}
bool ClearBuf()
{
// alles weglesen - puffer leeren
bool r =false;
while (serconn.read() > 0)
r=true;
if(r)
{
Serial.println("Tastaturpuffer geleert.");
}
return r;
}
bool CharAvailable()
{
return serconn.available();
}
char16_t ReadC()
{
if(CharAvailable())
{
int znummer = serconn.read();
Serial.print(znummer,16);
char zeichen = ddr2uni[znummer];
Serial.print(zeichen,16);
return zeichen;
}
return 0;
}
// Liest eine ganze Zeile bis \n und liefert das als utf-8 kodierten string
String ReadLine()
{
String eingabestring;
std::vector<char16_t> uniZeichenfolge;
while(true)
{
if(CharAvailable())
{
int ddrZeichen = serconn.read();
// breaker for while loop
if(ddrZeichen == 119) // newline
{
break;
}
switch (ddrZeichen)
{
case 114: // Backtab
{
if(uniZeichenfolge.size() > 0) //this does not work as expected
{
uniZeichenfolge.pop_back();
}
break;
}
case 117: { break; } // Einzug ignorieren
case 118: { break; } // Papier zurück ignorieren
case 119: { break; } // Newline hier ignorieren
case 121: { break; } // Tab ignorieren
default:
{ // Druckbare Zeichen anhängen
if (ddrZeichen > 127) {} // nothing - kann nicht sein // it can be ;-) it can be a command code!
else // gutes Zeichen auf der Erikatastatur
{
char16_t uniZeichen = ddr2uni[ddrZeichen]; // a bit dangerous before where it was (8 bit in for 7 bits lookup)
uniZeichenfolge.push_back(uniZeichen);
}
break;
}
};
}
else
{
// däumchen drehen
delay(100);
}
}
// Tastatur in unicode eingaben sind im vector
// codepoints in utf-8 bytefolge verwandeln.
for (size_t i = 0; i < uniZeichenfolge.size(); i++)
{
std::vector<uint8_t> utfbytes = numberToUtf8(uniZeichenfolge[i]);
String s(utfbytes.data(), utfbytes.size());
eingabestring.concat(s);
}
return eingabestring;
}
/*void WriteC(char zeichen) //char16_t
{
Serial.printf("WriteC: %c\n", zeichen);
if(zeichen=='@')
{
serconn.write((char)uni2ddr['O']);
serconn.write(114); // Backtab
serconn.write((char)uni2ddr['a']);
spalte++;
return;
}
else {
if (zeichen < 128) {
serconn.write(isolatin12ddr[zeichen]);
spalte++;
}
else {
Serial.print(zeichen);
if (zeichen == 0xc39f ) {serconn.write(0x47); }//ß
else if (zeichen == 0xc3a4 ) {serconn.write(0x65); }//ä
else if (zeichen == 0xc3b6 ) {serconn.write(0x66); }//ö
else if (zeichen == 0xc3bc ) {serconn.write(0x67); }//ü
else if (zeichen == 0xc384 ) {serconn.write(0x3F); }//Ä
else if (zeichen == 0xc396 ) {serconn.write(0x3C); }//Ö
else if (zeichen == 0xc39c ) {serconn.write(0x3A); }//Ü
else if (zeichen == 0xc2b0 ) {serconn.write(0x39); }//°
else if (zeichen == 0xc2a7 ) {serconn.write(0x3D); }//§
else {serconn.write(27);}
}
spalte++;
}
//char z = (char)uni2ddr[c];
//serconn.write(z);
//spalte++;
}*/
void Write(String wstring)
{
int i = 0;
int length = wstring.length();
SetDuplexMode();
while (i <= length)
{
if (IsReady()) {
int zeichen = wstring[i];
int charsToSpace = wstring.indexOf(' ', i) - i; //how far is the next space away?
if (charsToSpace > 0) {
if ((spalte + charsToSpace) >= num_spalten) { //next word will not fit on the remaining space of the line
NewLine();
}
}
if (spalte >= num_spalten) {
NewLine();
}
//TODO: avoid "\n " what would create a space as first character on new line
if(zeichen=='@')
{
serconn.write(uni2ddr['O']);
serconn.write(114); // Backtab
serconn.write(uni2ddr['a']);
}
else {
if (zeichen < 128) {
if (zeichen == '\n') {
NewLine();
}
else {
serconn.write(uni2ddr[zeichen]);
}
}
else {
i++;
uint8_t zeichen2 = wstring[i];//utf8chars[i];
zeichen = (zeichen<<8) | zeichen2;
if (zeichen == 0xc39f ) {serconn.write(0x47); }//ß
else if (zeichen == 0xc3a4 ) {serconn.write(0x65); }//ä
else if (zeichen == 0xc3b6 ) {serconn.write(0x66); }//ö
else if (zeichen == 0xc3bc ) {serconn.write(0x67); }//ü
else if (zeichen == 0xc384 ) {serconn.write(0x3F); }//Ä
else if (zeichen == 0xc396 ) {serconn.write(0x3C); }//Ö
else if (zeichen == 0xc39c ) {serconn.write(0x3A); }//Ü
else if (zeichen == 0xc2b0 ) {serconn.write(0x39); }//°
else if (zeichen == 0xc2a7 ) {serconn.write(0x3D); }//§
else {serconn.write(27);} // write an asteriks * for any unsupported UTF char
}
}
spalte++;
i++;
_wait = true;
delay(50); // RTS will not change stange instantly
}
else {
delay(10);
}
}
SetSimplexMode();
}
void WriteLn(String wstring)
{
Write(wstring);
NewLine();
}
void NewLine()
{
serconn.write(uni2ddr['\n']);
//serconn.write(ascii2ddr['\r']);
spalte = 0;
}
// Maschine bereit für nächstes Zeichen
bool IsReady()
{
int rtsState = digitalRead(_rtsPin);
if (rtsState == LOW) {
neopixelShowReady(); // I know, bad style...
}
else {
neopixelShowBusy(); // I know, bad style...
}
return rtsState == LOW;
}
void writeCommand(int commandCode) {
if(IsReady()) {
serconn.write(commandCode);
delay(100);
}
}
/*
Trennmode aktivieren (Trennung von Tastatur und Druckwerk)
Duplexbetrieb: Alle Tasteninformationen werden nur nach TxD ausgegeben und
nur die von RxD kommenden gelangen zum Druck (Korr, CREL und CRL sind nicht wirksam!)
*/
void SetDuplexMode(){
delay(500);
writeCommand(0x91);
delay(100);
}
/*
Trennmode aufheben
Simplexbetrieb: Alle Tasteninformationen werden gedruckt
*/
void SetSimplexMode(){
delay(1000);
writeCommand(0x92);
delay(100);
}
};
//int max_tokens = 356; // Für längere Antworten erhöhen
//char Nachricht_array[6096]; // Bei größeren max_tokens das Nachricht_array auch vergrößern
// Instantiiere die Schreibmaschine
ErikaSchreibmaschine erika(ERIKA_RX, ERIKA_TX, RTS_PIN, ERIKA_BAUD);
void WiFiEvent(WiFiEvent_t event) {
Serial.printf("[WiFi-event] event: %d\n", event);
switch (event) {
case ARDUINO_EVENT_WIFI_READY:
Serial.println("WiFi interface ready");
break;
case ARDUINO_EVENT_WIFI_SCAN_DONE:
Serial.println("Completed scan for access points");
break;
case ARDUINO_EVENT_WIFI_STA_START:
Serial.println("WiFi client started");
Serial.println("Connected to access point");
neopixel.setPixelColor(0, neopixel.Color(0, 0, 255));
neopixel.show();
break;
case ARDUINO_EVENT_WIFI_STA_STOP:
Serial.println("WiFi clients stopped");
break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
Serial.println("Connected to access point");
neopixel.setPixelColor(0, neopixel.Color(0, 255, 0));
neopixel.show();
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.println("Disconnected from WiFi access point");
neopixel.setPixelColor(0, neopixel.Color(255, 0, 0));
neopixel.show();
break;
case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
Serial.println("Authentication mode of access point has changed");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.print("Obtained IP address: ");
Serial.println(WiFi.localIP());
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
Serial.println("Lost IP address and IP address is reset to 0");
break;
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
Serial.println("Client connected");
break;
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
Serial.println("Client disconnected");
break;
case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
Serial.println("Assigned IP address to client");
break;
case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
Serial.println("Received probe request");
break;
case ARDUINO_EVENT_WIFI_AP_GOT_IP6:
Serial.println("AP IPv6 is preferred");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
Serial.println("STA IPv6 is preferred");
break;
default: break;
}
}
void setup()
{
delay(1000);
Serial.begin(115200);
delay(1000);
Serial.println("geht los ...");
neopixelInit();
erika.Init();
// Mit dem WLan verbinden
WiFi.mode(WIFI_STA);
WiFi.onEvent(WiFiEvent);
if (password != "") {
WiFi.begin(ssid, password);
}
else {
WiFi.begin(ssid);
}
WiFi.setAutoReconnect(true);
delay(3000);
erika.NewLine();
erika.WriteLn(helloMsg);
erika.NewLine();
while (WiFi.status() != WL_CONNECTED)
{
Serial.print('.');
delay(500);
}
}
void loop()
{
// EINGABE ###############################################################################
String eingabe= erika.ReadLine(); //waits until \n
Serial.println(eingabe);
// EINGABE ENDE ##########################################################################
// AUSGABE ###############################################################################
if (eingabe.length() == 0) {
Serial.println("leere Zeile");
}
else {
// Request
Serial.println();
Serial.print("Sende API Request an: "); Serial.println(url);
WiFiClientSecure client;
client.setInsecure(); // we do not check certificates or checksums
JsonDocument jsonaiapi;
jsonaiapi["model"] = model;
jsonaiapi["messages"][0]["role"] = "system";
jsonaiapi["messages"][0]["content"] = systemPrompt;
jsonaiapi["messages"][1]["role"] = "user";
jsonaiapi["messages"][1]["content"] = eingabe;
jsonaiapi["temperature"] = 0.7;
String request;
serializeJson(jsonaiapi, request);
//eingabe.replace("\"", "\\\""); // sanitize hyphens before we encode the input in JSON
//request = "{\"model\":\"" + model + "\",\"messages\": [{\"role\":\"system\",\"content\":\"You are a helpful assistant\"},{\"role\":\"user\",\"content\":\"" + eingabe + "\"}], \"temperature\":0.7}";
//request = "{\"model\":\"" + model + "\",\"messages\": [{\"role\":\"system\",\"content\":\"" + systemPrompt + "\"},{\"role\":\"user\",\"content\":\"" + eingabe + "\"}], \"temperature\":0.7}";
Serial.print("request: "); Serial.println(request);
HTTPClient http;
http.setTimeout(60000); //server response might take a while
http.addHeader("Content-Type", "application/json; charset=utf-8");
http.addHeader("Authorization", authMode + " " + authToken);
http.addHeader("Accept", "application/json");
neopixelShowConnect();
if (http.begin(client, url)) { // HTTPS
Serial.print("[HTTPS] POST...\n");
unsigned long millisStart = millis();
int httpCode = http.POST(request);
// httpCode will be negative on error
if (httpCode > 0) {
// HTTP header has been send and Server response header has been handled
Serial.printf("[HTTPS] POST... code: %d\n", httpCode);
// file found at server
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
unsigned long millisDuration = millis()-millisStart;
neopixelShowReady();
String payload = http.getString();
Serial.println(payload);
JsonDocument jsondoc;
deserializeJson(jsondoc, payload);
payload.clear();
Serial.println("geparst: ");
http.end();
String antwort = jsondoc["choices"][0]["message"]["content"];
Serial.println(antwort);
erika.NewLine();
erika.WriteLn(antwort);
WiFiClientSecure clientlog;
clientlog.setInsecure();
HTTPClient httplog;
httplog.setTimeout(10000);
httplog.addHeader("Content-Type", "application/json; charset=utf-8");
//httplog.addHeader("Accept", "application/json; charset=UTF-8");
if (httplog.begin(clientlog, logURL)) { // HTTPS
Serial.print("[HTTPS] POST Logging ...\n");
JsonDocument jsonlog;
jsonlog["request"] = eingabe;
jsonlog["response"] = antwort;
jsonlog["systemprompt"] = systemPrompt;
jsonlog["model"] = model;
jsonlog["duration"] = millisDuration;
//String logString = "{\"request\":\"" + eingabe + "\", \"response\":\"" + antwort + "\", \"systemprompt\":\"" + systemPrompt + "\", \"model\":\"" + model + "\", \"duration\":" + millisDuration + "}";
String logString;
serializeJson(jsonlog, logString);
Serial.println(logString);
int httpCodeLog = httplog.POST(logString);
// httpCode will be negative on error
if (httpCodeLog > 0) {
Serial.println("geloggt");
httplog.end();
} else {
Serial.printf("[HTTPS] POST Logging... failed, error: %s\n", http.errorToString(httpCodeLog).c_str());
httplog.end();
}
} else {
Serial.printf("[HTTPS] Unable to connect to log\n");
}
}
} else {
Serial.printf("[HTTPS] POST... failed, error: %s\n", http.errorToString(httpCode).c_str());
http.end();
neopixelShowFail();
delay(1000);
}
} else {
Serial.printf("[HTTPS] Unable to connect\n");
neopixelShowFail();
delay(1000);
}
Serial.println("Request+response fertig");
erika.WriteLn("======");
erika.NewLine();
// AUSGABE ENDE ##########################################################################
}
}