Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ This project is born from the idea of "bringing receipts" for the life you lead/

Affiliate links to grab the components (if you want to use them):

| Component | Amazon US | Amazon UK | AliExpress |
| --------------------------------- | ----------------------- | ----------------------- | ----------------------------------------- |
| Microcontroller (USB-C D1 Mini) | https://amzn.to/4h2zQYO | https://amzn.to/4gRFgFe | - |
| Thermal Printer (CSN-A4L) | https://amzn.to/4kr5ksq | - | https://s.click.aliexpress.com/e/_opjoNrw |
| Paper Rolls, BPA-free (57.5x30mm) | https://amzn.to/4kpOREP | https://amzn.to/44nqGCg | - |
| Component | Amazon US | Amazon UK | AliExpress | Europe |
| --------------------------------- | ----------------------- | ----------------------- | ----------------------------------------- | -------- |
| Microcontroller (USB-C D1 Mini) | https://amzn.to/4h2zQYO | https://amzn.to/4gRFgFe | - | https://amzn.eu/d/c7Gh3AX |
| Thermal Printer (CSN-A4L) | https://amzn.to/4kr5ksq | - | https://s.click.aliexpress.com/e/_opjoNrw | https://de.aliexpress.com/item/1005004083860562.html (TAKE TTL NOT RS232) |
| Paper Rolls, BPA-free (57.5x30mm) | https://amzn.to/4kpOREP | https://amzn.to/44nqGCg | - | hard to find |

**Important Note (Thanks to the community for pointed this out!):** Do your own due diligence regarding thermal paper types - the thermal paper we handle everyday (e.g. through receipts from the grocery store, restaurants, takeaway, taxis, etc.) will contain BPA. When choosing your rolls for this, you should definitely go for BPA-free paper just to be on the safer side - the links provided are for BPA-free paper. If you can, go a step further and look for “phenol-free” paper. Three types that do not contain BPA or BPS and are competitively priced contain either ascorbic acid (vitamin C), urea-based Pergafast 201, or a technology without developers, Blue4est.

Expand Down
78 changes: 69 additions & 9 deletions firmware v1/firmware v1.ino
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ void setInverse(bool enable);
void printLine(String line);
void advancePaper(int lines);
void printWrappedUpsideDown(String text);
String replaceUmlauts(String text); // umlaut replacement
void ensureUpsideDown(); // NEW: force upside-down mode each job

