<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Velocip]]></title><description><![CDATA[I run tiny experiments. Most end up in the trash, some sit in a "drawer," and others grow. This blog is the story of these experiments and what I have learnt by]]></description><link>https://velocip.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1766355244821/a70ba794-c81d-4bbc-ac1b-3fb066fcbd48.png</url><title>Velocip</title><link>https://velocip.com</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 03:38:03 GMT</lastBuildDate><atom:link href="https://velocip.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[OpenClaw on Raspberry Pi 5]]></title><description><![CDATA[I've been testing an OpenClaw on a Raspberry Pi 5. This post documents the journey of turning a Pi and a 7" Touch Display into a useful home IoT device without writing a single line of code.
1. The Se]]></description><link>https://velocip.com/openclaw-on-raspberry-pi-5</link><guid isPermaLink="true">https://velocip.com/openclaw-on-raspberry-pi-5</guid><category><![CDATA[openclaw]]></category><category><![CDATA[Raspberry Pi]]></category><dc:creator><![CDATA[Patryk Jakubik]]></dc:creator><pubDate>Wed, 25 Feb 2026 15:56:34 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6945480edb77aff6b3d854f9/4c8231f2-c36e-428d-862f-c651d5511ee7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been testing an <strong>OpenClaw</strong> on a <strong>Raspberry Pi 5</strong>. This post documents the journey of turning a Pi and a 7" Touch Display into a useful home IoT device without writing a single line of code.</p>
<h2>1. The Setup</h2>
<ul>
<li><p><strong>Model</strong> - I opted for Gemini 3 flash, which is provides a good balance between speed, reasoning and lower cost comparing to more capable models.</p>
</li>
<li><p><strong>Communication (VNC)</strong> - I mainly interact with the bot through a dashboard by connecting to the Raspberry Pi via VNC (TigerVNC Viewer).</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770120981554/210a8820-4461-42d3-ac13-e63f30f7d65e.png" alt="" style="display:block;margin-left:auto" />

<ul>
<li><strong>Communication (Telegram)</strong> - I also use Telegram to check on current tasks. I’ve noticed that even when the assistant says, “I will let you know once I’ve finished the task,” it doesn’t follow up, so Telegram is useful for monitoring a current progress, token usage, status etc.</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770120378238/4568cba9-a8b9-4cd3-bccf-e0b2ad9b2878.png" alt="" style="display:block;margin:0 auto" />

<h2>2. Main Dashboard + RestAPI + Database</h2>
<p>I iteratively prompted the assistant to built a Svelte-based dashboard that displays:</p>
<ul>
<li><p>Real-time <strong>GBP → PLN Exchange Rates</strong> (updated every morning).</p>
</li>
<li><p>A multi-day <strong>Weather Forecast</strong>.</p>
</li>
</ul>
<p>Then I asked it to persist the exchange rate and the maximum/minimum temperature every day and then expose it with a restful API. It decided to use FastAPI and SQLite for the task, which is what I was using in my personal projects on the RP 5 and 4 in the past. I think it was an excellent choice for this setup.</p>
<p>Score: 5/5</p>
<img src="https://cdn.hashnode.com/uploads/covers/6945480edb77aff6b3d854f9/89bd812e-f4b3-4aa8-ac45-9ea3214565b3.png" alt="" style="display:block;margin:0 auto" />

<h2>3. Temperature and Humidity (BME280 sensor)</h2>
<p>I started my IoT work by testing a BME280 sensor for indoor temperature and humidity. The sensor was detected without issues. I ran several tests, added a service that restarts automatically after boot. Then I asked to add two new cards showing temperature and humidity to the dashboard. I compared the values with a dedicated room thermometer, and both were almost identical.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770984888017/0cf64a2b-b7f1-4bcc-afc3-e499e85ce858.jpeg" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771327123142/fd8a20d1-456c-4ea8-888a-edc9654b8052.png" alt="" style="display:block;margin:0 auto" />

<p>Score: 5/5</p>
<h2>4. eCO2 and TVOCs (SGP30 sensor)</h2>
<p>My next IoT test measured air quality in the master bedroom using a Raspberry Pi Zero paired with an SGP30 sensor, which reports total volatile organic compounds (TVOC) and equivalent CO2 (eCO2). Research suggests that high CO2 levels during sleep can disrupt REM sleep, reduce heart-rate variability (HRV), and impair cognitive performance the next day, so maintaining good ventilation in the bedroom is important.</p>
<p>The SGP30 sensor is not a replacement for a true CO2 sensor, such as SCD41, which uses IR light pulses that are absorbed by CO2 molecules. This absorption, creates a tiny pressure wave (sound), which are detected by the sensor’s internal microphone.</p>
<p>The SGP30 uses entirely different technology - it must first heat-up a metal-oxide plate. Then, once an oxygen molecules land on it, they steal electrons from the metal, creating a layer of negative charge, which increases electrical resistance. Then when the gas hits the surface, it reacts with the oxygen letting electrons flow freely, which indicates a spike in gas levels.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771328935709/453c328b-6be7-48f1-9d94-883fe49ce479.webp" alt="" style="display:block;margin:0 auto" />

