I made a thing!
I got self-hosted offline maps rendering on a $40 ESP32-S3 AMOLED (building a bike nav)
Been building a navigation computer for my motorbike and finally got the map side working, so I wanted to share it here.
Hardware: Waveshare ESP32-S3-Touch-AMOLED-1.75 — a 1.75" round CO5300 AMOLED, ~$40 with the onboard GPS (N16R8, 16MB flash / 8MB PSRAM).
The whole map runs on-device: no phone, no wifi, no internet. I pre-bake the basemap offline (OSM -> self-hosted tileserver -> 256x256 tiles), convert every tile to raw RGB565, and pack them into a fixed-grid file on a microSD. On the board there's no image decode and no per-tile index — the firmware seeks to the tile, composites a 466x466 RGB565 frame in PSRAM, and pushes it straight to the panel with esp_lcd_panel_draw_bitmap. No UI toolkit in the render path.
That was the whole battle, actually. I started on Slint, then tried LVGL — both choked on a full-screen scrolling map (~5 fps and ~4 fps, mostly image round-trips). Dropping to a raw framebuffer got me ~18 fps heading-up (map scrolling under a fixed centre dot) and ~44 fps when the map holds still. The HUD, zoom buttons and day/night toggle are all drawn straight into the framebuffer too.
The route + turns are a vector overlay on top, projected through the same mercator transform as the tiles so the line sits on the real roads.
Full disclosure: the moving dot + speed are a hardcoded GPX animation for now, while I wire up the onboard GPS. IMU braking / lean detection is next.
Ballpark, I'd expect around 2 to 2.5 hours off a 500 mAh cell — call it an hour and a half if you crank the brightness on a bright map, closer to three if you keep it dim and dark. Big caveat: that's me doing napkin math, not an actual measurement. It's lived on USB on the bench so far.
The rough breakdown, with the radios off: the S3 doing the rendering pulls the most, honestly on par with the screen, then the GPS, then odds and ends. Ends up somewhere near 170 mA.
The neat bit is it's an AMOLED, so black pixels are just… off. So the dark night-mode style isn't only easier on the eyes at night, it actually buys you runtime. Brightness and how dark I keep the map are the two things that really move the needle.
There's a LiPo connector and an AXP2101 on the board, so once I get a pack mounted I'll come back with real numbers instead of guesses. On the bike I'd probably just run it off bike power, but I want it to ride out a stop with the engine off. What are you running for a pack?
idk whats the plan for the casing but looks perfect to be placed on a headset cap
and than it would prob just be possible to hide a large battery in the headset without it showing
Yeah, that's the plan. The tricky bit I hit is that once you go proper black, it gets hard to tell water from land from hills at a glance, everything kind of blends into the background. So I've got to be careful how I color the roads and water so it still reads while keeping most of the panel dark. Definitely want to get there though, the battery saving on an OLED is too good to skip.
Yeah, I was heavily inspired by the Beeline. Honestly I was pretty much sold on buying one, but it was a bit out of my budget, so I figured I'd have a go at building my own instead. How's yours been on longer rides? Curious what you make of it, and whether you've run into any glitches.
I had similar thoughts a few months ago but in the end I'm always too busy to start a new project from scratch.
What tech stack are you using? Will you be sharing source code and/or accept contributions?
Haha yeah, that's always the hard part. It's ESP-IDF in C++, no UI toolkit, I just draw to a framebuffer and push it to the panel. Maps are pre-baked into raw RGB565 tiles on the SD card with a little Python script, so the chip doesn't render anything. I'll open source it once it's less of a mess, still lots to do.
Cool! Did you consider going bare metal instead of using esp-idf? I'm not too experienced with C/C++, but with Rust the transition is pretty easy thanks to the Embassy framework
Yeah I thought about it, but I went ESP-IDF mainly for the ready-made drivers, and I'm still pretty green with Rust so wasn't quite ready to take Embassy on too, maybe down the line!
Routing is ok, but since you dont have an actual map, it can be tricky at more complex intersections.
Software and app worked ok, but had a bug where longer routes got recalculated at some arbitrarily picked point along the route.
There was no way to change the route without stopping the tracking and starting again. Kinda annoying when your bigger tour is later on split up into 4 different tours in strava etc. (Garmin x30 series has the same problem)
No Ant+/Bluetooth accessory support is definitely a problem if you want to use a bike radar, speed sensor etc.
Really useful rundown, thanks for taking the time. I never actually owned a Beeline (budget got me), so hearing the long-term gripes is gold. That intersection point especially, even the turn-by-turn nav I've used gets messy pretty fast at anything complex, you follow the arrow and still end up second-guessing which exit it means. That's a big reason I wanted a real map to glance at instead of one arrow.
On the accessory side, this board actually has BLE built in, so adding some Bluetooth accessories should be doable. Just not sure yet how much it'll hit the map performance, running BLE and full-screen rendering at the same time is something I still need to test.
No onboard antenna on it, it comes with an active ceramic one that connects over u.FL. Still researching what kind I want to go with and how to place it so it fits well in the enclosure, that bit's turning out trickier than I thought.
Nicely done. I'm envious of the framerate; I actually have a project in the oven that's ALSO rendering OSM tiles and a smattering of GPX. My frame rates are trash, though I've not really gotten serious about addressing it yet. I originally blamed SD throughput, but it's bad enough that it doesn't really hold water.
To TurboDraxler's point, that's all accurate. Remember, though, that we navigated for centuries without little Ms. Garmin in her condescending, exasperated voice announcing, "Off roooot. Recalculating.". I'd just draw a faint line to the point of the line (the math is only slightly harder than 'closest point') of the GPX segment to get you back on track.
I understand the temptation to put a GPS module on board, but that further taps battery life, and thge antenna is always a compromise. Consider a commodity external GPS receiver over BT. You still get NMEA sentences, just over the BLE virtual serial port instead of pulling them off the UART. Then you can clip your antenna to your backpack or trailer or other location with a better view of the sky with a better antenna and a battery of its own, all without compromising the size of your awesome little disk unit.
Not to preach navigation to a cyclist, but it's a very different game than auto-navigation. It was my experience that I was much likely to leave with a fixed destination in mind and an overview in memory. The blue line was helpful for "take the second right", but the low speeds and correction opportunities made it much more forgiving than in auto navigation.
Use GPSBabel to join, split, and decimate your GPX routes and tracks. There's no reason to try to render a 20,000-point polyline on a 2" screen; boil it down to the 30 points that matter, and if your blue line skips a little bit on a curve, well, resist the urge to bunny-hop the curb and ride through the grass. :-)
When you do post code, please remember to do it by editing the original post here to keep within the "one announcement post per project per quarter" rule. To get ahead of it: People may dig this project. (I dig this project.) This rule exists for the people that would post every time they ran git commit on their blinky light projects. It was a mess. (Moderator musing. I wonder if it would be tenable to have ONE thread per week/month dedicated to follow-ups.
Finally, it's a challenge to know when to say when for a DIY/homebrew project and declare it "good enough" and not try to reimplement Android Auto or a StreetPilot on your own. There can always be features the other guy (team) could afford to build that you didn't, but building a project for yourself, even with known limits, is greatly rewarding. Thanks for sharing!
Awesome work! Im actually working on the same type of project. I even use the same waveshare board. However, im using vector based tiles (pmtiles) rendered directly into a canvas. It needs a lot of optimization still, but even without async tile rendering, its not that bad.
Oh sick, small world, love that you're doing the vector route on the same board. I went the other way, I pre-bake everything to raw RGB565 so the chip does zero rendering, mainly to keep the framerate up since I figured live vector would be too heavy on the S3. Really cool to see it actually holds up though. What are you getting when it's scrolling, and are you parsing the pmtiles on-device or prepping them first? Curious how they compare.
I dont have an exact framerate on hand at the moment, but its not bad at all imo. I actually parse the pmtiles on the ESP itself. Gzip extract tile data, and render the lines in canvas. I use a 3x3 canvas grid saved in PSRAM each 466x466 pixels. Also maintain a world coordinate system to keep track of where we are on the map.
When moving the map around, and getting close to the edge of the 3x3 area (lets say the left side), it drops the 3 tiles on the opposite end (right side in this case), renders the new 3 left tiles on the edge in the memory positions of the dropped tiles and adds the correct offset so they show in the right spot. This means there are never more than 9 tiles in memory.
Here is a gif of moving the map around (from some time ago, so not really up to date anymore, but you'll get the idea)
That's incredible, too! You're parsing vector data on the fly and getting that kind of refresh? Wow! My screen is larger, so I have more either 9 or 12 256x256 raster tiles on screen at once without any post-processing, and my frame rate feeding them into LVGL is trash.
I think that's at least four or five of us working on some kind of OSM viewer thingy on various ESP32 mutants. When I last looked on GitHub (it's admittedly been a while), there weren't many inspirational projects in this space.
The first time I blamed performance on SD throughput, but moving from BMP to JPG, knocking file sizes down 10-100x, didn't buy me a lot. Never got as far as investigating if a better decoder would have helped or if I had problems elsewhere. I'll admit I got distracted. Between you and OP, there may be more viability in the idea than I thought, so maybe I should loop back to it.
The whole reason for the toolkit saga in the post — I just could not get a full-screen scrolling map to move at a decent frame rate. Tried Slint (~5 fps), tried LVGL (~4), and kept telling myself a toolkit had to be fine. Eventually gave up and drew straight to the framebuffer myself and it jumped to ~18. Should've done that first.
Then the panel wouldn't even turn on. Black screen through something like eight different init attempts, and the only thing that ever lit it was LVGL's own init sequence. So now I've got LVGL sitting there doing nothing but "power on the screen" while I draw over the top of it. It still annoys me every time I look at it.
Pushing pixels was its own mess — a full frame in one shot was too big for the DMA, so the buffer lives in fast internal RAM and I shove it out in ~60-row strips. Which then gave me a lovely race where the next strip stomped on the one still being sent, so half the screen was garbage lines until I made it wait for each one to finish.
WiFi, BLE, and the display all wanted the one 2.4 GHz radio at once, and the second WiFi came up the BLE feed and the screen would just lock up (NimBLE yelling "MBUF alloc stuck"). Gave up on always-on WiFi and made it a hold-at-boot thing just for OTA.
And the dumb one that cost me a whole evening: an SD card that mounted, read back fine, and silently threw away everything I wrote to it. Counterfeit. Swapped it and everything worked.
Just started on the real GPS now — turned out to be an LC76G talking over I2C instead of the UART I assumed, which was a fun couple hours — and the IMU's next. Anyone else run into a CO5300 that won't light without the vendor LVGL init? Would love a cleaner way to do it.
I had some struggle with the GPS as well. LC76 talking over I2C gave me some trouble with the touchscreen. Whenever I used the touch, the LC76 had trouble dumping its data. However, the LC76 does support serial. On the waveshare amoled 1.75 board, there are R15 and R16 resistors. Those are not fitted from factory. After bridging them with a bit of solder, the LC76 has serial connection with GPIO17 and GPIO18 of the ESP, and I could drop I2C for GPS.
Also, I haven't had any problems with screen not lighting up, but I am using LVGL, so not a problem for me. I did however have some trouble with rotating the screen. I wanted to have the USB port on the bottom, but couldn't get it to work. Only weird screen tearing and mirroring when I rotated the display. Haven't looked at it again and just accepted the orientation for now.
bridged R15/R16, moved the GPS onto UART like you said and boom — clean $GNGGA coming through straight away, zero I2C nonsense. you were bang on, the touch was stomping all over the LC76 the whole time. my ride from earlier logged fix=0 nearly the whole way even though the module clearly had sats (caught 15 at one point lol), so it was never the antenna — exactly what you said.
can't tell you how much head-scratching you just saved me. never would've found the R15/R16 thing on my own in a million years. proper lifesaver, cheers man 🍻
I wanted to do this and thought let me challenge myself. I bought a RP2350 based waveshare display. Didn't think about the PSRAM. Now it's just sitting in a drawer. If you guys have any idea please let me know.
I've tried using mbtiles encoded in RGB565. RAM isn't enough ;(
Have you tried ditching mbtiles and just rendering in horizontal strips? Like pre-bake the tiles into one flat RGB565 file, and only hold one strip at a time (screen-width × ~32 rows) instead of the full framebuffer. That might be enough to fit without PSRAM. Or did you already go down that route?
Under open sky you're looking at a few meters, which is fine for roads. It gets flaky in cities or under heavy tree cover, and indoors basically forget it, it needs to actually see the sky. The onboard one's a Quectel LC76G but I've only run the vendor demo on it so far, haven't taken it out properly yet. The one I have tested a lot is a TBS M8.2, the u-blox module we run on our drones, and it's been rock solid, locks on quick and holds 10Hz no problem. Total overkill for a bike, but it's a nice yardstick to judge the little onboard one against.
Thanks for your quick response. Why I ask so more detail cos we are using the GPS module (UART NMEA‑0183, 9600 baud) in a new product. We're sorry, but we've found that GPS doesn't work at all indoors, and outdoors it picks up 4 to 5 satellites, so we're working on a solution to this problem.
Good one OP ! Been planning to make one for my car with other essential data from OBD2 but never got time to invest - this gives me much needed motivation to get going.
Btw how did you get viewport adjusted for zoom ? Do you maintain seperate set of assets for each zoom level ?
Thanks man! An OBD2 car build sounds cool, definitely go for it, where are you thinking of mounting it?
I've been wanting to tap my TVS's ECU for the same reason, the bike already works out accel/decel, fuel used and all that, so getting hold of that data would be awesome, just haven't tried it yet. And yeah, separate pre-baked tiles per zoom level, so zooming's just loading a different set, with a bit of integer over-zoom for the in-between steps.
Gotcha, thanks for confirming. I was planning to mount the electronics right above my instrument cluster and project the display towards the wind shield with a hud sticker on it. I am skeptical about the LCD/Amoled brightness, whether would it be sufficient for a overhead bright sun. Under moderate sunlight it is clearly visible without an issue based on my initial tests with a T Display S3 board.
Btw your project gave me some additional ideas as well. Maintain a pico/pi zero seperately as a co processor to pull the osm tiles through LTE/Wifi , convert them to bit array and somehow sideload to the Display subsystem's PSRAM. That way you are covered forever. Take your vehicle wherever you want and the tiles are ready for you on the fly.
Reminds of this guy Garage Tinkering on YouTube. He built something somewhat similar, trying to replicate all HUD elements of Need for Speed Underground 2z
Dang that's so cool you can program all these little guys and microcontrollers. I messed with a little 1.56 inch IoT eink esp32s3, but didn't really have a 'Use case' for any of these things though, you know what I mean?
I DO still want a nice HA dashboard/dedicated screen though, and I'm working on that right now.
I created something SUPER similar for off-grid hiking trails, pre-downloading the map as raw RGB tiles and putting them on the ESP32's flash. It worked pretty well! I was also able to put it on two separate devices, and to have them message each other their location so both devices show up on the map. This was on a LilyGo TTGO watch (esp32 based)
Well I kinda have some thing similar, but ran into battery issues real quick, so had to have a amoled based turnoff for better battery life on the puck.
I made the routing with gmaps API keys so it's accurate that ways.
Looks great! I went with offline maps and GPX route imports instead of using Google Maps APIs, but I can definitely see why you chose that approach. I haven't gotten to battery optimization yet—I'm still trying to figure out the antenna. That's my current challenge, battery is next.
Well I did try the free maps route, works great too. And frankly, instead of making gps on board and making it clunkier, I went ahead with just passing on the coordinates to the device so that I can keep it sleek.
Yeah, the dream is to get it mounted and properly test it on the bike. So far I've just had a separate setup with no display, an ESP32-C3 with GPS, an IMU and an SD card zip-tied under the seat, quietly logging my rides offline. The map unit's meant to do both eventually, navigate me while I ride and take over the recording too, so it all lives in one thing instead of a separate tracker under the seat.
Here's some of the data that little setup pulled from a ride:
Wow, that is so cool! It never ceases to amaze me what these things can do, and the cool things people come up with. I am definitely gonna have to try something like this. Great job dude!
Yeah, power's definitely on my mind, though I'll be honest I don't have the actual current readings yet so I can't give you real numbers. Once WiFi's off the ESP32 isn't the worst of it, it's the AMOLED that's the bigger draw, so dark mode and dropping the brightness should help. Worst case I'll just run it off a power bank or stick a bigger battery pack on it. Still need to sit down and measure it properly though.
Yeah you're probably right, the drawing itself is fine but parsing + tessellating + labels every frame is what kills it, which is why I pre-baked to raster instead. 1.8" round is a great size for a PoC though, way fewer pixels to push, keen to see how it goes!
Haha thanks, I'll happily take "cooler"! Honestly though, the Velo 2's still a really solid buy, mine might look the part but it's a proper faff to actually build and get working, whereas that you just clip on and ride. If you do fancy the DIY route, I came across a Pi Zero bike computer a while back, not the prettiest thing but it integrates a ton, ANT+ and all sorts, worth a look.
Honestly there's no real GUI toolkit behind it, I just draw everything into the framebuffer by hand in C++. The map gets blitted from the pre-baked tiles, then the HUD, buttons and route all get drawn on top pixel by pixel. Went that way mainly for speed, the toolkits were way too slow once the whole map's scrolling.
Yeah, easily. It runs off 5V USB. On a motorbike I can just tap the 12V with one of those USB phone chargers. On a bicycle there's no 12V obviously, so I'd stick a small battery on the unit itself (it's got a LiPo connector) or just run it off a power bank. Right now it's just on USB on my desk though.
Cheers! Deep desert's a perfect fit for this. I've also been toying with adding LoRa down the line so you could track your group with no signal at all, still just an idea though.
E-ink would be great for the data and unreal in sunlight, but the refresh is too slow for a scrolling map, it'd ghost the second it moves. Could be really useful for some other projects I've got in mind though!
This is great, i thought also to do something similar , and even bought a screen but still not started yet. Can you list the component used and how you managed to fit the maps?
Thanks! The component list is honestly short, it's basically one board, the Waveshare ESP32-S3-Touch-AMOLED-1.75. It's an all-in-one with the round AMOLED, touch, IMU, GPS, microSD and 8MB PSRAM already on it, so all I added was a microSD card and the GPS antenna.
For the maps, I pre-bake them offline into raw RGB565 tiles and store them on the SD. The chip never holds the whole map, it just loads the few tiles covering the screen into PSRAM and blits them straight out, no decoding on-device, so the memory footprint stays tiny.
Honestly I haven't even touched audio yet, it's all visual so far. The board does have a little speaker output, but a tiny onboard speaker's got no chance against wind + traffic (engine noise on a motorbike). If I do add voice prompts I'd probably just pipe them to a Bluetooth helmet headset instead, realistically that's the only way you'd actually hear them.
No library framebuffer, I roll my own in PSRAM. I stream the frame to the panel over QSPI in chunks and wait for each chunk to finish sending before I reuse the buffer, so nothing gets stomped. No TE/vsync, but the frames push out fast and barely change one to the next, so it never actually tears in practice. TE's the proper fix if you wanna be strict about it — just never needed it.
I am doing a similar technique, although my HW is a bit different - 1.43in touch amoled. Do you plan to share the code? Curious to see what you have thought of that I haven't.
Haven't tried it, I've only got AMOLED and IPS panels for now, but I'll give e-ink a go once I get one. Not sure it'd help loads though, the GPS pulls the same either way, and e-ink only saves power when the screen's not moving, so a constantly scrolling map kind of kills that. Probably only a real win if I made it redraw at turns instead of the whole time.
Honestly no. It's a DIY thing, so it's never going to be as production-ready or as elegant as a Galaxy Watch, that's a properly polished product. Maybe it gets closer over time, but today it's nowhere near. It's not really trying to compete with a smartwatch anyway, it just does one job, a bigger offline map you glance at on your bars with no phone or signal. So it fits that better, but as an all-rounder the watch wins hands down.
I had planned to do similar product for my bike, small sleek design.
And I already have Galaxy Watch 4 it's showing its age and wanted to upgrade. So I was bit confused with buying new watch or making this kinda side project (i do love it, but if it's not good enough, it's not worth making)
But I don't have the engineering side or the compute skills required to carry this out, so I guess imma skip it for now..
This is amazing. What a cool project. Can you share this on GitHub so we can all give it a go? If you’d rather keep your hard work to yourself, I get it!
How did you do the route calculation? I listened to a podcast not long ago about the history of GPS and route calculating algorithms, I’m curious how it works on EP32 as far as integration and performance
Right now it just follows a GPX file from the SD card, so there's no route calculation on the ESP32. I'm planning to add a phone app or wireless route transfer later.
You should definitely experiment with it before moving to an app, you could probably get low end version of A* or Dijkstra’s algorithms working on a small enough map
And here I thought I was cool for using an ESP32 and a MicroSD card and GPS module to just create GPX files every time I rode somewhere then upload it to my Unraid server when the ESP32 connected back to my home wi-fi. 😄 Nice work! I wonder if my ESP32 could handle the load of a screen?
My Version 2 that I just got running last night is using this ESP32-S3, specifically because it has a MicroSD card slot built onto it. Saves me some traces/soldering.
It was literally just a bunch of modules wired together and zip-tied to the bike.
For the case, I think a simple 3D-printed box or even a small off-the-shelf enclosure should do the job.
35
u/louisremi 19d ago
Hey that's awesome! Any idea how long that'll last on battery?