<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vlad</title>
    <description>The latest articles on DEV Community by Vlad (@vldblkhn).</description>
    <link>https://dev.to/vldblkhn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4241202%2F55e1e212-27a3-4cf4-8230-19c97413d2ff.jpg</url>
      <title>DEV Community: Vlad</title>
      <link>https://dev.to/vldblkhn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vldblkhn"/>
    <language>en</language>
    <item>
      <title>DIY Long-Distance Friendship Lamps</title>
      <dc:creator>Vlad</dc:creator>
      <pubDate>Fri, 13 Nov 2026 14:35:33 +0000</pubDate>
      <link>https://dev.to/vldblkhn/diy-long-distance-friendship-lamps-2c4g</link>
      <guid>https://dev.to/vldblkhn/diy-long-distance-friendship-lamps-2c4g</guid>
      <description>&lt;p&gt;I have a sister. I remember thinking, when I enrolled at university far away from home, that I had a lot of time while she was a little girl, so I had time to build a life for myself. Unfortunately, as often happens, I'm still working on my life, and she is a teenager now. At some point I felt like I screwed up our relationship. We live 900 km apart, so I cannot see her as often as I would like to. We keep in touch from time to time, but I think about her way more often than I text her. That's why I decided to build something to just quickly say "I'm thinking about you".&lt;/p&gt;

&lt;p&gt;Friendship lamps are a thing you can buy. The pairs I looked at cost more than train tickets for a few visits. I work on &lt;a href="https://docs.uniot.io" rel="noopener noreferrer"&gt;Uniot&lt;/a&gt;, a platform for building solutions for SMEs (small and medium-sized enterprises), and it already does the one hard part: connecting devices without me hosting anything. So this time I used it for something personal, and made the lamps myself. Everything is on &lt;a href="https://github.com/belyokhin/friendship-lamps" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;: the firmware, the scripts (also as blocks), the STL files, the OpenSCAD model, the diagrams and the photos.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwg3wnn73huabn2s0l3c2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwg3wnn73huabn2s0l3c2.gif" alt="A finger holds one lamp's heart, and both lamps light up in the same color" width="559" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a lamp first.&lt;/strong&gt; A tap on the heart switches it on or off, in one of eight soft colors. Tap it off and straight back on, and it comes on in the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold the heart for two seconds&lt;/strong&gt; and both lamps fill with the same two colors, drifting into each other round the shade like ink in water, for ten minutes. Then they fade back to whatever each lamp was doing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The next hold sends the next color.&lt;/strong&gt; The old pair melts into a warm white glow and the new one seeps in, so you can see that the hug went out. While a hug is on, a tap does the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A button on the back&lt;/strong&gt; says good night: the hug goes out on both lamps, and yours switches off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WiFi trouble shows.&lt;/strong&gt; While the ring is dark, one LED blinks green: slowly when the lamp waits to be set up, fast when it can't find its network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both lamps run the same firmware and the same script. The lamps talk through the platform's MQTT broker: there is no server of mine, no port forwarding and no app. The person at the other end needs a power socket and their WiFi password. And everything the lamp does, the animation included, is a short script that I can change from the browser while the lamp is 900 km away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supplies
&lt;/h2&gt;