<p>I asked the assistant to create a service that polls measurements every 30 seconds for 15 minutes, computes the average, and saves the results to a SQLite database. Then I asked to create a FastAPI-based RESTful API to expose those measurements.</p>
<pre><code class="language-plaintext">Get all entries: 
http://192.168.50.79:8000/measurements

Get entries from date until now (Unix timestamp): 
http://192.168.50.79:8000/env_measurements?from_date=1771342047

Get entries from/to date (Unix timestamp): 
http://192.168.50.79:8000/env_measurements?from_date=1771342047&amp;to_date=1771455047
</code></pre>
<pre><code class="language-json"># Individual air quality item:

{
        "timestamp": 1771342763,
        "device_name": "RPZero",
        "eco2_ppm": 426,
        "tvoc_ppb": 84,
        "temperature": null,
        "humidity": null,
        "uptime": 104651,
        "location": "master bedroom"
 }
</code></pre>
<p>After a 24-hour calibration and stabilisation period, I requested a daily graph showing eCO2 (parts per million) and TVOC (parts per billion).</p>
<p>The results? I think I need to open a window before going to bed to let fresh air into the room 😉.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771414056222/2252873a-dc51-49ea-9c85-6a27930dbbc2.png" alt="" style="display:block;margin:0 auto" />

<p>For better visibility and immediate access, I built a mobile app that displays daily values for a selected date. I'll be checking these values alongside my Fitbit sleep score to see whether poor sleep correlates with higher CO2 concentrations.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6945480edb77aff6b3d854f9/843df6ac-c56c-4277-bc96-2a4120540ed9.png" alt="" style="display:block;margin:0 auto" />

<p>Score: 5/5</p>
<h2>5. Job Scrapping &amp; Web Research</h2>
<p>I created a detailed prompt to search for triathlon race results (sprint and standard distances) that are downloadable as CSV files. The files should be downloaded, renamed, sorted, and prepared for the cleaning process. I encouraged the assistant to ask about anything it was unsure of so we could discuss next steps. I also requested an action plan before proceeding so I could review and confirm it. The assistant used the Brave browser for this task.</p>
<h3>Result</h3>
<p>The assistant returned pages that did not include links to CSV files, and some pages had no results for triathlon races. I selected some pages myself where I knew there were CSV links, but the assistant reported it could not download the CSV files. However, it did a very good job cleaning the CSV files after I downloaded them.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770987016944/b033d195-562d-41c2-9afe-68ab04c40b92.png" alt="" style="display:block;margin:0 auto" />

<p>Score: 1/5</p>
<h2>6. Traffic Monitoring - car object detection</h2>
<p>The goal was simple but technically tricky: detect cars traveling uphill and downhill while ensuring each vehicle is counted only once. Each car remains in the frame for roughly four seconds. The object-recognition system should exclude any vehicle that has stopped for more than one minute, since that likely indicates it parked along the road. Traffic is light, so cars rarely stop in congestion for more than 30 seconds.</p>
<p>We chose YOLOv8-nano with a BoT-SORT tracker to assign a unique ID to every vehicle. To handle internal buffer lag, the assistant used a frame-flushing mechanism to ensure the system always operated on the live moment.</p>
<h3>Results</h3>
<p>The results were mixed. It did a fairly good job detecting cars despite using a low-quality USB camera and no AI hardware (e.g., an AI camera or AI HAT+). However, it failed to exclude stationary objects as requested in the prompt, and in some cases it detected two vehicles when a car was partially overlayed by a tree, producing duplicate IDs.</p>
<p>I believe this can be fixed with additional iterations. However, because per-car confidence was often low (frequently below 0.5), I plan to invest in an "AI camera" or an "AI HAT+ 2" and repeat the task to improve detection confidence.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770982285205/b3d838dd-e0a7-4dcc-9265-c820bab6169d.webp" alt="" style="display:block;margin:0 auto" />

<p>I also asked for a graph showing car detections per minute during a 10 minute test.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770933606006/04ad5205-3a69-4c6a-82ed-4cc00ce9dbd1.png" alt="" style="display:block;margin:0 auto" />

<p>Score: 2/5</p>
<h2>7. Traffic Monitoring - car noise detection</h2>
<p>Since the object detection was not giving me a good results. I decided to try another approach - detecting passing cars using a microphone.</p>
<p>Microphone specs:</p>
<ul>
<li><p>Frequency Response Range: 50-16000HZ</p>
</li>
<li><p>Mic Size: 6.0*2.7mm</p>
</li>
<li><p>Sensitivity: -46dB (0dB-1V/ubar, f-1kHz)</p>
</li>
<li><p>Polar Pattern: Omnidirectional</p>
</li>
<li><p>USB type: USB2.0/V1.1</p>
</li>
</ul>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6945480edb77aff6b3d854f9/97ab2b21-a960-4acb-9351-3faafbaa4431.webp" alt="" style="display:block;margin:0 auto" />

