PageSourceSearch

https://www.thilinag.com/projects/esp32-bike-computer

html thilinag.com collected 2026-10-01 14:03:09 UTC 14,457 bytes, 148 lines download raw bytes

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 &copy; 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.