&lt;p&gt;For &lt;strong&gt;two lamps&lt;/strong&gt;, with spares where parts are cheap or fragile.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Qty&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ESP32-C3 SuperMini (USB-C)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;One spare. Look for at least 1 mm between the crystal can and the chip antenna: some 2024 batches have bad WiFi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WS2812B ring, 16 LEDs, 68 mm&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Any 66-70 mm ring fits once you enter its size in the model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTP223 touch module (small, 3 pins)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Occasionally dead on arrival&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1N4007 diode&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;470 µF electrolytic capacitor, &lt;strong&gt;8 mm diameter&lt;/strong&gt;, 6.3-25 V&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A 10 mm one doesn't fit the junction board&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;470 Ω resistor&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Double-sided perfboard, 2.54 mm, 5 × 7 cm&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Gives both junction boards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JST-XH sockets, through-hole&lt;/td&gt;
&lt;td&gt;4 × 3-pin, 2 × 2-pin&lt;/td&gt;
&lt;td&gt;Often sold as "XH2.54"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JST-XH leads, plug on one end, 19-20 cm&lt;/td&gt;
&lt;td&gt;4 × 3-pin, 2 × 2-pin&lt;/td&gt;
&lt;td&gt;Ready-made, no crimping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tact switch 6 × 6 mm, 14 mm tall&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Other heights work: enter yours in the model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silicone wire 26 AWG, 6 colors, and bare tinned wire&lt;/td&gt;
&lt;td&gt;~1.5 m, ~50 cm&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-tapping screws 3 × 10 mm, countersunk&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-adhesive bumper feet, Ø 10-12 mm&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White PETG, 1.75 mm&lt;/td&gt;
&lt;td&gt;~340 g&lt;/td&gt;
&lt;td&gt;Every printed part, from one spool so the lamps match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USB charger, 5 V ≥ 1.5 A, and a USB-C cable&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; a 3D printer, soldering iron, flush cutters, wire stripper, multimeter, hot glue gun and a screwdriver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software:&lt;/strong&gt; &lt;a href="https://platformio.org/" rel="noopener noreferrer"&gt;PlatformIO&lt;/a&gt; for the firmware, a slicer, and a browser. OpenSCAD and Python only if you change the model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fko9fpb3eq6xedgviin9t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fko9fpb3eq6xedgviin9t.jpg" alt="The parts for both lamps" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two rolls of tape on the right, double-sided tape and copper tape for the touch module, were on my first parts list. The lamp needs neither, and I didn't use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it goes together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffl48hl9sis1aipv8cec7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffl48hl9sis1aipv8cec7.png" alt="Exploded: shade, LED ring, base, bottom plate with both boards (render)" width="800" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each lamp is three printed parts. The &lt;strong&gt;base&lt;/strong&gt; carries everything: the LED ring lies in a pocket on its top deck, a groove around the ring holds the shade, the front has a flat panel with an engraved heart, and the back has a notch for the USB-C cable and a hole for the button. The &lt;strong&gt;shade&lt;/strong&gt; is a hollow cup with a 0.8 mm wall, two printed lines, so it glows instead of showing sixteen dots. The &lt;strong&gt;bottom plate&lt;/strong&gt; carries both boards and screws on from below, so the whole controller lifts out in one piece.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5hksu0cm61wivbh1e31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5hksu0cm61wivbh1e31.png" alt="The lamp's circuit" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The circuit is small. The SuperMini's USB-C socket powers everything. Its 5V pin feeds the ring through a diode, which drops the supply to about 4.1 V so the board's 3.3 V data signal is read reliably, and a 470 µF capacitor steadies it. GPIO4 drives the ring's data line through 470 Ω. The TTP223 touch module reports on GPIO3, and the button pulls GPIO9 to ground, in parallel with the board's own BOOT button. The few extra parts sit on a small piece of perfboard, the junction board, and the ring, the touch module and the button each plug into it with a JST-XH lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Print the casing
&lt;/h2&gt;