<p>I asked the assistant to run detection for a few minutes while I counted the cars manually, then compared the results. It took only one tuning attempt to reach about 90% accuracy, which remained consistent across three additional tests. It even detected two cars that were very close to each other—something I expected it to struggle with. In one test I could hear a small airplane fly over the microphone, but the system did not classify that sound as a car.</p>
<p>Each detection produces a <code>.wav</code> file and triggers a <code>Python</code> script that generates an amplitude graph along with a <code>JSON</code> file containing the detection characteristics we agreed on during development.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6945480edb77aff6b3d854f9/2493053c-2d7d-4681-8f0e-51f085aed7d7.png" alt="" style="display:block;margin:0 auto" />

<h3>Some limitations:</h3>
<ul>
<li><p><strong>Weather condition</strong> - this setup works well only if there is no rain or strong winds.</p>
</li>
<li><p><strong>No live detection</strong> - this approach does not allow a real-time detection.</p>
</li>
<li><p><strong>False-positive</strong> - If a car enters or leaves a driveway in the neighborhood, it might be mistakenly detected as a passing car.</p>
</li>
</ul>
<p>Score: 4/5</p>
<h2>8. Total cost</h2>
<p>I used the most affordable Gemini model available at the time, which cost me £5.16 in total. While a higher-end model might enhance the test results, I am pleased with the current outcomes and intend to use more expensive models for more complex tasks.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6945480edb77aff6b3d854f9/4c910f41-cebc-4b82-88fa-93fc6a17ea41.png" alt="" style="display:block;margin:0 auto" />

