# Build your own piggy bank display

A small black-and-white screen that sits on a child's shelf and shows how much is in their piggy bank, how close they are to a savings goal, and reminds them on pocket money day. You update the numbers in a Google Sheet on your phone. The screen picks them up by itself every morning.

This guide has two parts:

- **Part 1 is for you.** What to buy, what to install, and what to do at each step. No technical knowledge assumed.
- **Part 2 is for your AI.** A complete brief you paste into Claude or ChatGPT so it can write the code. You don't need to understand it.

---

# Part 1: For you

## What it does

- **Page 1, piggy bank:** "in your piggy bank" and the balance in big numbers, e.g. **£42.50**.
- **Page 2, goal:** "Saving for: Bike helmet", a progress bar, "£42.50 of £100" and "42%". When the goal is reached it switches to a celebration screen with a trophy.
- **Page 3, pocket money day:** only appears on pocket money day (Wednesday in the original; you can pick your own day).
- The button on the board flips between pages.
- Every morning at 5am it wakes up, reads your Google Sheet over Wi-Fi, updates the screen and goes back to sleep. E-paper keeps its picture with no power, so the screen always shows something, and a battery lasts a long time.
- If it can't reach the sheet it keeps the last numbers and shows a small "!" in the corner.

## What to buy

| Item | Notes |
|---|---|
| **Waveshare ESP32-S3-ePaper-1.54** | The exact board matters. The code is written for this one. Search for that name on Amazon, The Pi Hut or Waveshare's own shop. About £20–£30. |
| **USB-C data cable** | Must be a *data* cable, not a charge-only one. If the computer doesn't notice the board when plugged in, the cable is the first suspect. |
| **Battery (optional)** | A small 3.7V LiPo battery with the plug that fits the board's battery socket. Check Waveshare's product page for the right connector. Without a battery it runs fine from a USB charger. |

You also need a Mac or Windows computer and your home Wi-Fi name and password. The board only works on **2.4GHz** Wi-Fi (most home routers provide this alongside 5GHz).

## Which AI to use

**Best option: Claude Code** (needs a paid Claude plan). It runs on your computer and can type the commands, install things and send the code to the board itself. You mostly watch and say yes. Install it by following the instructions at claude.com/claude-code.

**Also works: Claude or ChatGPT in the browser.** The AI writes the code and you copy it into files and press the buttons yourself. It takes longer, but this guide covers every step.

Either way, the routine is the same: **give the AI Part 2, do one milestone at a time, and tell it exactly what you see.**

## Step 1: Make the Google Sheet

1. Go to sheets.google.com and create a new blank sheet. Name it "Piggy bank".
2. Type this into the top-left corner, exactly like this, one item per cell:

   |   | A | B |
   |---|---|---|
   | 1 | key | value |
   | 2 | balance | £42.50 |
   | 3 | goal | £100 |
   | 4 | goal_label | New bike |

   Leave column A alone from now on. You only ever change column B.
3. Publish it so the board can read it: **File → Share → Publish to web**. In the box that appears, change "Entire document" to your sheet's tab (usually "Sheet1") and change "Web page" to **"Comma-separated values (.csv)"**. Click **Publish**, confirm, and **copy the link** it gives you. Save it somewhere: you'll need it in Step 4.

> **Privacy note:** anyone who has that link can see the numbers in this sheet. That's fine for pocket money, but don't put anything else in it.

To test the link, paste it into a browser. It should download or show something like `key,value` / `balance,£42.50`. If it shows a web page instead, you picked "Web page" by mistake. Publish again and choose CSV.