// === WiFi Configuration ===
const char* ssid = "Your WIFI name";
Expand Down Expand Up @@ -142,7 +144,7 @@ void handleRoot() {
<script defer>
function handleInput(el) {
const counter = document.getElementById('char-counter');
const remaining = 200 - el.value.length;
const remaining = 250 - el.value.length;
counter.textContent = `${remaining} characters left`;
counter.classList.toggle('text-red-500', remaining <= 20);
}
Expand Down Expand Up @@ -173,12 +175,12 @@ void handleRoot() {
</script>
</head>
<body class="flex flex-col min-h-screen justify-between items-center py-12 px-4 font-sans">
<main class="w-full max-w-md text-center">
<main class="w-full max-w-md text center">
<h1 class="text-3xl font-semibold mb-10 text-gray-900 tracking-tight">Life Receipt:</h1>
<form id="receipt-form" onsubmit="handleSubmit(event)" action="/submit" method="post" class="bg-white shadow-2xl rounded-3xl p-8 space-y-6 border border-gray-100">
<textarea
name="message"
maxlength="200"
maxlength="250"
oninput="handleInput(this)"
onkeypress="handleKeyPress(event)"
placeholder="Type your receipt…"
Expand All @@ -187,7 +189,7 @@ void handleRoot() {
required
autofocus
></textarea>
<div id="char-counter" class="text-sm text-gray-500 text-right">200 characters left</div>
<div id="char-counter" class="text-sm text-gray-500 text-right">250 characters left</div>
<button type="submit" class="w-full bg-gray-900 hover:bg-gray-800 text-white py-3 rounded-xl font-medium transition-all duration-200 hover:scale-[1.02] hover:shadow-lg">
Send
</button>
Expand Down Expand Up @@ -340,16 +342,30 @@ void initializePrinter() {

// Enable 180° rotation (which also reverses the line order)
printer.write(0x1B); printer.write('{'); printer.write(0x01); // ESC { 1

// Prime a newline so the first real command/char isn't dropped
printer.write('\n');
delay(20);

Serial.println("Printer initialized");
}

// Force upside-down mode; resend cmd to survive dropped first byte
void ensureUpsideDown() {
printer.write(0x1B); printer.write('{'); printer.write(0x01); // ESC { 1
delay(10);
}

void printReceipt() {
ensureUpsideDown(); // ensure orientation before each job
Serial.println("Printing receipt...");

// Print wrapped message first (appears at bottom after rotation)
printWrappedUpsideDown(currentReceipt.message);

// Insert a blank line before the date for an easier tear line
printLine(""); // blank line

// Print header last (appears at top after rotation)
setInverse(true);
printLine(currentReceipt.timestamp);
Expand All @@ -362,6 +378,7 @@ void printReceipt() {
}

void printServerInfo() {
ensureUpsideDown(); // ensure orientation before this job
Serial.println("=== Server Info ===");
Serial.print("Local IP: ");
Serial.println(WiFi.localIP());
Expand Down Expand Up @@ -389,6 +406,8 @@ void setInverse(bool enable) {
}

void printLine(String line) {
// Replace umlauts before printing (German to ASCII workaround)
line = replaceUmlauts(line);
printer.println(line);
}

Expand All @@ -398,26 +417,67 @@ void advancePaper(int lines) {
}
}

// Updated text wrapping with explicit newline handling and CR/LF normalization
void printWrappedUpsideDown(String text) {
// Normalize line endings to '\n' to handle Windows/Mac inputs
text.replace("\r\n", "\n");
text.replace("\r", "\n");

// Apply umlaut workaround
text = replaceUmlauts(text);

String lines[100];
int lineCount = 0;


// First split the incoming text by explicit newline characters
int nlIndex = text.indexOf('\n');
while (nlIndex != -1) {
String oneLine = text.substring(0, nlIndex);
text = text.substring(nlIndex + 1);

// Wrap this single logical line
while (oneLine.length() > 0) {
if (oneLine.length() <= maxCharsPerLine) {
lines[lineCount++] = oneLine;
break;
}
int lastSpace = oneLine.lastIndexOf(' ', maxCharsPerLine);
if (lastSpace == -1) lastSpace = maxCharsPerLine;
lines[lineCount++] = oneLine.substring(0, lastSpace);
oneLine = oneLine.substring(lastSpace);
oneLine.trim();
}

nlIndex = text.indexOf('\n');
}

// Wrap the remaining text after the last newline
while (text.length() > 0) {
int breakIndex = maxCharsPerLine;
if (text.length() <= maxCharsPerLine) {
lines[lineCount++] = text;
break;
}

int lastSpace = text.lastIndexOf(' ', maxCharsPerLine);
if (lastSpace == -1) lastSpace = maxCharsPerLine;

lines[lineCount++] = text.substring(0, lastSpace);
text = text.substring(lastSpace);
text.trim();
}


// Print in reverse order to achieve upside-down final layout
for (int i = lineCount - 1; i >= 0; i--) {
printLine(lines[i]);
}
}

// === umlaut replacement ===
String replaceUmlauts(String text) {
text.replace("ä", "ae");
text.replace("ö", "oe");
text.replace("ü", "ue");
text.replace("Ä", "Ae");
text.replace("Ö", "Oe");
text.replace("Ü", "Ue");
text.replace("ß", "ss");
return text;
}