&lt;p&gt;Print &lt;code&gt;base.stl&lt;/code&gt;, &lt;code&gt;bottom.stl&lt;/code&gt; and one of the three shades for each lamp. They're in the repository's &lt;a href="https://github.com/belyokhin/friendship-lamps/tree/main/hardware/stl" rel="noopener noreferrer"&gt;hardware/stl&lt;/a&gt; folder, already in print orientation. I used white PETG for everything: the deck under the LEDs gets warm, and PETG keeps its shape where PLA would start to soften.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Mass&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base, printed upside down&lt;/td&gt;
&lt;td&gt;&lt;code&gt;base.stl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;≤ 86 g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bottom plate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bottom.stl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;≤ 35 g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shade, printed upside down&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;shade_twist.stl&lt;/code&gt;, &lt;code&gt;shade_smooth.stl&lt;/code&gt; or &lt;code&gt;shade_ribs.stl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;44-46 g&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Base and plate:&lt;/strong&gt; 0.2 mm layers, 6 walls (the 1.2 mm skin behind the heart has to print solid), 15 % infill, no supports, elephant foot compensation 0.15 mm so the shade's groove stays open, Z seam at the back. &lt;strong&gt;Shade:&lt;/strong&gt; the normal mode, &lt;em&gt;not&lt;/em&gt; spiral vase: the file is already hollow, with a wall of exactly two 0.4 mm lines. Use 2 walls, 0 % infill and no supports. The &lt;a href="https://github.com/belyokhin/friendship-lamps/blob/main/hardware/README.md#printed-parts" rel="noopener noreferrer"&gt;hardware README&lt;/a&gt; has the rest, with each slicer's menu names.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2k7hxfbyk3p39yztyei.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2k7hxfbyk3p39yztyei.jpg" alt="The printed parts for both lamps: twist shades, bases and bottom plates" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpq6he7xku6recd40x7wt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpq6he7xku6recd40x7wt.png" alt="The three shade styles: smooth, twist, ribs (render)" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohazlj8ajcjepv8rqx0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohazlj8ajcjepv8rqx0f.png" alt="The base from below: ring pocket, heart panel, button pocket, USB notch (render)" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftp042aws1g6etvp8mgaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftp042aws1g6etvp8mgaz.png" alt="The bottom plate: cradle for the SuperMini and holder for the junction board (render)" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your parts measure differently?&lt;/strong&gt; The model is parametric OpenSCAD. If your ring, board, switch or cable plug differ from mine, measure them and export again with your numbers, for example &lt;code&gt;./build.sh -D ring_od=67.6 -D usb_over=1.6&lt;/code&gt;. It refuses to export if a part would collide or a design rule breaks. The &lt;a href="https://github.com/belyokhin/friendship-lamps/blob/main/hardware/README.md#key-dimensions" rel="noopener noreferrer"&gt;key dimensions&lt;/a&gt; list what each number is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Wire the circuit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvfvwhs7ri2n8gjasup4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvfvwhs7ri2n8gjasup4.png" alt="Every part and wire of one lamp" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4aa3wp57ak1mrdo03uyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4aa3wp57ak1mrdo03uyq.png" alt="The junction board, both sides, hole by hole" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz53u0ser7wa044mbo85.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz53u0ser7wa044mbo85.jpg" alt="The junction board, component side, with its link wires" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffw61pdng4dk73i3udlrj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffw61pdng4dk73i3udlrj.jpg" alt="The junction board, solder side: bare-wire trails" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8joipgc9nfzaj1bmj0pc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8joipgc9nfzaj1bmj0pc.jpg" alt="The SuperMini with its link wires: red 5V, black GND, blue 3V3, green GPIO4, yellow GPIO3, and white GPIO9 on the far row" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Assemble the lamp
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxfn0rhy3ac6phmjznk14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxfn0rhy3ac6phmjznk14.png" alt="Where everything goes, seen from above. With the base upside down, everything is mirrored" width="800" height="766"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbp1zt3pt26ssdr6ttd9u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbp1zt3pt26ssdr6ttd9u.jpg" alt="The ring glued in its pocket" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ring.&lt;/strong&gt; Push the ring's plug down through the slot in the deck, lay the ring in its pocket with the LEDs up, and fix it with a thin bead of hot glue along its inner edge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1o3u13wqj862sv5tp8bc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1o3u13wqj862sv5tp8bc.jpg" alt="The touch module glued behind the heart" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch.&lt;/strong&gt; Hot-glue the TTP223 flat against the inside of the heart, pad side (the side without components) to the plastic, with no air gap. I first tried a copper-tape electrode behind the heart, and it made the pad so sensitive that the lamp reacted to a hand a few centimetres away. The bare module through 1.2 mm of PETG is just right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fil36b2cpcolhfl8pnppm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fil36b2cpcolhfl8pnppm.jpg" alt="Inside the base: the switch glued in its pocket, and the ring's lead coming through the deck" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Button.&lt;/strong&gt; Push the tact switch into its pocket from inside until the plunger stands out of the back wall. Check that it clicks, then glue it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plate.&lt;/strong&gt; Press the junction board into its holder, sockets towards the plate's flat front edge, and the SuperMini into its cradle, USB-C towards the tongue. Fix each with a dot of hot glue, never at the SuperMini's antenna end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6d2771e99uvdsuewy08.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6d2771e99uvdsuewy08.jpg" alt="Both boards on the plate, with the leads plugged in" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Close.&lt;/strong&gt; Plug the three leads in, lay the link wires along the right wall and the button lead along the left one, with nothing over the antenna end, and screw the plate on: its three holes line up one way only. Stick on four feet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shade.&lt;/strong&gt; Set it into the groove without glue for now. It gets glued once the lamp works, in Step 7.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create an account
&lt;/h2&gt;

