1<!DOCTYPE html><html lang="en" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg"><link rel="manifest" href="/manifest.json"><link rel="sitemap" href="/sitemap-index.xml"><meta name="generator" content="Astro v5.2.3"><meta name="title" content="ESP32 Bike Computer | Thilina Gunasekara"><meta name="description" content="Web BLE + ESP32 powered Bike computer which shows speed, heart rate, time and weather"><title>ESP32 Bike Computer | Thilina Gunasekara</title><meta name="astro-view-transitions-enabled" content="true"><meta name="astro-view-transitions-fallback" content="animate">
1<script type="module" src="/_astro/ClientRouter.astro_astro_type_script_index_0_lang.rasoniT7.js"></script>
1<style>.tags[data-astro-cid-pjkakwps]{display:flex;flex-wrap:wrap;gap:1rem;list-style:none;margin:1em 0;padding:0}@media (min-width: 768px){.tags[data-astro-cid-pjkakwps]{margin:2em 0}}.tag[data-astro-cid-pjkakwps]{color:var(--color);text-decoration:none;display:inline-block;font-size:1rem} 2</style> 3<link rel="stylesheet" href="/_astro/_slug_.Ch_HCeA3.css"> 4<style>model-viewer{width:100%;min-height:300px;background:#0000001a;margin:2rem 0;border-radius:1rem} 5[data-astro-image]{width:100%;height:auto;object-fit:var(--fit);object-position:var(--pos);aspect-ratio:var(--w) / var(--h)}[data-astro-image=responsive]{max-width:calc(var(--w) * 1px);max-height:calc(var(--h) * 1px)}[data-astro-image=fixed]{width:calc(var(--w) * 1px);height:calc(var(--h) * 1px)} 6</style></head> <body data-astro-cid-sckkx6r4> <header class="header" data-astro-cid-sckkx6r4> <div class="headerContainer" data-astro-cid-pux6a34n> <div class="backLinkContainer" data-astro-cid-pux6a34n> <a class="back" href="/projects/" data-astro-cid-sckkx6r4> <svg viewBox="0 0 24 24" data-astro-cid-sckkx6r4="true" width="18" height="18" astro-icon="mdi:arrow-back"><path fill="currentColor" d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg> <span data-astro-cid-sckkx6r4>All Projects</span> </a> </div> <nav data-astro-cid-pux6a34n> <button type="button" class="menuToggle" data-astro-cid-pux6a34n> <svg viewBox="0 0 24 24" data-astro-cid-pux6a34n="true" class="menuOpen" width="18" height="18" astro-icon="mdi:menu-open"><path fill="currentColor" d="M21 15.61 19.59 17l-5.01-5 5.01-5L21 8.39 17.44 12 21 15.61M3 6h13v2H3V6m0 7v-2h10v2H3m0 5v-2h13v2H3z"/></svg> <span data-astro-cid-pux6a34n>Menu</span> </button> <ul data-astro-cid-pux6a34n> <li data-astro-cid-pux6a34n> <a class href="/projects/" data-astro-cid-pux6a34n>Projects</a> </li> <li data-astro-cid-pux6a34n> <a class href="/writing/" data-astro-cid-pux6a34n>Writing</a> </li> <li data-astro-cid-pux6a34n> <a class href="/hiking/" data-astro-cid-pux6a34n>Hiking</a> </li> </ul> </nav> <div class="colorSchemeSwitcher" data-astro-cid-pux6a34n> <button id="theme" type="button" aria-label="Switch colour scheme" data-astro-cid-y3umwdic> <span data-astro-cid-y3umwdic><i data-astro-cid-y3umwdic></i></span> </button>
6<script> 7 document.addEventListener("astro:page-load", () => { 8 const playSoundEffect = () => { 9 const audio = document.querySelector("#audio"); 10 11 if (!audio) { 12 return; 13 } 14 15 audio.currentTime = 0; 16 audio.play(); 17 18 setTimeout(() => { 19 audio.pause(); 20 }, 500); 21 }; 22 23 const toggleButton = document.getElementById("theme"); 24 const getTheme = () => { 25 if ( 26 typeof localStorage !== "undefined" && 27 localStorage.getItem("theme") 28 ) { 29 return localStorage.getItem("theme"); 30 } 31 32 if (window.matchMedia("(prefers-color-scheme: light)").matches) { 33 return "light"; 34 } 35 36 return "dark"; 37 }; 38 39 const setTheme = (theme) => { 40 document.documentElement.classList.add(`theme-${theme}`); 41 document.documentElement.classList.remove( 42 `theme-${theme === "light" ? "dark" : "light"}` 43 ); 44 toggleButton.setAttribute( 45 "aria-label", 46 `Switch to ${theme === "light" ? "Dark" : "Light"} Theme` 47 ); 48 toggleButton.setAttribute( 49 "title", 50 `Switch to ${theme === "light" ? "Dark" : "Light"} Theme` 51 ); 52 }; 53 54 function setMode() { 55 const theme = getTheme(); 56 if (theme === "light") { 57 setTheme("light"); 58 } else { 59 setTheme("dark"); 60 } 61 } 62 63 toggleButton.addEventListener("click", () => { 64 playSoundEffect(); 65 66 const theme = getTheme(); 67 if (theme === "light") { 68 localStorage.setItem("theme", "dark"); 69 setTheme("dark"); 70 } else { 71 localStorage.setItem("theme", "light"); 72 setTheme("light"); 73 } 74 }); 75 setMode(); 76 }); 77</script>
77 </div> </div> </header> <main data-astro-cid-sckkx6r4>
77<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"ESP32 Bike Computer","author":[{"@type":"Person","name":"Thilina Gunasekara","url":"https://www.thilinag.com"}]}</script>
77 <article data-astro-cid-uogrpsp5> <h1 data-astro-cid-uogrpsp5>ESP32 Bike Computer</h1> <p>Iâve been tinkering with an ESP32 that comes with a built-in 1.47-inch display, and I thoughtâwhy not turn it into a bike computer? The idea is simple: use the ESP32 as a BLE device that pairs with my phone via Web BLE and also communicates with my Garmin watch. With a bit of work, I managed to display my current speed, heart rate, time, and live weather updates. Hereâs what I did;</p> 78<h2 id="the-setup">The Setup</h2> 79<p>The UI is designed using SquareLine Studio, which generates LVGL code for the ESP32 display. This allows for nice interactive interface with minimal effort.</p> 80<p>The ESP32 acts as a BLE peripheral, listening to data from the phone which acts as a hub, relaying data to the ESP32 via Web BLE. The Garmin watch also connects to the same web page using BLE, sharing heart rate data.</p> 81<ul> 82<li>Speed: Pulled from the Geolocation Web API on my phone.</li> 83<li>Heart Rate: Fetched via BLE from my Garmin watch.</li> 84<li>Time: JavaScript time</li> 85<li>Weather: Retrieved from OpenWeatherMap API.</li> 86</ul> 87<p><img alt="first iteration of ui" sizes="(min-width: 1542px) 1542px, 100vw" loading="lazy" decoding="async" fetchpriority="auto" width="1542" height="2048" src="/_astro/GhovOEvbQAAjRDe.C3jKE1sr_ZISpKm.webp" srcset="/_astro/GhovOEvbQAAjRDe.C3jKE1sr_cl6MB.webp 640w, /_astro/GhovOEvbQAAjRDe.C3jKE1sr_Z1GGvlY.webp 750w, /_astro/GhovOEvbQAAjRDe.C3jKE1sr_1LxhjO.webp 828w, /_astro/GhovOEvbQAAjRDe.C3jKE1sr_kqBDu.webp 1080w, /_astro/GhovOEvbQAAjRDe.C3jKE1sr_1lVB9b.webp 1280w, /_astro/GhovOEvbQAAjRDe.C3jKE1sr_ZISpKm.webp 1542w"> 88<img alt="Current ui showing speed, heart rate time and weather" sizes="(min-width: 771px) 771px, 100vw" loading="lazy" decoding="async" fetchpriority="auto" width="771" height="1024" src="/_astro/GhovOEtb0AE-lsH.CKLAvIFa_1RAmmM.webp" srcset="/_astro/GhovOEtb0AE-lsH.CKLAvIFa_1Jnxt4.webp 640w, /_astro/GhovOEtb0AE-lsH.CKLAvIFa_Z1mtBTQ.webp 750w, /_astro/GhovOEtb0AE-lsH.CKLAvIFa_1RAmmM.webp 771w"></p> 89<h2 id="the-good-stuff">The Good Stuff</h2> 90<p>Everything runs directly in the browser using Web BLE thanks to project fugu!. No bulky hardware, just a little ESP32 module on the bike powered by a small powerbank.</p> 91<h2 id="the-challenges">The Challenges</h2> 92<p>There are a few challenges to tackle. One of the biggest drawbacks is the screen limitationsâthe built-in display is too small and not bright enough, making it difficult to read in sunlight. Iâm considering switching to a different ESP32 form factor with an e-ink or brighter AMOLED display. Another issue is the Web Geolocation limitation; since the phone has to stay active, geolocation data stops updating if the browser tab is inactive.because of this power consumption is a concern, as keeping the phone active drains the battery.</p> 93<p>This has been a fun little experiment, and while itâs not a perfect solution yet, thereâs definitely room for improvementâlike finding a way to keep geolocation running in the backgroundâbut for now, itâs a cool, lightweight bike computer that does the job.</p> 94<h2 id="todo-feature-list">TODO feature list</h2> 95<ul> 96<li>Till web BLE is ready, make a native app to get geolocation data in the background, another option is to get a GPS module which can be attached to ESP32 directly.</li> 97<li>Make a 3d printed mount for the ESP32 device + the powerbank</li> 98<li>Get a better display, potentially a round one.</li> 99<li>Check out google navigation API for turn by turn navigation</li> 100</ul> <footer data-astro-cid-uogrpsp5> <ul class="tags" data-astro-cid-pjkakwps> <li data-astro-cid-pjkakwps> <a class="tag" href="/tags/electronics" data-astro-cid-pjkakwps> 101#electronics </a> </li><li data-astro-cid-pjkakwps> <a class="tag" href="/tags/making" data-astro-cid-pjkakwps> 102#making </a> </li><li data-astro-cid-pjkakwps> <a class="tag" href="/tags/BLE" data-astro-cid-pjkakwps> 103#BLE </a> </li><li data-astro-cid-pjkakwps> <a class="tag" href="/tags/javascript" data-astro-cid-pjkakwps> 104#javascript </a> </li> </ul> </footer> </article> </main> <footer data-astro-cid-sz7xmlte> <ul class="social" data-astro-cid-yxtifmrq> <li data-astro-cid-yxtifmrq> <a aria-label="@thilinag on twitter" class="link" href="https://twitter.com/thilinag" target="_blank" data-astro-cid-yxtifmrq> <svg viewBox="0 0 24 24" height="1.5rem" data-astro-cid-yxtifmrq="true" astro-icon="mdi:twitter">
104<path fill="currentColor" d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z"/></svg> </a> </li><li data-astro-cid-yxtifmrq> <a aria-label="Thilina Gunasekara on LinkedIn" class="link" href="https://lk.linkedin.com/in/thilinagunasekara" target="_blank" data-astro-cid-yxtifmrq> <svg viewBox="0 0 24 24" height="1.5rem" data-astro-cid-yxtifmrq="true" astro-icon="mdi:linkedin"><path fill="currentColor" d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77z"/></svg> </a> </li><li data-astro-cid-yxtifmrq> <a aria-label="Thilina on GitHub" class="link" href="https://github.com/thilinag" target="_blank" data-astro-cid-yxtifmrq> <svg viewBox="0 0 24 24" height="1.5rem" data-astro-cid-yxtifmrq="true" astro-icon="mdi:github"><path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"/></svg> </a> </li> </ul> <ul data-astro-cid-sz7xmlte> <li data-astro-cid-sz7xmlte> <a href="/" data-astro-cid-sz7xmlte>Home</a> </li> <li data-astro-cid-sz7xmlte> <a href="/projects/" data-astro-cid-sz7xmlte>Projects</a> </li> <li data-astro-cid-sz7xmlte> <a href="/writing/" data-astro-cid-sz7xmlte>Writing</a> </li> <li data-astro-cid-sz7xmlte> <a href="/hiking/" data-astro-cid-sz7xmlte>Hiking</a> </li> <li data-astro-cid-sz7xmlte> <a href="/games/" data-astro-cid-sz7xmlte>Games</a> </li> <li data-astro-cid-sz7xmlte> <a href="/uses/" data-astro-cid-sz7xmlte>Uses</a> </li> <li data-astro-cid-sz7xmlte> <a href="mailto:[email protected]" title="Send an email to contact me" data-astro-cid-sz7xmlte> 105Contact 106</a> </li> </ul> <div data-astro-cid-sz7xmlte>Thilina Gunasekara © 2025 à·´</div> </footer> <audio id="audio" src="/thilinag.mp3" preload="auto" data-astro-cid-sckkx6r4></audio>
106<script> 107 window.addEventListener("deviceorientation", (evt) => { 108 let rotateX, rotateY; 109 if (evt.gamma < 0 && evt.beta < 0) { 110 rotateX = "-2deg"; 111 rotateY = "2deg"; 112 } 113 114 if (evt.gamma > 0 && evt.beta > 0) { 115 rotateX = "2deg"; 116 rotateY = "-2deg"; 117 } 118 119 if (evt.gamma > 0 && evt.beta < 0) { 120 rotateX = "-2deg"; 121 rotateY = "-2deg"; 122 } 123 124 if (evt.gamma < 0 && evt.beta > 0) { 125 rotateX = "2deg"; 126 rotateY = "2deg"; 127 } 128 129 if (Math.abs(evt.gamma) < 3 && Math.abs(evt.beta) < 3) { 130 rotateX = "0deg"; 131 rotateY = "0deg"; 132 } 133 134 document.documentElement.style.setProperty( 135 "--rotateX", 136 rotateX 137 ); 138 document.documentElement.style.setProperty( 139 "--rotateY", 140 rotateY 141 ); 142 }); 143 </script>
143 <!-- 144 ________ _ ___ _____ __ 145/_ __/ / (_) (_)__ ___ _ / ___/_ _____ ___ ____ ___ / /_____ ________ _ 146 / / / _ \/ / / / _ \/ _ `/ / (_ / // / _ \/ _ `(_-</ -_) '_/ _ `/ __/ _ `/ 147/_/ /_//_/_/_/_/_//_/\_,_/ \___/\_,_/_//_/\_,_/___/\__/_/\_\\_,_/_/ \_,_/ 148--> </body></html>
148<script type="module" src="/_astro/Project.astro_astro_type_script_index_0_lang.CFuZ73ng.js"></script>
148
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.