<h2>9. Stability</h2>
<p>During my testing I had only one instance where I had to restart the session. Apart from the OpenClaw on RP5 was very stable and I didn’t noticed any issues.</p>
<h2>10. Next steps</h2>
<ol>
<li><p>Optimisation techniques</p>
<ul>
<li><p>Choose the right model for a task.</p>
</li>
<li><p>Define max_tokens for completions and adjust dynamically based on the task.</p>
</li>
<li><p>Use batching and caching</p>
</li>
</ul>
</li>
<li><p>Prompt engineering</p>
<ul>
<li><p>Use prompt templates</p>
</li>
<li><p>Give different roles for the specific tasks</p>
</li>
<li><p>Break large requirements into smaller tasks</p>
</li>
<li><p>Think about an output before prompting (e.g JSON, csv, text etc.)</p>
</li>
<li><p>Providing context and examples</p>
</li>
</ul>
</li>
<li><p>Monitoring</p>
<ul>
<li>Track token consumption, errors, latency etc.</li>
</ul>
</li>
<li><p>Define custom routing (cost saving)</p>
</li>
<li><p>More IoT projects.</p>
</li>
</ol>
<h2>11. Conclusion</h2>
<p>With the ability to attach various sensors, cameras and screens to the Raspberry Pi, you can do a wide range of projects. It easily connects to Raspberry Pi Zero W or Pico 2 W, allowing tasks to run on those devices. You can offload CPU/GPU-intensive tasks to AI HAT+ 2, which remains much cheaper than a MacBook Mini. If you make a mistake, simply swap the SD card and start over. If you have a Raspberry Pi available, I recommend installing OpenClaw and tackling a project you've been putting off.</p>
]]></content:encoded></item><item><title><![CDATA[Android, Raspberry Pi and Ktor - Expanding Capabilities]]></title><description><![CDATA[Project Overview
The goal of this experiment was to explore a full-stack architecture by hosting both an Android UI (Jetpack Compose) and a backend server (Ktor) within a single project on a Raspberry Pi.
At the time of development, Kotlin Multiplatf...]]></description><link>https://velocip.com/android-raspberry-pi-and-ktor-expanding-capabilities</link><guid isPermaLink="true">https://velocip.com/android-raspberry-pi-and-ktor-expanding-capabilities</guid><category><![CDATA[Android]]></category><category><![CDATA[ktor]]></category><category><![CDATA[Raspberry Pi]]></category><dc:creator><![CDATA[Patryk Jakubik]]></dc:creator><pubDate>Mon, 22 Dec 2025 01:11:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766407137445/79c53e13-732d-40e1-aac3-885f762f687a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-project-overview">Project Overview</h2>
<p>The goal of this experiment was to explore a <strong>full-stack</strong> architecture by hosting both an Android UI (<strong>Jetpack Compose</strong>) and a backend server (<strong>Ktor</strong>) within a single project on a Raspberry Pi.</p>
<p>At the time of development, Kotlin Multiplatform (KMP) was still in its early, unstable stages, so I was trying to find an alternative ways to share code and logic between a client and a server on a dedicated hardware device.</p>
<h2 id="heading-benefits-of-running-full-stack-on-raspberry-pi">Benefits of running full stack on Raspberry Pi</h2>
<ol>
<li><p>Full stack in a single project</p>
<ul>
<li><p><strong>Streamlined deployment</strong> - No need to switch between IDEs or manage separate projects.</p>
</li>
<li><p><strong>Sharing code</strong> - similar to KMP I was able to share data classes and some business logic between the front-end and server.</p>
</li>
<li><p><strong>Re-using persistence layer</strong> - I used Room in both the UI and server with Mutex, which worked well for simple scenarios.</p>
</li>
</ul>
</li>
<li><p>Availability and Practical use cases</p>
<ul>
<li><p><strong>No Li-ion battery</strong> - the project was running 24h with an always-on display, so there was no risk of battery overheating or degrading.</p>
</li>
<li><p><strong>Restful API</strong> - there are number of use cases of running restful api on the local network, but my favourite was sending reminder messages or task notes from my phone and displaying them as info banners.</p>
</li>
<li><p><strong>Interactive Dashboard</strong> - My favourite dashboard is a weather forecast with reminders and calendar events. Since I’m using 7.5 Inch touch-based display, it is possible to switch between different dashboards.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-which-build-to-choose-from">Which Build to choose from?</h2>
<p>There are at least two methods for installing Android OS on the Raspberry Pi.</p>
<ul>
<li><p><strong>Konstakang</strong> provides <a target="_blank" href="https://konstakang.com/devices/rpi5/">AOSP</a> and <a target="_blank" href="https://konstakang.com/devices/rpi5/">LineageOS</a> builds with additional features and enhancements.</p>
</li>
<li><p><strong>Emteria</strong> provides <a target="_blank" href="https://emteria.com/kb/install-android-raspberry-pi-5-imager">Emteria.OS</a>, which is a commercial solution, though it is free for personal use.</p>
</li>
</ul>
<p>Numerous online tutorials detail the installation process for each version, so I will not discuss that here. I decided to use Emteria.OS, which I found to be more stable than LineageOS-based builds on Raspberry Pi. However, if you plan to connect sensors, cameras, or other peripherals, Konstakang's builds might be a better choice.</p>
<p>You can run both a <a target="_blank" href="https://ktor.io/">Ktor</a> service and a UI within the same Android application on a Raspberry Pi. However, for administrative tasks like database updates or Ktor service maintenance, it's beneficial to create separate "<strong>admin</strong>" and "<strong>user</strong>" build <strong>flavors</strong>. This approach allows you to isolate and manage admin-specific use cases in a dedicated application, ensuring a cleaner separation of concerns.</p>
<pre><code class="lang-kotlin">productFlavors {
   create(<span class="hljs-string">"server"</span>) {
       isDefault = <span class="hljs-literal">true</span>
       dimension = <span class="hljs-string">"environment"</span>
       buildConfigField(<span class="hljs-string">"String"</span>, <span class="hljs-string">"ENVIRONMENT"</span>, <span class="hljs-string">"\"server\""</span>)
   }
   create(<span class="hljs-string">"admin"</span>) {
       dimension = <span class="hljs-string">"environment"</span>
       buildConfigField(<span class="hljs-string">"String"</span>, <span class="hljs-string">"ENVIRONMENT"</span>, <span class="hljs-string">"\"admin\""</span>)
   }
}
</code></pre>
<h2 id="heading-energy-consumption">Energy consumption</h2>
<p>A Raspberry Pi with a 7.5-inch screen, running continuously, typically consumes between 6.0W and 6.5W. Occasional power spikes may occur depending on the running processes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766398938778/9bf9d3b5-d8d6-4a03-9372-228c02d77239.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-pre-installation-tasks">Pre-installation tasks</h2>
<h3 id="heading-stay-awake">Stay Awake</h3>
<p>To prevent the screen from entering sleep mode, enable the 'Stay Awake' setting. While not strictly required, this ensures the UI remains continuously visible, which is desirable for this application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766398995083/cf92bafc-37f5-409f-b5d7-ea9099b6c92a.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-ip-binding">IP Binding</h3>
<p>Modern routers usually retain IP-to-MAC address mappings, reassigning the same IP when a device reconnects. However, older routers might assign a new IP to your Raspberry Pi (RP) if it goes offline and the DHCP lease expires. To prevent this, manually bind a static IP address to the RP's MAC address.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399019158/78d63b47-eb98-411e-b7ac-a8f047927156.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-disable-pause-app-activity">Disable Pause App Activity</h3>
<p>While a foreground service inherently minimises pausing, disabling 'Pause app activity if unused' provides an additional layer of reliability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399088706/d519c134-5466-4483-9e52-ca15d6fabeac.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-enable-notifications">Enable Notifications</h3>
<p>While the service may function with notifications disabled, enabling them provides valuable insight into the currently running services.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399112557/925db738-0e21-4fc0-8a46-0dd3f9120c45.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-add-user-permissions-in-manifest">Add User Permissions in Manifest</h3>
<p>Since I'm going to connect to internet and run a foreground service with notifications, few permissions in the app manifest must be included.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399169978/cb26348a-ee4f-435b-81da-aa329d74a90d.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-add-foreground-service-in-manifest">Add Foreground Service in Manifest</h3>
<p>Define a foreground service with a type "dataSync", which handles data transfer operations, such as data uploading or downloading, fetching data or local file processing.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399208830/ae6589be-1441-4454-8a33-994a7ef47cd9.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-enable-insecure-connections">Enable Insecure Connections</h3>
<p>Since I’m just testing and playing around on my local network right now, skipping a certificate setup for insecure connections is fine. Create a <em>network_security_config.xml</em> file in the Android project's xml folder and specify the Raspberry Pi's IP address to allow these insecure connections for development purposes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399265273/53c9dbce-7cff-4ca3-a733-7125eb40c328.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It is crucial to remember that in a real-world, production environment, you should always enable secure connection and set up proper certificates. The setup I’m showing here is used for testing and experimentation only.</div>
</div>