**Using it later:** to add money, change the balance in B2. To set a new goal, change B3 and B4. To show "No goal set", put `0` in B3 (don't leave it empty, as an empty goal counts as an error). Changes reach the screen the next morning, or straight away if you press the board's reset button.

## Step 2: Install the tools

**If you're using Claude Code:** skip this step. In Step 3, ask Claude Code to install what it needs ("Install PlatformIO for me").

**If you're using a browser chatbot:**

1. Install **Visual Studio Code** (free) from code.visualstudio.com.
2. Open it, click the **Extensions** icon on the left (four little squares), search for **PlatformIO IDE** and click **Install**. Wait until it says it has finished (it can take 5–10 minutes the first time). Restart VS Code when it asks.
3. You'll now see an ant-head icon on the left. That's PlatformIO.

The three buttons you'll use are on the blue bar at the bottom of VS Code:

- **✓ (tick)** = Build. It checks the code compiles. Errors show at the bottom.
- **→ (arrow)** = Upload. It sends the code to the board.
- **🔌 (plug)** = Serial Monitor. It shows the messages the board prints. This is how you find out what the board is doing.

## Step 3: Start the project with your AI

1. Make a folder called `piggy-bank` somewhere that **isn't** synced to iCloud, OneDrive or Dropbox. Sync services can quietly break the build.
2. Start a new conversation with your AI and paste **all of Part 2** below, followed by:

   > I'm not technical. I'm using [Claude Code / VS Code with PlatformIO and copying files by hand]. My computer is a [Mac / Windows PC]. Please do Milestone 1 only. Tell me exactly which files to create, what goes in each one, and what to click. Wait for me to report back before moving on.

3. If you're copying by hand: for each file the AI gives you, in VS Code go to **File → Open Folder** and choose your `piggy-bank` folder, then right-click in the file list on the left → **New File**, type the exact name it gave you (including any folder, such as `src/main.cpp`), paste the code and save with Cmd+S (Mac) or Ctrl+S (Windows).

## Step 4: Give it your Wi-Fi and sheet link, safely

The AI will ask you to create a file called `include/secrets.h`. **Type your Wi-Fi password into that file yourself. Never paste it into the chat.** It looks like this:

```c
#define WIFI_SSID "YourWiFiName"
#define WIFI_PASSWORD "YourWiFiPassword"
#define DATA_URL "https://docs.google.com/spreadsheets/d/e/....../pub?gid=0&single=true&output=csv"
```

Wi-Fi names and passwords are case-sensitive. Keep the quote marks.

## Step 5: Plug in and upload

1. Plug the board into the computer with the USB-C cable.
2. Click **Upload (→)**. The first time takes a few minutes while it downloads things.
3. Click **Serial Monitor (🔌)** to watch what it says.

**If upload fails with "no serial port", "port not found" or similar:** put the board into download mode. Hold down the **BOOT** button, press and release **RESET** (or unplug and replug the cable while holding BOOT), then let go of BOOT. Click Upload again. After it finishes, press RESET once to start your code.

## Step 6: Work through the milestones

The AI builds this in six small stages (listed in Part 2). After each one:

1. Upload it.
2. Look at the screen and the Serial Monitor.
3. Tell the AI **exactly** what you see. Copy and paste anything from the Serial Monitor or any red error text, and describe the screen in plain words ("the screen is blank", "it shows Hello but the text is upside down").
4. Only move on when that stage works.

A photo of the screen helps if your AI accepts images.

## When things go wrong

| What you see | What to try |
|---|---|
| Computer doesn't notice the board | Try a different USB-C cable (charge-only cables are common). Try another USB port. Use download mode (Step 5). |
| Screen stays completely blank | Tell the AI. The most common cause is the screen's power pin. Part 2 explains it, so ask: "Did you drive the panel power pin LOW?" |
| "wifi down" in the Serial Monitor | Check the name and password in `secrets.h`. Make sure it's 2.4GHz Wi-Fi. Move the board closer to the router. |
| "http 400", "http 404" or "bad url" | The sheet link is wrong or not published as CSV. Redo Step 1, part 3. |
| "balance missing" or "balance invalid" | Check column A says exactly `balance` and B2 is a number like `£42.50`. |
| Works on USB but dies on battery | Tell the AI: "It switches off on battery. Check the battery latch pin is held HIGH, including in deep sleep." |
| Screen shows a "!" in the corner | The last update failed. The numbers shown are the last good ones. Check Wi-Fi and the sheet. |
| AI goes round in circles | Start a fresh conversation, paste Part 2 again and say which milestone you're on and what's already working. |

## Things you can change later

Ask the AI for any of these once the basics work:

- A different pocket money day, or a different wake-up time.
- Different wording on the pages (e.g. the child's name).
- A different time zone, if you're not in the UK.

---

# Part 2: For your AI (paste everything below this line)

## Brief: piggy bank e-paper display

You are helping a **non-technical person** build an e-paper piggy bank display. They will do the physical steps and report back. Rules for working with them:

- Work in the milestones below, **one at a time**. After each one, tell them exactly what to click, what they should see on the screen and in the serial monitor, and wait for their report before continuing.
- Give complete files, never fragments or diffs. Give full file paths (e.g. `src/main.cpp`).
- Never ask them to paste their Wi-Fi password into the chat. Credentials go in a gitignored `include/secrets.h` that they type themselves.
- When they report an error, ask for the full serial output or build error text before guessing.
- Keep explanations short and plain. No jargon without a one-line explanation.

### Hardware (verified facts; don't override them from memory)

**Board:** Waveshare ESP32-S3-ePaper-1.54 (V2): ESP32-S3-PICO-1-N8R8 (8MB flash, 8MB PSRAM), 1.54" 200×200 black/white e-paper, BOOT key, PWR key, battery socket. Native USB (CDC), so no USB-serial driver is needed. Waveshare's demo code is at github.com/waveshareteam/ESP32-S3-ePaper-1.54 (`02_Example/Arduino/*/user_config.h` and the schematic are the source of truth if anything below is in doubt).

**Pins:**

| Function | GPIO | Notes |
|---|---|---|
| EPD BUSY | 8 | |
| EPD RST | 9 | |
| EPD DC | 10 | |
| EPD CS | 11 | |
| EPD SCK | 12 | |
| EPD MOSI | 13 | |
| EPD power enable | 6 | **ACTIVE LOW** (drives a P-FET). Drive LOW to power the panel, HIGH to cut it. **The panel shows nothing until this is LOW.** This is the number one cause of a blank screen. |
| Button | 0 | The BOOT key, active low, use INPUT_PULLUP. It is also the only user button, so it doubles as the page button and the deep-sleep wake source. |
| Battery latch (VBAT_PWR) | 17 | On battery the board **only stays powered while this is HIGH**. Drive it HIGH first thing in `setup()`. Driving it LOW turns the board off. Harmless on USB, so a bug here is invisible until it runs on battery. |
| Battery voltage | 4 | ADC1 ch3 through a ½ divider: volts = 2 × `analogReadMilliVolts(4)` / 1000. |
| Audio amp power | 42 | Active low. Not used: hold it HIGH (off) during deep sleep. |

**Display driver:** GxEPD2 with `GxEPD2_BW<GxEPD2_154_D67, GxEPD2_154_D67::HEIGHT>` and Adafruit GFX. Initialise SPI on the pins above. Supports partial refresh. Do a full refresh every few partial refreshes to clear ghosting, and always after waking from deep sleep (the panel controller loses its RAM when its supply is cut). Call `powerOff()` after every refresh.

### Toolchain

PlatformIO, Arduino framework. Use this `platformio.ini`:

```ini
[platformio]
default_envs = board

[env:board]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
framework = arduino
board = esp32-s3-devkitc-1
board_upload.flash_size = 4MB
board_build.partitions = default.csv
monitor_speed = 115200
build_flags =
    -DARDUINO_USB_MODE=1
    -DARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
    zinggjm/GxEPD2
    adafruit/Adafruit GFX Library
test_ignore = *

[env:native]
platform = native
build_flags = -std=gnu++17
```

`include/secrets.h` (gitignored; provide a `secrets.h.example`):

```c
#pragma once
#define WIFI_SSID "your-network-name"
#define WIFI_PASSWORD "your-password"
#define DATA_URL "https://docs.google.com/spreadsheets/d/e/.../pub?gid=0&single=true&output=csv"
```

If upload can't find the port: hold BOOT, tap RESET (or replug), release BOOT, upload, then press RESET.

### Data source

A Google Sheet published to the web as CSV (File → Share → Publish to web → CSV). The CSV looks like:

```
key,value
balance,£42.50
goal,£100
goal_label,New bike
```

- Look up rows by key in column 0 (trim, case-insensitive). Ignore the header and unknown keys. Take the first occurrence of each key. Skip rows whose cells are all empty (sheets export trailing `,,` rows).
- `balance` is required. `goal` is required and must be a valid amount. `0` means "no goal" (an empty value is `goal invalid`). `goal_label` is optional.
- Store money as integer pence. Accept `£42.50`, `42.5`, `£100`, `£1,250`, `-£5.50`, `£-5`. Reject anything else and treat it as a failed refresh. Errors: `balance missing`, `balance invalid`, `goal missing`, `goal invalid`.
- Format money as `£42.50`, `£100` (no decimals for whole pounds), `£1,250`, `£1,250.75`.
- The CSV parser must handle quoted fields, escaped quotes, CRLF, a UTF-8 BOM, newlines inside quotes and a missing final newline. Sheets wraps `£1,250` in quotes.

**Fetching:** HTTPS via `WiFiClientSecure` with `setInsecure()` (acceptable here: public read-only data). **Google redirects the publish URL, so set `HTTPC_FORCE_FOLLOW_REDIRECTS`.** 10s timeout. Cap the body at 16KB. Treat truncated or empty bodies, or anything other than HTTP 200, as failure. **On any failure, keep the last good data** and show the failure mark.

**Pound sign:** the built-in Adafruit GFX font is cp437, not UTF-8. Convert UTF-8 `£` (`\xC2\xA3`, or a lone `0xA3`) to byte `0x9C`, which is the font's own pound glyph. Map other non-ASCII to `?` and control characters to spaces.

### Pages (200×200, black and white, built-in font)

Built-in font size *s* = 6*s* px wide × 8*s* px tall per character. Text never wraps, so truncate to fit.

1. **Piggy bank.** A black band across the top (0–54px) with white text "in your" / "piggy bank" (size 2) and a small white coin-stack icon on the right. Below, the balance centred, as big as fits within 176px (size 5 down to 1). A dashed line near the bottom. Before any data arrives: the band says "Hello!" and the body says "Waiting for data...".
2. **Goal.** States:
   - *In progress:* "Saving for:" (size 1), the goal label (size 2, wrapped to two lines of 15 characters at word boundaries, with the second line ending in ".." if too long), a 168×22 progress bar with a 2px border filled by percent, "£42.50 of £100" (size 2, or size 1 if wider than 168px), and the percent large (size 3). Percent = floor(balance×100/goal), clamped 0–100.
   - *Reached* (balance ≥ goal): inverted (black background), a trophy icon with sparkles, "You've reached / your goal!", a dotted divider, the label, then the amounts.
   - *No goal:* a boxed "No goal set".
   - *No data yet:* a boxed "Waiting for data...".
3. **Pocket money day.** Inverted, "Pocket / money / day!" in size 4 with sparkles and a coin. **Only included on Wednesdays** (make the day a constant) and only when the clock is known. On that day it is also the page shown after every refresh.

**Failure mark:** when the last refresh failed, draw a small "!" icon in the top-right corner of every page (white on black pages, with a white notch in the piggy page's band).

Icons are small 1-bit bitmaps in a header (`icons.h`). Generate them with a script or draw them as simple shapes with GFX primitives. Simple is fine.

The button cycles through the pages for today, wrapping around. If the day rolls over while showing a page that no longer exists, go to the first page.

### Time

SNTP via `configTzTime` with servers `pool.ntp.org` and `time.google.com`. UK time zone string: `GMT0BST,M3.5.0/1,M10.5.0` (change it for other countries). **The TZ setting does not survive deep sleep, so call `setenv("TZ", ...)`/`tzset()` at the start of every boot.** Use non-blocking `getLocalTime(&t, 0)`. "Time known" means it has returned true. After a deep-sleep wake the clock is set but drifts, so wait up to 10s for a real SNTP sync.

### Power and sleep

The board deep-sleeps almost all the time. Three ways to wake:

- **Timer (daily 05:00 local, or a retry):** connect Wi-Fi, sync time, fetch, parse, draw the home page, sleep. If the fetch fails, retry hourly up to 3 times, then go back to the daily schedule (or 6 hours if the time was never known). The chip's clock drifts and can wake a bit *before* 05:00. If the next 05:00 is under 2 hours away, aim for tomorrow's instead of refreshing twice.
- **BOOT button (EXT1, `ESP_EXT1_WAKEUP_ANY_LOW` on GPIO0):** restore saved state, wait for the button to be released (so the waking press doesn't count twice), show the *next* page, stay awake 15s for more presses, then sleep for the time remaining until the planned wake. No Wi-Fi on button wakes.
- **Power-on/reset/after upload:** do a full refresh, then **stay awake 2 minutes** before sleeping, so the board can be re-flashed and its serial log read. Without this, a sleeping board is hard to upload to.

Keep the model, current page, retry count and next-wake epoch in `RTC_DATA_ATTR` memory with a magic/version check, so they survive deep sleep.

**Before deep sleep:** stop Wi-Fi. Drive panel power (GPIO6) HIGH and audio power (GPIO42) HIGH, and hold them (`rtc_gpio_hold_en` for 6, `gpio_hold_en` plus `gpio_deep_sleep_hold_en` for 42). **Hold the battery latch (GPIO17) HIGH with `rtc_gpio_hold_en`, permanently, awake and asleep, and never release it.** Without the hold, the pin falls in deep sleep and the board switches itself off on battery and never wakes. A pad hold also survives panics and watchdog resets, which an ordinary output doesn't. If any hold or wake source fails to set up, `esp_restart()` instead of sleeping.

**On wake:** set the GPIO output registers to the held levels (17 HIGH, 6 HIGH) *before* releasing any hold, so nothing glitches. Release 6 (and 42), but never 17. Power the panel (6 LOW) only right before the first draw.

Log one summary line per refresh to serial: wifi state, IP, fetch ok, rows, error, weekday, uptime, reset reason, battery volts, wake cause, sleep seconds, retries. The user will paste this to you.

### Code structure

Small units, each with one job. Keep pure logic free of Arduino headers so it can be unit tested on the computer (`pio test -e native`):

- `lib/csvdata`: CSV parse and display sanitiser *(unit tested)*
- `lib/sheetdata`: money parse/format, model from rows, percent, label wrap *(unit tested)*
- `lib/schedule`: which pages today, next page, home page *(unit tested)*
- `lib/wake`: sleep-duration maths *(unit tested)*
- `src/display`: panel init, full/partial refresh, power off
- `src/input`: button with debounce
- `src/wifi_link`, `src/clock`, `src/fetch`
- `src/render`: draws each page from state (the only file that changes when page designs change)
- `src/sleep`, `src/rtc_state`, `src/board_power`, `src/main.cpp`

### Milestones (each one must be confirmed working on the device by the user before the next)

1. **Hello screen.** Latch HIGH, panel power LOW, draw "Hello" in large text. Confirm the serial monitor shows output. *Expect: text on screen.*
2. **Button and pages.** Three placeholder pages, BOOT cycles them, partial refresh with periodic full refresh. *Expect: each press changes the page.*
3. **Wi-Fi.** Connect using `secrets.h` and show the status and IP on screen and in serial. *Expect: "connected" and an IP like 192.168.x.x.*
4. **Data.** Fetch the sheet, parse and show the real balance on the placeholder pages. Add unit tests for the CSV and money logic. *Expect: the balance from the sheet; change it in the sheet, press RESET, and see it update.*
5. **Real page designs.** Implement the three pages and every goal state described above, plus the failure mark. *Expect: pages match the descriptions. Test by editing the sheet (goal reached, no goal, a bad value to trigger "!").*
6. **Deep sleep and daily refresh.** Implement the power and sleep section exactly. For testing, add a build flag that makes every sleep last 90 seconds and the cold-boot awake window 20 seconds, so the user can watch a few cycles quickly. Then test **on battery, unplugged**: it must still be alive and update the next morning. *Expect: the screen keeps its picture while asleep, the button wakes it, and it updates after the test interval.*

Done means: it runs on battery for days, updates every morning, and the button works at any time.