&lt;p&gt;Both lamps live on one account. Open &lt;a href="https://app.uniot.io" rel="noopener noreferrer"&gt;app.uniot.io&lt;/a&gt;, choose &lt;strong&gt;Create Account&lt;/strong&gt;, enter an email and a password, and confirm the email. The free plan takes three devices, so a pair fits.&lt;/p&gt;

&lt;p&gt;Then log in, open the &lt;strong&gt;Devices&lt;/strong&gt; page and click &lt;strong&gt;Add new device&lt;/strong&gt;. The window shows your &lt;strong&gt;Account ID&lt;/strong&gt; with a &lt;strong&gt;Copy&lt;/strong&gt; button; the &lt;strong&gt;My Account&lt;/strong&gt; page has it too. The firmware needs it in the next step: it is how a new board finds its way into your account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18n0mana35avzjh73gil.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18n0mana35avzjh73gil.png" alt="The Add New Device window: the Account ID (hidden here) with its Copy button, and the steps to connect a device" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Upload the firmware
&lt;/h2&gt;

&lt;p&gt;The firmware is the PlatformIO project in &lt;a href="https://github.com/belyokhin/friendship-lamps/tree/main/firmware/lamp" rel="noopener noreferrer"&gt;firmware/lamp&lt;/a&gt;. Open it in VS Code with the PlatformIO extension, or use the command line, and change three things.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;platformio.ini&lt;/code&gt;, replace my &lt;code&gt;VLDBLKHN&lt;/code&gt; with a short name of yours. The web app shows it as the device's &lt;strong&gt;Creator&lt;/strong&gt; on its &lt;strong&gt;General&lt;/strong&gt; tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="na"&gt;-D UNIOT_CREATOR_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;\&lt;span class="s2"&gt;"VLDBLKHN\"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;src/main.cpp&lt;/code&gt;, your WiFi network (2.4 GHz) and the Account ID from Step 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="nc"&gt;Uniot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configWiFiCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"YourSSID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"YourPassword"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;Uniot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"your_account_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For lamp B, the one that travels, comment out the &lt;code&gt;configWiFiCredentials&lt;/code&gt; line. Without WiFi settings, the lamp opens its own setup hotspot, so it can join whatever network it ends up near. Then upload, with the lamp connected by its USB-C cable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; firmware/lamp
pio run -e esp32c3_supermini --target upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no serial port appears, hold the back button while you plug the lamp in: it is wired to the board's BOOT button. Upload, then unplug the lamp and plug it in again. Until the lamp is online, one ring LED blinks green, which also tells you the ring is wired right.&lt;/p&gt;

&lt;p&gt;The firmware knows nothing about hugs or colors. It adds three words to the board's script language: &lt;code&gt;glow_led&lt;/code&gt; paints one LED, &lt;code&gt;glow&lt;/code&gt; paints the whole ring, and &lt;code&gt;bheld&lt;/code&gt; says a button was held rather than tapped. The heart is button 1 and the back button is button 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Authorize the lamps
&lt;/h2&gt;

&lt;p&gt;Once lamp A is online, it shows up on the &lt;strong&gt;Devices&lt;/strong&gt; page under &lt;strong&gt;Unauthorized&lt;/strong&gt;. Click &lt;strong&gt;Authorize&lt;/strong&gt;, give it a name (I called mine &lt;code&gt;Lamp (Me)&lt;/code&gt;) and &lt;strong&gt;Confirm&lt;/strong&gt;. Open it and look at the &lt;strong&gt;Primitives&lt;/strong&gt; tab: &lt;code&gt;glow&lt;/code&gt;, &lt;code&gt;glow_led&lt;/code&gt; and &lt;code&gt;bheld&lt;/code&gt; are listed. The lamp reported them when it connected, and the Sandbox builds its blocks from that list.&lt;/p&gt;