<p><strong>Next</strong> - in the manifest, add this file under networkSecurityConfig</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399284676/7c9b4627-a24f-438d-8516-7c9f6e8c8e1c.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-create-a-foreground-service">Create a Foreground Service</h2>
<p>Creating a foreground service with a restart mechanism ensures the server will run continuously. The order matters here so…</p>
<ol>
<li><p>Create a notification channel.</p>
</li>
<li><p>Build a notification.</p>
</li>
<li><p>Start a foreground service.</p>
</li>
</ol>
<p>If the service is killed by the system, it will be recreated and <strong>onStartCommand()</strong> will be called with a <strong>null</strong> intent.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">createNotificationChannel</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> serviceChannel = NotificationChannel(
        CHANNEL_ID,
        <span class="hljs-string">"Server Service Channel"</span>,
        NotificationManager.IMPORTANCE_DEFAULT
    )
    <span class="hljs-keyword">val</span> manager = getSystemService(NOTIFICATION_SERVICE) <span class="hljs-keyword">as</span> NotificationManager
    manager.createNotificationChannel(serviceChannel)
}

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">buildNotification</span><span class="hljs-params">()</span></span>: Notification {
    <span class="hljs-keyword">return</span> NotificationCompat.Builder(<span class="hljs-keyword">this</span>, CHANNEL_ID)
        .setContentTitle(<span class="hljs-string">"Server Service"</span>)
        .setContentText(<span class="hljs-string">"Running..."</span>)
         setSmallIcon(R.drawable.ic_download)
        .build()
}

<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">startForegroundService</span><span class="hljs-params">()</span></span> {
    startForeground(NOTIFICATION_ID, buildNotification())
}

