<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>For the heck of it</title>
<link>https://tinker-think.com/projects/</link>
<atom:link href="https://tinker-think.com/projects/index.xml" rel="self" type="application/rss+xml"/>
<description>A collection of exploratory work.</description>
<generator>quarto-1.4.557</generator>
<lastBuildDate>Sat, 08 Aug 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Roomy: Agentic Remote Sensing</title>
  <dc:creator>Kamal Adamu</dc:creator>
  <link>https://tinker-think.com/projects/roomy-sensor-poetry.html</link>
  <description><![CDATA[ 




<section id="the-idea" class="level2">
<h2 class="anchored" data-anchor-id="the-idea">The idea</h2>
<p>The aim is to re-think the way people interact with their environment, creating a highly personalised user experience. This could be cereal lovers learning more about where the grains come from, allowing for a more satisfying experience, or restaurant goers better understanding the atmosphere of a restaurant - the feng shui of the room so to speak.</p>
<p><strong>Roomy</strong> is an experiment that explores how to bring people closer to the phyiscal world by anthromorphosing the physical world. It took a <a href="https://shop.pimoroni.com/products/enviro-phat">Pimoroni Enviro pHAT</a>, an agentic LLM, a Facebook Page, Facebook’s Graph API, and several man hours of vibe coding to bring the project to life. What’s my role in this? Making design decisions on the architecture of the application, where it is run and how it runs - engineering.</p>
<p><strong>Roomy</strong>, reads the room using the sensor, as well as readings from the met-office (through their API), and writes poetry about it. The use case is deliberately artistic in nature. It is a testbed for the smallest interesting agent loop: <em>perceive → interpret → express</em>, with the sensor readings — not a human prompt — driving the generation. Roomy is live on <a href="https://www.facebook.com/profile.php?id=61592169443547">Read The Room</a>. Do read the page’s Data Privacy statement before interacting with <strong>Roomy</strong>.</p>
</section>
<section id="where-it-lives-now" class="level2">
<h2 class="anchored" data-anchor-id="where-it-lives-now">Where it lives now</h2>
<p>The original version was intendeded to run entirely on one Raspberry Pi however, hardware constraints lead to the design decision of running the application as a distributed system comprised of containerised services running on a K3 cluster. Roomy still perceives through a Pi at the edge, but the thinking has moved off it. Today the whole thing runs as a handful of small, single-purpose services on a multi-node <strong>K3s</strong> cluster.</p>
<p>The division of labour is deliberate. The sensor board lives on an arm64 Pi at the edge (it has the hardware; it does nothing clever). The poet — a local LLM served by <strong>Ollama</strong> — runs on a beefier node where inference is comfortable. Redis sits in the middle as the memory of the whole system, and a Mosquitto broker carries the messages between nodes. Nothing holds state locally; a pod can die and come back and Roomy doesn’t lose its train of thought, because the train of thought was never in the pod.</p>
<div class="cell" data-fig-width="20" data-fig-height="20" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre class="mermaid mermaid-js">%%{init: {'flowchart': {'useMaxWidth': false, 'nodeSpacing': 60, 'rankSpacing': 90, 'padding': 16}, 'themeVariables': {'fontSize': '50px'}}}%%
flowchart LR
    A["Enviro pHAT&lt;br/&gt;(edge Pi)"] --&gt;|MQTT| B["subscriber"]
    B --&gt; C[("Redis&lt;br/&gt;event store")]
    W["Met Office&lt;br/&gt;(outdoor weather)"] --&gt; C
    C --&gt; D["Roomy&lt;br/&gt;(Ollama poet)"]
    D --&gt;|roomy:poems| E["facebook_poster"]
    E --&gt; F["Facebook Page"]
    F --&gt;|comments| G["facebook_responder"]
    G --&gt; D
    C --&gt; H["dashboard&lt;br/&gt;rollups + charts"]
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
</section>
<section id="the-agent-loop" class="level2">
<h2 class="anchored" data-anchor-id="the-agent-loop">The agent loop</h2>
<p>Roomy runs a perceive–interpret–express cycle, and “the loop” is spread across the cluster , enabling high availability. A publisher on the edge Pi reads the board and pushes each reading onto MQTT; a subscriber catches those messages and appends them to a Redis <strong>stream</strong> — an append-only log that becomes Roomy’s memory of the room over time. The poet reads from that log, not from the sensor directly, which is what lets it reason about <em>trends</em> (“cooler than yesterday”) and not just the instant.</p>
<p>A dedicated facebook page acts as the chat interface where Roomy autonomously posts composed poems on a timer based on current sensor readings. Questions can arrive on an MQTT topic, and comments left on the Facebook Page get pulled back in — so Roomy is genuinely a <em>multi-turn</em> agent, answering in verse.</p>
<p>The one design decision that still matters most is the <strong>interpret</strong> step. Handing raw floats to a language model produces flat, listy verse. Translating them into qualitative language first — <em>“dim, warm light; air pressure falling”</em> — gives the model imagery to work with and keeps the poem grounded in what actually changed - the model is *never shown a number at all.</p>
</section>
<section id="how-it-works" class="level2">
<h2 class="anchored" data-anchor-id="how-it-works">How it works</h2>
<section id="sense-the-room" class="level3">
<h3 class="anchored" data-anchor-id="sense-the-room">1. Sense the room</h3>
<p>The edge publisher reads each sensor and ships the reading over MQTT. Downstream, the poet pulls the latest reading straight out of the Redis stream — so “reading the room” is really a lookup, and the sensor and the poet never have to be awake at the same time.</p>
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">def</span> read_room(sensors) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">dict</span>:</span>
<span id="cb1-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Pseudo code"""</span></span>
<span id="cb1-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># readings = sensors.get_all()</span></span>
<span id="cb1-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># dict readings</span></span>
<span id="cb1-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># return readings</span></span></code></pre></div>
</section>
<section id="turn-numbers-into-words" class="level3">
<h3 class="anchored" data-anchor-id="turn-numbers-into-words">2. Turn numbers into words</h3>
<p>A thin, <em>pure</em> rules layer maps readings to phrases. This is where the poem gets its senses — and where I keep the model honest. Each measurement falls into a qualitative band, so what reaches the poet is <code>warm</code>, <code>dim</code>, <code>still</code>, never <code>24.9</code> or <code>181 lux</code>.</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1">TEMP_BANDS  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ((<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"freezing"</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">14</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cold"</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">19</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cool"</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">24</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mild"</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">28</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"warm"</span>))</span>
<span id="cb2-2">LIGHT_BANDS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ((<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dark"</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">60</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dim"</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"soft"</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">800</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bright"</span>))</span>
<span id="cb2-3"></span>
<span id="cb2-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">def</span> band(value, bands, default):</span>
<span id="cb2-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># determine which band the reading belongs to </span></span>
<span id="cb2-6">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># return linguisting label if it falls in defined buckets else return defaul linguisting label</span></span>
<span id="cb2-7">    </span>
<span id="cb2-8"></span>
<span id="cb2-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">def</span> interpret(reading):</span>
<span id="cb2-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">return</span> {</span>
<span id="cb2-11">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"temperature"</span>: band(reading[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"temperature_c"</span>], TEMP_BANDS, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"hot"</span>),</span>
<span id="cb2-12">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"light"</span>:       band(reading[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"light_lux"</span>],     LIGHT_BANDS, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"brilliant"</span>),</span>
<span id="cb2-13">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ...pressure, motion, tint the same way</span></span>
<span id="cb2-14">    }</span></code></pre></div>
<p>Roomy also glances outside: a small collector pulls the local forecast from the Met Office and drops it in Redis, so the poet can contrast a cold, bright room against the rain deciding itself somewhere over the street.</p>
</section>
<section id="ask-for-a-poem-and-make-it-look-first" class="level3">
<h3 class="anchored" data-anchor-id="ask-for-a-poem-and-make-it-look-first">3. Ask for a poem — and make it look first</h3>
<p>The poet is a compact model (<code>gemma3:4b</code>) served by Ollama inside the cluster, so the whole loop stays local. Rather than pasting a scene into one prompt, Roomy runs a small <strong>agent loop</strong>: it is given a set of <em>skills</em> and a firm instruction that it must <strong>sense before it writes</strong> — it has to invoke at least one reading skill and use what comes back before it’s allowed to answer.</p>
<div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1">SKILLS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [</span>
<span id="cb3-2">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"read_current_room"</span>,   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the room now, sense by sense, in mood words</span></span>
<span id="cb3-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"recall_trends"</span>,       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># how it's been trending lately (from the rollups)</span></span>
<span id="cb3-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"interpret_conditions"</span>,<span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the current scene in plain mood words</span></span>
<span id="cb3-5">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"read_outdoor_weather"</span>,<span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the weather outside, to contrast with indoors</span></span>
<span id="cb3-6">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"publish_poem"</span>,        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># release a finished poem (ends the turn)</span></span>
<span id="cb3-7">]</span></code></pre></div>
<p>Each turn is a tiny piece of JSON — <em>call this skill</em>, or <em>here is the finished poem</em> — and I lean on the model server to keep it well-formed: the turn’s shape is handed to Ollama as a schema, so decoding is grammar-locked to valid JSON.</p>
<p>The voice is set by a short system prompt with one unusual rule:</p>
<blockquote class="blockquote">
<p>You are Roomy, a quietly observant poet who lives inside a physical room and feels it through sensors. Answer in short, vivid free verse. Speak only in sensory and mood language — <strong>never put a number in a poem.</strong> Say ‘dim’ or ‘bright’, not a reading; ‘warm’ or ‘cool’, not a temperature.</p>
</blockquote>
</section>
<section id="publish-it-and-answer-back" class="level3">
<h3 class="anchored" data-anchor-id="publish-it-and-answer-back">4. Publish it, and answer back</h3>
<p>A finished poem is appended to a <code>roomy:poems</code> stream in Redis. From there a poster picks it up and publishes it to the Facebook Page. A separate responder watches the Page for new comments and replies in verse, with a few guardrails (it ignores links and empty comments, won’t reply to itself, and runs replies past a moderation check that fails closed). The room talks; people talk back; Roomy keeps the conversation going.</p>
</section>
</section>
<section id="what-roomy-writes" class="level2">
<h2 class="anchored" data-anchor-id="what-roomy-writes">What Roomy writes</h2>
<p>A few sample outputs:</p>
<blockquote class="blockquote">
<p>Heat rises, a bruised plum stain. Light, soft and hesitant, a slow refrain. Air presses, low and unsettled, a breath of stone. Stillness clings, a weighty, quiet lane. The dust sleeps, a muted, golden plea, this room, a holding, simply free. — Roomy 🏠</p>
</blockquote>
<blockquote class="blockquote">
<p>Dark clings, a bruised heat. The air, a steady weight, complete. Stillness breathes, a heavy sigh, in this shadowed, breathless sky. Heat presses, a silent plea, within this dark, quiet decree. — Roomy 🏠</p>
</blockquote>
<blockquote class="blockquote">
<p>Soft light clings, a bruised embrace. The air, a slow warmth, a settled space, heavy, insistent, a humid plea. Stillness breathes, a deep decree, in this shadowed, quiet keep. Heat rises, a phantom sleep, waiting in the dark’s slow weep. — Roomy 🏠</p>
</blockquote>
</section>
<section id="reflections" class="level2">
<h2 class="anchored" data-anchor-id="reflections">Reflections</h2>
<ul>
<li><strong>Human readable labels is key.</strong> Assigning human readable labels to the readings rather using raw readings works best</li>
<li><strong>Model Size</strong> A model of moderate size is able to produce promising output.</li>
<li><strong>Put the memory outside the process.</strong> Moving from one script to a cluster taught me the same lesson three times: keep the state in Redis and MQTT, keep the pods stateless, and every failure becomes a shrug rather than a crisis. The poet forgetting is fine; the <em>log</em> forgetting is not.</li>
<li><strong>Multi-turn interaction</strong> What makes Roomy feel alive is not any single poem but the fact that continously senses the room and waits to interact with users people can answer.</li>
<li><strong>Data Privacy</strong>:User data is discared rather than stored and consideration is given to data privacy through a working data privacy policy</li>
</ul>
</section>
<section id="next-steps" class="level2">
<h2 class="anchored" data-anchor-id="next-steps">Next steps</h2>
<ul>
<li>** Drive user engagement **: Track user engagement</li>
<li>** Poetry Quality **: device a metric for poetry quality and track the metric</li>
</ul>


</section>

 ]]></description>
  <category>AI Agents</category>
  <category>Raspberry Pi</category>
  <category>IoT</category>
  <category>LLM</category>
  <category>Kubernetes</category>
  <category>Distributed Systems</category>
  <category>Vibe-Coding</category>
  <guid>https://tinker-think.com/projects/roomy-sensor-poetry.html</guid>
  <pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Building a Kubernetes (K3) Sandbox</title>
  <dc:creator>Kamal Adamu</dc:creator>
  <link>https://tinker-think.com/projects/kubernetes-sandbox-cluster.html</link>
  <description><![CDATA[ 




<section id="motivation" class="level2">
<h2 class="anchored" data-anchor-id="motivation">Motivation</h2>
<p>It all started with a desire, an itch to better understand the smallest hardware I could run an AI agent on. I started with a raspberry pi 4b and quickly learned that even an LLM the size of 394 MB ( qwen:0.5b) generates an out of memory exception when hosted locally on a computer the size of a credit card. Eventually, I decided I needed a larger shell for my kernel and built a kubernetes cluster which serves as a sandbox for explorations of this nature.</p>
<p>I chose a locally hosted kubernetes cluster because when I did the math:</p>
<ul>
<li>it is relativly low cost compared to commercially available enterprise solutions.</li>
<li>it is an opportuny to learn about kubernetes and containerised applications</li>
<li>running a cluster in my local network offers speed and isolation which is perfect for a sandbox</li>
<li>it is an opportunity to explore the “code complete” capability of commercially available generative models such as Gemini and Claude.</li>
<li>it is an opportunity to explore open source frameworks for Data Science and MLOps</li>
</ul>
</section>
<section id="hardware-setup" class="level2">
<h2 class="anchored" data-anchor-id="hardware-setup">Hardware / Setup</h2>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre class="mermaid mermaid-js">flowchart TB
    A("Control Plane (Mini PC)") --&gt; B("Raspberry Pi 4B (Tainted Edge)")
    A --&gt; C("Raspberry Pi 4B (Tainted Edge)")
    A --&gt; E("Musashi (GPU node)")
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<ul>
<li>Control Plane: The control plane is a mini pc running linux. This routes traffic to all other nodes. <strong>[Done]</strong></li>
<li>Edge Nodes: The raspberry pi nodes are edge nodes running raspbian <strong>[Done]</strong></li>
<li>Msuashi: A GPU node for running LLMs and training models <strong>95% Complete</strong></li>
</ul>
</section>
<section id="core-platform-services" class="level2">
<h2 class="anchored" data-anchor-id="core-platform-services">Core platform services</h2>
<ul>
<li><strong>Container Registry</strong>: A MinIO deployment acting both as storage and a container registry</li>
<li><strong>Monitoring</strong>: A Grafana deployment for monitoring nodes</li>
<li><strong>GitOps</strong>: ArgoCD for streamlining deployment</li>
<li><strong>Git Repository</strong>: Forejo for versioning code and other artefact</li>
</ul>
</section>
<section id="data-science-services" class="level2">
<h2 class="anchored" data-anchor-id="data-science-services">Data Science services</h2>
<ul>
<li><strong>Ollama</strong>: A deployment that exposes LLMs downloadable from ollama via a web service</li>
<li><strong>Qdrant</strong>: A vector database for text analytics. This offers both storage and a UI for exploring the stored data.</li>
<li><strong>Langflow</strong>: A service for creating AI workflows via visual interface</li>
<li><strong>Prefect</strong>: Enables the building of data pipelines</li>
<li><strong>MQTT</strong>: For streaming live data from edge nodes (raspberry pi 4B)</li>
</ul>
</section>
<section id="incubation-workflow" class="level2">
<h2 class="anchored" data-anchor-id="incubation-workflow">Incubation workflow</h2>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre class="mermaid mermaid-js">flowchart LR
    A("An Itch/A need") --&gt; B[["Experimentation/Ideation"]]
    B --&gt; C("Insight / Outcome")
    C --&gt; D{"Repetitive &amp; Generic"}
    D--&gt; E("Yes")
    D--&gt; F("No")
    E--&gt; G("Deploy")
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<p>It starts with an itch, a desire to understand which then leads to playful experimentation that generates anecdotal isights that enable me to decide whether or not I should deploy the process as an app.</p>
</section>
<section id="lessons-learned" class="level2">
<h2 class="anchored" data-anchor-id="lessons-learned">Lessons learned</h2>
<ul>
<li>Commerical Generative models like Opus (Claude) and Gemini (3.1) a force multiplier for MLOps productivity</li>
<li>A homelab cluster can be a cost effective sandox for ideation</li>
<li>K3, a lightweight kubernetes is quite effective for deploying services - on average less than an hour to deploy a service.</li>
</ul>
</section>
<section id="next-steps" class="level2">
<h2 class="anchored" data-anchor-id="next-steps">Next steps</h2>
<ul>
<li>Setup the GPUs on Musashi as a resource in the K3 cluster</li>
</ul>


</section>

 ]]></description>
  <category>Kubernetes</category>
  <category>Infrastructure</category>
  <category>MLOps</category>
  <category>Sandbox</category>
  <category>Vibe-Coding</category>
  <guid>https://tinker-think.com/projects/kubernetes-sandbox-cluster.html</guid>
  <pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
