931 lines
71 KiB
HTML
931 lines
71 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" dir="auto">
|
||
|
||
<head><meta charset="utf-8">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||
<meta name="robots" content="noindex, nofollow">
|
||
<title>INET Logo That Breathes — From CAD to LEDs to a Calm Little Server | AlipourIm journeys</title>
|
||
<meta name="keywords" content="raspberrypi, ws2812, scd41, iot, flask, fabrication, soldering">
|
||
<meta name="description" content="
|
||
I didn’t add a warning sign to the room. I taught the logo to breathe. ;-D
|
||
The Rotunde is a good room for bold ideas and yummy coffee. The door sighs shut and the outside world gives up on us. The only thing it’s not good at is ventilating itself. Forty minutes into a group meeting, or a sressful defence, and we all feel like sleeping and running back to our offices!">
|
||
<meta name="author" content="Iman Alipour">
|
||
<link rel="canonical" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/inet_logo/">
|
||
<link crossorigin="anonymous" href="/assets/css/stylesheet.31b150d909186c48d6dbbf653acc2489945fa9ac2c8f8cd3fe8448e89e40fccf.css" integrity="sha256-MbFQ2QkYbEjW279lOswkiZRfqawsj4zT/oRI6J5A/M8=" rel="preload stylesheet" as="style">
|
||
<link rel="icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon.ico">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-16x16.png">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-32x32.png">
|
||
<link rel="apple-touch-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/apple-touch-icon.png">
|
||
<link rel="mask-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/safari-pinned-tab.svg">
|
||
<meta name="theme-color" content="#2e2e33">
|
||
<meta name="msapplication-TileColor" content="#2e2e33">
|
||
<link rel="alternate" hreflang="en" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/inet_logo/">
|
||
<noscript>
|
||
<style>
|
||
#theme-toggle,
|
||
.top-link {
|
||
display: none;
|
||
}
|
||
|
||
</style>
|
||
<style>
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--theme: rgb(29, 30, 32);
|
||
--entry: rgb(46, 46, 51);
|
||
--primary: rgb(218, 218, 219);
|
||
--secondary: rgb(155, 156, 157);
|
||
--tertiary: rgb(65, 66, 68);
|
||
--content: rgb(196, 196, 197);
|
||
--code-block-bg: rgb(46, 46, 51);
|
||
--code-bg: rgb(55, 56, 62);
|
||
--border: rgb(51, 51, 51);
|
||
}
|
||
|
||
.list {
|
||
background: var(--theme);
|
||
}
|
||
|
||
.list:not(.dark)::-webkit-scrollbar-track {
|
||
background: 0 0;
|
||
}
|
||
|
||
.list:not(.dark)::-webkit-scrollbar-thumb {
|
||
border-color: var(--theme);
|
||
}
|
||
}
|
||
|
||
</style>
|
||
</noscript><script>
|
||
(function () {
|
||
|
||
|
||
window.goatcounter = { endpoint: "/count" };
|
||
|
||
const s = document.createElement("script");
|
||
s.async = true;
|
||
s.src = "/js/count.js";
|
||
document.head.appendChild(s);
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
(function () {
|
||
|
||
const GC = "";
|
||
const nf = new Intl.NumberFormat();
|
||
const SING = "visit";
|
||
const PLUR = "visits";
|
||
|
||
function ready(fn) {
|
||
if (document.readyState === "complete" || document.readyState === "interactive") setTimeout(fn, 0);
|
||
else document.addEventListener("DOMContentLoaded", fn);
|
||
}
|
||
|
||
function gcPath() {
|
||
try {
|
||
return window.goatcounter && window.goatcounter.get_data
|
||
? window.goatcounter.get_data().p
|
||
: location.pathname;
|
||
} catch (_) {
|
||
return location.pathname;
|
||
}
|
||
}
|
||
|
||
async function fetchCount(path, period) {
|
||
try {
|
||
const q = new URLSearchParams({ t: String(Date.now()) });
|
||
if (period) q.set("start", period);
|
||
const url = `${GC}/counter/${encodeURIComponent(path)}.json?${q}`;
|
||
const r = await fetch(url, { credentials: "omit" });
|
||
if (r.status === 404) return 0;
|
||
if (!r.ok) throw 0;
|
||
const j = await r.json();
|
||
|
||
const n = Number(String(j.count).replace(/,/g, ""));
|
||
return Number.isFinite(n) ? n : 0;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const labelVisits = (n) =>
|
||
n === null ? "—" : `${nf.format(n)} ${n === 1 ? SING : PLUR}`;
|
||
|
||
ready(async function () {
|
||
const post = document.querySelector(".post-single");
|
||
if (post) {
|
||
const meta = post.querySelector(
|
||
".post-header .post-meta, .post-header .entry-meta, .post-meta"
|
||
);
|
||
const visits = await fetchCount(gcPath());
|
||
if (meta) {
|
||
let span = meta.querySelector(".post-views");
|
||
if (!span) {
|
||
span = document.createElement("span");
|
||
span.className = "post-views";
|
||
meta.appendChild(span);
|
||
}
|
||
span.textContent = labelVisits(visits ?? 0);
|
||
}
|
||
}
|
||
|
||
const cards = document.querySelectorAll("article.post-entry");
|
||
await Promise.all(
|
||
Array.from(cards).map(async (card) => {
|
||
const meta = card.querySelector(".entry-footer, .post-meta");
|
||
const link = card.querySelector("a.entry-link, h2 a, .entry-title a");
|
||
if (!meta || !link) return;
|
||
try {
|
||
const u = new URL(link.getAttribute("href"), location.origin);
|
||
if (u.origin !== location.origin) return;
|
||
let span = meta.querySelector(".post-views");
|
||
if (!span) {
|
||
span = document.createElement("span");
|
||
span.className = "post-views";
|
||
meta.appendChild(span);
|
||
}
|
||
span.textContent = labelVisits((await fetchCount(u.pathname)) ?? 0);
|
||
} catch {}
|
||
})
|
||
);
|
||
|
||
const uvEl = document.getElementById("gc_site_visitors_week");
|
||
if (uvEl) {
|
||
const n = await fetchCount("TOTAL", "week");
|
||
uvEl.textContent = n === null ? "—" : nf.format(n);
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
</head>
|
||
|
||
<body class="" id="top">
|
||
<script>
|
||
if (localStorage.getItem("pref-theme") === "dark") {
|
||
document.body.classList.add('dark');
|
||
} else if (localStorage.getItem("pref-theme") === "light") {
|
||
document.body.classList.remove('dark')
|
||
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||
document.body.classList.add('dark');
|
||
}
|
||
|
||
</script>
|
||
|
||
<header class="header">
|
||
<nav class="nav">
|
||
<div class="logo">
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/" accesskey="h" title="AlipourIm journeys (Alt + H)">AlipourIm journeys</a>
|
||
<div class="logo-switches">
|
||
<button id="theme-toggle" accesskey="t" title="(Alt + T)" aria-label="Toggle theme">
|
||
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
|
||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||
stroke-linejoin="round">
|
||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||
</svg>
|
||
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
|
||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||
stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="5"></circle>
|
||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<ul id="menu">
|
||
<li>
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/" title="Posts">
|
||
<span>Posts</span>
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/phd_journey/" title="PhD_journey">
|
||
<span>PhD_journey</span>
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/about/" title="About">
|
||
<span>About</span>
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</header>
|
||
<main class="main">
|
||
|
||
<article class="post-single">
|
||
<header class="post-header">
|
||
<div class="breadcrumbs"><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">Home</a> » <a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/">Posts</a></div>
|
||
<h1 class="post-title entry-hint-parent">
|
||
INET Logo That Breathes — From CAD to LEDs to a Calm Little Server
|
||
</h1>
|
||
<div class="post-meta"><span title='2025-10-17 00:00:00 +0000 UTC'>October 17, 2025</span> · 17 min · Iman Alipour
|
||
|
||
<span class="post-meta-item">
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/inet_logo/#isso-thread" class="isso-comments-link">Comments</a>
|
||
</span>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</div>
|
||
</header>
|
||
<figure class="entry-cover">
|
||
<img loading="eager" src="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/images/inet/inet_animation_collage.jpg" alt="Six looks of the INET LED logo">
|
||
<figcaption>Design → print → solder → code → glow.</figcaption>
|
||
</figure>
|
||
<div class="post-content"><blockquote>
|
||
<p>I didn’t add a warning sign to the room. I taught the <strong>logo</strong> to breathe. ;-D</p></blockquote>
|
||
<p>The Rotunde is a good room for bold ideas and yummy coffee. The door sighs shut and the outside world gives up on us. The only thing it’s not good at is <strong>ventilating</strong> itself. Forty minutes into a group meeting, or a sressful defence, and we all feel like sleeping and running back to our offices!</p>
|
||
<p>So I crafted the INET logo and gave it a 9-5 job: be a <strong>polite barometer</strong>. If the air is fresh, it glows cool and calm. If it’s getting stale, it warms up. No blinking warnings, no sound effects — just mood.</p>
|
||
<p>This post is the story of how I designed the channel and diffuser, printed the parts, cut, layed out, and soldered the snippets of LED strips, wrote the software, and wrapped it all in a systemd service so it wakes up with the Pi. Feel free to grab a cup of coffee and enjoy the post! I’ll try to include as much detail as I can.</p>
|
||
<hr>
|
||
<h2 id="1-sketch--cad--print">1) Sketch → CAD → Print<a hidden class="anchor" aria-hidden="true" href="#1-sketch--cad--print">#</a></h2>
|
||
<p><img alt="Fusion design + logotype" loading="lazy" src="/images/inet/inet_logo_fusion_merged.png"></p>
|
||
<p>I started the way all sensible hardware projects start: with <strong>overconfidence</strong> and a sketch. The INET logotype looks simple from the front but it’s a small maze inside. I modeled pockets for each letter in Fusion: a tall <strong>I</strong>, the curving <strong>N</strong>, three stacked bars for <strong>E</strong>, and the long arm of <strong>T</strong>. Each pocket got:</p>
|
||
<ul>
|
||
<li><strong>Mounting bosses</strong> for the front diffusers (M2 screws),</li>
|
||
<li><strong>Cable holes</strong> between chambers (rounded to avoid cutting silicone wire), and</li>
|
||
<li>A small <strong>wiring bay</strong> for the controller and power.</li>
|
||
</ul>
|
||
<p>I printed the channel parts in matte black PLA for heat resilience and the diffusers in white PLA at <strong>1-2 mm</strong> thick to hide hotspots without wasting brightness. That thickness ended up perfect; you can still see motion, but the individual LED package disappears.
|
||
The print process took around 24h, including the misprints, and the in-between prints for adjusting the sizes and calibrating everything. Each letter was either printed in multiple parts, or individually so that the logo becomes as large as possible. I used around 1Kg of fillament to print the whole thing.</p>
|
||
<p>The design process took around 18 iterations, with initial iteration being simple letters in italic form, and the later ones being more similar to the original INET logo of our group.
|
||
I had some experience with CAD, and had designed some simple things to address my everyday problems, but this was a more serious one. Overall it took me a few days to design the logo and print it part by part. The design is not the most artistic, but as authentic as I could get it.</p>
|
||
<hr>
|
||
<h2 id="2-the-look-i-wanted">2) The Look I Wanted<a hidden class="anchor" aria-hidden="true" href="#2-the-look-i-wanted">#</a></h2>
|
||
<p><img alt="Animations collage" loading="lazy" src="/images/inet/inet_animation_collage.jpg"></p>
|
||
<p>I wasn’t building a stage light. The goal was <strong>calm</strong>: slow waves, breathing brightness, gentle comets. Everything reads through those letter chambers like a tiny light sculpture. The cyan frame in the collage is the <strong>CO₂ monitor</strong> in a “fresh air” state (more on the color language below). There are many many other animations that I stole from different git repositories, but the most used one is the Co2 monitor with breathing animation. I did want to include more animations other than breathing, but that will be a future me problem with creativity left to spare; right now my creativity well is as dry as the paint on my wall! For now, I’m happy with it and want to let it be there doing it’s job.</p>
|
||
<hr>
|
||
<h2 id="3-the-tidy-mess-behind-the-panel">3) The Tidy Mess Behind the Panel<a hidden class="anchor" aria-hidden="true" href="#3-the-tidy-mess-behind-the-panel">#</a></h2>
|
||
<p><img alt="Hardware collage" loading="lazy" src="/images/inet/inet_hardware_collage.jpg"></p>
|
||
<p>Inside the box there’s a Raspberry Pi zero 2 W, a short run of WS2812B LEDs (78 pixels total), a 5 V 3–4 A supply, jumpers that mind their manners, and two little hardware choices that pay rent every day:</p>
|
||
<ul>
|
||
<li>A <strong>330–470 Ω</strong> series resistor on the <em>data</em> line right at the first pixel. It damps ringing and prevents weird flickers.</li>
|
||
<li>A <strong>1000 µF</strong> electrolytic across <strong>5V/GND</strong> at the strip start. It handles inrush so the first LED doesn’t faint at boot.</li>
|
||
</ul>
|
||
<p>I route the strip <strong>DIN → DOUT</strong> through the chambers and use short silicone jumpers at the bends. Every joint gets heat‑shrink and a tiny dab of hot glue as strain relief. I continuity‑check <strong>before</strong> power and bring brightness up slowly on a bench supply.</p>
|
||
<p>The soldering was a big mess. I had never done hardware debugging before. My soldering skills were definitly challenged for this project, as the connections kept breaking when I moved the logo. I did learn how to perform better soldering, but it was already too late. I had to tin the wires after adding flux, and let the soldering wire go up the wire to get stronger connections and leave no naked coppers. A note to remember for next soldering journeys. It took me around two whole days to solder everything and debug it. At some point there was a faulty LED in the middle that caused shorting, and I found and cut it out. This was the most annoying part of the debugging as I had to go through the LEDs, measuring voltage difference to see what is causing the problem. I honestly gave up somewhere in here and thought I won’t be able to push through the marathon, but here I am, done with the logo, happy as a four young old licking their ice cream! xD</p>
|
||
<hr>
|
||
<h2 id="4-a-web-panel-that-stays-out-of-the-way">4) A Web Panel that Stays Out of the Way<a hidden class="anchor" aria-hidden="true" href="#4-a-web-panel-that-stays-out-of-the-way">#</a></h2>
|
||
<p><img alt="UI collage" loading="lazy" src="/images/inet/inet_ui_collage.jpg"></p>
|
||
<p>The web UI is quiet on purpose: a single navbar, a <code>/control</code> page with big same‑size buttons, a <code>/schedule</code> table, a drag‑and‑drop <code>/calendar</code>, a <code>/status</code> page that updates once a second, and <code>/history</code> charts for CO₂/temperature/humidity with white backgrounds so they’re legible on a projector. Temprature values are not about room, but the box, as I could not mount the sensor outside of the box easily. I just let it sit inside the box. The panel was created with the help of trusty vibe-coding (!). I used chat GPT to create a template, and expanded it for the purpose. Flask made things very easy.</p>
|
||
<p>There’s also a <strong>Safe Mode</strong> switch that hides flashier patterns. Meeting rooms are for thinking, not strobe tests, and you never know who might have photosensitive epilepsy, and it’s not funny for anyone to fidn this out when looking at your creation. So… yea, I took precautions not to see people getting seizures looking at my creation. :-)</p>
|
||
<hr>
|
||
<h2 id="5-the-color-language-for-air">5) The Color Language for Air<a hidden class="anchor" aria-hidden="true" href="#5-the-color-language-for-air">#</a></h2>
|
||
<p>The <code>co2_monitor</code> animation maps CO₂ ppm → color and adds slow motion so it doesn’t feel like a stoplight:</p>
|
||
<ul>
|
||
<li><strong>< 500 ppm</strong>: Cyan — outdoor‑like fresh air.</li>
|
||
<li><strong>500–799</strong>: Green — good.</li>
|
||
<li><strong>800–1199</strong>: Yellow — getting stale.</li>
|
||
<li><strong>1200–1499</strong>: Orange — poor; you’ll feel it.</li>
|
||
<li><strong>1500–1999</strong>: Red — ventilate now.</li>
|
||
<li><strong>≥ 2000</strong>: Purple — the logo is politely yelling.</li>
|
||
</ul>
|
||
<p>I blend the readings with an <strong>exponential moving average</strong> and use <strong>hysteresis</strong> around thresholds so it doesn’t ping‑pong colors when the room hovers at 800 ppm. Then I ease the current hue toward the target color and apply a very slow <strong>breathing brightness</strong>. The result feels alive but never flashy.</p>
|
||
<p>By default, the lights turn off from 20 to 6, then from 6 to 9, and 5 to 8 the standby red pulse is shown. From 9-5 on workdays the logo does it’s daily job of Co2 monitoring. Well, technically it does that all the time, but during those hours it shows the Co2 level by it’s color, the remaining time it just keeps a record of the Co2, temprature and humidity of room in the database.</p>
|
||
<hr>
|
||
<h2 id="6-the-code--a-guided-tour-no-giant-blob-promise">6) The Code — a guided tour (no giant blob, promise)<a hidden class="anchor" aria-hidden="true" href="#6-the-code--a-guided-tour-no-giant-blob-promise">#</a></h2>
|
||
<p>This whole project runs from a single Python file. Think of it like a tiny orchestra:<br>
|
||
one thread conducts the <strong>LEDs</strong>, one listens politely to the <strong>CO₂ sensor</strong>, one keeps time as the <strong>scheduler</strong>, and a small <strong>web server</strong> hands you the baton when you want to improvise.</p>
|
||
<p>Below is what each section does, with just enough code to be useful (and not enough to make your eyes cross).</p>
|
||
<hr>
|
||
<h3 id="61-configuration-the-knobs">6.1 Configuration (the knobs)<a hidden class="anchor" aria-hidden="true" href="#61-configuration-the-knobs">#</a></h3>
|
||
<p>Let’s define where the LEDs live, how bright they can get, and where to save tiny bits of state (schedule and sensor DB). All of it can be overridden with environment variables so you don’t edit code to change pin numbers.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>LED_COUNT<span style="color:#f92672">=</span><span style="color:#ae81ff">78</span>, LED_PIN<span style="color:#f92672">=</span><span style="color:#ae81ff">18</span>, LED_BRIGHT<span style="color:#f92672">=</span><span style="color:#ae81ff">255</span>
|
||
</span></span><span style="display:flex;"><span>SCHEDULE_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">"schedule.json"</span>
|
||
</span></span><span style="display:flex;"><span>DB_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">"sensor.db"</span>
|
||
</span></span><span style="display:flex;"><span>SAFE_MODE<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span> <span style="color:#75715e"># hide flashy animations by default</span>
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> simple, explicit defaults → less “why is it dark” debugging.</p>
|
||
<hr>
|
||
<h3 id="62-strip-setup--frame-diff-no-flicker-magic">6.2 Strip setup + frame-diff (no flicker magic)<a hidden class="anchor" aria-hidden="true" href="#62-strip-setup--frame-diff-no-flicker-magic">#</a></h3>
|
||
<p>Now initialize <code>rpi_ws281x.PixelStrip</code> and put a <strong>shadow frame buffer</strong> in front of it.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>frame <span style="color:#f92672">=</span> [(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">0</span>)] <span style="color:#f92672">*</span> LED_COUNT
|
||
</span></span><span style="display:flex;"><span>_dirty <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">_set_pixel</span>(i, r, g, b):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># only touch hardware if the value actually changed</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> frame[i] <span style="color:#f92672">!=</span> (r,g,b):
|
||
</span></span><span style="display:flex;"><span> strip<span style="color:#f92672">.</span>setPixelColorRGB(i, r, g, b)
|
||
</span></span><span style="display:flex;"><span> frame[i] <span style="color:#f92672">=</span> (r,g,b)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">global</span> _dirty; _dirty <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">flush</span>():
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> _dirty: strip<span style="color:#f92672">.</span>show(); _dirty<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> LEDs are zen monks—disturb them only when you must. The frame-diff prevents those mysterious “one frame flash” moments that happen when you call <code>show()</code> with identical data.</p>
|
||
<hr>
|
||
<h3 id="63-a-tiny-color-wheel-helper">6.3 A tiny color wheel helper<a hidden class="anchor" aria-hidden="true" href="#63-a-tiny-color-wheel-helper">#</a></h3>
|
||
<p>Classic rainbow helper used by a few animations:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">wheel</span>(pos):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># 0..255 → (r,g,b)</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">...</span>
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> I’ll use it again and again; it keeps color math out of the animations.</p>
|
||
<hr>
|
||
<h3 id="64-state--orchestration">6.4 State & orchestration<a hidden class="anchor" aria-hidden="true" href="#64-state--orchestration">#</a></h3>
|
||
<p>Let’s hold a registry of animations, a stop flag, and the currently playing thread.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>animations <span style="color:#f92672">=</span> {}
|
||
</span></span><span style="display:flex;"><span>stop_event <span style="color:#f92672">=</span> threading<span style="color:#f92672">.</span>Event()
|
||
</span></span><span style="display:flex;"><span>current_thread <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">register</span>(name, safe<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">wrap</span>(fn):
|
||
</span></span><span style="display:flex;"><span> animations[name] <span style="color:#f92672">=</span> {<span style="color:#e6db74">"fn"</span>: fn, <span style="color:#e6db74">"safe"</span>: safe}
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> fn
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> wrap
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_animation</span>(fn):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># cooperative hand-off: stop current, start next</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> current_thread <span style="color:#f92672">and</span> current_thread<span style="color:#f92672">.</span>is_alive():
|
||
</span></span><span style="display:flex;"><span> stop_event<span style="color:#f92672">.</span>set(); current_thread<span style="color:#f92672">.</span>join()
|
||
</span></span><span style="display:flex;"><span> stop_event<span style="color:#f92672">.</span>clear()
|
||
</span></span><span style="display:flex;"><span> t <span style="color:#f92672">=</span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>fn, name<span style="color:#f92672">=</span>fn<span style="color:#f92672">.</span>__name__, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
|
||
</span></span><span style="display:flex;"><span> t<span style="color:#f92672">.</span>start(); globals()[<span style="color:#e6db74">"current_thread"</span>] <span style="color:#f92672">=</span> t
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> adding a new animation is a one-liner <code>@register("name")</code>. Clean exits prevent torn frames and half-drawn comets.</p>
|
||
<hr>
|
||
<h3 id="65-animations-the-mood">6.5 Animations (the mood)<a hidden class="anchor" aria-hidden="true" href="#65-animations-the-mood">#</a></h3>
|
||
<p>Each animation is a loop that checks <code>stop_event.is_set()</code> and draws frames with <code>_set_pixel(...)</code> + <code>flush()</code>.</p>
|
||
<ul>
|
||
<li><strong><code>redpulse</code></strong> — calm breathing in red; great default.</li>
|
||
<li><strong><code>colorwave</code></strong> — slow rainbow that reads clearly behind diffusers.</li>
|
||
<li><strong><code>comet</code></strong> — a single head with a fading tail orbiting the logo.</li>
|
||
</ul>
|
||
<p>They all end with <code>finally: clear_strip()</code> so the panel doesn’t freeze on the last pose if you stop mid-frame.</p>
|
||
<p><em>Why it’s like this:</em> cooperative loops + frame-diff = smooth, interruption-safe effects.</p>
|
||
<hr>
|
||
<h3 id="66-co-color-language-with-smoothing--hysteresis">6.6 CO₂ color language (with smoothing + hysteresis)<a hidden class="anchor" aria-hidden="true" href="#66-co-color-language-with-smoothing--hysteresis">#</a></h3>
|
||
<p>The star of the show. Now map ppm to color <strong>bands</strong> and keep transitions human-pleasant.</p>
|
||
<ul>
|
||
<li><code>< 500</code>: <strong>Cyan</strong> (fresh)</li>
|
||
<li><code>500–799</code>: <strong>Green</strong></li>
|
||
<li><code>800–1199</code>: <strong>Yellow</strong></li>
|
||
<li><code>1200–1499</code>: <strong>Orange</strong></li>
|
||
<li><code>1500–1999</code>: <strong>Red</strong></li>
|
||
<li><code>≥ 2000</code>: <strong>Purple</strong></li>
|
||
</ul>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>BANDS<span style="color:#f92672">=</span>[(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">500</span>,(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">214</span>,<span style="color:#ae81ff">255</span>)), (<span style="color:#ae81ff">500</span>,<span style="color:#ae81ff">800</span>,(<span style="color:#ae81ff">17</span>,<span style="color:#ae81ff">204</span>,<span style="color:#ae81ff">85</span>)), <span style="color:#f92672">...</span> (<span style="color:#ae81ff">2000</span>,<span style="color:#960050;background-color:#1e0010">∞</span>,(<span style="color:#ae81ff">123</span>,<span style="color:#ae81ff">44</span>,<span style="color:#ae81ff">191</span>))]
|
||
</span></span><span style="display:flex;"><span>EMA_ALPHA<span style="color:#f92672">=</span><span style="color:#ae81ff">0.15</span>; HYST<span style="color:#f92672">=</span><span style="color:#ae81ff">35</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Every tick:</span>
|
||
</span></span><span style="display:flex;"><span>ema <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">-</span>EMA_ALPHA)<span style="color:#f92672">*</span>ema <span style="color:#f92672">+</span> EMA_ALPHA<span style="color:#f92672">*</span>co2
|
||
</span></span><span style="display:flex;"><span>band <span style="color:#f92672">=</span> hysteresis_aware_band(ema, last_band, HYST)
|
||
</span></span><span style="display:flex;"><span>target <span style="color:#f92672">=</span> BANDS[band]<span style="color:#f92672">.</span>color
|
||
</span></span><span style="display:flex;"><span>current <span style="color:#f92672">=</span> lerp(current, target, <span style="color:#ae81ff">0.10</span>) <span style="color:#75715e"># gentle hue easing</span>
|
||
</span></span><span style="display:flex;"><span>level <span style="color:#f92672">=</span> breathe(<span style="color:#ae81ff">0.25</span> Hz, low<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>, high<span style="color:#f92672">=</span><span style="color:#ae81ff">220</span>)
|
||
</span></span><span style="display:flex;"><span>draw all pixels <span style="color:#f92672">=</span> current <span style="color:#f92672">*</span> level
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> EMA + hysteresis prevents “800↔801 disco.” The slow breathing keeps the panel feeling alive, not like a traffic light.</p>
|
||
<hr>
|
||
<h3 id="67-optional-scd41-sensor-thread-the-polite-listener">6.7 Optional SCD41 sensor thread (the polite listener)<a hidden class="anchor" aria-hidden="true" href="#67-optional-scd41-sensor-thread-the-polite-listener">#</a></h3>
|
||
<p>If the SCD41 is present, a <strong>dedicated thread</strong> owns I²C and updates a global <code>co2_data</code> dict every few seconds. It also logs to a tiny SQLite database.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">_start_sensor</span>():
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">try</span>:
|
||
</span></span><span style="display:flex;"><span> scd4x <span style="color:#f92672">=</span> adafruit_scd4x<span style="color:#f92672">.</span>SCD4X(i2c); scd4x<span style="color:#f92672">.</span>start_periodic_measurement()
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">except</span>: <span style="color:#66d9ef">return</span> <span style="color:#75715e"># sensor optional</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">loop</span>():
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
|
||
</span></span><span style="display:flex;"><span> time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">5</span>)
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> scd4x<span style="color:#f92672">.</span>data_ready:
|
||
</span></span><span style="display:flex;"><span> co2_data<span style="color:#f92672">.</span>update({<span style="color:#f92672">...</span>})
|
||
</span></span><span style="display:flex;"><span> db<span style="color:#f92672">.</span>insert(timestamp, co2, temperature, humidity)
|
||
</span></span><span style="display:flex;"><span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>loop, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)<span style="color:#f92672">.</span>start()
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> one thread owns the bus → no read contention. The controller keeps working even without a sensor.</p>
|
||
<hr>
|
||
<h3 id="68-scheduler--90-minute-override-humans-win-then-reset">6.8 Scheduler + 90-minute override (humans win, then reset)<a hidden class="anchor" aria-hidden="true" href="#68-scheduler--90-minute-override-humans-win-then-reset">#</a></h3>
|
||
<p>A background thread asks, every few seconds, <strong>which</strong> mode should be running:</p>
|
||
<ol>
|
||
<li>If the user chose something recently (override), respect it for <code>TTL = 90 min</code> (or the duration set in the UI).</li>
|
||
<li>Otherwise, apply the <strong>default schedule</strong> (Wednesday 14–17 → <code>slow</code>, else <code>redpulse</code>).</li>
|
||
<li>Save/load the schedule atomically to <code>schedule.json</code>.</li>
|
||
</ol>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">scheduler_pick</span>():
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> now <span style="color:#f92672"><</span> override_until: <span style="color:#66d9ef">return</span> override_mode
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> load_schedule():
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> matches(now, row): <span style="color:#66d9ef">return</span> row[<span style="color:#e6db74">"mode"</span>]
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">"redpulse"</span>
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> you can play DJ for a meeting, and the room quietly returns to its routine afterward.</p>
|
||
<hr>
|
||
<h3 id="69-the-web-panel-tiny-flask-big-buttons">6.9 The web panel (tiny Flask, big buttons)<a hidden class="anchor" aria-hidden="true" href="#69-the-web-panel-tiny-flask-big-buttons">#</a></h3>
|
||
<p>Three pages, no fuss:</p>
|
||
<ul>
|
||
<li><code>/status</code> — live JSON (<code>/status_data</code>) every second → current mode + CO₂/Temp/Humidity.</li>
|
||
<li><code>/control</code> — grid of same-size buttons (respects <strong>Safe Mode</strong>), optional <strong>duration</strong> (0–180 min) with validation.</li>
|
||
<li><code>/schedule</code> — simple table editor; <strong>Add Row</strong> and <strong>Save</strong>; writes atomically.</li>
|
||
</ul>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">"/set"</span>)
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">set_mode</span>():
|
||
</span></span><span style="display:flex;"><span> mode <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>form[<span style="color:#e6db74">"mode"</span>]
|
||
</span></span><span style="display:flex;"><span> minutes <span style="color:#f92672">=</span> clamp( int(form[<span style="color:#e6db74">"duration"</span>]), <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">180</span> )
|
||
</span></span><span style="display:flex;"><span> set_override(mode, minutes)
|
||
</span></span><span style="display:flex;"><span> run_animation(animations[mode][<span style="color:#e6db74">"fn"</span>])
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> redirect(<span style="color:#e6db74">"/control"</span>)
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> minimal routes are easier to maintain and don’t compete with the art piece.</p>
|
||
<hr>
|
||
<h3 id="610-boot-choreography">6.10 Boot choreography<a hidden class="anchor" aria-hidden="true" href="#610-boot-choreography">#</a></h3>
|
||
<p>At startup I:</p>
|
||
<ol>
|
||
<li>Try the <strong>sensor thread</strong> (if hardware is there).</li>
|
||
<li>Start the <strong>scheduler thread</strong>.</li>
|
||
<li>Immediately play <strong>redpulse</strong> (so there’s a friendly glow right away).</li>
|
||
<li>Start Flask; on shutdown I <strong>clear the strip</strong>.</li>
|
||
</ol>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
|
||
</span></span><span style="display:flex;"><span> _start_sensor()
|
||
</span></span><span style="display:flex;"><span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>scheduler_loop, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)<span style="color:#f92672">.</span>start()
|
||
</span></span><span style="display:flex;"><span> run_animation(redpulse)
|
||
</span></span><span style="display:flex;"><span> app<span style="color:#f92672">.</span>run(host<span style="color:#f92672">=</span><span style="color:#e6db74">"0.0.0.0"</span>, port<span style="color:#f92672">=</span><span style="color:#ae81ff">5000</span>)
|
||
</span></span></code></pre></div><p><em>Why it’s like this:</em> the room sees something alive instantly; the scheduler will swap in the “real” choice within a few seconds.</p>
|
||
<hr>
|
||
<h3 id="tldr-but-keep-the-vibes">TL;DR (but keep the vibes)<a hidden class="anchor" aria-hidden="true" href="#tldr-but-keep-the-vibes">#</a></h3>
|
||
<ul>
|
||
<li><strong>Frame-diff</strong> = no flashes.</li>
|
||
<li><strong>EMA + hysteresis</strong> = smooth, truthful color.</li>
|
||
<li><strong>Breathing</strong> = presence, not signage.</li>
|
||
<li><strong>Threads per thing</strong> (LEDs / sensor / scheduler) = no fights.</li>
|
||
<li><strong>Atomic files</strong> = no corrupted schedules.</li>
|
||
<li><strong>Safe Mode</strong> by default = people > pixels.</li>
|
||
</ul>
|
||
<p>That’s it — the code behaves like a considerate colleague: dependable, quiet, and occasionally very pretty.</p>
|
||
<h3 id="why-this-shape-of-code">Why this shape of code?<a hidden class="anchor" aria-hidden="true" href="#why-this-shape-of-code">#</a></h3>
|
||
<ul>
|
||
<li><strong>One thread per hardware thing.</strong> The sensor thread owns I²C. The animation thread owns LEDs. The scheduler just decides <em>what</em> to run and when. No bus fights.</li>
|
||
<li><strong>Frame diff.</strong> I only call <code>strip.show()</code> when a pixel actually changes. That’s why it doesn’t randomly flash during web requests.</li>
|
||
<li><strong>Atomic schedule saves.</strong> The code writes to a temporary file and replaces the old one so a mid‑save power cut can’t corrupt the schedule.</li>
|
||
</ul>
|
||
<blockquote>
|
||
<p>No, you don’t <em>need</em> the sensor. If it’s not connected, the app still runs; <code>co2_monitor</code> just sits at the default value. Well, technically it shows NaN as the numbers! But it is fun to have a sensor, don’t you agree?</p></blockquote>
|
||
<hr>
|
||
<h2 id="7-sensor-database--what-it-stores-where-it-lives-and-how-far-it-goes">7) Sensor Database — what it stores, where it lives, and how far it goes<a hidden class="anchor" aria-hidden="true" href="#7-sensor-database--what-it-stores-where-it-lives-and-how-far-it-goes">#</a></h2>
|
||
<p>This project logs room conditions to a <strong>tiny SQLite database</strong> so you can graph trends, spot stuffy meetings, and keep a record without running a separate server.</p>
|
||
<h3 id="whats-stored">What’s stored<a hidden class="anchor" aria-hidden="true" href="#whats-stored">#</a></h3>
|
||
<p>A single table called <code>readings</code>:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">IF</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">EXISTS</span> readings (
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">timestamp</span> TEXT <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span>, <span style="color:#75715e">-- "YYYY-MM-DD HH:MM:SS"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> co2 INTEGER, <span style="color:#75715e">-- ppm
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> temperature REAL, <span style="color:#75715e">-- °C
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> humidity REAL <span style="color:#75715e">-- %
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>);
|
||
</span></span></code></pre></div><ul>
|
||
<li>One row per sample (typically every <strong>5–60 seconds</strong>; slow it down to reduce writes).</li>
|
||
<li><code>timestamp</code> is the primary key → easy “latest” queries and natural time ordering.</li>
|
||
</ul>
|
||
<h3 id="where-the-file-lives">Where the file lives<a hidden class="anchor" aria-hidden="true" href="#where-the-file-lives">#</a></h3>
|
||
<ul>
|
||
<li>Path is configurable via env var <code>DB_PATH</code> (see your systemd unit).</li>
|
||
<li>Default: <code>sensor.db</code> in the app’s working directory (e.g. <code>/home/pi/inet-led/sensor.db</code>).</li>
|
||
</ul>
|
||
<p>Check size:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ls -lh /home/pi/inet-led/sensor.db
|
||
</span></span></code></pre></div><h3 id="how-writes-happen-and-why-its-safe">How writes happen (and why it’s safe)<a hidden class="anchor" aria-hidden="true" href="#how-writes-happen-and-why-its-safe">#</a></h3>
|
||
<ul>
|
||
<li>The <strong>sensor thread</strong> owns I²C and inserts a row only when the sensor reports <code>data_ready</code>.</li>
|
||
<li>Inserts are short and journaling protects against power loss.</li>
|
||
<li>For friendlier read/write concurrency, enable WAL once at startup:</li>
|
||
</ul>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span>PRAGMA journal_mode <span style="color:#f92672">=</span> WAL;
|
||
</span></span><span style="display:flex;"><span>PRAGMA synchronous <span style="color:#f92672">=</span> NORMAL;
|
||
</span></span></code></pre></div><h3 id="typical-size--retention">Typical size & retention<a hidden class="anchor" aria-hidden="true" href="#typical-size--retention">#</a></h3>
|
||
<p>Back-of-envelope:</p>
|
||
<ul>
|
||
<li>~100–200 bytes per row (including overhead).</li>
|
||
<li>1 sample/minute → ~1,440 rows/day → <strong>~150–300 KB/day</strong> → <strong>55–110 MB/year</strong>.</li>
|
||
<li>If you log every 5 seconds, multiply by ~12 (consider slower logging or downsampling).</li>
|
||
</ul>
|
||
<p>Prune old data (keep last 90 days):</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">FROM</span> readings
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672"><</span> date(<span style="color:#e6db74">'now'</span>,<span style="color:#e6db74">'-90 day'</span>);
|
||
</span></span></code></pre></div><p>(Optionally run <code>VACUUM;</code> after large deletes to reclaim file space.)</p>
|
||
<h3 id="useful-queries">Useful queries<a hidden class="anchor" aria-hidden="true" href="#useful-queries">#</a></h3>
|
||
<p><strong>Latest reading:</strong></p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">FROM</span> readings <span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#66d9ef">DESC</span> <span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">1</span>;
|
||
</span></span></code></pre></div><p><strong>Range for charts (last 7 days):</strong></p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">FROM</span> readings
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">>=</span> datetime(<span style="color:#e6db74">'now'</span>,<span style="color:#e6db74">'-7 day'</span>)
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">timestamp</span>;
|
||
</span></span></code></pre></div><p><strong>Downsample to hourly averages (30 days):</strong></p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> strftime(<span style="color:#e6db74">'%Y-%m-%d %H:00:00'</span>, <span style="color:#66d9ef">timestamp</span>) <span style="color:#66d9ef">AS</span> hour,
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AVG</span>(co2) <span style="color:#66d9ef">AS</span> co2, <span style="color:#66d9ef">AVG</span>(temperature) <span style="color:#66d9ef">AS</span> t, <span style="color:#66d9ef">AVG</span>(humidity) <span style="color:#66d9ef">AS</span> h
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> readings
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">>=</span> datetime(<span style="color:#e6db74">'now'</span>,<span style="color:#e6db74">'-30 day'</span>)
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GROUP</span> <span style="color:#66d9ef">BY</span> hour
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> hour;
|
||
</span></span></code></pre></div><p><strong>Export to CSV (example via sqlite3 CLI):</strong></p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sqlite3 /home/pi/inet-led/sensor.db <span style="color:#e6db74"><<'SQL'
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.headers on
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.mode csv
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.output /home/pi/inet-led/export_readings.csv
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT * FROM readings ORDER BY timestamp;
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.output stdout
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SQL</span>
|
||
</span></span></code></pre></div><h3 id="sd-card-friendliness">SD card friendliness<a hidden class="anchor" aria-hidden="true" href="#sd-card-friendliness">#</a></h3>
|
||
<ul>
|
||
<li>Choose a sensible interval (e.g., <strong>30–60 s</strong>).</li>
|
||
<li>Optionally buffer in memory and write every N samples.</li>
|
||
<li>Prefer WAL mode; periodically prune & vacuum.</li>
|
||
<li>If you care about card wear, place the DB on USB/SSD.</li>
|
||
</ul>
|
||
<h3 id="backups--restore">Backups & restore<a hidden class="anchor" aria-hidden="true" href="#backups--restore">#</a></h3>
|
||
<p><strong>Nightly backup (simple):</strong></p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sqlite3 /home/pi/inet-led/sensor.db <span style="color:#e6db74">".backup '/home/pi/backup/sensor-</span><span style="color:#66d9ef">$(</span>date +%F<span style="color:#66d9ef">)</span><span style="color:#e6db74">.db'"</span>
|
||
</span></span></code></pre></div><p><strong>Restore:</strong></p>
|
||
<ol>
|
||
<li><code>sudo systemctl stop inet-led</code></li>
|
||
<li>Copy backup file back to <code>/home/pi/inet-led/sensor.db</code></li>
|
||
<li><code>sudo systemctl start inet-led</code></li>
|
||
</ol>
|
||
<h3 id="security--permissions">Security & permissions<a hidden class="anchor" aria-hidden="true" href="#security--permissions">#</a></h3>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chown pi:pi /home/pi/inet-led/sensor.db
|
||
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">600</span> /home/pi/inet-led/sensor.db
|
||
</span></span></code></pre></div><p>Keep the app directory non-world-readable.</p>
|
||
<h3 id="is-sqlite-the-right-choice">Is SQLite the right choice?<a hidden class="anchor" aria-hidden="true" href="#is-sqlite-the-right-choice">#</a></h3>
|
||
<p><strong>Yes, for a single Pi</strong> logging every few seconds and rendering local charts:</p>
|
||
<ul>
|
||
<li>✅ Zero admin, a single file, reliable journaling, great performance at this scale.</li>
|
||
<li>⚠️ One writer at a time (I only have the sensor thread writing, so it’s fine).</li>
|
||
<li>⬆️ If you later need multi-device ingestion, alerts, or >10s of millions of rows, consider a time-series DB (TimescaleDB/InfluxDB/VictoriaMetrics) and migrate using CSV exports.</li>
|
||
</ul>
|
||
<h3 id="tldr">TL;DR<a hidden class="anchor" aria-hidden="true" href="#tldr">#</a></h3>
|
||
<p>SQLite is perfect here: <strong>simple, robust, easy to back up</strong>. Start with it, and only upgrade when your ambitions outgrow a single Raspberry Pi.</p>
|
||
<hr>
|
||
<h2 id="8-run-at-boot-systemd">8) Run at Boot (systemd)<a hidden class="anchor" aria-hidden="true" href="#8-run-at-boot-systemd">#</a></h2>
|
||
<p>I wrote this simple systemd to <code>/etc/systemd/system/inet-led.service</code> so that it runs the script when RPi boots up:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">INET LED Panel</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network.target</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/bin/python3 /home/pi/inet-led/inet_led_panel.py</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WorkingDirectory</span><span style="color:#f92672">=</span><span style="color:#e6db74">/home/pi/inet-led</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Restart</span><span style="color:#f92672">=</span><span style="color:#e6db74">always</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">User</span><span style="color:#f92672">=</span><span style="color:#e6db74">pi</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">LED_COUNT=78</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">SCHEDULE_FILE=/home/pi/inet-led/schedule.json</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">DB_PATH=/home/pi/inet-led/sensor.db</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span>
|
||
</span></span></code></pre></div><p>Then you can run:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl daemon-reload
|
||
</span></span><span style="display:flex;"><span>sudo systemctl enable --now inet-led
|
||
</span></span><span style="display:flex;"><span>journalctl -u inet-led -f
|
||
</span></span></code></pre></div><p>to control the systemd service.</p>
|
||
<hr>
|
||
<h2 id="9-soldering-notes-a-love-letter-to-hot-glue">9) Soldering Notes (a love letter to hot glue)<a hidden class="anchor" aria-hidden="true" href="#9-soldering-notes-a-love-letter-to-hot-glue">#</a></h2>
|
||
<ul>
|
||
<li><strong>Tin first, solder second.</strong> Tiny pads like tiny puddles. Fast in/out, no lifted pads.</li>
|
||
<li><strong>Stagger joints</strong> so nothing stacks under the diffuser.</li>
|
||
<li><strong>Heat‑shrink + a dot of hot glue</strong> = happier future you.</li>
|
||
<li><strong>Continuity before power.</strong> Multimeter first, electrons last.</li>
|
||
<li>If a pad <em>does</em> lift: magnet wire to a trace, UV mask to seal. Ugly heroics, but it works.</li>
|
||
</ul>
|
||
<p>Besides the soldering part, I also used m2 screws to fix the diffusers. Initially magnets were suggested, but I felt like figuring that out might take me long, and screws just feel more maintainable… So… yea.</p>
|
||
<hr>
|
||
<h2 id="10-why-these-design-choices">10) Why these design choices?<a hidden class="anchor" aria-hidden="true" href="#10-why-these-design-choices">#</a></h2>
|
||
<ul>
|
||
<li><strong>Calm motion, not flashy.</strong> Meeting rooms breed fatigue; the light should be a helper, not a distraction.</li>
|
||
<li><strong>Cyan→Purple language.</strong> Easy to learn, visible at a glance, meaningful without numbers.</li>
|
||
<li><strong>White charts, dark UI.</strong> Data should be legible on a projector; buttons shouldn’t shout.</li>
|
||
<li><strong>Safe Mode default.</strong> People first. Demos are opt‑in.</li>
|
||
<li><strong>PLA body, PLA diffuser.</strong> Fast and easy rapid prototyping, easy and fast printing.</li>
|
||
</ul>
|
||
<hr>
|
||
<h2 id="11-what-i-learned">11) What I learned<a hidden class="anchor" aria-hidden="true" href="#11-what-i-learned">#</a></h2>
|
||
<ul>
|
||
<li>A logo is a better messenger than a dashboard.</li>
|
||
<li>Everyone knows what <strong>orange</strong> means without a legend.</li>
|
||
<li>If you show the room a mirror, it corrects itself. Someone will open the door long before you have to ask, and if someone notices the orange/red color of the logo, they would hint the end of the gathering!</li>
|
||
<li>Sometimes debugging is not fun at all, and you want to bang your head to the wall, but try not to! Life is short. ^^</li>
|
||
</ul>
|
||
<hr>
|
||
<h2 id="12-final-notes">12) Final notes<a hidden class="anchor" aria-hidden="true" href="#12-final-notes">#</a></h2>
|
||
<p>I did this project as a fun distraction and “not work” as my advisor tells me to do all the time. My advisor provided the LED strip, RPi zero 2 W, and the voltage divider. I got the sensor, designed and coded everything else, and had a lot of fun doing so. I’m so so thankful of my advisor for this cool idea, and all the support. It’s not the first time I’ve been gifted such toys, and as I have recieved other things after that, I know it’s not the last.</p>
|
||
<p>There is a bug I never was able to figure out, sometimes when I stop the script, I get segmentation fault. I know it is not directly my code with extensive testing, and that the problem is with the library, but I never understood why it happens. Let’s hope that doesn’t turn into a backdoor into my logo. * Shakingly crosses fingers and sighs in distress!*</p>
|
||
<p>The whole project took around 8 days, 4 of which were part of a long weekend. I did do some “not work” as Tobias always tells me to do, and honestly it was fun and refreshing! I really enjoyed it. I don’t know how the group feels/thinks about the logo, but I love it! I hope it won’t die after I leave, but even if so, so be it. I had my fun with it. :)</p>
|
||
<hr>
|
||
<div class="signature-block" style="margin-top:1rem">
|
||
<p><strong>Downloads:</strong>
|
||
<a href="/sources/posts/inet_logo.md">Markdown</a> ·
|
||
<a href="/sources/posts/inet_logo.md.asc">Signature (.asc)</a>
|
||
</p><details class="signature">
|
||
<summary>View OpenPGP signature</summary>
|
||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
|
||
|
||
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuIACgkQtYgoUOBM
|
||
jSoc5w/+Ij7a9UuglnzXQSMNA8VkN84qokmVTaI9mN6BY/aJQLjWWwJFi5Ih7qKw
|
||
0oWl2ZZ6FHKdAo2+gAajxhg0vf0DUGZNwsD0NJsHAuei8ezvgb4TKIfAMspKC+9J
|
||
CgcvqbZdC+M2c3PcPPA4UV5reYclf9PisEsmJSiR+cyDaCtNJkYjQ9SSZMO+BV93
|
||
k6I20tEILeR/l72ahSGyGCQxFTkI+cE5EOglG+AJP7HnLArcBUplixjLS7j/gq13
|
||
U/TeRhI5R6RG0sCzaTsyUMhfc/7be0PeU5EZTf8e21dv6c6RRWiYe+kXXv1wH1yE
|
||
sfJnMxiQ2YJqxUXDjJNJt1R+4yWpznmqYoOcW1/T8ySuYCrzx5XBdGB9XThQAxZg
|
||
sDsV6dgcPJUSvpuZqPmQicTu/+BL9QdmB4bASxDhRUX2KkK1RKRTBGP73l79vUmP
|
||
QUuBm7o7sHpSUax7CEE+vbjC9FubL5D6i6nSIesTImpR2P7ycaWboFPYREC2q3ma
|
||
B/WmnHiYbrhiLMNRrAHFdiCSHPd9JKiNK+9C8ASsDpEz8yvf2Ef9yhp0sYZ6ZBkb
|
||
Bs+BKOoDWDsI7NkT/hFBeWMrTTWpTDoRf2UGk1HI2Iaz7Fh+hXljpEPyQ/Mq3s0X
|
||
o9h8Z1rq9m7nIwmpLNW+vEiL81/SrnjJE8/+kA/+J2p9TNBYcn8=
|
||
=tAeg
|
||
-----END PGP SIGNATURE-----
|
||
</pre>
|
||
</details><p><em>Verify locally:</em></p>
|
||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/inet_logo.md
|
||
torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/inet_logo.md.asc
|
||
gpg --verify inet_logo.md.asc inet_logo.md
|
||
</pre>
|
||
</div>
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<footer class="post-footer">
|
||
<ul class="post-tags">
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/raspberrypi/">Raspberrypi</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/ws2812/">Ws2812</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/scd41/">Scd41</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/iot/">Iot</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/flask/">Flask</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/fabrication/">Fabrication</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/soldering/">Soldering</a></li>
|
||
</ul>
|
||
<nav class="paginav">
|
||
<a class="prev" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/papermod-views-debugging-story/">
|
||
<span class="title">« Prev</span>
|
||
<br>
|
||
<span>A Tiny 'Views' Badge Sent Me Down a Rabbit Hole (PaperMod + Busuanzi + Debugging --> swapped with GoatCounter the GOAT)</span>
|
||
</a>
|
||
<a class="next" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/scent_of_a_woman/">
|
||
<span class="title">Next »</span>
|
||
<br>
|
||
<span>Movie review: Scent of a Woman</span>
|
||
</a>
|
||
</nav>
|
||
|
||
</footer><div class="comments-switch" style="display:flex;gap:.5rem;margin:.5rem 0 1rem;">
|
||
<button id="btn-isso" type="button" aria-pressed="true">Anonymous (Isso)</button>
|
||
<button id="btn-giscus" type="button" aria-pressed="false">GitHub (Giscus)</button>
|
||
<a href="https://github.com/AlipourIm/blog-comments/discussions/categories/comments" target="_blank" rel="noopener">Open on GitHub ↗</a>
|
||
</div>
|
||
|
||
|
||
|
||
|
||
<div id="panel-giscus" style="display:none">
|
||
<section id="giscus-thread"></section>
|
||
</div>
|
||
|
||
|
||
<div id="panel-isso">
|
||
<section id="isso-thread"
|
||
data-isso-id="/posts/inet_logo/"
|
||
data-title="INET Logo That Breathes — From CAD to LEDs to a Calm Little Server">
|
||
<noscript>Javascript needs to be activated to view comments.</noscript>
|
||
</section>
|
||
<script
|
||
src="/isso/js/embed.min.js"
|
||
data-isso="/isso/"
|
||
data-isso-css="false"
|
||
data-isso-lang="en"
|
||
data-isso-max-comments-nested="5"
|
||
data-isso-sorting="newest"
|
||
async>
|
||
</script>
|
||
<small class="isso-powered" style="display:block;margin:.5rem 0;color:var(--secondary,#888)">
|
||
Comments powered by <a href="https://isso-comments.de" target="_blank" rel="noopener">Isso</a>
|
||
</small>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
var panelIsso = document.getElementById('panel-isso');
|
||
var panelGisc = document.getElementById('panel-giscus');
|
||
var issoBtn = document.getElementById('btn-isso');
|
||
var giscBtn = document.getElementById('btn-giscus');
|
||
var gLoaded = false;
|
||
|
||
function isDark() {
|
||
var pref = localStorage.getItem('pref-theme');
|
||
if (pref === 'dark') return true;
|
||
if (pref === 'light') return false;
|
||
|
||
var html = document.documentElement, body = document.body;
|
||
if (
|
||
(html.classList && html.classList.contains('dark')) ||
|
||
(body.classList && body.classList.contains('dark')) ||
|
||
html.getAttribute('data-theme') === 'dark' ||
|
||
body.getAttribute('data-theme') === 'dark' ||
|
||
(html.classList && html.classList.contains('theme-dark')) ||
|
||
(body.classList && body.classList.contains('theme-dark'))
|
||
) return true;
|
||
|
||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
}
|
||
function gTheme(){ return isDark() ? 'dark_dimmed' : 'noborder_light'; }
|
||
|
||
function loadGiscus() {
|
||
if (gLoaded) return;
|
||
var s = document.createElement('script');
|
||
s.src = 'https://giscus.app/client.js';
|
||
s.async = true;
|
||
s.crossOrigin = 'anonymous';
|
||
s.setAttribute('data-repo','AlipourIm/blog-comments');
|
||
s.setAttribute('data-repo-id','R_kgDOQGARyA');
|
||
s.setAttribute('data-category','Comments');
|
||
s.setAttribute('data-category-id','DIC_kwDOQGARyM4Cw3x-');
|
||
s.setAttribute('data-mapping','pathname');
|
||
s.setAttribute('data-strict','0');
|
||
s.setAttribute('data-reactions-enabled','1');
|
||
s.setAttribute('data-emit-metadata','0');
|
||
s.setAttribute('data-input-position','bottom');
|
||
s.setAttribute('data-lang','en');
|
||
s.setAttribute('data-theme', gTheme());
|
||
document.getElementById('giscus-thread').appendChild(s);
|
||
|
||
function reTheme(){
|
||
var iframe = document.querySelector('iframe.giscus-frame');
|
||
if (iframe && iframe.contentWindow) {
|
||
iframe.contentWindow.postMessage(
|
||
{ giscus: { setConfig: { theme: gTheme() } } },
|
||
'https://giscus.app'
|
||
);
|
||
}
|
||
}
|
||
|
||
var tbtn = document.getElementById('theme-toggle');
|
||
if (tbtn) tbtn.addEventListener('click', function(){ setTimeout(reTheme, 0); });
|
||
|
||
var mo = new MutationObserver(function(){ reTheme(); });
|
||
mo.observe(document.documentElement, { attributes:true, attributeFilter:['class','data-theme'] });
|
||
mo.observe(document.body, { attributes:true, attributeFilter:['class','data-theme'] });
|
||
|
||
gLoaded = true;
|
||
}
|
||
|
||
function show(which) {
|
||
var showIsso = which === 'isso';
|
||
panelIsso.style.display = showIsso ? 'block' : 'none';
|
||
panelGisc.style.display = showIsso ? 'none' : 'block';
|
||
if (issoBtn) issoBtn.setAttribute('aria-pressed', showIsso ? 'true' : 'false');
|
||
if (giscBtn) giscBtn.setAttribute('aria-pressed', showIsso ? 'false' : 'true');
|
||
if (!showIsso) loadGiscus();
|
||
}
|
||
|
||
if (issoBtn) issoBtn.addEventListener('click', function(e){ e.preventDefault(); show('isso'); });
|
||
if (giscBtn) giscBtn.addEventListener('click', function(e){ e.preventDefault(); show('giscus'); });
|
||
|
||
|
||
show('isso');
|
||
})();
|
||
</script>
|
||
|
||
|
||
</article>
|
||
</main>
|
||
|
||
<footer class="footer">
|
||
<span>© 2026 <a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">AlipourIm journeys</a></span> ·
|
||
|
||
<span>
|
||
Powered by
|
||
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
|
||
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
|
||
</span>
|
||
</footer>
|
||
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
|
||
<path d="M12 6H0l6-6z" />
|
||
</svg>
|
||
</a>
|
||
|
||
|
||
|
||
<div class="site-meta-row" style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;">
|
||
<a href="/index.xml" rel="alternate" type="application/rss+xml" title="RSS"
|
||
class="rss-link" style="display:inline-flex;align-items:center;gap:.35rem;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M4 11a9 9 0 0 1 9 9" />
|
||
<path d="M4 4a16 16 0 0 1 16 16" />
|
||
<circle cx="5" cy="19" r="1" />
|
||
</svg>
|
||
<span>RSS</span>
|
||
</a>
|
||
|
||
<span aria-hidden="true">·</span>
|
||
|
||
<span>Visitors this week: <span id="gc_site_visitors_week">—</span></span>
|
||
</div>
|
||
|
||
|
||
<script>
|
||
let menu = document.getElementById('menu')
|
||
if (menu) {
|
||
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
|
||
menu.onscroll = function () {
|
||
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||
anchor.addEventListener("click", function (e) {
|
||
e.preventDefault();
|
||
var id = this.getAttribute("href").substr(1);
|
||
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
|
||
behavior: "smooth"
|
||
});
|
||
} else {
|
||
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
|
||
}
|
||
if (id === "top") {
|
||
history.replaceState(null, null, " ");
|
||
} else {
|
||
history.pushState(null, null, `#${id}`);
|
||
}
|
||
});
|
||
});
|
||
|
||
</script>
|
||
<script>
|
||
var mybutton = document.getElementById("top-link");
|
||
window.onscroll = function () {
|
||
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
|
||
mybutton.style.visibility = "visible";
|
||
mybutton.style.opacity = "1";
|
||
} else {
|
||
mybutton.style.visibility = "hidden";
|
||
mybutton.style.opacity = "0";
|
||
}
|
||
};
|
||
|
||
</script>
|
||
<script>
|
||
document.getElementById("theme-toggle").addEventListener("click", () => {
|
||
if (document.body.className.includes("dark")) {
|
||
document.body.classList.remove('dark');
|
||
localStorage.setItem("pref-theme", 'light');
|
||
} else {
|
||
document.body.classList.add('dark');
|
||
localStorage.setItem("pref-theme", 'dark');
|
||
}
|
||
})
|
||
|
||
</script>
|
||
<script>
|
||
document.querySelectorAll('pre > code').forEach((codeblock) => {
|
||
const container = codeblock.parentNode.parentNode;
|
||
|
||
const copybutton = document.createElement('button');
|
||
copybutton.classList.add('copy-code');
|
||
copybutton.innerHTML = 'copy';
|
||
|
||
function copyingDone() {
|
||
copybutton.innerHTML = 'copied!';
|
||
setTimeout(() => {
|
||
copybutton.innerHTML = 'copy';
|
||
}, 2000);
|
||
}
|
||
|
||
copybutton.addEventListener('click', (cb) => {
|
||
if ('clipboard' in navigator) {
|
||
navigator.clipboard.writeText(codeblock.textContent);
|
||
copyingDone();
|
||
return;
|
||
}
|
||
|
||
const range = document.createRange();
|
||
range.selectNodeContents(codeblock);
|
||
const selection = window.getSelection();
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
try {
|
||
document.execCommand('copy');
|
||
copyingDone();
|
||
} catch (e) { };
|
||
selection.removeRange(range);
|
||
});
|
||
|
||
if (container.classList.contains("highlight")) {
|
||
container.appendChild(copybutton);
|
||
} else if (container.parentNode.firstChild == container) {
|
||
|
||
} else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
|
||
|
||
codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
|
||
} else {
|
||
|
||
codeblock.parentNode.appendChild(copybutton);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|