<span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">val</span> CHANNEL_ID = <span class="hljs-string">"ServerServiceChannel"</span>
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">val</span> NOTIFICATION_ID = <span class="hljs-number">1</span>
}
</code></pre>
<h3 id="heading-restart-foreground-service">Restart Foreground Service</h3>
<p>When the system restarts a service that returned <strong>START_STICKY</strong>, it calls <strong>onCreate()</strong> and then <strong>onStartCommand()</strong> again. In this restarted onStartCommand() call, the intent parameter will be null (there should not be any pending start commands here). Then <strong>createNotificationChannel()</strong> and <strong>startForegroundService()</strong> will re-establish the service as a foreground service with its persistent notification.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onStartCommand</span><span class="hljs-params">(intent: <span class="hljs-type">Intent</span>?, flags: <span class="hljs-type">Int</span>, startId: <span class="hljs-type">Int</span>)</span></span>: <span class="hljs-built_in">Int</span> {
    Log.d(<span class="hljs-string">"ServerService"</span>, <span class="hljs-string">"restarting server service"</span>)
    startForegroundService()
    <span class="hljs-keyword">return</span> START_STICKY
}
</code></pre>
<h3 id="heading-run-the-foreground-service">Run the Foreground Service</h3>
<p>The easiest way is to start the service from the main activity in the onCrated() method.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">runRestfulService</span><span class="hljs-params">()</span></span> {
      <span class="hljs-keyword">val</span> intent = Intent(<span class="hljs-keyword">this</span>, ServerService::<span class="hljs-keyword">class</span>.java)
      startService(intent)
}
</code></pre>
<p>At this point you should see a notification from the foreground service informing about the running server.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399312668/c68fc6e4-6813-4375-8616-c95dfab501dc.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-data-aggregation-and-display">Data aggregation and display</h2>
<p>Before we jump into the service portion, I want to emphasize that aggregating and displaying data from APIs, while running the service concurrently, is perfectly acceptable with this setup. This is a key benefit of running an Android and Ktor service simultaneously.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399333124/c61f3bf8-b6c0-4d5c-8f23-aa0a8fbbd47a.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-local-crud-operations">Local CRUD operations</h2>
<p>While my Google and Outlook calendars contain numerous events, not all include notifications, making it easy to overlook some. Having a single home for all these events would definitely help me stay on top of things.</p>
<p>To implement this, I created a POST endpoint in the service that accepts multiple events. When it comes to a <strong>concurrent writes</strong> to the database, several alternative solutions exist:</p>
<ul>
<li><p><strong>PostgreSQL</strong>, which utilizes Multiversion Concurrency Control (MVCC).</p>
</li>
<li><p><strong>MongoDB</strong>, which employs MVCC and internal locking mechanisms.</p>
</li>
<li><p><strong>Firestore</strong>, which implements optimistic concurrency.</p>
</li>
</ul>
<p>A simpler alternative when using Room is to use a <strong>Mutex</strong>. If data loss during unexpected service termination is acceptable, periodically or conditionally (e.g., when a ConcurrentHashMap reaches a specific size) persisting data from a <strong>ConcurrentHashMap</strong> to the Room database is another viable option.</p>
<p>The GET endpoint retrieves all events from the database, splits them into old and new based on the current time, deletes old events in a thread-safe way, and responds with the list of new (upcoming) events.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">get</span> {
     <span class="hljs-keyword">try</span> {
         <span class="hljs-keyword">val</span> now = Clock.System.now()
         <span class="hljs-keyword">val</span> events = eventDao.getEvents().map { it.toEventDto() }
         <span class="hljs-keyword">val</span> oldEvents = events.filter { it.date &lt; now }
          <span class="hljs-keyword">val</span> newEvents = events.filter { it.date &gt;= now }

          <span class="hljs-comment">// Delete old events from the database in a batch operation</span>
          <span class="hljs-keyword">if</span> (oldEvents.isNotEmpty()) {
                eventMutex.withLock {
                    oldEvents.map { it.id }.forEach { id -&gt;
                        eventDao.deleteEvent(id)
                    }
               }
          }

          call.respond(newEvents)
     } <span class="hljs-keyword">catch</span> (e: Exception) {
          call.respond(HttpStatusCode.InternalServerError, <span class="hljs-string">"Failed to retrieve events: <span class="hljs-subst">${e.message}</span>"</span>)
     }
  }
</code></pre>
<p>We also use eventMutex.withLock for <code>post("/single")</code> and <code>post("/multiple")</code> POST events endpoints to ensure thread-safe operations when writing to the database.</p>
<pre><code class="lang-kotlin">post(<span class="hljs-string">"/multiple"</span>) {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">val</span> newEvents = call.receive&lt;List&lt;EventDto&gt;&gt;()
        eventMutex.withLock {
            eventDao.upsertEvents(newEvents.map { it.toEventEntity() })
        }
        call.respond(HttpStatusCode.Created)
    } <span class="hljs-keyword">catch</span> (e: IllegalStateException) {
        call.respond(HttpStatusCode.BadRequest, <span class="hljs-string">"Invalid event data: <span class="hljs-subst">${e.message}</span>"</span>)
    } <span class="hljs-keyword">catch</span> (e: JsonConvertException) {
        call.respond(HttpStatusCode.BadRequest, <span class="hljs-string">"Invalid JSON format: <span class="hljs-subst">${e.message}</span>"</span>)
    } <span class="hljs-keyword">catch</span> (e: Exception) {
        call.respond(HttpStatusCode.InternalServerError, <span class="hljs-string">"Failed to save events: <span class="hljs-subst">${e.message}</span>"</span>)
    }
}
</code></pre>
<hr />
<h3 id="heading-test-post-with-postman">Test POST with Postman</h3>
<p>Let’s test the POST by generating a few sample events in Postman.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399358267/b856329d-dec9-4b62-8b3c-ff6246ccc99f.png" alt class="image--center mx-auto" /></p>
<p>I listed events below the weather forecast, but you can create a separate dashboard just for the events.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766399378789/4ab7be9b-306c-4987-b07e-dc42d7fa19dc.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-upload-images">Upload Images</h2>
<p>There are at least four ways of receiving images in Ktor. There are pros and cons of each method, but I decided to use <strong>receiveMultipart()</strong>, which is scalable since it allows iterating through the individual parts of the requests, and process each part separately if needed.</p>
<p>I also tried using <strong>receiveChannel()</strong>, which is simpler, but highly efficient way of receiving larger images. This approach uses Kotlin coroutines and channels for asynchronous and non-blocking I/O</p>
<p><code>receive&lt;ByteArray&gt;()</code> - Simple to use. Loads entire ByteArray in memory. Best for smaller images.</p>
<p><code>receiveStream()</code> – More efficient than receive. Best for medium size images.</p>
<p><code>receiveChannel()</code> – Most efficient. Requires a coroutine scope. Best for large images.</p>
<p><code>receiveMultipart()</code> – Best for complex form submissions involving files.</p>
<p>We can get a MultiPartData with the <code>call.receiveMultipart()</code> and then iterate over each part and processes it if it's a PartData.FileItem type. Each part is disposed of after processing to free resources.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> io.ktor.server.routing.Route.<span class="hljs-title">setupFilesRoutes</span><span class="hljs-params">()</span></span> {
        route(<span class="hljs-string">"/files"</span>) {
            post(<span class="hljs-string">"/upload/photo"</span>) {
                <span class="hljs-keyword">try</span> {
                    <span class="hljs-keyword">val</span> multipart = call.receiveMultipart()
                    multipart.forEachPart { part -&gt;
                        <span class="hljs-keyword">if</span> (part <span class="hljs-keyword">is</span> PartData.FileItem) {
                            handlePhotoUpload(part)
                        }
                        part.dispose() <span class="hljs-comment">// Ensure all parts are disposed</span>
                    }
                    call.respond(HttpStatusCode.OK, <span class="hljs-string">"File uploaded"</span>)
                } <span class="hljs-keyword">catch</span> (e: Exception) {
                    call.respond(HttpStatusCode.InternalServerError, <span class="hljs-string">"Failed to upload file: <span class="hljs-subst">${e.message}</span>"</span>)
                }
            }
        }
    }
