A printable desk robot of Goli, the Goliash mascot: tired, kind, always one more alert away from a coffee. His whole face is a round AMOLED with an ESP32-S3 behind it, so the eyes, brows and mouth are animated.
| Something new | Something on fire | Good news |
|---|---|---|
![]() |
![]() |
![]() |
| Exploded | Back |
|---|---|
![]() |
![]() |
Try him in your browser · Install the firmware from your browser · 3D viewer (also viewer.html here, with an exploded-assembly slider)
Status: designed and checked in CAD, first prints under way. Print the bezel and bracket first (plate 3 of either 3MF, about 1 h) and test-fit them on the display before you print the body.
About 108 × 65 × 150 mm with the bell on. Every part fits the Bambu Lab A1 mini (180 mm³).
Goli mini is the same robot at 90 %: about 97 × 58 × 135 mm with the bell on (body 109 × 58 × 130 mm
with the arms). The display is the same, so his face is a little bigger for his size. Its files are in
stl/mini/, named goli_mini_* (the bezel, bracket and mug are identical to the full-size ones).
It needs a smaller battery: a 803040 LiPo (40 × 30 × 8 mm, about 1000 mAh) instead of the 103450.
The body prints in about 6 h 45 min in one colour (108 g) or 14 h 40 min with the AMS, and the back
plate in 52 min.
| Part | Notes |
|---|---|
| Waveshare ESP32-S3-Touch-AMOLED-1.75 | 466×466 round AMOLED with touch, ESP32-S3, IMU, mic, speaker amp, LiPo charger and RTC. Get the version without the case. |
| Speaker, 8 Ω 1 W, about 20×30 mm, MX1.25 plug | Glue it behind the belly card; the two "message lines" are the grille. |
| 3× M2×6 machine screws | Bracket → the board's M2 nuts |
| 3× M2×6 self-tapping screws | Bracket → bezel |
| 3× M2×8 self-tapping screws | Back plate → body |
| USB-C cable with a right-angle (90°) plug | The board's port points down inside the face socket, so a straight plug doesn't fit. The plug's head may reach 8 mm from the port and be up to 13 × 7.5 mm; the cable then runs backwards inside him and out through the slot in the back plate. Up- or down-angled both work (USB-C plugs in either way round). A 90° USB-C adapter of the same size works too. |
| Optional: 3.7 V LiPo, 1000 mAh, size 103450 (50 × 34 × 10 mm), MX1.25 plug (Goli mini: 803040, 40 × 30 × 8 mm) | Lies on a cradle in the base (three rails and stops, printed with the body); a strip of double-sided tape holds it. The board's power button is inside the body, so plan for USB power or leave the board on. On battery the screen goes dark after he has slept for 30 min; touch or move him to wake him. |
| File | Colour | Orientation | Notes |
|---|---|---|---|
goli_body_single.3mf, or goli_body_blue.stl alone |
blue | upright, as exported | The quick way: about 8 h, paint the details after. Normal (grid) supports, also inside. Plates 2–4: the other parts (below). |
goli_body_multicolor.3mf (goli_body_blue.stl + goli_belly_cream.stl + goli_accents_red.stl) |
blue / cream / red | upright, as exported | One multi-colour object for the AMS lite, about 18 h. Plates 2–4: the other parts (below). |
goli_bezel_navy.stl |
navy | face down | No supports. With the bracket on plate 3 of both 3MF files (57 min, 15 g). |
goli_bracket_any.stl |
any | as exported | No supports. |
goli_backplate_blue.stl |
blue | outer face down | No supports; about 1 h. Plate 2 of both 3MF files. |
goli_bell_yellow.stl |
yellow | lip down | No supports. With the mug on plate 4 of both 3MF files, at 0.12 mm (29 min). |
goli_mug_white.stl |
white | upright | Optional prop: "coffee #4". |
PLA, 0.2 mm layers (0.12 mm for the bell and mug), 3 walls, 15 % gyroid.
The body needs supports. The arms start as islands beside the body at about 2 cm, and the ceiling inside by the face is nearly flat. Use normal (grid) supports, not on the build plate only: a tree's thin tip let the arms' first layers go into the air. The roof, the face socket and the screw bosses inside have 45° undersides. The inner supports come out through the open back.
The quick way (one colour, about 8 h): open stl/goli_body_single.3mf in Bambu Studio and print it
in blue PLA (0.20 mm, 3 walls, 15 % gyroid, normal supports; 130 g). Then paint the belly,
card, badge and cheeks (see Painting below). The back plate is on the project's second plate (about
1 h, 23 g): print it after the body, the same blue, no supports. Plate 3 has the face bezel and the display bracket
(navy, about 1 h), plate 4 the bell and the mug (yellow and white, 0.12 mm layers, about 30 min); load that
filament before you print the plate. The multi-colour print below takes 18 h, and 10 of those
hours are the AMS lite changing filament about 280 times.
Multi-colour body (AMS lite): open stl/goli_body_multicolor.3mf in Bambu Studio. It is one object
with three parts already on filaments 1–3 (blue, cream, red), set up for the A1 mini with 0.20 mm layers,
3 walls, 15 % gyroid and normal supports. Map the three filaments to your AMS slots and print:
about 18 h and 163 / 58 / 13 g of blue / cream / red PLA, purging included. Plate 2 is the back plate in
blue (filament 1), about 1 h; plate 3 the bezel and bracket (navy, filament 4), plate 4 the bell and mug
(yellow and white, filaments 5 and 6).
With a single colour, use the quick way above.
The belly, the card on it, the red badge and the cheeks all stand out from the body by 0.8-2.8 mm, so their edges stop the paint by themselves.
You need acrylic paint markers with a fine tip, for example Posca PC-3M (or PC-1MR for the cheeks' edges): ivory or beige for the belly and card, red for the badge and cheeks.
- Wipe the print with isopropyl alcohol so the paint sticks; let it dry.
- Shake the marker and prime it on paper until the paint flows evenly.
- Paint each area's raised edge first, going round it with the tip held flat against the step, then fill the middle. Thin coats: two thin ones cover better than one thick one. Wait about 15 minutes between coats.
- Belly: leave the stitching (the little dashes sunk into its edge) blue; it then reads as a seam. Paint the card on top of the belly in the same cream; its two slots are the speaker grille.
- Badge: red; the "!" is sunk in, leave it blue or dot it with a dark marker.
- Cheeks: red, with a light touch: they're the thinnest (0.8 mm).
- Optional: a light coat of matte clear spray once everything is dry (a day), so it doesn't wear off where you pick him up.
A slip of the marker comes off with a cotton bud dipped in water while the paint is wet, or with a fingernail once it's dry.
To rebuild the 3MF files after changing the model, run python3 make_3mf.py (and python3 make_3mf.py --mini). It uses Bambu Studio's command line,
so Bambu Studio must be installed.
- Glue the speaker behind the belly grille, with the plug pointing up.
- Drop the display into the bezel from behind, glass first. The lip holds the glass by its black border.
- Screw the bracket to the board's three back nuts (M2×6). The board's USB-C port points down, through the notch in the bezel. Plug in the right-angle USB-C cable now, its cable pointing backwards.
- Feed the cable in through the face opening and push the bezel into the face socket from the front: the plug's head goes into the pocket under the opening. From inside, screw the bracket to the bezel tube (3× M2×6 self-tapping). The body wall is clamped between the bezel and the bracket.
- Plug in the speaker. With a battery: stick it onto the cradle in the base (label up, cable towards the back) and plug it into BAT. Route the USB-C cable down and out through the back plate slot.
- Close the back plate (3× M2×8). Press the bell onto the antenna peg; add a drop of glue if it is loose.
firmware/ is a PlatformIO project: Arduino core 3 (via pioarduino), LVGL 8.4, the CO5300 driver from
GFX Library for Arduino, SensorLib for touch and IMU, Espressif's ES8311 codec driver, and ArduinoJson.
| Input | Goli |
|---|---|
| nothing happening | tired: heavy lids, worried brows, wavy mouth; blinks and glances around |
| new alert (Goliash, GitHub, a script...) | ding of a desk bell, surprised face, count badge, title scrolling under the mouth |
| something critical (agent gone quiet, vulnerability, production drift...) | ding-ding, alarmed face, red badge |
| good news (an agent is back, a drift was resolved) | chirp, happy face, green badge |
| tap the face | boop: surprised, then happy with blushing cheeks; also marks alerts as seen |
| 2 quick taps | wink |
| 3 quick taps | focus mode (a pomodoro) on / off: 25 minutes by default, a blue ring counts down, only critical alerts ring; happy when it's over, then a 5-minute break with a green ring and "back to it?" (both lengths on the web page; 3 taps skip a break) |
| 5 quick taps | setup mode (QR code on the screen) |
| 10 quick taps | demo mode for a booth (any tap stops it) |
| tap, then hold and talk | ask him something: he listens while the finger stays and answers out loud (with API keys, see Ask Goli) |
| swipe across the face | latest alerts → Goliash overview → today's meetings → big clock → Goli's day → status |
| knock twice on the desk | snooze: alerts stay silent for 10 minutes |
| a meeting in 10 minutes | an amber ring counts down, its name under his mouth; a ding when it starts |
| during a meeting | alerts stay silent (badge only, critical ones still ring), "in a meeting" under his mouth |
| an alert arrives | he hops, and the badge breathes until you've seen it (faster when critical) |
| nothing happens for a while | every few minutes a glance around, a look at the clock, a silent yawn |
| rain / sun / frost coming | umbrella / sunglasses / scarf |
| below 0 °C / above 28 °C outside | a shiver now and then / a drop of sweat |
| every 15 minutes (web page: never, now and then, always) | the date, the time and the temperature under his mouth for a few seconds |
| you pick him up | the time and the temperature, like a glance at a watch |
| your birthday, December | party hat, Santa hat |
| a Goli next to him rings | he looks that way |
| finger resting on / stroking the face | petting: blissful closed eyes |
| clap | marks alerts as seen (wink), or just startles him |
| loud noise for a while | winces |
| tilt him | pupils roll with the tilt |
| shake him | dizzy, pupils spinning, wobbly sound |
| bump or pick him up | surprised |
| 1 min idle → 5 min idle, or lying flat | drowsy → asleep with a yawn (screen dims, "z Z", breathing, the time under his eyes) |
| hold a finger still for 3 s | turns the face by 90° (saved): use it once after assembly |
| finger on the face while he starts | self-test screen (see Checking the assembly) |
| battery low, or charging | battery icon next to the bell |
| production deploy on a Friday after 15:00 | winces: "on a Friday?!" |
The whole face drifts by a few pixels over minutes so the AMOLED does not burn in.
Swipe across his face for the latest alerts (time coloured by level), Goliash at a glance (drifting services, updates waiting, updates that fix
exploited vulnerabilities, stale targets; from GET /api/v1/summary every 5 minutes; only when Goliash is
set up), today's meetings, a big clock, Goli's day (alerts, good news, how often you petted
him, focus sessions done, and whether yesterday was rough) and Goli's status (version, IP, Wi-Fi, connections, battery,
weather). A tap or 30 s goes back to the face.
The time comes from NTP and is kept by the board's RTC, so the clock is right after a restart even before Wi-Fi is up.
The board has two microphones. Goli only measures how loud it is, on the device; nothing is recorded or sent, and he ignores his own sounds. A clap marks alerts as seen; a noisy room makes him wince. Turn it off on the web page. The one exception is Ask Goli below, and only while your finger holds him.
Tap him, then keep the finger on him and ask something; let go when you're done (up to 12 s). He boops, shows listening..., then hmm..., and answers out loud in his language while the answer scrolls across his face. He remembers the last three questions for five minutes, so "and tomorrow?" works.
It needs two API keys, set in the Ask Goli card on the Goli tab of his web page:
- an Anthropic key for the answers (Claude,
claude-opus-5-5by default, at low effort for a quick reply; a declined question falls back to another model on Anthropic's side); - a key for an OpenAI-compatible speech service that turns your voice into text and the answer into
his voice (OpenAI by default:
whisper-1andtts-1with thealloyvoice; the address and models can be changed).
What you said while holding him goes to the speech service as audio and to Anthropic as text; nothing else is recorded. Both services bill your accounts: a question costs about a cent with the defaults. The keys are never shown back on the web page (they are in the backup file). Without both keys, tap and hold is petting, as before. The screen has no accents, so Slovak, Czech and German answers show without them, but he says them properly.
-
Language: Goli's words on his face (greetings, pages, hints) in English, Slovak, Czech or German, chosen on the web page. The fonts have no accents, so it's "dobre rano!".
-
Reminders: "14:00 call mum" once, every day or on weekdays, set on the web page. He rings with the title at that minute, even in quiet hours (you asked for it).
-
Mood follows yesterday: after a day with many alerts (critical ones count three times) his resting lids are heavier the next morning. At midnight today becomes yesterday, and it survives a restart.
-
Desk buddy (on by default): the first time someone is at the desk in the morning he says good morning (happy birthday on your birthday) with the day in one line, like "3 meetings, rain later"; after 50 minutes at the desk he suggests a stretch and some water; on Friday after 17:00 he tells you to go home. "At the desk" means a touch, or voices and typing heard by the microphones in the last few minutes.
-
Knock knock: two quick knocks on the desk (felt by the IMU) snooze alerts for 10 minutes. The threshold is a guess until it meets a real desk.
-
Night: with Screen off at night on, his screen goes dark while he sleeps in quiet hours; a touch wakes him.
-
A team of Golis: Golis on the same network find each other. On each one's web page, pick who sits on its left and right; when one rings, its neighbours look its way.
Tap him 10 times: he loops through his faces and a few made-up Goliash alerts (a deploy, a critical drift, an agent coming back) with sounds, no Wi-Fi needed. Any tap stops it. Made for a booth.
firmware/test/run.sh runs host tests without a board (CI runs them too): the calendar reader against
real-world iCalendar cases (time zones, weekly and daily repeats, EXDATE, moved and cancelled instances,
VALARM, free time, folded lines, COUNT/UNTIL), what rings and how loud (Goliash items and the filter,
Alertmanager, Grafana and Uptime Kuma webhooks), and every translation's format string. It needs
ArduinoJson from one firmware build.
pio run -e behaviour && .pio/build/behaviour/program (in firmware/) runs Goli's behaviour (bot.cpp)
through a day with fake time and records what he plays: taps and the wink, alerts and the badge, quiet
hours, focus with its break, the kitchen timer through quiet, meetings, the knock-knock snooze, setup and
demo taps, the time and temperature on his face, falling asleep, and the morning greeting once a day.
https://pipozzz.github.io/goli-bot/sim/ runs Goli's firmware in the browser: bot.cpp (every
gesture, mood, alert, focus, demo and page), the face and the sounds, compiled to WebAssembly with LVGL.
The mouse is the finger; buttons stand in for Goliash alerts, the IMU (lean, nod, shake, bump, lay him
down), the microphones and the battery. Only the hardware glue in main.cpp is left out.
pio run -e snapshot -d firmware # fetches LVGL
sim/build.sh # needs Emscripten (brew install emscripten)
python3 -m http.server -d sim/outNo tools needed: open https://pipozzz.github.io/goli-bot/ in Chrome or Edge on a computer, plug the
board in by USB-C and click Install. The page is built by GitHub Actions from every commit to main.
- Use a USB-C cable that carries data. Pick the port (USB JTAG/serial debug unit or similar).
- On the first install, let it erase the board. For updates, don't erase: Goli keeps his settings.
- If no port shows up or it can't connect: hold BOOT, press and release RESET, release BOOT and try again. Press RESET once more when it's done.
- Choose Connect to Wi-Fi when the installer offers it (Goli speaks Improv over USB). Then open the link it shows to add your Goliash address and token on Goli's setup page. No Wi-Fi step? Set him up from your phone instead (below).
The page always installs the latest release.
Once Goli is on your Wi-Fi he updates himself: a few minutes after starting and then once a day he reads
ota.json from the installer site, and if a different release is out he downloads it, says updating
myself... and restarts. The download only comes from a server with a trusted certificate (the roots are in
src/tls_roots.h), and the ESP32 checks the image before it boots it. Turn it off on the web page.
Builds you make yourself are versioned dev and never update themselves.
To not wait for the daily check, open Updates on his web page: Check now shows the latest release,
and Update to vX installs it right away (also on a dev build, or with the daily updates off).
Safari and phones can't flash (no Web Serial). Every release also has goli-firmware.bin, a merged image for
offset 0x0: esptool.py --chip esp32s3 write_flash 0x0 goli-firmware.bin.
The firmware hasn't met real hardware yet: docs/bring-up.md is the order to check the display, touch, IMU, sound, microphones and battery, and what to change when something is off.
Keep a finger on his face while he starts (about a second) for the self-test. A ring follows your finger, a bubble level shows the tilt, every tap rings the bell, and it shows Wi-Fi signal and battery. Red means the part didn't answer. Hold 3 s to leave. If the ring isn't under your finger, the touch panel is rotated or mirrored against the display.
cd firmware
pio run -t upload # board in USB-C; hold BOOT while plugging in if upload does not start
pio device monitorThe build also writes .pio/build/goli/goli-firmware.bin (tools/merge_bin.py).
Releasing: push a tag like v0.2.0. GitHub Actions builds that version, creates the release with both
images, and publishes the installer and ota.json to Pages; every Goli on Wi-Fi picks it up within a day.
Pushes to main and pull requests only build and render the faces. To try the installer page
locally, run web/build_site.sh and serve site/ with python3 -m http.server -d site; Web Serial works on
localhost.
include/config.h holds brightness, default volume, timezone, the idle timeouts and the tilt directions. If the pupils roll the wrong way
when you tilt Goli, flip TILT_ROLL_SIGN / TILT_PITCH_SIGN.
Preview every expression without the board (renders docs/faces.png):
pio run -e snapshot && .pio/build/snapshot/program && python3 tools/sheet.pyGoli synthesizes all his sounds (no audio files). Pick one for each kind of alert on the web page:
| Sound | What it is |
|---|---|
| Desk bell, Desk bell twice | a brass desk bell (inharmonic partials with different decays); twice and a tone lower for critical |
| Ship's bell | lower and longer, struck twice like on a ship |
| Marimba, 8-bit, Coin, Water drop | a wooden arpeggio, a chiptune arpeggio, a game coin, a drop |
| Whisper | a soft "pff" and a tick, for an office |
| Goli: "hm?", "ou!", "ehh...", "ya-ay!" | his own voice: formant synthesis, a buzz shaped into vowels, nothing recorded |
| Your own sound | any audio file, uploaded on the web page (first 3 seconds, converted in the browser) |
With Goli talks on, he also says "hm?" when booped and "ya-ay!" when happy. Hear everything in
docs/sounds/ (re-render: the command at the top of firmware/tools/render_sounds.cpp) or in the
simulator. The speaker amp is only on while a sound plays. During quiet hours (22:00 to 07:00 by default)
alerts are silent and only the badge appears.
On first start, Goli has no Wi-Fi yet, so he opens a setup hotspot and shows a QR code. To set him up again later, tap his face 5 times quickly.
| On Goli's screen | On your phone |
|---|---|
![]() |
![]() |
- Scan the QR code with the phone camera to join
Goli-XXXX. The password is on the screen and changes every time. - The setup page opens by itself (captive portal). If it doesn't, scan the second QR code or open
192.168.4.1. - Pick your Wi-Fi, enter your Goliash address and API token, and set volume and quiet hours. Then tap Save. Goli restarts and joins your Wi-Fi.
A PC works the same way: join the Goli-XXXX Wi-Fi and open 192.168.4.1. Everything is stored in Goli's flash.
Saved passwords and the token are never shown back on the page; leave a field empty to keep the old value.
If the Wi-Fi or the token stops working, a hint appears at the bottom of his face.
Why not Bluetooth: setting a device up over Bluetooth from a web page (Web Bluetooth) doesn't work on iPhone. The hotspot works on every phone and computer without an app.
Once Goli is on your Wi-Fi, open http://goli-xxxx.local/ (the status page shows the name and IP).
The first time, click Show me a code: Goli shows a 6-digit code on his face, and typing it pairs the
browser (5 wrong tries and the code is gone, and every further round makes you wait twice as long). Only someone standing next to Goli can pair. From then on
everything is there:
- Home: connections, his mood, a kitchen timer (orange ring, a ding at the end) and a quiet switch, alerts today by hour (a small chart), his face live (a snapshot every 2 seconds), reminders, today's meetings, the latest alerts; boop him, start focus or the demo, test alerts.
- Sounds: a sound per kind of alert with a play button, Goli talks, volume, quiet hours, your own sound.
- Alerts: Goliash (optional), which alerts ring, GitHub, URLs to keep an eye on, the key for scripts.
- Day: calendar, weather, desk buddy, birthday, the microphones, the team.
- Goli: Ask Goli (API keys), Wi-Fi, Home Assistant, language, night, updates, install a firmware file (
goli-app.binfrom a release, or your own build: no USB needed once he's assembled), backup and restore (every setting and reminder, secrets included, in one JSON file), restart, setup mode, forget this browser.
Secrets (Wi-Fi password, tokens, API keys, the calendar address) are never shown back. The setup hotspot (tap 5 times) still works for the first setup or a new Wi-Fi.
Everything is optional; Goli is happy with none of them, and Goliash is just one of them.
| Source | What Goli does |
|---|---|
| Goliash | notifications by severity, the overview page (see below) |
| Calendar: secret iCal addresses (Google, Outlook, iCloud, Nextcloud), up to 4, separated by spaces | today's meetings on a page, an amber countdown ring 10 minutes before each, a ding when it starts. Daily and weekly repeats (with skipped dates) are expanded; monthly ones and moved single instances are not. Times are taken in Goli's time zone. Checked every 10 minutes. |
| Weather: a location (Use my location on the web page) | from Open-Meteo every 30 minutes: an umbrella with rain falling when rain is likely in the next 8 hours, sunglasses on a sunny 20 °C+ day, a scarf at 2 °C or snow; the temperature (°C or °F) with the time on his face and in the morning greeting |
GitHub: a classic token with the notifications scope |
review requests and mentions ring; failed CI on your pull requests and security alerts ring as critical. Checked every minute. |
| Keep an eye on: up to 6 URLs | fetched every minute; two failures in a row is critical ("shop.example.com is down"), and he's happy when it's back |
| ntfy: a topic on ntfy.sh or your own server | every message posted to it rings; JSON in a webhook format he knows is read like POST /notify |
| MQTT: Home Assistant's broker, or a cloud one (TLS) | goli/goli-xxxx/notify takes a title or webhook JSON |
Anything else: POST /notify |
see Ring Goli from anything |
HTTPS to these services is checked against the Mozilla root certificates built into ESP-IDF; the watched URLs only check that a site answers, so their certificates are not checked.
Create a read-only token for the web page (or the setup page): goliash token create -name goli-bot -role viewer
(or Settings → Users → API tokens). Make sure a notify rule sends what you care about; any channel counts.
To keep alerts inside Goliash only, use the In Goliash channel.
Every 30 s Goli asks GET /api/v1/notifications/sent?since=…, the same list the bell in the Goliash UI
shows. Each message newer than the last one he saw rings once. The first poll after power-on only learns
where things stand, so he doesn't ring for old news. If you leave the certificate field empty, the
Goliash certificate is not checked.
Which alerts ring is up to you on the web page: everything or only critical ones, only production, only some services (comma-separated). Items without an environment or service, like an agent going quiet, always pass the production and service filters.
Instant alerts with a webhook: if your Goliash can reach Goli on your network, add a Webhook
channel in Goliash with the URL http://goli-xxxx.local/notify (or his IP; the status page shows both)
and Goli's key as the secret, point a notify rule at it, and tick Goliash sends alerts to me on the web page. Goli checks the
X-Goliash-Signature on every request and stops polling for notifications. He still asks for the summary
every 5 minutes, which also tells him the token works. (Goliash in Docker may not resolve .local
names: use the IP then, and give Goli a fixed address in your router.)
Fill in your MQTT broker on the web page (Goli tab; the Mosquitto add-on works). Goli announces himself with MQTT discovery and shows up as a device:
| Entity | What it is |
|---|---|
| Unseen alerts, Mood, Alerts today | sensors |
| Focus, Someone at the desk | binary sensors (an occupancy sensor for automations) |
| Boop, Focus on/off, Demo on/off | buttons |
| Timer | a number (minutes): a kitchen timer with an orange ring |
| Quiet | a switch: alerts only as a badge until it's off |
| In a meeting | a binary sensor from the calendar |
| Ring Goli | a notify entity: notify.send_message with a message rings him |
For a level, publish {"title": "Washing machine done", "level": "good"} to goli/goli-xxxx/notify.
Goli marks himself offline in Home Assistant when he loses the broker.
Webhooks (POST /notify) only reach Goli from inside your network: a Goliash, Grafana or Uptime Kuma
running at home, a script on your laptop. Everything Goli asks for works from anywhere, because he
connects out: Goliash (polling, the default), GitHub, calendars, weather, updates. For services in the
cloud to push to him, give them something both sides connect to:
ntfy (simplest): pick a long random topic, e.g. https://ntfy.sh/goli-7f3k9q2xw, put it on Goli's web
page (Alerts → ntfy), and post to it from anywhere:
curl -d "deploy finished" -H "Tags: tada" https://ntfy.sh/goli-7f3k9q2xw # good news
curl -d "prod is down" -H "Priority: urgent" https://ntfy.sh/goli-7f3k9q2xw # criticalPriority 5 (urgent) or a rotating_light, warning, x tag rings as critical; white_check_mark,
tada, +1 as good news. Grafana Cloud, Uptime Kuma or Alertmanager can use the topic as their webhook
URL: the JSON they post becomes the message, and Goli reads it like POST /notify. On ntfy.sh anyone who
knows the topic can post to it, so keep it secret, or run your own ntfy with an access token.
MQTT: a broker in the cloud (HiveMQ Cloud and EMQX Cloud have free plans) with TLS ticked on the
web page, port 8883. Publish a title or webhook JSON to goli/goli-xxxx/notify. From GitHub Actions:
- if: failure()
run: |
sudo apt-get install -y mosquitto-clients
mosquitto_pub -h ${{ secrets.MQTT_HOST }} -p 8883 --capath /etc/ssl/certs \
-u ${{ secrets.MQTT_USER }} -P ${{ secrets.MQTT_PASS }} -t goli/goli-xxxx/notify \
-m '{"title":"${{ github.repository }}: build failed","level":"critical"}'Or from ntfy in a GitHub Actions step: curl -d "build failed" -H "Priority: urgent" https://ntfy.sh/<topic>.
Goli listens on your network: POST /notify with a JSON title and an optional level
(info, critical or good), and his key from the web page (Alerts tab).
curl -H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
-d '{"title":"nightly build failed","level":"critical"}' http://goli-xxxx.local/notifyHe answers 202 with ringing, or filtered out when only critical is on and it isn't; without the
JSON content type he answers 415. GET /status
returns his version, Wi-Fi signal and Goliash connection, without secrets.
From GitHub Actions on a self-hosted runner in the same network:
- if: failure()
run: |
curl -fsS -H "Authorization: Bearer ${{ secrets.GOLI_KEY }}" -H "Content-Type: application/json" \
-d '{"title":"${{ github.repository }}: ${{ github.workflow }} failed","level":"critical"}' \
http://goli-xxxx.local/notifyFrom Home Assistant (configuration.yaml), then call rest_command.goli from any automation:
rest_command:
goli:
url: http://goli-xxxx.local/notify
method: post
headers:
Authorization: !secret goli_bearer # "Bearer <key>"
content_type: application/json
payload: '{"title": "{{ title }}", "level": "{{ level | default(''info'') }}"}'Monitoring tools, as they are: Alertmanager (Prometheus), Grafana alerting and Uptime Kuma can post
straight to http://goli-xxxx.local/notify. Goli understands their webhook formats: firing alerts ring
(critical when the severity label is critical), resolved ones and monitors coming back up are good
news, a monitor going down is critical. Give the key as a Bearer header, or in the address
(/notify?key=...) for tools that can't set headers; the address then contains the key, so keep it
inside your network.
Titles show in ASCII; common accented letters are folded (č → c).
Developers can pre-fill the settings at build time: copy firmware/include/secrets.example.h to secrets.h
(git-ignored). It is only used while nothing has been saved on the device.
Everything is generated by goli_bot.py: the body silhouette is the Bézier path from the Goli artwork, and the
display's dimensions are constants at the top of the file.
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python goli_bot.py # writes stl/ (print-oriented) and stl/assembly/ (assembled positions)
.venv/bin/python goli_bot.py --mini # Goli mini: stl/mini/
.venv/bin/python make_viewer.py # rebuilds viewer.htmlUseful knobs: S (overall scale), FACE_TILT (how far the face looks up), K (body depth),
FIT (clearance for press fits), and the PCB_NUTS / GLASS_R / NUT_DEPTH constants if you use another display.
The display's mechanical drawing is in docs/waveshare-amoled-1.75-dimensions.jpg.
| What | License |
|---|---|
Firmware (firmware/), web installer (web/), CI and the Python tools |
MIT |
3D model (stl/, goli_bot.py and what it generates, viewer.html), the Goli character, renders and artwork in docs/ |
CC BY-NC-SA 4.0 |
ES8311 codec driver (firmware/lib/es8311/) |
Apache-2.0, © Espressif Systems |
Clock digits (firmware/src/font_clock.c, rendered from Montserrat) |
SIL Open Font License 1.1 |
ES7210 register sequence in firmware/src/mic.cpp |
follows Espressif's esp_codec_dev driver, Apache-2.0 |
Print, remix and share Goli for non-commercial use: credit "Goli desk bot by Peter Gonda" with a link to this repository and share remixes under the same license. Selling prints or remixes needs written permission.