&lt;p&gt;Lamp B has no WiFi yet, so it opens a hotspot named &lt;code&gt;UNIOT-&lt;/code&gt; and a few characters, and one LED blinks green, slowly. Join it from your phone, pick your own network for now, type its password, leave &lt;strong&gt;New Account ID&lt;/strong&gt; empty and save. Then authorize it too: mine is &lt;code&gt;Lamp (Sister)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F975f7p6kzcwpbag8cq2c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F975f7p6kzcwpbag8cq2c.png" alt="Both lamps authorized and online on the Devices page" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Deploy the script
&lt;/h2&gt;

&lt;p&gt;On Uniot, what a device does is a script, not firmware. Scripts are written in UniotLisp, a small Lisp that runs on the board, either as text or as blocks in the web app's &lt;strong&gt;Sandbox&lt;/strong&gt;, which turn into the same Lisp. Deploying sends the script to the device over MQTT. The device stores it and runs it from then on, and you can replace it from the browser at any time, even after the lamp has shipped.&lt;/p&gt;

&lt;p&gt;The lamp script is &lt;a href="https://github.com/belyokhin/friendship-lamps/blob/main/scripts/lamp.lisp" rel="noopener noreferrer"&gt;scripts/lamp.lisp&lt;/a&gt;. Twenty times a second it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;reads the heart and the button.&lt;/strong&gt; A tap switches the lamp. A hold publishes the event &lt;code&gt;hug&lt;/code&gt; with the next color number, 1 to 8. The back button publishes &lt;code&gt;hug 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;listens for &lt;code&gt;hug&lt;/code&gt;&lt;/strong&gt; from either lamp; its own comes back too, which does no harm. A number shows that color pair for ten minutes, and &lt;code&gt;0&lt;/code&gt; puts the hug out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ends its own hugs.&lt;/strong&gt; The broker keeps the last value of every event for devices that connect later, which is how a lamp that reboots mid-hug lights up again. So when the countdown runs out, the lamp publishes &lt;code&gt;hug 0&lt;/code&gt; itself. Otherwise a lamp that reboots tomorrow would replay yesterday's hug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;paints every LED itself.&lt;/strong&gt; Two waves run round the ring in opposite directions and mix the hug's color with its partner, three colors further round the palette. The firmware only puts on the ring what the script paints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The colors, the flow speed and the hug length are plain numbers at the top of the script. The repository's README explains each one.&lt;/p&gt;

&lt;p&gt;I built the script as blocks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9wf9ttjh1f4zg6q8l1c7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9wf9ttjh1f4zg6q8l1c7.png" alt="The lamp script in the Sandbox's block editor" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather read blocks than Lisp, the repository has the &lt;a href="https://github.com/belyokhin/friendship-lamps/blob/main/images/lamp-script.svg" rel="noopener noreferrer"&gt;whole script as blocks&lt;/a&gt;, drawn as the Sandbox draws them.&lt;/p&gt;

&lt;p&gt;There's no way to import blocks yet, so paste the text instead. In the &lt;strong&gt;Sandbox&lt;/strong&gt;, click &lt;strong&gt;New Script&lt;/strong&gt;, flip the editor switch from &lt;strong&gt;Blockly&lt;/strong&gt; to &lt;strong&gt;Advanced&lt;/strong&gt;, paste &lt;code&gt;lamp.lisp&lt;/code&gt;, and press &lt;strong&gt;Compile&lt;/strong&gt; (Ctrl+B). Then press the &lt;strong&gt;Deploy&lt;/strong&gt; button and pick &lt;code&gt;Lamp (Me)&lt;/code&gt;. Deploy it again and pick &lt;code&gt;Lamp (Sister)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopptkv1vpdibco30cvx6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopptkv1vpdibco30cvx6.png" alt="The lamp script pasted into the Sandbox's Advanced editor" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now hold one heart for two seconds. Amber and purple seep into the ring under your finger, and a moment later into the other lamp too. Hold it again for coral and blue, and press the back button to say good night.&lt;/p&gt;

&lt;p&gt;Once both lamps work, glue the shades: six pea-sized dots of hot glue in the groove, then push the shade all the way in with a slight twist. Work quickly, because the glue sets in seconds.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance, and so were the enclosure model, the circuit layout and the firmware. The build, the tests and the photos are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>iot</category>
      <category>esp32</category>
      <category>3dprinting</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