</code></pre>
<p>In the Postman, select "Body" and "form-data". Then select am image file you want to send and add short description. Next, send the image.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766403902396/9bf87a9b-87be-4a34-b00a-2faf610e424c.png" alt class="image--center mx-auto" /></p>
<p>You can preview the uploaded image in the device, by going to Device Explorer -&gt; Data -&gt; Data -&gt; com.yourapp.name -&gt; files</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766404355934/ecc304cf-51b1-4f3c-9e66-653e4275cb39.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-websockets">Websockets</h2>
<p>For scenarios requiring immediate reception or broadcast of updated data upon change, WebSockets offer an effective solution. This is particularly beneficial when connecting a sensor to a Raspberry Pi and needing to push value changes to the client, either upon any change or conditionally to reflect specific alterations.</p>
<p>For the sake of example, I'll demonstrate a chat application between two or more users instead of broadcasting sensor values.</p>
<pre><code class="lang-kotlin">webSocket(<span class="hljs-string">"/chat"</span>) {
    <span class="hljs-keyword">val</span> username = call.request.queryParameters[<span class="hljs-string">"username"</span>] ?: run {
        <span class="hljs-keyword">this</span>.close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, <span class="hljs-string">"No username"</span>))
        <span class="hljs-keyword">return</span><span class="hljs-symbol">@webSocket</span>
    }

    users[username] = <span class="hljs-keyword">this</span>
    send(Frame.Text(<span class="hljs-string">"You are connected! There are <span class="hljs-subst">${users.size}</span> users here."</span>))

    <span class="hljs-keyword">try</span> {
        incoming.consumeEach { frame: Frame -&gt;
            <span class="hljs-keyword">if</span> (frame <span class="hljs-keyword">is</span> Frame.Text) {
                <span class="hljs-keyword">try</span> {
                    <span class="hljs-keyword">val</span> msg = Json.decodeFromString&lt;MessageDto&gt;(frame.readText())
                    <span class="hljs-keyword">if</span> (msg.to.isNullOrBlank()) {
                        users.values.forEach { usr -&gt;
                            usr.send(Frame.Text(<span class="hljs-string">"[<span class="hljs-variable">$username</span>]: <span class="hljs-subst">${msg.text}</span>"</span>))
                        }
                    } <span class="hljs-keyword">else</span> {
                        users[msg.to]?.send(Frame.Text(<span class="hljs-string">"[<span class="hljs-variable">$username</span>]: <span class="hljs-subst">${msg.text}</span>"</span>))
                    }
                } <span class="hljs-keyword">catch</span> (e: Exception) {
                    send(Frame.Text(<span class="hljs-string">"Error processing message: <span class="hljs-subst">${e.message}</span>"</span>))
                }
            }
        }
    } <span class="hljs-keyword">finally</span> {
        users.remove(username)
        <span class="hljs-keyword">this</span>.close()
    }
}
</code></pre>
<p>In the Postman select New -&gt; WebSocket, and enter "<strong>ws://ip_address/end_point username=your_name</strong>" in the address bar. Then click "Connect".</p>
<p>In the body, enter the message you want to send to the user, and click "Send".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766404580772/234fd6e7-16fe-498e-bd2f-e958c04a8171.png" alt class="image--center mx-auto" /></p>
<p>Sending a message to a user.</p>
<p>You can also send a message to a group chat.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766404881314/3ab29d45-52f6-4310-892c-01d8dbffbfe1.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-send-an-image-to-a-group-chat">Send an image to a group chat</h3>
<p>I found broadcasting images to be a more engaging than sending messages. To test this approach in Postman, an image must be converted to <strong>Base64</strong> binary encoding. Subsequently, the message type in Postman should be set to Binary/Base64 before sending the message. It is crucial to avoid sending excessively large images, as this can result in a substantial binary message that may be discarded during transmission.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766404998948/723c27a4-b181-4676-a62d-c7f3c052202c.png" alt class="image--center mx-auto" /></p>
<p>Sending an image in binary base64 format</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766405033720/604636aa-da91-413a-b7d9-c0cd67ded1f6.png" alt class="image--center mx-auto" /></p>
<p>Receive an image via Websockets</p>
<hr />
<h2 id="heading-hosting-a-simple-website">Hosting a simple website</h2>
<p>If you want to host a simple website, you can do this by creating a resources (if not existing already) and adding a subfolder for html, js and css files. You can access the site at <code>http://[your_server_IP_address]:8080/</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">get</span>(<span class="hljs-string">"/"</span>) {
     <span class="hljs-keyword">val</span> indexFile = <span class="hljs-keyword">this</span>.javaClass.classLoader?.getResource(<span class="hljs-string">"serveradmin/dist/index.html"</span>)
     <span class="hljs-keyword">if</span> (indexFile != <span class="hljs-literal">null</span>) {
         call.respondText(indexFile.readText(), ContentType.Text.Html)
     } <span class="hljs-keyword">else</span> {
         <span class="hljs-keyword">val</span> altFile = File(filesDir, <span class="hljs-string">"serveradmin/dist/index.html"</span>)
         <span class="hljs-keyword">if</span> (altFile.exists()) {
             call.respondText(altFile.readText(), ContentType.Text.Html)
         } <span class="hljs-keyword">else</span> {
             call.respond(HttpStatusCode.NotFound, <span class="hljs-string">"index.html not found"</span>)
         }
     }
 }
 staticResources(<span class="hljs-string">"/"</span>, <span class="hljs-string">"serveradmin/dist"</span>) {
     default(<span class="hljs-string">"index.html"</span>)
 }
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766405098819/2b6b4ec4-0761-479b-b572-738b20bf3fb4.png" alt class="image--center mx-auto" /></p>
<p>Create a resources folder with a subfolder for HTML, JS, and CSS files</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766405191318/6bd271d0-99ef-49a0-bec9-95112372c41a.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-problems">Problems</h2>
<ol>
<li><p><strong>Limited hardware support -</strong> many hardware components attached to the Raspberry Pi are not supported by Android OS. For example, I couldn't get the camera to work.</p>
</li>
<li><p><strong>Hard to debug</strong> - The Android Studio recognised the device couple of times only.The wireless debugging works sporadically. I had to restart the debugging multiple times to make it work.This is despite enabling both Wireless Debugging and ADB over Ethernet in Amteria settings. I had to develop using an emulator, and then spend significant amount of time trying to deploy to the Raspberry Pi</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766405223684/f104f69e-c16e-4e6d-847e-e880686eb054.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<hr />
<h2 id="heading-reading-data-from-sensors">Reading data from Sensors</h2>
<p>I tired using the <a target="_blank" href="https://github.com/Pi4J/pi4j">Pi4J</a> library to read temperature, humidity, and pressure from a BME280 sensor, but I was unsuccessful. I’ve found that Python libraries are much more user-friendly for reading data from sensors connected to a Raspberry Pi.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766447355213/9e0adabe-52cd-4803-ad5d-5a6ada99f161.webp" alt class="image--center mx-auto" /></p>
<h2 id="heading-final-thoughts">Final thoughts</h2>
<p>Implementing Android OS with a touchscreen on a Raspberry Pi allows Android developers to leverage powerful client-side dependencies, reduce context switching, and promote code sharing and logic reuse. This setup provides continuous operation without battery concerns and enables microservices implementation, making it cost-effective and versatile for various applications.</p>
<p>By integrating both a Ktor service and UI within the same Android application, developers can achieve seamless service and user interface integration, enhancing efficiency and functionality.</p>
<p>A class service code can be found <a target="_blank" href="https://gist.github.com/pitoszud/55dd33eabdc1d384e304fd84aebaef32">here</a>.</p>
<h2 id="heading-next-steps">Next steps</h2>
<p>Since I faced issues with deployment and limited hardware and sensor support, I plan to create a similar service using Svelte and FastAPI. This should let me fully utilise Python libraries and connect sensors to the Raspberry Pi.</p>
]]></content:encoded></item></channel></rss>