summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSadeep Madurange <sadeep@asciimx.com>2025-12-24 16:29:32 +0800
committerSadeep Madurange <sadeep@asciimx.com>2025-12-24 16:29:32 +0800
commit1b991a54cc834e8ef9ccc8bb15dce7ff70cdf8a3 (patch)
treea9efb89b4f08341ca6ce22fcff4ede20ef72931a
parent08e594268ed20c5c2355a249ac691c007e38aed9 (diff)
downloadwww-1b991a54cc834e8ef9ccc8bb15dce7ff70cdf8a3.tar.gz
Matrix post.
-rw-r--r--_log/_site/arduino-due.html109
-rw-r--r--_log/_site/arduino-due/connections.jpegbin0 -> 29090 bytes
-rw-r--r--_log/_site/arduino-due/schematic.pngbin0 -> 68688 bytes
-rw-r--r--_log/_site/arduino-due/source.tar.gzbin0 -> 1174 bytes
-rw-r--r--_log/_site/arduino-uno.html76
-rw-r--r--_log/_site/arduino-uno/3v3.Makefile46
-rw-r--r--_log/_site/arduino-uno/Makefile43
-rw-r--r--_log/_site/arduino-uno/breadboard.jpegbin0 -> 54319 bytes
-rw-r--r--_log/_site/arduino-uno/pinout.pngbin0 -> 247197 bytes
-rw-r--r--_log/_site/bumblebee.html40
-rw-r--r--_log/_site/bumblebee/bee.mp4bin0 -> 2352029 bytes
-rw-r--r--_log/_site/bumblebee/poster.pngbin0 -> 18024 bytes
-rw-r--r--_log/_site/bumblebee/thumb_sm.pngbin0 -> 6189 bytes
-rw-r--r--_log/_site/e-reader.html85
-rw-r--r--_log/_site/e-reader/circuit.svg145
-rw-r--r--_log/_site/e-reader/ereader.mp4bin0 -> 3101166 bytes
-rw-r--r--_log/_site/e-reader/poster.pngbin0 -> 674187 bytes
-rw-r--r--_log/_site/e-reader/source.tar.gzbin0 -> 14304 bytes
-rw-r--r--_log/_site/e-reader/thumb_sm.pngbin0 -> 240117 bytes
-rw-r--r--_log/_site/etlas.html103
-rw-r--r--_log/_site/etlas/circuit.svg105
-rw-r--r--_log/_site/etlas/dash.jpgbin0 -> 85874 bytes
-rw-r--r--_log/_site/etlas/etlas_arch.pngbin0 -> 47732 bytes
-rw-r--r--_log/_site/etlas/pcb.jpgbin0 -> 75769 bytes
-rw-r--r--_log/_site/etlas/schematic.svg4
-rw-r--r--_log/_site/etlas/source.tar.gzbin0 -> 46871 bytes
-rw-r--r--_log/_site/etlas/thumb_sm.jpgbin0 -> 55678 bytes
-rw-r--r--_log/_site/feed.xml1
-rw-r--r--_log/_site/fpm-door-lock.html105
-rw-r--r--_log/_site/fpm-door-lock/breadboard.jpgbin0 -> 46771 bytes
-rw-r--r--_log/_site/fpm-door-lock/footprint.pngbin0 -> 198127 bytes
-rw-r--r--_log/_site/fpm-door-lock/gerber.zipbin0 -> 89431 bytes
-rw-r--r--_log/_site/fpm-door-lock/pcb.jpgbin0 -> 68237 bytes
-rw-r--r--_log/_site/fpm-door-lock/pcb1.jpgbin0 -> 37068 bytes
-rw-r--r--_log/_site/fpm-door-lock/source.tar.gzbin0 -> 29473 bytes
-rw-r--r--_log/_site/fpm-door-lock/thumb_sm.jpgbin0 -> 18380 bytes
-rw-r--r--_log/_site/fpm-door-lock/video.mp4bin0 -> 13264594 bytes
-rw-r--r--_log/_site/matrix-digital-rain.html103
-rw-r--r--_log/_site/matrix-digital-rain/katakana.pngbin0 -> 133709 bytes
-rw-r--r--_log/_site/matrix-digital-rain/matrix.mp4bin0 -> 696574 bytes
-rw-r--r--_log/_site/matrix-digital-rain/poster.pngbin0 -> 233077 bytes
-rw-r--r--_log/_site/matrix-digital-rain/source.tar.gzbin0 -> 3602 bytes
-rw-r--r--_log/_site/matrix-digital-rain/thumb_sm.pngbin0 -> 52762 bytes
-rw-r--r--_log/_site/mosfet-switches.html110
-rw-r--r--_log/_site/mosfet-switches/bjt.pngbin0 -> 12838 bytes
-rw-r--r--_log/_site/mosfet-switches/n_high_side.pngbin0 -> 10825 bytes
-rw-r--r--_log/_site/mosfet-switches/p_high_side.pngbin0 -> 10724 bytes
-rw-r--r--_log/_site/my-first-pcb.html57
-rw-r--r--_log/_site/my-first-pcb/back.jpegbin0 -> 34023 bytes
-rw-r--r--_log/_site/my-first-pcb/back_design.jpegbin0 -> 31946 bytes
-rw-r--r--_log/_site/my-first-pcb/front.jpegbin0 -> 28997 bytes
-rw-r--r--_log/_site/my-first-pcb/front_design.jpegbin0 -> 32174 bytes
-rw-r--r--_log/_site/my-first-pcb/gerber_back.zipbin0 -> 48217 bytes
-rw-r--r--_log/_site/my-first-pcb/gerber_front.zipbin0 -> 49605 bytes
-rw-r--r--_log/_site/my-first-pcb/source.tar.gzbin0 -> 6660 bytes
-rw-r--r--_log/_site/my-first-pcb/thumb_sm.jpegbin0 -> 6181 bytes
-rw-r--r--_log/_site/neo4j-a-star-search.html307
-rw-r--r--_log/_site/robots.txt1
-rw-r--r--_log/_site/sitemap.xml36
-rw-r--r--_log/_site/suckless-software.html78
-rw-r--r--_log/matrix-digital-rain.md127
-rw-r--r--_log/my-first-pcb.md1
-rw-r--r--_log/neo4j-a-star-search.md317
-rw-r--r--_site/feed.xml2
-rw-r--r--_site/log/index.html13
-rw-r--r--_site/log/matrix-digital-rain/index.html125
-rw-r--r--_site/log/neo4j-a-star-search/index.html370
-rw-r--r--_site/posts.xml2
-rw-r--r--_site/robots.txt2
-rw-r--r--_site/sitemap.xml32
70 files changed, 2401 insertions, 144 deletions
diff --git a/_log/_site/arduino-due.html b/_log/_site/arduino-due.html
new file mode 100644
index 0000000..172ee1a
--- /dev/null
+++ b/_log/_site/arduino-due.html
@@ -0,0 +1,109 @@
+<p>This article is a step-by-step guide for programming bare-metal ATSAM3X8E chips
+found on Arduino Due boards. It also includes notes on the chip’s memory layout
+relevant for writing linker scripts. The steps described in this article were
+tested on an OpenBSD workstation.</p>
+
+<h2 id="toolchain">Toolchain</h2>
+
+<p>To interact directly with a bare-metal ATSAM3X8E chips, we must bypass the
+embedded bootloader. To do that, we need a hardware programmer capable of
+communicating with the chip over the Serial Wire Debug (SWD) protocol. Since
+the workstation we upload the program from presumably doesn’t speak SWD, the
+hardware programmer acts as a SWD-USB adapter. The <a href="https://www.st.com/en/development-tools/st-link-v2.html" class="external" target="_blank" rel="noopener noreferrer">ST-LINK/V2</a> programmer fits this
+bill.</p>
+
+<p>The <a href="https://openocd.org/" class="external" target="_blank" rel="noopener noreferrer">OpenOCD</a> on-chip debugger software supports
+ATSAM3X8E chips. OpenOCD, on startup, runs a telnet server that we can connect to
+to issue commands to the ATSAM3X8E chip. OpenOCD translates plain-text commands
+into the binary sequences the chip understands, and sends them over the wire.</p>
+
+<p>Finally, we need the <a href="https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain" class="external" target="_blank" rel="noopener noreferrer">ARM GNU Compiler
+Toolchain</a> to compile C programs for the chip. The ARM GNU compiler
+toolchain and OpenOCD, as a consequence of being free software, are available
+on every conceivable platform, including OpenBSD.</p>
+
+<h2 id="electrical-connections">Electrical connections</h2>
+
+<p>The following photos illustrate the electrical connections between the Arduino
+Due, PC, and the ST-LINK/V2 programmer required to transfer a compiled program
+from a PC to the MCU.</p>
+
+<table style="border: none; width: 100%;">
+ <tr style="border: none;">
+ <td style="border: none; width: 50%; vertical-align: top; background-color: transparent;">
+ <img src="schematic.png" alt="Pinout" style="width: 100%" />
+ <p style="text-align: center;">Wiring</p>
+ </td>
+ <td style="border: none; width: 50%; vertical-align: top; background-color: transparent;">
+ <img src="connections.jpeg" alt="Circuit" style="width: 100%" />
+ <p style="text-align: center;">Arduino Due</p>
+ </td>
+ </tr>
+</table>
+
+<p>Arduino Due exposes the ATSAM3X8E’s SWD interface via its DEBUG port. The
+ST-LINK/v2 programmer connects to that to communicate with the chip.</p>
+
+<h2 id="uploading-the-program">Uploading the program</h2>
+
+<p>The source.tar.gz tarball at the end of this page contains a sample C program
+(the classic LED blink program) with OpenOCD configuration and linker scripts.
+First, use the following command to build it:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -T script.ld \
+ -nostartfiles \
+ -nostdlib \
+ -o a.elf main.c
+</code></pre></div></div>
+
+<p>Then, open a telnet session with OpenOCD and issue the following sequence of
+commands to configure the chip and upload the compiled program to it:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ openocd -f openocd-due.cfg
+$ telnet localhost 4444
+ &gt; halt
+ &gt; at91sam3 gpnvm show
+ &gt; at91sam3 gpnvm set 1
+ &gt; at91sam3 gpnvm show
+$ openocd -f openocd-due.cfg -c "program a.elf verify reset exit"
+</code></pre></div></div>
+
+<p>The first of the above commands starts OpenOCD. In the telnet session, the
+first command halts the chip in preparation for receiving commands. Next, we
+inspect the current GPNVM bit setting (more on this later). If the bit is unset
+(the gpnvm show command returns 0), we set it to 1 and verify the update.</p>
+
+<p>The final command, issued from outside the telnet session, uploads the program
+to the chip. Those are the bare minimum set of commands required to program the
+chip. The AT91SAM3 flash driver section of the OpenOCD manual lists all
+available commands for the ATSAM3X8E chip.</p>
+
+<h2 id="gpnvm-bits">GPNVM bits</h2>
+
+<p>By design, ARM chips boot into address 0x00000. ATSAM3X8E’s memory consists of
+a ROM and a dual-banked flash (flash0 and flash1), residing in different
+locations of the chip’s address space. The GPNVM bits control which of them
+maps to 0x00000. When GPNVM1 is cleared (the default), the chip boots from the ROM,
+which contains Atmel’s SAM-BA bootloader.</p>
+
+<p>Conversely, when the GPNVM1 bit is 1 (and the GPNVM2 bit is 0), flash0 at
+address 0x80000 maps to 0x00000. When both GPNVM bits are 0, flash1 maps to
+0x00000. Since we place our program in flash0 in the linker script, we set the
+GPNVM1 bit and leave the GPNVM2 bit unchanged to ensure the chip
+executes our program instead of the embedded bootloader at startup.</p>
+
+<h2 id="linker-script">Linker script</h2>
+
+<p>At a minimum, the linker script must place the vector table at the first
+address of the flash. This is mandatory for ARM chips unless we relocate the
+vector table using the VTOR register.</p>
+
+<p>The first entry of the vector table must be the stack pointer. The stack
+pointer must be initialized to the highest memory location available to
+accommodate the ATSAM3X8E’s descending stack.</p>
+
+<p>The second entry of the vector table must be the reset vector. In the reset
+vector, we can perform tasks such as zeroing out memory and initializing
+registers before passing control to the main program.</p>
+
+<p>Files: <a href="source.tar.gz">source.tar.gz</a></p>
diff --git a/_log/_site/arduino-due/connections.jpeg b/_log/_site/arduino-due/connections.jpeg
new file mode 100644
index 0000000..081e6d4
--- /dev/null
+++ b/_log/_site/arduino-due/connections.jpeg
Binary files differ
diff --git a/_log/_site/arduino-due/schematic.png b/_log/_site/arduino-due/schematic.png
new file mode 100644
index 0000000..62ddadd
--- /dev/null
+++ b/_log/_site/arduino-due/schematic.png
Binary files differ
diff --git a/_log/_site/arduino-due/source.tar.gz b/_log/_site/arduino-due/source.tar.gz
new file mode 100644
index 0000000..496567b
--- /dev/null
+++ b/_log/_site/arduino-due/source.tar.gz
Binary files differ
diff --git a/_log/_site/arduino-uno.html b/_log/_site/arduino-uno.html
new file mode 100644
index 0000000..d8ffac4
--- /dev/null
+++ b/_log/_site/arduino-uno.html
@@ -0,0 +1,76 @@
+<p>This is a quick reference for wiring up ATmega328P ICs to run at 5V and 3.3V.
+While the 5V configuration is common, the 3.3V configuration can be useful in
+low-power applications and when interfacing with parts that themselves run at
+3.3V. In this guide, the 5V setup is configured with a 16MHz crystal
+oscillator, while the 3.3V configuration makes use of an 8MHz crystal
+oscillator.</p>
+
+<p>The steps that follow refer to the following pinout.</p>
+
+<table style="border: none; width: 100%;">
+ <tr style="border: none;">
+ <td style="border: none; width: 50%; vertical-align: top;">
+ <img src="pinout.png" alt="Pinout" style="width: 100%" />
+ <p style="text-align: center;">Pinout</p>
+ </td>
+ <td style="border: none; width: 50%; vertical-align: top;">
+ <img src="breadboard.jpeg" alt="Circuit" style="width: 100%" />
+ <p style="text-align: center;">Breadboard</p>
+ </td>
+ </tr>
+</table>
+
+<h2 id="5v-16mhz-configuration">5V-16MHz configuration</h2>
+
+<p>Powering ATmega328P microcontrollers with 5V is the most common setup. This is
+also how Arduino Uno boards are wired.</p>
+
+<p>In this configuration, the microcontroller’s pin 1 is connected to 5V via a
+10kΩ resistor. Pins 9 and 10 are connected to a 16MHz crystal oscillator via
+two 22pF capacitors connected to ground. The microcontroller is powered by
+connecting pins 7, 20, and 21 to a 5V DC power supply. Lastly, pins 8 and 22
+are connected to ground. In addition to the these connections, which are
+required, it’s a good idea to add 0.1μF decoupling capacitors between pins 7,
+20, and 21 and ground.</p>
+
+<p><a href="Makefile">Here’s</a> a sample Makefile for compiling C programs for ATmega328P
+microcontrollers using avr-gcc/avrdude toolchain.</p>
+
+<h2 id="33v-8mhz-configuration">3.3V-8MHz configuration</h2>
+
+<p>Electrical connections for running an ATmega328P at 3.3V are identical to that
+of the 5V circuit. The only differences are that all the 5V connections are
+replaced with a 3.3V power source and a 8MHz crystal oscillator takes the place
+of the 16MHz crystal.</p>
+
+<p>However, standard ATmega328P chips are preconfigured to run at 5V. To run one
+at 3.3V, we must first modify its fuses that control characteristics like the
+BOD level. If a bootloader that expects a 16MHz clock (e.g., Arduino
+bootloader) is pre-installed on the ATmega328P, it must be swapped with one
+that accepts an 8MHz clock. To accomplish that, we need an in-system programmer
+(ISP).</p>
+
+<p>Fortunately, we can turn an ordinary Arduino Uno board into an ISP by uploading
+the ‘ArduinoISP’ sketch found in the Arduino IDE. The ISP communicates with the
+microcontroller using a Serial Peripheral Interface (SPI). So, connect the SPI
+port of the ATmega328P to that of the Arduino Uno, and the Uno’s SS pin
+to the ATmega328P’s RESET pin.</p>
+
+<p>Power up the the ATmega328P by connecting its V<sub>CC</sub> to a 5V supply (we
+can use Arduino Uno’s 5V pin). From the Arduino IDE, select ‘ATmega328P (3.3V,
+8MHz)’ for processor from the tools menu. Also from the tools menu, select
+‘Arduino as ISP’ as programmer. Finally, upload the new bootloader by selecting
+‘Burn Bootloader’ from the tools menu.</p>
+
+<p>The ATmega328P is now ready to run at 8MHz with a 3.3V power supply. You can
+upload programs to the ATmega328P as you normally would using avrdude.
+<a href="3v3.Makefile">Here’s</a> a sample Makefile with adjusted parameters (e.g., baud
+rate) for an 8MHz clock.</p>
+
+<h2 id="remarks">Remarks</h2>
+
+<p>In both configurations, if you intend to use the ATmega328P’s analog-to-digital
+converter with the internal 1.1V or AV<sub>cc</sub> voltage as reference, do
+not connect AREF (pin 21) to V<sub>cc</sub>. Refer to section 23.5.2 in the
+datasheet for more information.</p>
+
diff --git a/_log/_site/arduino-uno/3v3.Makefile b/_log/_site/arduino-uno/3v3.Makefile
new file mode 100644
index 0000000..4ca89d4
--- /dev/null
+++ b/_log/_site/arduino-uno/3v3.Makefile
@@ -0,0 +1,46 @@
+CC = avr-gcc
+MCU = atmega328p
+PORT = /dev/cuaU0
+TARGET = app
+
+SRC = main.c
+OBJ = $(SRC:.c=.o)
+
+CFLAGS = -std=gnu99
+CFLAGS += -Os
+CFLAGS += -Wall
+CFLAGS += -mmcu=$(MCU)
+CFLAGS += -DBAUD=57600
+CFLAGS += -DF_CPU=8000000UL
+CFLAGS += -ffunction-sections -fdata-sections
+
+LDFLAGS = -mmcu=$(MCU)
+LDFLAGS += -Wl,--gc-sections
+
+HEX_FLAGS = -O ihex
+HEX_FLAGS += -j .text -j .data
+
+AVRDUDE_FLAGS = -p $(MCU)
+AVRDUDE_FLAGS += -c arduino
+AVRDUDE_FLAGS += -b 57600
+AVRDUDE_FLAGS += -P $(PORT)
+AVRDUDE_FLAGS += -D -U
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+elf: $(OBJ)
+ $(CC) $(LDFLAGS) $(OBJ) -o $(TARGET).elf
+
+hex: elf
+ avr-objcopy $(HEX_FLAGS) $(TARGET).elf $(TARGET).hex
+
+upload: hex
+ avrdude $(AVRDUDE_FLAGS) flash:w:$(TARGET).hex:i
+
+.PHONY: clean
+
+clean:
+ rm -f *.o *.elf *.hex
+
+
diff --git a/_log/_site/arduino-uno/Makefile b/_log/_site/arduino-uno/Makefile
new file mode 100644
index 0000000..9db7b09
--- /dev/null
+++ b/_log/_site/arduino-uno/Makefile
@@ -0,0 +1,43 @@
+CC = avr-gcc
+MCU = atmega328p
+PORT = /dev/cuaU0
+TARGET = app
+
+SRC = main.c
+OBJ = $(SRC:.c=.o)
+
+CFLAGS = -std=gnu99
+CFLAGS += -Os
+CFLAGS += -Wall
+CFLAGS += -mmcu=$(MCU)
+CFLAGS += -DBAUD=115200
+CFLAGS += -DF_CPU=16000000UL
+CFLAGS += -ffunction-sections -fdata-sections
+
+LDFLAGS = -mmcu=$(MCU)
+LDFLAGS += -Wl,--gc-sections
+
+HEX_FLAGS = -O ihex
+HEX_FLAGS += -j .text -j .data
+
+AVRDUDE_FLAGS = -p $(MCU)
+AVRDUDE_FLAGS += -c arduino
+AVRDUDE_FLAGS += -P $(PORT)
+AVRDUDE_FLAGS += -D -U
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+elf: $(OBJ)
+ $(CC) $(LDFLAGS) $(OBJ) -o $(TARGET).elf
+
+hex: elf
+ avr-objcopy $(HEX_FLAGS) $(TARGET).elf $(TARGET).hex
+
+upload: hex
+ avrdude $(AVRDUDE_FLAGS) flash:w:$(TARGET).hex:i
+
+.PHONY: clean
+
+clean:
+ rm *.o *.elf *.hex
diff --git a/_log/_site/arduino-uno/breadboard.jpeg b/_log/_site/arduino-uno/breadboard.jpeg
new file mode 100644
index 0000000..bd74907
--- /dev/null
+++ b/_log/_site/arduino-uno/breadboard.jpeg
Binary files differ
diff --git a/_log/_site/arduino-uno/pinout.png b/_log/_site/arduino-uno/pinout.png
new file mode 100644
index 0000000..59acfbc
--- /dev/null
+++ b/_log/_site/arduino-uno/pinout.png
Binary files differ
diff --git a/_log/_site/bumblebee.html b/_log/_site/bumblebee.html
new file mode 100644
index 0000000..a466d0b
--- /dev/null
+++ b/_log/_site/bumblebee.html
@@ -0,0 +1,40 @@
+<p>Bumblebee is a tool I built for one of my employers to automate the generation
+of web scraping scripts.</p>
+
+<video style="max-width:100%; margin-bottom: 10px" controls="" poster="poster.png">
+ <source src="bee.mp4" type="video/mp4" />
+</video>
+
+<p>In 2024, we were tasked with collecting market data using various methods,
+including scraping data from authorized websites for traders’ use.</p>
+
+<p>Manual authoring of such scripts took time. The scripts were often brittle due
+to the complexity of the modern web, and they lacked optimizations such as
+bypassing the UI and retrieving the data files directly when possible, which
+would have significantly reduced our compute costs.</p>
+
+<p>To alleviate these challenges, I, with the help of a colleague, Andy Zhang,
+built Bumblebee: a web browser powered by C# Windows Forms, Microsoft Edge <a src="https://developer.microsoft.com/en-us/microsoft-edge/webview2" class="external" target="_blank" rel="noopener noreferrer">WebView2</a>, and
+the <a src="https://github.com/desjarlais/Scintilla.NET" class="external" toarget="_blank" rel="noopener noreferrer">Scintilla.NET</a> text editor.</p>
+
+<p>Bumblebee works by injecting a custom JavaScript program that intercepts
+client-side events and sends them to Bumblebee for analysis. In addition to
+front-end events, Bumblebee also captures internal browser events, which it
+then interprets to generate code in real time. Note that we developed Bumblebee
+before the advent of now-popular LLMs. Bumblebee supports dynamic websites,
+pop-ups, developer tools, live manual override, event debouncing, and filtering
+hidden elements and scripts.</p>
+
+<p>Before settling on a desktop application, we contemplated designing Bumblebee
+as a browser extension. We chose the desktop app because extensions don’t offer
+the deep, event-based control we needed. Besides, the company’s security
+policy, which prohibited browser extensions, would have complicated the
+deployment of an extension-based solution. My first prototype used a C# binding
+of the Chromium project. WebView’s more intuitive API and its seamless
+integration with Windows Forms led us to choose it over the Chromium wrapper.</p>
+
+<p>What began as a personal side project to improve my own workflow enabled us to
+collectively improve the quality of our web scripts at a much larger scale.
+Bumblebee predictably reduced the time we spent on authoring scripts from hours
+to a few minutes.</p>
+
diff --git a/_log/_site/bumblebee/bee.mp4 b/_log/_site/bumblebee/bee.mp4
new file mode 100644
index 0000000..835600d
--- /dev/null
+++ b/_log/_site/bumblebee/bee.mp4
Binary files differ
diff --git a/_log/_site/bumblebee/poster.png b/_log/_site/bumblebee/poster.png
new file mode 100644
index 0000000..6dc955e
--- /dev/null
+++ b/_log/_site/bumblebee/poster.png
Binary files differ
diff --git a/_log/_site/bumblebee/thumb_sm.png b/_log/_site/bumblebee/thumb_sm.png
new file mode 100644
index 0000000..f7cfbf3
--- /dev/null
+++ b/_log/_site/bumblebee/thumb_sm.png
Binary files differ
diff --git a/_log/_site/e-reader.html b/_log/_site/e-reader.html
new file mode 100644
index 0000000..f60c485
--- /dev/null
+++ b/_log/_site/e-reader.html
@@ -0,0 +1,85 @@
+<p>This project features an experimental e-reader powered by an ESP-WROOM-32
+development board and a 7.5-inch <a href="https://www.waveshare.com/" class="external" target="_blank" rel="noopener noreferrer">Waveshare</a>
+e-paper display built with the intention of learning about e-paper displays.</p>
+
+<video style="max-width:100%;" controls="" poster="poster.png">
+ <source src="ereader.mp4" type="video/mp4" />
+</video>
+
+<h2 id="introduction">Introduction</h2>
+
+<p>The prototype e-reader comprises an ESP32 microcontroller, an e-paper display
+HAT, and three buttons: yellow, blue, and white for turning the page backwards,
+forwards, and putting the device to sleep, respectively. The prototype does not
+store books on the microcontroller. It streams books from a server over HTTP.
+The e-reader employs RTC memory to record the reading progress between
+sessions.</p>
+
+<p>The most formidable challenge when trying to build an e-reader with an ESP32 is
+its limited memory and storage. My ESP-WROOM-32 has a total of 512KB of SRAM
+and 4MB of flash memory, which the freeRTOS, ESP-IDF, and the e-reader
+application must share. To put things into perspective, a Kindle Paperwhite has
+at least 256MB of memory and 8GB of storage. That is 500x more memory than what
+I’d have to work with.</p>
+
+<p>Despite its size, as microcontrollers go, ESP32 is a powerful system-on-a-chip
+with a 160MHz dual-core processor and integrated WiFi. So, I thought it’d be
+amusing to embrace the constraints and build my e-reader using a $5 MCU and the
+power of C programming.</p>
+
+<h2 id="the-file-format">The file format</h2>
+
+<p>The file format dictates the complexity of the embedded software. So, I’ll
+begin there. The e-reader works by downloading and rendering a rasterized
+monochrome image of a page (a .ebm file).</p>
+
+<p>The EBM file contains a series of bitmaps, one for each page of the book. The
+dimensions of each bitmap are equal to the size of the display. Each byte of
+the bitmap encodes information for rendering eight pixels. For my display,
+which has a resolution of 480x800, the bitmaps are laid out along 48KB
+boundaries. This simple file format lends well to HTTP streaming, which is its
+main advantage, as we will soon see.</p>
+
+<p>The pdftoebm.py script enclosed in the tarball at the end of the page converts
+PDF documents to EBM files.</p>
+
+<h2 id="how-does-it-work">How does it work?</h2>
+
+<p>As the e-reader has no storage, it can’t store books locally. Instead, it
+downloads pages of the EBM file over HTTP from the location pointed to by the
+<code class="language-plaintext highlighter-rouge">EBM_ARCH_URL</code> setting in the Kconfig.projbuild file on demand. To read a
+different book, we have to replace the old file with the new one or change the
+<code class="language-plaintext highlighter-rouge">EBM_ARCH_URL</code> value. The latter requires us to recompile the embedded
+software.</p>
+
+<p>Upon powering up, the e-reader checks the reading progress stored in the RTC
+memory. It then downloads three pages (current, previous, and next) to a
+circular buffer in DMA-capable memory. When the user turns a page by pressing a
+button, one of the microprocessor’s two cores transfers it from the buffer to
+the display over a Serial Peripheral Interface (SPI). The other downloads a new
+page in the background. I used the ESP-IDF task API to schedule the two tasks
+on different cores of the multicore processor to make the reader more
+responsive.</p>
+
+<p>I designed the EBM format with HTTP streaming in mind. Since the pages are laid
+out in the EBM file along predictable boundaries, the e-reader can request
+pages by specifying the offset and the chunk size in the HTTP Range header. Any
+web server will process this request without custom logic.</p>
+
+<h2 id="epilogue">Epilogue</h2>
+
+<p>My fascination with e-paper began back in 2017, when I was tasked with
+installing a few displays in a car park. Having no idea how they worked, I
+remember watching the languid screens refresh like a Muggle witnessing magic.
+This project was born out of that enduring curiosity and love of e-paper
+technology.</p>
+
+<p>Why did I go to the trouble of building a rudimentary e-reader when I could
+easily buy a more capable commercial e-reader? First of all, it’s to prove to
+myself that I can. More importantly, there’s a quiet satisfaction to reading on
+hardware you built yourself. You are no longer the powerless observer watching
+the magic happen from the sidelines. You become the wizard who makes the
+invisible particles swirl into form by whispering C to them. There’s only one
+way to experience that.</p>
+
+<p>Files: <a href="source.tar.gz">source.tar.gz</a></p>
diff --git a/_log/_site/e-reader/circuit.svg b/_log/_site/e-reader/circuit.svg
new file mode 100644
index 0000000..fd7508b
--- /dev/null
+++ b/_log/_site/e-reader/circuit.svg
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Circuit Diagram, cdlibrary.dll 4.0.0.0 -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" width="680" height="360" xmlns="http://www.w3.org/2000/svg">
+ <line x1="360" y1="130" x2="360" y2="330" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="330" x2="360" y2="330" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="250" x2="70" y2="260" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="260" x2="70" y2="275" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="315" x2="70" y2="330" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 70,275 L 70,277 L 63,280 L 77,286 L 63,292 L 77,298 L 63,304 L 77,310 L 70,313 L 70,315" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="56" y="295" style="font-family:Arial;font-size:11px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 56, 295)">10 kΩ</text>
+ <line x1="70" y1="250" x2="100" y2="250" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="130" y1="250" x2="190" y2="250" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="100" y1="250" x2="101" y2="250" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="129" y1="250" x2="130" y2="250" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <ellipse cx="104" cy="250" rx="3" ry="3" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <ellipse cx="126" cy="250" rx="3" ry="3" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <line x1="103" y1="242" x2="127" y2="242" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="109" y1="236" x2="121" y2="236" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="115" y1="236" x2="115" y2="242" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="140" x2="190" y2="140" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="60" x2="70" y2="140" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="100" x2="100" y2="100" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="130" y1="100" x2="190" y2="100" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="70" y1="60" x2="100" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="130" y1="60" x2="190" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="100" y1="100" x2="101" y2="100" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="129" y1="100" x2="130" y2="100" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <ellipse cx="104" cy="100" rx="3" ry="3" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <ellipse cx="126" cy="100" rx="3" ry="3" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <line x1="103" y1="92" x2="127" y2="92" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="109" y1="86" x2="121" y2="86" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="115" y1="86" x2="115" y2="92" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="100" y1="60" x2="101" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="129" y1="60" x2="130" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <ellipse cx="104" cy="60" rx="3" ry="3" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <ellipse cx="126" cy="60" rx="3" ry="3" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <line x1="103" y1="52" x2="127" y2="52" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="109" y1="46" x2="121" y2="46" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="115" y1="46" x2="115" y2="52" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="130" x2="410" y2="130" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="120" x2="410" y2="120" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="110" x2="410" y2="110" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="100" x2="410" y2="100" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="90" x2="410" y2="90" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="80" x2="410" y2="80" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="70" x2="410" y2="70" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="310" y1="60" x2="420" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="450" y1="160" x2="500" y2="160" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:3" />
+ <path d="M 430,120 L 430,110 L 420,100 L 410,100" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 430,120 L 420,110 L 410,110" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 430,130 L 420,120 L 410,120" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 430,140 L 420,130 L 410,130" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 510,180 L 520,190 L 530,190" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 510,190 L 520,200 L 530,200" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 510,200 L 520,210 L 530,210" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 510,200 L 510,210 L 520,220 L 530,220" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="524" y="185" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 185)">1</text>
+ <text x="524" y="195" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 195)">2</text>
+ <text x="524" y="205" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 205)">3</text>
+ <text x="524" y="215" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 215)">4</text>
+ <line x1="445" y1="155" x2="450" y2="165" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:1" />
+ <line x1="495" y1="155" x2="490" y2="165" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:1" />
+ <path d="M 455,160 L 440,160 L 430,150 L 430,80" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:3" />
+ <path d="M 485,160 L 500,160 L 510,170 L 510,240" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:3" />
+ <path d="M 430,80 L 430,70 L 420,60 L 410,60" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 430,80 L 420,70 L 410,70" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 430,90 L 420,80 L 410,80" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 430,100 L 420,90 L 410,90" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="413" y="55" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 55)">1</text>
+ <text x="413" y="65" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 65)">2</text>
+ <text x="413" y="75" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 75)">3</text>
+ <text x="413" y="85" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 85)">4</text>
+ <text x="413" y="95" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 95)">5</text>
+ <text x="413" y="105" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 105)">6</text>
+ <text x="413" y="115" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 115)">7</text>
+ <text x="413" y="125" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 413, 125)">8</text>
+ <path d="M 510,220 L 520,230 L 530,230" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 510,230 L 520,240 L 530,240" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 510,240 L 520,250 L 530,250" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 510,240 L 510,250 L 520,260 L 530,260" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="524" y="225" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 225)">5</text>
+ <text x="524" y="235" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 235)">6</text>
+ <text x="524" y="245" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 245)">7</text>
+ <text x="524" y="255" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 524, 255)">8</text>
+ <text x="470" y="155" style="font-family:Arial;font-size:11px;text-anchor:middle" dominant-baseline="baseline" transform="rotate(0, 470, 155)">BUS</text>
+ <text x="590" y="170" style="font-family:Arial;font-size:11px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 590, 170)">E-Paper Display HAT</text>
+ <rect x="540" y="180" width="100" height="90" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <line x1="530" y1="190" x2="540" y2="190" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="190" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 190)">CS</text>
+ <line x1="530" y1="200" x2="540" y2="200" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="200" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 200)">DC</text>
+ <line x1="530" y1="210" x2="540" y2="210" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="210" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 210)">DIN</text>
+ <line x1="530" y1="220" x2="540" y2="220" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="220" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 220)">CLK</text>
+ <line x1="530" y1="230" x2="540" y2="230" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="230" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 230)">BUSY</text>
+ <line x1="530" y1="240" x2="540" y2="240" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="240" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 240)">RST</text>
+ <line x1="530" y1="250" x2="540" y2="250" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="250" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 250)">GND</text>
+ <line x1="530" y1="260" x2="540" y2="260" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="260" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 544, 260)">VCC</text>
+ <rect x="200" y="50" width="100" height="210" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <text x="250" y="40" style="font-family:Arial;font-size:11px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 250, 40)">ESP-WROOM-32</text>
+ <line x1="190" y1="60" x2="200" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="204" y="60" style="font-family:Arial;font-size:10px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 204, 60)">IO21</text>
+ <text x="196" y="58" style="font-family:Arial;font-size:8px;text-anchor:end" dominant-baseline="baseline" transform="rotate(0, 196, 58)"></text>
+ <line x1="300" y1="60" x2="310" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="60" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 60)">IO5</text>
+ <text x="304" y="58" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 58)"></text>
+ <line x1="300" y1="70" x2="310" y2="70" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="70" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 70)">IO16</text>
+ <text x="304" y="68" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 68)"></text>
+ <line x1="300" y1="80" x2="310" y2="80" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="80" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 80)">IO23</text>
+ <text x="304" y="78" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 78)"></text>
+ <line x1="300" y1="90" x2="310" y2="90" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="90" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 90)">IO18</text>
+ <text x="304" y="88" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 88)"></text>
+ <line x1="190" y1="100" x2="200" y2="100" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="204" y="100" style="font-family:Arial;font-size:10px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 204, 100)">IO22</text>
+ <text x="196" y="98" style="font-family:Arial;font-size:8px;text-anchor:end" dominant-baseline="baseline" transform="rotate(0, 196, 98)"></text>
+ <line x1="300" y1="100" x2="310" y2="100" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="100" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 100)">IO4</text>
+ <text x="304" y="98" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 98)"></text>
+ <line x1="300" y1="110" x2="310" y2="110" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="110" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 110)">IO2</text>
+ <text x="304" y="108" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 108)"></text>
+ <line x1="300" y1="120" x2="310" y2="120" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="120" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 120)">GND</text>
+ <text x="304" y="118" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 118)"></text>
+ <line x1="300" y1="130" x2="310" y2="130" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="296" y="130" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 296, 130)">3V3</text>
+ <text x="304" y="128" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 304, 128)"></text>
+ <line x1="190" y1="140" x2="200" y2="140" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="204" y="140" style="font-family:Arial;font-size:10px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 204, 140)">GND</text>
+ <text x="196" y="138" style="font-family:Arial;font-size:8px;text-anchor:end" dominant-baseline="baseline" transform="rotate(0, 196, 138)"></text>
+ <line x1="190" y1="250" x2="200" y2="250" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="204" y="250" style="font-family:Arial;font-size:10px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 204, 250)">IO15</text>
+ <text x="196" y="248" style="font-family:Arial;font-size:8px;text-anchor:end" dominant-baseline="baseline" transform="rotate(0, 196, 248)"></text>
+ <ellipse cx="360" cy="130" rx="2" ry="2" style="fill-opacity:1;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <ellipse cx="70" cy="100" rx="2" ry="2" style="fill-opacity:1;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+</svg> \ No newline at end of file
diff --git a/_log/_site/e-reader/ereader.mp4 b/_log/_site/e-reader/ereader.mp4
new file mode 100644
index 0000000..89e05eb
--- /dev/null
+++ b/_log/_site/e-reader/ereader.mp4
Binary files differ
diff --git a/_log/_site/e-reader/poster.png b/_log/_site/e-reader/poster.png
new file mode 100644
index 0000000..1e222d2
--- /dev/null
+++ b/_log/_site/e-reader/poster.png
Binary files differ
diff --git a/_log/_site/e-reader/source.tar.gz b/_log/_site/e-reader/source.tar.gz
new file mode 100644
index 0000000..3e343a7
--- /dev/null
+++ b/_log/_site/e-reader/source.tar.gz
Binary files differ
diff --git a/_log/_site/e-reader/thumb_sm.png b/_log/_site/e-reader/thumb_sm.png
new file mode 100644
index 0000000..7c971e8
--- /dev/null
+++ b/_log/_site/e-reader/thumb_sm.png
Binary files differ
diff --git a/_log/_site/etlas.html b/_log/_site/etlas.html
new file mode 100644
index 0000000..06e3ce7
--- /dev/null
+++ b/_log/_site/etlas.html
@@ -0,0 +1,103 @@
+<p>Etlas is a news, stock market, and weather tracker powered by an ESP32 NodeMCU
+D1, featuring a 7.5-inch <a href="https://www.waveshare.com/" class="external" target="_blank" rel="noopener noreferrer">Waveshare</a> e-paper display and a
+DHT22 sensor module.</p>
+
+<table style="border: none;">
+ <tr style="border: none;">
+ <td style="border: none;"><img src="dash.jpg" alt="front" style="width: 100%" /></td>
+ <td style="border: none;"><img src="pcb.jpg" alt="back" style="width: 100%" /></td>
+ </tr>
+</table>
+
+<p>The top-left panel shows two weeks of end-of-day prices—the maximum the ESP32’s
+SRAM can hold—from the Polygon.io API. The price feed is relayed through a
+FastCGI-wrapped Flask app hosted on a VPS. This lets me configure stock symbols
+in its application settings. The app cycles through them as requests come in
+from the ESP32. Running the Flask app as a FastCGI process while exposing it
+via httpd with htpasswd authentication keeps the server code simple and secure.</p>
+
+<p>The following diagram outlines the Etlas’s overall system architecture.</p>
+
+<p><img src="etlas_arch.png" alt="architecture" /></p>
+
+<p>The more prominent panel on the right of the display shows local and world news
+from Channel NewsAsia. The MCU downloads and parses XML data from the RSS feed
+directly before rendering it to the display. The character glyphs used are
+stored as bitmaps in the sprites directory. I skipped the proxy for news to
+avoid writing more server code, but in hindsight it limits the feeds Etlas can
+handle. I will fix this in a future version.</p>
+
+<p>The middle and bottom right panels display the temperature and relative
+humidity from the DHT22 sensor. The DHT22 uses pulse-width modulation to
+transmit data to the host. The 26µs, 50µs, and 70µs pulses are too fast for the
+ESP32 to measure reliably with standard APIs. Instead, the driver compares
+relative pulse widths to differentiate zeros from ones:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static inline int dht_await_pin_state(int state, int timeout)
+{
+ int t;
+ static const uint16_t delta = 1;
+
+ for (t = 0; t &lt; timeout; t += delta) {
+ ets_delay_us(delta);
+ if (gpio_get_level(DHT_PIN) == state)
+ return t;
+ }
+ return 0;
+}
+
+static inline int dht_get_raw_data(unsigned char buf[DHT_DATA_LEN])
+{
+ int rc;
+ unsigned char i, pwl, pwh;
+
+ gpio_set_level(DHT_PIN, 0);
+ ets_delay_us(1100);
+ gpio_set_level(DHT_PIN, 1);
+
+ if (!dht_await_pin_state(0, 40)) {
+ rc = 1;
+ xQueueSend(dht_evt_queue, &amp;rc, (TickType_t) 0);
+ return 0;
+ }
+ if (!dht_await_pin_state(1, 80)) {
+ rc = 2;
+ xQueueSend(dht_evt_queue, &amp;rc, (TickType_t) 0);
+ return 0;
+ }
+ if (!dht_await_pin_state(0, 80)) {
+ rc = 3;
+ xQueueSend(dht_evt_queue, &amp;rc, (TickType_t) 0);
+ return 0;
+ }
+
+ for (i = 0; i &lt; DHT_DATA_LEN; i++) {
+ if (!(pwl = dht_await_pin_state(1, 50))) {
+ rc = 4;
+ xQueueSend(dht_evt_queue, &amp;rc, (TickType_t) 0);
+ return 0;
+ }
+ if (!(pwh = dht_await_pin_state(0, 70))) {
+ rc = 5;
+ xQueueSend(dht_evt_queue, &amp;rc, (TickType_t) 0);
+ return 0;
+ }
+ buf[i] = pwh &gt; pwl;
+ }
+ return 1;
+}
+</code></pre></div></div>
+
+<p>I ported <a href="https://github.com/Fonger/ESP8266-RTOS-DHT" class="external" target="_blank" rel="noopener noreferrer">this</a> implementation from ESP8266
+to ESP32—all credit for the algorithm belongs to them.</p>
+
+<p>Etlas is a networked embedded system. All acquisition, processing, and
+rendering of data are performed on the ESP32’s 160MHz microprocessor using less
+than 512KB of SRAM. The embedded software that makes this possible is written
+in C using ESP-IDF v5.2.1. The e-paper display driver is derived from Waveshare
+<a href="https://github.com/waveshareteam/e-Paper" class="external" target="_blank" rel="noopener noreferrer">examples</a> for Arduino and STM32
+platforms.</p>
+
+<p>Etlas has been running reliably for over a year since August 2024.</p>
+
+<p>Files: <a href="source.tar.gz">source.tar.gz</a></p>
diff --git a/_log/_site/etlas/circuit.svg b/_log/_site/etlas/circuit.svg
new file mode 100644
index 0000000..6255045
--- /dev/null
+++ b/_log/_site/etlas/circuit.svg
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Circuit Diagram, cdlibrary.dll 4.0.0.0 -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" width="700" height="300" xmlns="http://www.w3.org/2000/svg">
+ <line x1="380" y1="220" x2="380" y2="280" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="220" x2="550" y2="220" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="210" x2="550" y2="210" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="200" x2="550" y2="200" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="190" x2="550" y2="190" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="180" x2="550" y2="180" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="130" y1="50" x2="210" y2="50" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="130" y1="70" x2="160" y2="70" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="160" y1="280" x2="380" y2="280" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="160" y1="70" x2="160" y2="280" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="130" y1="60" x2="210" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="70" y="30" style="font-family:Arial;font-size:11px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 70, 30)">AM2302</text>
+ <rect x="20" y="40" width="100" height="40" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <line x1="120" y1="50" x2="130" y2="50" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="116" y="50" style="font-family:Arial;font-size:11px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 116, 50)">DATA</text>
+ <line x1="120" y1="60" x2="130" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="116" y="60" style="font-family:Arial;font-size:11px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 116, 60)">GND</text>
+ <line x1="120" y1="70" x2="130" y2="70" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="116" y="70" style="font-family:Arial;font-size:11px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 116, 70)">VCC</text>
+ <text x="610" y="120" style="font-family:Arial;font-size:11px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 610, 120)">E-Paper display HAT</text>
+ <rect x="560" y="130" width="100" height="100" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <line x1="550" y1="140" x2="560" y2="140" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="140" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 140)">CS</text>
+ <line x1="550" y1="150" x2="560" y2="150" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="150" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 150)">DC</text>
+ <line x1="550" y1="160" x2="560" y2="160" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="160" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 160)">DIN</text>
+ <line x1="550" y1="170" x2="560" y2="170" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="170" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 170)">CLK</text>
+ <line x1="550" y1="180" x2="560" y2="180" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="180" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 180)">BUSY</text>
+ <line x1="550" y1="190" x2="560" y2="190" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="190" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 190)">RST</text>
+ <line x1="550" y1="200" x2="560" y2="200" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="200" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 200)">PWR</text>
+ <line x1="550" y1="210" x2="560" y2="210" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="210" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 210)">GND</text>
+ <line x1="550" y1="220" x2="560" y2="220" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="564" y="220" style="font-family:Arial;font-size:11px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 564, 220)">VCC</text>
+ <line x1="340" y1="80" x2="430" y2="80" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="70" x2="430" y2="70" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="60" x2="430" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="340" y1="50" x2="430" y2="50" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <line x1="470" y1="110" x2="520" y2="110" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:3" />
+ <path d="M 450,70 L 450,60 L 440,50 L 430,50" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 450,70 L 440,60 L 430,60" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 450,80 L 440,70 L 430,70" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 450,90 L 440,80 L 430,80" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 530,130 L 540,140 L 550,140" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 530,140 L 540,150 L 550,150" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 530,150 L 540,160 L 550,160" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <path d="M 530,150 L 530,160 L 540,170 L 550,170" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="544" y="135" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 544, 135)">1</text>
+ <text x="544" y="145" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 544, 145)">2</text>
+ <text x="544" y="155" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 544, 155)">3</text>
+ <text x="544" y="165" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 544, 165)">4</text>
+ <line x1="465" y1="105" x2="470" y2="115" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:1" />
+ <line x1="515" y1="105" x2="510" y2="115" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:1" />
+ <path d="M 475,110 L 460,110 L 450,100 L 450,70" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:3" />
+ <path d="M 505,110 L 520,110 L 530,120 L 530,150" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:3" />
+ <text x="433" y="45" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 433, 45)">1</text>
+ <text x="433" y="55" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 433, 55)">2</text>
+ <text x="433" y="65" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 433, 65)">3</text>
+ <text x="433" y="75" style="font-family:Arial;font-size:8px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 433, 75)">4</text>
+ <rect x="220" y="40" width="100" height="210" style="fill-opacity:0;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+ <text x="270" y="30" style="font-family:Arial;font-size:11px;text-anchor:middle" dominant-baseline="middle" transform="rotate(0, 270, 30)">ESP32 Mini NodeMCU D1</text>
+ <line x1="200" y1="50" x2="220" y2="50" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="224" y="50" style="font-family:Arial;font-size:10px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 224, 50)">IO19</text>
+ <text x="216" y="48" style="font-family:Arial;font-size:8px;text-anchor:end" dominant-baseline="baseline" transform="rotate(0, 216, 48)"></text>
+ <line x1="320" y1="50" x2="340" y2="50" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="50" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 50)">IO15</text>
+ <text x="324" y="48" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 48)"></text>
+ <line x1="200" y1="60" x2="220" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="224" y="60" style="font-family:Arial;font-size:10px;text-anchor:start" dominant-baseline="middle" transform="rotate(0, 224, 60)">GND</text>
+ <text x="216" y="58" style="font-family:Arial;font-size:8px;text-anchor:end" dominant-baseline="baseline" transform="rotate(0, 216, 58)"></text>
+ <line x1="320" y1="60" x2="340" y2="60" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="60" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 60)">IO27</text>
+ <text x="324" y="58" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 58)"></text>
+ <line x1="320" y1="70" x2="340" y2="70" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="70" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 70)">IO14</text>
+ <text x="324" y="68" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 68)"></text>
+ <line x1="320" y1="80" x2="340" y2="80" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="80" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 80)">IO13</text>
+ <text x="324" y="78" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 78)"></text>
+ <line x1="320" y1="180" x2="340" y2="180" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="180" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 180)">IO25</text>
+ <text x="324" y="178" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 178)"></text>
+ <line x1="320" y1="190" x2="340" y2="190" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="190" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 190)">IO26</text>
+ <text x="324" y="188" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 188)"></text>
+ <line x1="320" y1="200" x2="340" y2="200" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="200" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 200)">IO16</text>
+ <text x="324" y="198" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 198)"></text>
+ <line x1="320" y1="210" x2="340" y2="210" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="210" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 210)">GND</text>
+ <text x="324" y="208" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 208)"></text>
+ <line x1="320" y1="220" x2="340" y2="220" style="stroke:rgb(0, 0, 0);stroke-linecap:square;stroke-width:2" />
+ <text x="316" y="220" style="font-family:Arial;font-size:10px;text-anchor:end" dominant-baseline="middle" transform="rotate(0, 316, 220)">3V3</text>
+ <text x="324" y="218" style="font-family:Arial;font-size:8px;text-anchor:start" dominant-baseline="baseline" transform="rotate(0, 324, 218)"></text>
+ <ellipse cx="380" cy="220" rx="2" ry="2" style="fill-opacity:1;fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);stroke-width:2" />
+</svg> \ No newline at end of file
diff --git a/_log/_site/etlas/dash.jpg b/_log/_site/etlas/dash.jpg
new file mode 100644
index 0000000..cf4efc6
--- /dev/null
+++ b/_log/_site/etlas/dash.jpg
Binary files differ
diff --git a/_log/_site/etlas/etlas_arch.png b/_log/_site/etlas/etlas_arch.png
new file mode 100644
index 0000000..241e9f1
--- /dev/null
+++ b/_log/_site/etlas/etlas_arch.png
Binary files differ
diff --git a/_log/_site/etlas/pcb.jpg b/_log/_site/etlas/pcb.jpg
new file mode 100644
index 0000000..fcb40fa
--- /dev/null
+++ b/_log/_site/etlas/pcb.jpg
Binary files differ
diff --git a/_log/_site/etlas/schematic.svg b/_log/_site/etlas/schematic.svg
new file mode 100644
index 0000000..3070dd1
--- /dev/null
+++ b/_log/_site/etlas/schematic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Do not edit this file with editors other than diagrams.net -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" style="background-color: rgb(255, 255, 255);" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="741px" height="371px" viewBox="-0.5 -0.5 741 371" content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2024-01-04T10:11:48.655Z&quot; agent=&quot;Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0&quot; etag=&quot;QhztmAZcJcNMJFkbNl2E&quot; version=&quot;21.2.9&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Page-1&quot; id=&quot;Hvsc3-8LFRnBvTan2Q2q&quot;&gt;7VzbcuI4EP0aHkPZEr49gpNJqjbJpoYku5mXlMdWwLsGMbZIYL9+JVvyVYAJBpOBqtQgt2RJ7nPU6m7Z04H2ZHEdOrPxHfZQ0AGKt+jAyw4AqmIZ9IdJlolEs5REMAp9jzfKBEP/PyTu5NK576Go0JBgHBB/VhS6eDpFLinInDDEH8VmbzgojjpzRqgiGLpOUJX+5XtknEhNTcnkN8gfjcXIqsJrJo5ozAXR2PHwR04ErzrQDjEmSWmysFHAlCf0ktz3bUVtOrEQTUmdG66NwIeLl/HLD+sFfn8e2PbTjwutl3Tz7gRz/sR8tmQpVBDi+dRDrBelAwcfY5+g4cxxWe0HBZ3KxmQS0CuVFiMS4n+RjQMcUskUT2mzAR8DhQQtVs5eTXVCyYTwBJFwSZvwG3qgqyX3cCLpPSH4yHAxelzZ4xwm0OBCh3NhlPaeqYsWuMa20J4gaU5ZyKP04Zc4JGM8wlMnuMqkg6I6sza3GM+4Ev9BhCz5WnDmBBdVjBY++ZvfzsovufLlIn/BVKWJqwcU+vSxUShaTL0+WyAZTFTyzWca4PVURfE4XcvqCUE8WFftpYJsxPhqmb8qD5loi6loPQuoRvE8dNE61XNb4IQjRNa0s+SsClHgEP+9OA8ZP/itD9inM0zZCATPOBnVMsWSefG7MpZRhTvLXLMZaxCtGQdY0nEy0iY9ZhROn3EHVldNwNGx2ljNas5atchYoG0k7Prl8AW5u5ttg8fPAmtbFkBjMw0kduvMjdIke8fPDeqJbUmOs4XYkgXaF2AB3MyCrqLoRSboNXaLs5nYTBD9CxBkjX8sNxM1tpAzDfKD98w2aaDWo8EaV+Lo0DT2EtNoZjGmAfp+YhrNVAvjQPUQMU2nnOa4Gj5AJrrzpz79ucceurOfaIkyBlTj+jR7wzD3nGicctMJ/NGUll2KN2NNNRsiEkexZOzMWJeTxYglzLooQC4JfdcJugEe+e7ryCEo6v56m7367op0S+D8RMG9M+F5MRs+U1/drv5d318mBdWS1m/8Yzu8DQz2j84u2TDM97OBJmkPBzN/esvm9riMn9GdR6SZ7A9USvG2QCiX+wGA+yP53E+afWvcqmntxEdZUkYrbEsb9qQGrZRR00oBs81Nx6gs+Mubx12Tmw0wWdP1IpPNKpNVkZXNM9kw96Qpq2oaL2bUSIVUeNN/bF1jqlLcL1RNojFdkvdVyxtYYyoD1e2kagwyv8ENnCiKrXlOM0U11l2fVS3ltKBJlCBkOzoHKpBv2mkXidmoOAebO6qZOW3KEQBtm21TLQSb6gbD3XagKZh93CGGmOW6BRn7pSi8ekfMPU00zwwT871ir+UBRz7xMfPlfmJC8CRx1z7h9olu+7wNYTRZ5/pFtJ0TvCZoRN1ZiAmto2p7RU7IlncThhSoxcMEKDGkUh9qb2a0evgIu/C5ghx9ZCLzq0unjG90AZREFZDKyEx8z4uXuGwfk5nonRCwVti+HACyjQzuzYdVK7o+iDE8nPXSd7RK8k1Mh0Unjsa0tTaxbUNloxT2iHH2GioD/WxLN65k3TSLSxm2bUvNE7OlujigXIPAQY2p6Ph0goLUgShDsG1QUOnIPGxQAKvx3O+9eCAoKdywWl487Zwop2HZ6pCMnwwcVVAG6wZlK1hwmKBMzPLsSKx7r9EoORJG9a3GgzoSUJOgpgfM7r3h2Ghn8Om/5lhUXETxKuvTBtZsEStHVNPSiP3aQ9ETnVnSWVLx+xpaIF6VSZOX9QytSFM1D2/VT2wK3kv7DG/NfXR/8Faz+U3B+334eIL4giK+Vsv49mRBRkPW+faPk8eXAiz5quCwCMvOlppB+O7P4cvpQawUHSwgdsDWAJa5xc0APHg6SYCLxxqgbRerB/cG8LN9ej5W6dAEmPUM9N5SFT3ZJ3PNwBu/sHVi8AJFLX3Z1z7C+wuBT3EBlzP1QCT7WsNXdsJ1XsGfPwvTy/hKnGjxJeNhEK6+/tdYlqP/2D85iDVdK0NcARhKspSfAJheZt/EJwc82f8sAK/+Bw==&lt;/diagram&gt;&lt;/mxfile&gt;"><defs/><g><rect x="0.5" y="0.5" width="740" height="370" fill="rgb(255, 255, 255)" stroke="none" pointer-events="all"/><path d="M 258 106 L 187 106 L 187 109 L 218.04 109.04" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 258 126 L 219 126" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 258 146 L 219 146" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 258 166 L 219 166" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 258 186 L 219.96 186" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 258 206 L 219 206" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 483 246 L 539 246 L 539 194" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 273 66 L 468 66 L 473 71 L 473 261 L 468 266 L 273 266 L 268 261 L 268 71 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="277.5" y="109.3">15</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="277.5" y="129.3">27</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="277.5" y="149.3">26</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="277.5" y="169.3">13</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="277.5" y="189.3">14</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="277.5" y="209.3">25</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="462.5" y="229.3">19</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="307.5" y="259.3">3V3</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"><text x="447.5" y="259.3">GND</text></g><g fill="rgb(0, 0, 0)" font-family="Arial,Helvetica" text-anchor="middle" font-size="9.600000000000001px"/><path d="M 258 86 L 268 86 M 473 86 L 483 86 M 258 106 L 268 106 M 473 106 L 483 106 M 258 126 L 268 126 M 473 126 L 483 126 M 258 146 L 268 146 M 473 146 L 483 146 M 258 166 L 268 166 M 473 166 L 483 166 M 258 186 L 268 186 M 473 186 L 483 186 M 258 206 L 268 206 M 473 206 L 483 206 M 258 226 L 268 226 M 473 226 L 483 226 M 258 246 L 268 246 M 473 246 L 483 246 M 288 56 L 288 66 M 288 266 L 288 276 M 308 56 L 308 66 M 308 266 L 308 276 M 328 56 L 328 66 M 328 266 L 328 276 M 348 56 L 348 66 M 348 266 L 348 276 M 368 56 L 368 66 M 368 266 L 368 276 M 388 56 L 388 66 M 388 266 L 388 276 M 408 56 L 408 66 M 408 266 L 408 276 M 428 56 L 428 66 M 428 266 L 428 276 M 448 56 L 448 66 M 448 266 L 448 276" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><ellipse cx="278" cy="256" rx="5" ry="5" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 223px; height: 1px; padding-top: 166px; margin-left: 259px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">ESP32 Mini NodeMCU D1 </div></div></div></foreignObject><text x="371" y="170" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">ESP32 Mini NodeMCU D1 </text></switch></g><path d="M 601 194 L 601 259.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 601 264.88 L 597.5 257.88 L 601 259.63 L 604.5 257.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="524" y="116" width="154" height="78" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 152px; height: 1px; padding-top: 155px; margin-left: 525px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">DHT22</div></div></div></foreignObject><text x="601" y="159" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">DHT22</text></switch></g><rect x="59" y="86" width="160" height="160" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 166px; margin-left: 60px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">E-paper HAT</div></div></div></foreignObject><text x="139" y="170" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">E-paper HAT</text></switch></g><path d="M 79 246 L 79 299.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 79 304.88 L 75.5 297.88 L 79 299.63 L 82.5 297.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 189.5 286 L 189.5 266 L 189.56 246" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><rect x="177" y="286" width="25" height="20" fill="none" stroke="none" pointer-events="all"/><path d="M 177 291 L 202 291 M 179 293.5 L 200 293.5 M 181 296 L 198 296 M 185.25 301 L 193.75 301 M 187.25 303.5 L 191.75 303.5 M 189.5 286 L 189.5 291 M 183.25 298.5 L 195.75 298.5 M 189.25 306 L 189.75 306" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="49" y="306" width="60" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 321px; margin-left: 50px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">3.3V</div></div></div></foreignObject><text x="79" y="325" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">3.3V</text></switch></g><path d="M 658.5 266 L 658.5 194 L 594 194" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><rect x="646" y="266" width="25" height="20" fill="none" stroke="none" pointer-events="all"/><path d="M 646 271 L 671 271 M 648 273.5 L 669 273.5 M 650 276 L 667 276 M 654.25 281 L 662.75 281 M 656.25 283.5 L 660.75 283.5 M 658.5 266 L 658.5 271 M 652.25 278.5 L 664.75 278.5 M 658.25 286 L 658.75 286" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="571" y="266" width="60" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 281px; margin-left: 572px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">3.3V</div></div></div></foreignObject><text x="601" y="285" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">3.3V</text></switch></g><path d="M 308 266 L 308 309.63" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><path d="M 308 314.88 L 304.5 307.88 L 308 309.63 L 311.5 307.88 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="278" y="315" width="60" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 330px; margin-left: 279px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">3.3V</div></div></div></foreignObject><text x="308" y="334" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">3.3V</text></switch></g><path d="M 448.5 311 L 448.5 291 L 448 276" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/><rect x="436" y="311" width="25" height="20" fill="none" stroke="none" pointer-events="all"/><path d="M 436 316 L 461 316 M 438 318.5 L 459 318.5 M 440 321 L 457 321 M 444.25 326 L 452.75 326 M 446.25 328.5 L 450.75 328.5 M 448.5 311 L 448.5 316 M 442.25 323.5 L 454.75 323.5 M 448.25 331 L 448.75 331" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="172" y="95" width="60" height="23" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 107px; margin-left: 173px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">CS</font></div></div></div></foreignObject><text x="202" y="110" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">CS</text></switch></g><rect x="172" y="115" width="60" height="23" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 127px; margin-left: 173px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">DC</font></div></div></div></foreignObject><text x="202" y="130" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">DC</text></switch></g><rect x="170" y="135" width="60" height="23" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 147px; margin-left: 171px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">RST</font></div></div></div></foreignObject><text x="200" y="150" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">RST</text></switch></g><rect x="170" y="155.5" width="60" height="23" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 167px; margin-left: 171px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">CLK</font></div></div></div></foreignObject><text x="200" y="171" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">CLK</text></switch></g><rect x="166" y="174" width="60" height="23" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 186px; margin-left: 167px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">MOSY</font></div></div></div></foreignObject><text x="196" y="189" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">MOSY</text></switch></g><rect x="167" y="195" width="60" height="23" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 207px; margin-left: 168px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">BUSY</font></div></div></div></foreignObject><text x="197" y="210" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">BUSY</text></switch></g><rect x="49" y="221" width="60" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 236px; margin-left: 50px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">VCC</font></div></div></div></foreignObject><text x="79" y="240" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">VCC</text></switch></g><rect x="159.5" y="221" width="60" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 236px; margin-left: 161px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">GND</font></div></div></div></foreignObject><text x="190" y="240" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">GND</text></switch></g><rect x="571" y="170" width="60" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 58px; height: 1px; padding-top: 185px; margin-left: 572px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">VCC</font></div></div></div></foreignObject><text x="601" y="189" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">VCC</text></switch></g><rect x="644" y="170.5" width="29" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 27px; height: 1px; padding-top: 186px; margin-left: 645px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">GND</font></div></div></div></foreignObject><text x="659" y="189" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">GND</text></switch></g><rect x="523" y="170" width="35" height="30" fill="none" stroke="none" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 33px; height: 1px; padding-top: 185px; margin-left: 524px;"><div style="box-sizing: border-box; font-size: 0px; text-align: center;" data-drawio-colors="color: rgb(0, 0, 0); "><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;"><font style="font-size: 9px;">DATA</font></div></div></div></foreignObject><text x="541" y="189" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">DATA</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.drawio.com/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg> \ No newline at end of file
diff --git a/_log/_site/etlas/source.tar.gz b/_log/_site/etlas/source.tar.gz
new file mode 100644
index 0000000..8b12cf6
--- /dev/null
+++ b/_log/_site/etlas/source.tar.gz
Binary files differ
diff --git a/_log/_site/etlas/thumb_sm.jpg b/_log/_site/etlas/thumb_sm.jpg
new file mode 100644
index 0000000..a374879
--- /dev/null
+++ b/_log/_site/etlas/thumb_sm.jpg
Binary files differ
diff --git a/_log/_site/feed.xml b/_log/_site/feed.xml
new file mode 100644
index 0000000..ab90c82
--- /dev/null
+++ b/_log/_site/feed.xml
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-12-24T08:01:11+08:00</updated><id>/feed.xml</id></feed> \ No newline at end of file
diff --git a/_log/_site/fpm-door-lock.html b/_log/_site/fpm-door-lock.html
new file mode 100644
index 0000000..820af3f
--- /dev/null
+++ b/_log/_site/fpm-door-lock.html
@@ -0,0 +1,105 @@
+<p>This project features a fingerprint door lock powered by an ATmega328P
+microcontroller.</p>
+
+<video style="max-width:100%;" controls="" poster="pcb.jpg">
+ <source src="video.mp4" type="video/mp4" />
+</video>
+
+<h2 id="overview">Overview</h2>
+
+<p>The lock comprises three subsystems: the ATmega328P microcontroller, an R503
+fingerprint sensor, and an FS5106B high-torque servo. The sensor mounted on the
+front surface of the door enables users to unlock it from the outside. The
+servo is attached to the interior door knob. The MCU must be installed at the
+back of the door to prevent unauthorized users from tampering with it.</p>
+
+<p>When no one is interacting with the lock, the MCU is in deep sleep. The sensor
+and the servo each draw 13.8mA and 4.6mA of quiescent currents. To prevent this
+idle current draw, the MCU employs MOSFETs to cut off power to them before
+entering deep sleep. Doing so is crucial for conserving the battery.</p>
+
+<p>Without power, the sensor remains in a low-power state, drawing approximately
+2.9μA through a separate power rail. When a finger comes into contact with the
+sensor, the sensor triggers a pin change interrupt, waking up the MCU. The MCU
+activates a MOSFET, which in turn activates the sensor. Over UART, the MCU
+unlocks the sensor and issues commands to scan and match the fingerprint.</p>
+
+<p>If the fingerprint matches an enrolled fingerprint, the MCU activates the blue
+LED on the sensor, turns on the MOSFET connected to the servo, and sends a PWM
+signal to the servo to unlock the door. Otherwise, the MCU activates the red
+LED on the sensor. Finally, the MCU deactivates the MOSFETS and goes back to
+sleep.</p>
+
+<h2 id="embedded-software">Embedded software</h2>
+
+<p>The embedded software, written in C, includes a driver for the sensor, servo
+control routines, and a battery monitoring system.</p>
+
+<p>In addition to controlling the sensor and the servo, the program strives to
+maintain precise control over the microcontroller’s sleep modes, as well as
+when the peripherals are activated and for how long they remain active. I
+thoroughly enjoyed writing the embedded software. There’s something magical
+about being able to alter the physical world around you by uttering a few lines
+of C code.</p>
+
+<p>The source code of the project, which includes a driver for the R503
+fingerprint sensor module, is enclosed in the tarball linked at the end of the
+page.</p>
+
+<h2 id="the-pcb">The PCB</h2>
+
+<p>For this project, I designed a custom PCB and had it fabricated by JLCPCB. Like
+the software, the circuit is primarily concerned with optimizing power
+consumption and extending battery life.</p>
+
+<table style="border: none; width: 100%">
+ <tr style="border: none;">
+ <td style="border: none; width: 49.9%; background-color: transparent; text-align: center;">
+ <img src="breadboard.jpg" alt="PCB" style="width: 100%" />
+ </td>
+ <td style="border: none; background-color: transparent; text-align: center;">
+ <img src="pcb1.jpg" alt="Design" style="width: 100%" />
+ </td>
+ </tr>
+ <tr style="border: none;">
+ <td colspan="2" style="border: none; background-color: transparent; text-align: center;">
+ <img src="footprint.png" alt="PCB footprint" style="width: 100%" />
+ </td>
+ </tr>
+</table>
+
+<p>Consequently, the principal components of the circuit are the 2N7000 and
+NDP6020P field-effect transistors. They switch power electronically to the
+servo and the fingerprint sensor, the two most power-hungry parts of the
+circuit. The two MP1584EN buck converters play an axial role in efficiently
+regulating power to the MCU and the sensor.</p>
+
+<p>The ATmega328P typically operates at 5V with a 16MHz crystal oscillator. To
+further reduce power consumption, I modified the ATmega328P’s fuses to run at
+3.3V with an 8MHz crystal oscillator.</p>
+
+<p>The bottom right area of the PCB isolates the power supply of the servo from
+the rest of the circuit. This shields components such as the MCU from the
+servo’s high current draw, which can exceed 1A. The IN4007 diode in slot U2
+serves as a flyback diode, protecting the MOSFET from reverse currents
+generated by the servo.</p>
+
+<p>Lastly, the 56kΩ and 10kΩ resistors in slots R10 and R11 form a voltage divider
+circuit. Its output is fed to the ADC of the MCU, which measures the supply
+voltage by comparing it to the internal bandgap reference voltage.</p>
+
+<h2 id="epilogue">Epilogue</h2>
+
+<p>This project began nearly a year ago when I attempted to unlock our door
+wirelessly by writing to the UART ports of two MCUs connected to inexpensive
+433MHz RF transceivers. Although I failed, it led me down a rabbit hole of RF
+communications, MOSFETs, PCB design, and low-power circuits.</p>
+
+<p>During the project, I reinvented the wheel many times. I implemented a
+low-level network stack using only RF modules and an 8-bit microcontroller,
+designed my first PCB, and developed drivers from scratch. The project was far
+from a smooth sail. Bad electrical connections, soldering and desoldering, and
+the heartache of purchasing the wrong parts were routine. It was a long but
+rewarding journey from the messy breadboard to the shiny PCB.</p>
+
+<p>Files: <a href="source.tar.gz">source.tar.gz</a>, <a href="gerber.zip">gerber.zip</a></p>
diff --git a/_log/_site/fpm-door-lock/breadboard.jpg b/_log/_site/fpm-door-lock/breadboard.jpg
new file mode 100644
index 0000000..2bf47a9
--- /dev/null
+++ b/_log/_site/fpm-door-lock/breadboard.jpg
Binary files differ
diff --git a/_log/_site/fpm-door-lock/footprint.png b/_log/_site/fpm-door-lock/footprint.png
new file mode 100644
index 0000000..5511bf1
--- /dev/null
+++ b/_log/_site/fpm-door-lock/footprint.png
Binary files differ
diff --git a/_log/_site/fpm-door-lock/gerber.zip b/_log/_site/fpm-door-lock/gerber.zip
new file mode 100644
index 0000000..19a9d19
--- /dev/null
+++ b/_log/_site/fpm-door-lock/gerber.zip
Binary files differ
diff --git a/_log/_site/fpm-door-lock/pcb.jpg b/_log/_site/fpm-door-lock/pcb.jpg
new file mode 100644
index 0000000..fbd800b
--- /dev/null
+++ b/_log/_site/fpm-door-lock/pcb.jpg
Binary files differ
diff --git a/_log/_site/fpm-door-lock/pcb1.jpg b/_log/_site/fpm-door-lock/pcb1.jpg
new file mode 100644
index 0000000..367187d
--- /dev/null
+++ b/_log/_site/fpm-door-lock/pcb1.jpg
Binary files differ
diff --git a/_log/_site/fpm-door-lock/source.tar.gz b/_log/_site/fpm-door-lock/source.tar.gz
new file mode 100644
index 0000000..ef23422
--- /dev/null
+++ b/_log/_site/fpm-door-lock/source.tar.gz
Binary files differ
diff --git a/_log/_site/fpm-door-lock/thumb_sm.jpg b/_log/_site/fpm-door-lock/thumb_sm.jpg
new file mode 100644
index 0000000..a8fa534
--- /dev/null
+++ b/_log/_site/fpm-door-lock/thumb_sm.jpg
Binary files differ
diff --git a/_log/_site/fpm-door-lock/video.mp4 b/_log/_site/fpm-door-lock/video.mp4
new file mode 100644
index 0000000..a907a9b
--- /dev/null
+++ b/_log/_site/fpm-door-lock/video.mp4
Binary files differ
diff --git a/_log/_site/matrix-digital-rain.html b/_log/_site/matrix-digital-rain.html
new file mode 100644
index 0000000..85bac5d
--- /dev/null
+++ b/_log/_site/matrix-digital-rain.html
@@ -0,0 +1,103 @@
+<p>The Matrix digital rain implemented in raw C using ANSI escape sequences with
+zero dependencies—not even ncurses.</p>
+
+<video style="max-width:100%;" controls="" poster="poster.png">
+ <source src="matrix.mp4" type="video/mp4" />
+</video>
+
+<p>This project began over three years ago as a fork of Domsson’s unique rendition
+of the Matrix rain: <a href="https://github.com/domsson/fakesteak" class="external" target="_blank" rel="noopener noreferrer">Fakesteak</a>. I
+aimed to modify the algorithm to produce a rain that resembled the original
+with high visual fidelity.</p>
+
+<h2 id="unicode-support">Unicode support</h2>
+
+<p>Unicode support in the 2022 version lacked flexibility. The charset used in the
+rain had to be a single contiguous block defined by <code class="language-plaintext highlighter-rouge">UNICODE_MIN</code> and
+<code class="language-plaintext highlighter-rouge">UNICODE_MAX</code> settings:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define UNICODE_MIN 0x0021
+#define UNICODE_MAX 0x007E
+
+static inline void insert_code(matrix *mat,
+ size_t row, size_t col)
+{
+ mat-&gt;code[index(mat, row, col)] = rand()
+ % (UNICODE_MAX - UNICODE_MIN)
+ + UNICODE_MIN;
+}
+</code></pre></div></div>
+
+<p>There was no way, for instance, to use both ASCII and Katakana at the same
+time. The user had to pick one. In the new version, the user can use any number
+of Unicode blocks using <code class="language-plaintext highlighter-rouge">glyphs</code> array. In fact, the default rain now includes
+both ASCII and half-width Katakana characters:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define UNICODE(min, max) (((uint64_t)max &lt;&lt; 32) | min)
+
+static uint64_t glyphs[] = {
+ UNICODE(0x0021, 0x007E), /* ASCII */
+ UNICODE(0xFF65, 0xFF9F), /* Half-width Katakana */
+};
+
+static uint8_t glyphlen = (sizeof glyphs) / (sizeof glyphs[0]);
+
+static inline void insert_code(matrix *mat,
+ size_t row, size_t col)
+{
+ uint64_t block;
+ uint32_t unicode_min, unicode_max;
+
+ block = glyphs[(rand() % glyphlen)];
+ unicode_min = (uint32_t)block;
+ unicode_max = (uint32_t)(block &gt;&gt; 32);
+
+ mat-&gt;code[index(mat, row, col)] = rand()
+ % (unicode_max - unicode_min)
+ + unicode_min;
+}
+</code></pre></div></div>
+
+<p>Entries in the <code class="language-plaintext highlighter-rouge">glyphs</code> array are Unicode blocks bit-packed in an 8-byte
+container: the four low bytes forms the first codepoint and the four high bytes
+the last.</p>
+
+<h2 id="phosphor-decay">Phosphor decay</h2>
+
+<p>The dim afterglow of monochrome CRT displays is achieved by carefully scaling
+the RGB channels individually and mixing them:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define DECAY_MPLIER 2
+
+static inline void blend(matrix *mat,
+ size_t row, size_t col)
+{
+ unsigned char *color;
+
+ color = mat-&gt;rgb[index(mat, row, col)].color;
+ color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
+ color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
+ color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
+}
+</code></pre></div></div>
+
+<p>The blending function emulates the phosphor decay by gradually transitioning
+each raindrop’s color towards the background color. The multiplier is the
+number of passes over the rain track needed before the afterglow disappears.</p>
+
+<p>Nonetheless, the rain resembles the original with high visual fidelity. It’s
+highly customizable and gentle on the CPU. On my 14” ThinkPad T490, which has a
+resolution of 1920x1080 and 4GHz CPU, it uses 2-3% of the CPU with occasional
+jumps of up to about 8%. Not too bad for a weekend project. The program has
+been tested with xterm and urxvt terminal emulators on OpenBSD and Arch Linux
+systems. Someone has managed to get it moving on a Raspberry Pi as well.</p>
+
+<p>Lastly, to compile and run:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cc -O3 main.c -o matrix
+$ ./matrix
+</code></pre></div></div>
+
+<p>“All I see is blonde, brunette, red head.”</p>
+
+<p>Files: <a href="source.tar.gz">source.tar.gz</a></p>
diff --git a/_log/_site/matrix-digital-rain/katakana.png b/_log/_site/matrix-digital-rain/katakana.png
new file mode 100644
index 0000000..b9df873
--- /dev/null
+++ b/_log/_site/matrix-digital-rain/katakana.png
Binary files differ
diff --git a/_log/_site/matrix-digital-rain/matrix.mp4 b/_log/_site/matrix-digital-rain/matrix.mp4
new file mode 100644
index 0000000..7edf5d6
--- /dev/null
+++ b/_log/_site/matrix-digital-rain/matrix.mp4
Binary files differ
diff --git a/_log/_site/matrix-digital-rain/poster.png b/_log/_site/matrix-digital-rain/poster.png
new file mode 100644
index 0000000..1f68ca4
--- /dev/null
+++ b/_log/_site/matrix-digital-rain/poster.png
Binary files differ
diff --git a/_log/_site/matrix-digital-rain/source.tar.gz b/_log/_site/matrix-digital-rain/source.tar.gz
new file mode 100644
index 0000000..5a69236
--- /dev/null
+++ b/_log/_site/matrix-digital-rain/source.tar.gz
Binary files differ
diff --git a/_log/_site/matrix-digital-rain/thumb_sm.png b/_log/_site/matrix-digital-rain/thumb_sm.png
new file mode 100644
index 0000000..940965a
--- /dev/null
+++ b/_log/_site/matrix-digital-rain/thumb_sm.png
Binary files differ
diff --git a/_log/_site/mosfet-switches.html b/_log/_site/mosfet-switches.html
new file mode 100644
index 0000000..c1a7b52
--- /dev/null
+++ b/_log/_site/mosfet-switches.html
@@ -0,0 +1,110 @@
+<p>Recently, I needed a low-power circuit for one of my battery-operated projects.
+Much of the system’s power savings depended on its ability to electronically
+switch off components, such as servos, that draw high levels of quiescent
+currents. My search for a solution led me to MOSFETs, transistors capable of
+controlling circuits operating at voltages far above their own.</p>
+
+<h2 id="acknowledgments">Acknowledgments</h2>
+
+<p>This article is a summary of what I learnt about using MOSFETs as switches.
+I’m not an electronics engineer, and this is not an authoritative guide. The
+circuits in this post must be considered within the narrow context in which
+I’ve used them. All credits for the schematics belong to <a href="https://electronics.stackexchange.com/users/292884/simon-fitch" class="external" target="_blank" rel="noopener noreferrer">Simon Fitch</a>.</p>
+
+<h2 id="preamble">Preamble</h2>
+
+<p>For a typical MOSFET-based switch, we can connect a GPIO pin of a
+microcontroller to the gate of a logic-level N-channel MOSFET placed on the low
+side of the load and tie the gate and the drain pins of the MOSFET with a
+pull-down resistor. This would work as long as the power supplies of the
+microcontroller and the load don’t share a common ground. Things become more
+complicated when they do (e.g., controlling power to a component driven by the
+same microcontroller).</p>
+
+<p>In that scenario, the source potential visible to the load is the difference
+between the gate and the threshold potentials of the MOSFET. For example, when
+the gate and the threshold potentials are 3.3 V and 1.5 V, the potential the
+load sees is 1.8 V. So, to use a low-side N-channel MOSFET, we need the gate
+potential to be higher than the source potential, which may not always be
+practical. The alternative would be a hide-side switch.</p>
+
+<h2 id="p-channel-high-side-switch">P-channel high-side switch</h2>
+
+<p>The following schematic shows how a high-side P-channel MOSFET (M1) could
+switch power to a 6 V servo driven by a 3.3 V MCU.</p>
+
+<p><img src="p_high_side.png" alt="P-channel high-side switching circuit" /></p>
+
+<p>When the microcontroller outputs low, the M2 N-channel MOSFET stops conducting.
+The R1 resistor pulls the gate of the M1 P-channel MOSFET up to +6 V, switching
+the servo off. When the microcontroller outputs high on the GPIO pin, M2’s
+source-drain connection starts conducting, causing M1’s gate potential to drop
+to 0 V, which switches on power to the servo.</p>
+
+<h2 id="n-channel-high-side-switch">N-channel high-side switch</h2>
+
+<p>The P-channel high-side switch would be the typical architecture for our use
+case. However, if we have access to a potential high enough to safely raise the
+gate potential above the threshold such that their difference outputs the source
+potential required to drive the load, we can switch on the high side using an
+N-channel MOSFET:</p>
+
+<p><img src="n_high_side.png" alt="N-channel high-side switching circuit" /></p>
+
+<p>In the schematic, both M1 and M2 are N-channel MOSFETs. When the
+microcontroller output is low, M2 stops conducting. This causes the M1’s gate
+potential to rise above the threshold, turning the servo on. Conversely, a high
+output on the GPIO line switches M2 on, which lowers M1’s gate potential. This
+switches the servo off. The R2 pull-up resistor prevents the high impedance of
+the output pins at power-up from switching the servo on.</p>
+
+<p>Both topologies require M2 to act as a level converter between circuits
+containing the microcontroller and the servo, converting between 0 V and +6 V
+or +9 V. M2 is a low-power signal converter carrying less than a milliamp of
+current. The gate-source threshold voltage of M2 must be lower than the MCU’s
+supply voltage. 2N7000, 2N7002, and BSS138 are popular choices for M2.</p>
+
+<p>The D1 flyback diodes used in the two topologies safeguard the MOSFET from
+voltage spikes caused by inductive loads such as servos.</p>
+
+<h2 id="a-bjt-alternative">A BJT alternative</h2>
+
+<p>A Bipolar Junction Transistor (BJT) is a simpler, cheaper, and more widely
+available type of transistor that can be used as a switch.</p>
+
+<p><img src="bjt.png" alt="BJT architecture" /></p>
+
+<p>In the schematic, when the MCU outputs high, Q2 starts conducting. Q2 amplifies
+Q1’s base current. Unlike MOSFETs, which are voltage-driven, BJTs are driven by
+base current. Resistors R3 and R4 must be chosen carefully to output the
+desired base currents. <a href="https://teachmetomake.wordpress.com/how-to-use-a-transistor-as-a-switch/" class="external" target="_blank" rel="noopener noreferrer">“How to choose a
+transistor as a switch”</a> is an excellent guide on using BJTs as electronic
+switches.</p>
+
+<h2 id="which-topology-to-choose">Which topology to choose?</h2>
+
+<p>The professional community appears to prefer MOSFETs over BJTs. MOSFETs are
+more efficient when the switch is on. However, they are more challenging to
+drive, especially with a 3.3 V MCU, due to the V<sub>GS</sub> potentials
+required to achieve specified R<sub>DS(on)</sub> values (i.e., to turn them on
+fully).</p>
+
+<p>N-channel MOSFETs have lower on-resistance values, making them more efficient
+than P-channel ones. They are also cheaper. However, they are harder to drive
+on the high side as their gate potential must be higher than the source
+potential. This often requires extra circuitry such as MOSFET drivers.</p>
+
+<h2 id="further-reading">Further reading</h2>
+
+<ul>
+ <li><a href="https://www.embeddedrelated.com/showarticle/98.php" class="external" target="_blank" rel="noopener noreferrer">Different MOSFET
+topologies</a></li>
+ <li><a href="https://www.embeddedrelated.com/showarticle/809.php" class="external" target="_blank" rel="noopener noreferrer">How to read
+MOSFET datasheets</a></li>
+ <li><a src="https://teachmetomake.wordpress.com/how-to-use-a-transistor-as-a-switch/" class="external" target="_blank" rel="noopener noreferrer">How to use a
+transistor as a switch</a></li>
+ <li><a src="https://forum.digikey.com/t/guide-to-selecting-and-controlling-a-mosfet-for-3-3-vdc-logic-applications/42606" class="external" target="_blank" rel="noopener noreferrer">Guide to
+selecting and controlling a MOSFET for 3.3 VDC logic applications</a></li>
+ <li><a src="https://forum.digikey.com/t/driving-a-large-relay-from-a-3-3-vdc-microcontroller-using-an-npn-darlington-transistor/41751" class="external" target="_blank" rel="noopener noreferrer">Driving a large
+relay from a 3.3 VDC microcontroller using an NPN Darlington transistor</a></li>
+</ul>
diff --git a/_log/_site/mosfet-switches/bjt.png b/_log/_site/mosfet-switches/bjt.png
new file mode 100644
index 0000000..9858fa7
--- /dev/null
+++ b/_log/_site/mosfet-switches/bjt.png
Binary files differ
diff --git a/_log/_site/mosfet-switches/n_high_side.png b/_log/_site/mosfet-switches/n_high_side.png
new file mode 100644
index 0000000..c851768
--- /dev/null
+++ b/_log/_site/mosfet-switches/n_high_side.png
Binary files differ
diff --git a/_log/_site/mosfet-switches/p_high_side.png b/_log/_site/mosfet-switches/p_high_side.png
new file mode 100644
index 0000000..9f5397a
--- /dev/null
+++ b/_log/_site/mosfet-switches/p_high_side.png
Binary files differ
diff --git a/_log/_site/my-first-pcb.html b/_log/_site/my-first-pcb.html
new file mode 100644
index 0000000..d5e886b
--- /dev/null
+++ b/_log/_site/my-first-pcb.html
@@ -0,0 +1,57 @@
+<p>In 2023, I started tinkering with DIY electronics as a hobby. Until now, I’ve
+been using development boards like the Arduino Uno and ESP-32-WROOM so that I
+can focus on the software. Recently, I decided to step outside of my comfort
+zone and design a PCB from scratch for a door lock I’m working on.</p>
+
+<p>The lock comprises two subsystems: a fingerprint sensor in front of the door
+and a servo connected to the physical lock behind the door. The fingerprint
+sensor authenticates the person and signals the servo behind the door to unlock
+the door over an encrypted RF channel.</p>
+
+<table style="border: none; width: 100%">
+ <tr style="border: none;">
+ <td style="border: none; width: 49.5%; vertical-align: top; text-align: center;">
+ <img src="front_design.jpeg" alt="Design (front)" style="width: 100%" />
+ <p>Footprint (front)</p>
+ </td>
+ <td style="border: none; vertical-align: top; text-align: center;">
+ <img src="front.jpeg" alt="PCB (front)" style="width: 100%" />
+ <p>PCB (front)</p>
+ </td>
+ </tr>
+ <tr style="border: none;">
+ <td style="border: none; width: 49.5%; vertical-align: top; text-align: center;">
+ <img src="back_design.jpeg" alt="Design (back)" style="width: 100%" />
+ <p>Footprint (back)</p>
+ </td>
+ <td style="border: none; vertical-align: top; text-align: center;">
+ <img src="back.jpeg" alt="PCB (back)" style="width: 100%" />
+ <p>PCB (back)</p>
+ </td>
+ </tr>
+</table>
+
+<p>The PCBs have two layers. A copper region serves as the ground plane. The 0.3mm
+wide 1oz/ft<sup>2</sup> copper traces can carry up to 500mA (the tracks
+connecting the power source and the linear regulators have a width of 0.5mm).
+Both subsystems were functional. I was able to control the servo reliably using
+the fingerprint sensor.</p>
+
+<p>The designs aren’t without flaws, however. The main shortcoming of the circuits
+is that they draw significant amounts of quiescent currents despite employing
+sleep modes. The linear regulators were a poor choice as they dissipate too
+much heat. The fingerprint sensor and the servo draw 13.8mA (3.3V) and 4.6mA
+(5V) respectively, as long as they are connected to the power supply.</p>
+
+<p>Although the circuit didn’t draw more than 200mA without a load, the servo
+under load could draw up to 600mA. I’m sailing too close to the wind with 0.3mm
+copper traces. Instead, 0.4mm wide 2oz/ft<sup>2</sup> traces would have been
+safer.</p>
+
+<p>I’m working on improving the design to reduce idle current consumption and
+extend the battery life. Despite its deficiencies, this was my first PCB
+design, and I’m glad that it worked as well as it did. Custom PCB design marks
+an important milestone in my DIY electronics journey.</p>
+
+<p>Files: <a href="gerber_back.zip">gerber_back.zip</a>, <a href="gerber_front.zip">gerber_front.zip</a>,
+ <a href="source.tar.gz">source.tar.gz</a></p>
diff --git a/_log/_site/my-first-pcb/back.jpeg b/_log/_site/my-first-pcb/back.jpeg
new file mode 100644
index 0000000..f458e69
--- /dev/null
+++ b/_log/_site/my-first-pcb/back.jpeg
Binary files differ
diff --git a/_log/_site/my-first-pcb/back_design.jpeg b/_log/_site/my-first-pcb/back_design.jpeg
new file mode 100644
index 0000000..b6c0f5d
--- /dev/null
+++ b/_log/_site/my-first-pcb/back_design.jpeg
Binary files differ
diff --git a/_log/_site/my-first-pcb/front.jpeg b/_log/_site/my-first-pcb/front.jpeg
new file mode 100644
index 0000000..2b2931f
--- /dev/null
+++ b/_log/_site/my-first-pcb/front.jpeg
Binary files differ
diff --git a/_log/_site/my-first-pcb/front_design.jpeg b/_log/_site/my-first-pcb/front_design.jpeg
new file mode 100644
index 0000000..f81f09c
--- /dev/null
+++ b/_log/_site/my-first-pcb/front_design.jpeg
Binary files differ
diff --git a/_log/_site/my-first-pcb/gerber_back.zip b/_log/_site/my-first-pcb/gerber_back.zip
new file mode 100644
index 0000000..26659ad
--- /dev/null
+++ b/_log/_site/my-first-pcb/gerber_back.zip
Binary files differ
diff --git a/_log/_site/my-first-pcb/gerber_front.zip b/_log/_site/my-first-pcb/gerber_front.zip
new file mode 100644
index 0000000..864334e
--- /dev/null
+++ b/_log/_site/my-first-pcb/gerber_front.zip
Binary files differ
diff --git a/_log/_site/my-first-pcb/source.tar.gz b/_log/_site/my-first-pcb/source.tar.gz
new file mode 100644
index 0000000..c31aa22
--- /dev/null
+++ b/_log/_site/my-first-pcb/source.tar.gz
Binary files differ
diff --git a/_log/_site/my-first-pcb/thumb_sm.jpeg b/_log/_site/my-first-pcb/thumb_sm.jpeg
new file mode 100644
index 0000000..c275b12
--- /dev/null
+++ b/_log/_site/my-first-pcb/thumb_sm.jpeg
Binary files differ
diff --git a/_log/_site/neo4j-a-star-search.html b/_log/_site/neo4j-a-star-search.html
new file mode 100644
index 0000000..9903abc
--- /dev/null
+++ b/_log/_site/neo4j-a-star-search.html
@@ -0,0 +1,307 @@
+<p>Back in 2018, we used <a href="https://neo4j.com/" class="external" target="_blank" rel="noopener noreferrer">Neo4J</a> graph database to track the
+movement of marine vessels. We were interested in the shortest path a ship
+could take through a network of about 13,000 route points. Graph theoretic
+algorithms provide optimal solutions to such problems, and the set of route
+points lends itself well to graph-based modelling.</p>
+
+<p>A graph is a finite set of vertices, and a subset of vertex pairs (edges).
+Edges can have weights. In the case of vessel tracking, the route points form
+the vertices of a graph; the routes between them the edges; and the distances
+between them the weights. For various reasons, people are interested in
+minimizing (or maximizing) the weight of a path through a set of vertices, such
+as the shortest path between two ports to predict a vessel’s arrival time.</p>
+
+<p>Given a graph, an algorithm like Dijkstra’s search could compute the shortest
+path between two vertices. In fact, this was the algorithm Neo4J shipped with
+at the time. One drawback of Dijkstra’s algorithm is that it computes all the
+shortest paths from the source to all other vertices before terminating at the
+destination vertex. The time complexity of this exhaustive search prevented our
+database from scaling beyond 4,000 route points.</p>
+
+<p>The following enhancement to Dijkstra’s search, also known as the A* search,
+employs a heuristic to steer the search in the direction of the destination
+more quickly. In the case of our network of vessels, which are on the earth’s
+surface, spherical distance is a good candidate for a heuristic:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package org.neo4j.graphalgo.impl;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.neo4j.graphalgo.api.Graph;
+import org.neo4j.graphalgo.core.utils.ProgressLogger;
+import org.neo4j.graphalgo.core.utils.queue.IntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.queue.SharedIntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.traverse.SimpleBitSet;
+import org.neo4j.graphdb.Direction;
+import org.neo4j.graphdb.Node;
+import org.neo4j.kernel.internal.GraphDatabaseAPI;
+
+import com.carrotsearch.hppc.IntArrayDeque;
+import com.carrotsearch.hppc.IntDoubleMap;
+import com.carrotsearch.hppc.IntDoubleScatterMap;
+import com.carrotsearch.hppc.IntIntMap;
+import com.carrotsearch.hppc.IntIntScatterMap;
+
+public class ShortestPathAStar extends Algorithm&lt;ShortestPathAStar&gt; {
+
+ private final GraphDatabaseAPI dbService;
+ private static final int PATH_END = -1;
+
+ private Graph graph;
+ private final int nodeCount;
+ private IntDoubleMap gCosts;
+ private IntDoubleMap fCosts;
+ private double totalCost;
+ private IntPriorityQueue openNodes;
+ private IntIntMap path;
+ private IntArrayDeque shortestPath;
+ private SimpleBitSet closedNodes;
+ private final ProgressLogger progressLogger;
+
+ public static final double NO_PATH_FOUND = -1.0;
+
+ public ShortestPathAStar(
+ final Graph graph,
+ final GraphDatabaseAPI dbService) {
+
+ this.graph = graph;
+ this.dbService = dbService;
+
+ nodeCount = Math.toIntExact(graph.nodeCount());
+ gCosts = new IntDoubleScatterMap(nodeCount);
+ fCosts = new IntDoubleScatterMap(nodeCount);
+ openNodes = SharedIntPriorityQueue.min(
+ nodeCount,
+ fCosts,
+ Double.MAX_VALUE);
+ path = new IntIntScatterMap(nodeCount);
+ closedNodes = new SimpleBitSet(nodeCount);
+ shortestPath = new IntArrayDeque();
+ progressLogger = getProgressLogger();
+ }
+
+ public ShortestPathAStar compute(
+ final long startNode,
+ final long goalNode,
+ final String propertyKeyLat,
+ final String propertyKeyLon,
+ final Direction direction) {
+
+ reset();
+
+ final int startNodeInternal =
+ graph.toMappedNodeId(startNode);
+ final double startNodeLat =
+ getNodeCoordinate(startNodeInternal, propertyKeyLat);
+ final double startNodeLon =
+ getNodeCoordinate(startNodeInternal, propertyKeyLon);
+
+ final int goalNodeInternal =
+ graph.toMappedNodeId(goalNode);
+ final double goalNodeLat =
+ getNodeCoordinate(goalNodeInternal, propertyKeyLat);
+ final double goalNodeLon =
+ getNodeCoordinate(goalNodeInternal, propertyKeyLon);
+
+ final double initialHeuristic =
+ computeHeuristic(startNodeLat,
+ startNodeLon,
+ goalNodeLat,
+ goalNodeLon);
+
+ gCosts.put(startNodeInternal, 0.0);
+ fCosts.put(startNodeInternal, initialHeuristic);
+ openNodes.add(startNodeInternal, 0.0);
+
+ run(goalNodeInternal,
+ propertyKeyLat,
+ propertyKeyLon,
+ direction);
+
+ if (path.containsKey(goalNodeInternal)) {
+ totalCost = gCosts.get(goalNodeInternal);
+ int node = goalNodeInternal;
+ while (node != PATH_END) {
+ shortestPath.addFirst(node);
+ node = path.getOrDefault(node, PATH_END);
+ }
+ }
+ return this;
+ }
+
+ private void run(
+ final int goalNodeId,
+ final String propertyKeyLat,
+ final String propertyKeyLon,
+ final Direction direction) {
+
+ final double goalLat =
+ getNodeCoordinate(goalNodeId, propertyKeyLat);
+ final double goalLon =
+ getNodeCoordinate(goalNodeId, propertyKeyLon);
+
+ while (!openNodes.isEmpty() &amp;&amp; running()) {
+ int currentNodeId = openNodes.pop();
+ if (currentNodeId == goalNodeId) {
+ return;
+ }
+
+ closedNodes.put(currentNodeId);
+
+ double currentNodeCost =
+ this.gCosts.getOrDefault(
+ currentNodeId,
+ Double.MAX_VALUE);
+
+ graph.forEachRelationship(
+ currentNodeId,
+ direction,
+ (source, target, relationshipId, weight) -&gt; {
+ double neighbourLat =
+ getNodeCoordinate(target, propertyKeyLat);
+ double neighbourLon =
+ getNodeCoordinate(target, propertyKeyLon);
+ double heuristic =
+ computeHeuristic(
+ neighbourLat,
+ neighbourLon,
+ goalLat,
+ goalLon);
+
+ updateCosts(
+ source,
+ target,
+ weight + currentNodeCost,
+ heuristic);
+
+ if (!closedNodes.contains(target)) {
+ openNodes.add(target, 0);
+ }
+ return true;
+ });
+
+ progressLogger.logProgress(
+ (double) currentNodeId / (nodeCount - 1));
+ }
+ }
+
+ private double computeHeuristic(
+ final double lat1,
+ final double lon1,
+ final double lat2,
+ final double lon2) {
+
+ final int earthRadius = 6371;
+ final double kmToNM = 0.539957;
+ final double latDistance = Math.toRadians(lat2 - lat1);
+ final double lonDistance = Math.toRadians(lon2 - lon1);
+ final double a = Math.sin(latDistance / 2)
+ * Math.sin(latDistance / 2)
+ + Math.cos(Math.toRadians(lat1))
+ * Math.cos(Math.toRadians(lat2))
+ * Math.sin(lonDistance / 2)
+ * Math.sin(lonDistance / 2);
+ final double c = 2
+ * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ final double distance = earthRadius * c * kmToNM;
+ return distance;
+ }
+
+ private double getNodeCoordinate(
+ final int nodeId,
+ final String coordinateType) {
+
+ final long neo4jId = graph.toOriginalNodeId(nodeId);
+ final Node node = dbService.getNodeById(neo4jId);
+ return (double) node.getProperty(coordinateType);
+ }
+
+ private void updateCosts(
+ final int source,
+ final int target,
+ final double newCost,
+ final double heuristic) {
+
+ final double oldCost =
+ gCosts.getOrDefault(target, Double.MAX_VALUE);
+
+ if (newCost &lt; oldCost) {
+ gCosts.put(target, newCost);
+ fCosts.put(target, newCost + heuristic);
+ path.put(target, source);
+ }
+ }
+
+ private void reset() {
+ closedNodes.clear();
+ openNodes.clear();
+ gCosts.clear();
+ fCosts.clear();
+ path.clear();
+ shortestPath.clear();
+ totalCost = NO_PATH_FOUND;
+ }
+
+ public Stream&lt;Result&gt; resultStream() {
+ return StreamSupport.stream(
+ shortestPath.spliterator(), false)
+ .map(cursor -&gt; new Result(
+ graph.toOriginalNodeId(cursor.value),
+ gCosts.get(cursor.value)));
+ }
+
+ public IntArrayDeque getFinalPath() {
+ return shortestPath;
+ }
+
+ public double getTotalCost() {
+ return totalCost;
+ }
+
+ public int getPathLength() {
+ return shortestPath.size();
+ }
+
+ @Override
+ public ShortestPathAStar me() {
+ return this;
+ }
+
+ @Override
+ public ShortestPathAStar release() {
+ graph = null;
+ gCosts = null;
+ fCosts = null;
+ openNodes = null;
+ path = null;
+ shortestPath = null;
+ closedNodes = null;
+ return this;
+ }
+
+ public static class Result {
+
+ /**
+ * the neo4j node id
+ */
+ public final Long nodeId;
+
+ /**
+ * cost to reach the node from startNode
+ */
+ public final Double cost;
+
+ public Result(Long nodeId, Double cost) {
+ this.nodeId = nodeId;
+ this.cost = cost;
+ }
+ }
+}
+</code></pre></div></div>
+
+<p>The heuristic function is domain-specific. If chosen wisely, it can
+significantly speed up the search. In our case, we achieved a 300x speedup,
+enabling us to expand our search from 4,000 to 13,000 route points. The <a href="https://github.com/neo4j-contrib/neo4j-graph-algorithms/releases/tag/3.4.0.0" class="external" target="_blank" rel="noopener noreferrer">v3.4.0</a> of the
+Neo4J graph algorithms shipped with our A* search algorithm.</p>
+
diff --git a/_log/_site/robots.txt b/_log/_site/robots.txt
new file mode 100644
index 0000000..e087884
--- /dev/null
+++ b/_log/_site/robots.txt
@@ -0,0 +1 @@
+Sitemap: /sitemap.xml
diff --git a/_log/_site/sitemap.xml b/_log/_site/sitemap.xml
new file mode 100644
index 0000000..8c167bd
--- /dev/null
+++ b/_log/_site/sitemap.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+<url>
+<loc>/arduino-due.html</loc>
+</url>
+<url>
+<loc>/arduino-uno.html</loc>
+</url>
+<url>
+<loc>/bumblebee.html</loc>
+</url>
+<url>
+<loc>/e-reader.html</loc>
+</url>
+<url>
+<loc>/etlas.html</loc>
+</url>
+<url>
+<loc>/fpm-door-lock.html</loc>
+</url>
+<url>
+<loc>/matrix-digital-rain.html</loc>
+</url>
+<url>
+<loc>/mosfet-switches.html</loc>
+</url>
+<url>
+<loc>/my-first-pcb.html</loc>
+</url>
+<url>
+<loc>/neo4j-a-star-search.html</loc>
+</url>
+<url>
+<loc>/suckless-software.html</loc>
+</url>
+</urlset>
diff --git a/_log/_site/suckless-software.html b/_log/_site/suckless-software.html
new file mode 100644
index 0000000..d91ed16
--- /dev/null
+++ b/_log/_site/suckless-software.html
@@ -0,0 +1,78 @@
+<p>Since <a href="https://suckless.org/" class="external" target="_blank" rel="noopener noreferrer">suckless</a> software requires users to modify the
+source code and recompile to customize, I need a way to maintain patches over
+the long term while retaining the ability to upgrade the software as new
+versions are released.</p>
+
+<h2 id="initial-setup">Initial setup</h2>
+
+<p>When using a suckless program, I usually begin by cloning the project and
+setting the remote push URL to my own git repository:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git://git.suckless.org/dwm
+git reset --hard &lt;tag&gt;
+git remote set-url --push origin git@git.asciimx.com:/repos/dwm
+</code></pre></div></div>
+
+<p>This way, I can pull updates from the upstream project whenever I want, while
+committing my changes to my git repository. The git reset command aligns my
+branch head with a stable release before applying patches or installing the
+software.</p>
+
+<p>If all I want to do is reconfigure the software (e.g., change key bindings),
+which is what I need most of the time, the recommended approach is to modify
+the config.h file. If the config.h isn’t yet in the project, the
+<code class="language-plaintext highlighter-rouge">make clean &lt;target&gt;</code> command will generate it from the defaults and compile
+the software. The <code class="language-plaintext highlighter-rouge">&lt;target&gt;</code> is the name of the application (e.g., dwm) found
+in the Makefile. I modify the resulting config.h file and run <code class="language-plaintext highlighter-rouge">make clean
+install</code> to install the software before committing and pushing my changes to
+the git repo.</p>
+
+<h2 id="dwm-and-slstatus">dwm and slstatus</h2>
+
+<p>Since dwm and slstatus are always running, <code class="language-plaintext highlighter-rouge">make install</code> will likely fail for
+them. The operating system may prevent the installer from replacing running
+executables with new ones. Hence, we must first stop the running instances of
+these programs (in my case, using Mod + Shift + q). Then, switch to a tty
+(Ctrl + Alt + F1), log in, and change the directory to where dwm/slstatus is.
+We can run <code class="language-plaintext highlighter-rouge">make install</code> to install the software and switch back to the
+graphical session (Ctrl + Alt + F5).</p>
+
+<p>The key combinations for switching to the tty and back may differ across
+systems. The ones listed above are for OpenBSD.</p>
+
+<h2 id="subsequent-upgrades">Subsequent upgrades</h2>
+
+<p>When suckless releases a new version, I run <code class="language-plaintext highlighter-rouge">git pull --rebase</code> to fetch the
+upstream changes and rebase my patches on top of them. Because I tend to use
+stable versions, I perform another interactive rebase to drop the commits
+between the latest stable version tag and my patch before installing the
+software.</p>
+
+<p>Commit log before upgrading:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dt236 My patch.
+3fkdf Version 6.5.
+</code></pre></div></div>
+
+<p>Commit log after pulling:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>w467d My patch.
+gh25g A commit.
+g525g Another commit.
+3fkdf Version 6.6.
+vd425 Old commit.
+q12vu Another old commit.
+3fkdf Version 6.5.
+</code></pre></div></div>
+
+<p>Commit log after the interactive rebase:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>h57jh My patch.
+3fkdf Version 6.6.
+vd425 Old commit.
+q12vu Another old commit.
+3fkdf Version 6.5.
+</code></pre></div></div>
+
+<p>And finally, I commit and push all the changes to my git repository.</p>
+
diff --git a/_log/matrix-digital-rain.md b/_log/matrix-digital-rain.md
index 2c35361..8c97ed3 100644
--- a/_log/matrix-digital-rain.md
+++ b/_log/matrix-digital-rain.md
@@ -6,43 +6,62 @@ project: true
thumbnail: thumb_sm.png
---
-The Matrix digital rain implemented in raw C using ANSI escape sequences with
-zero dependencies—not even ncurses.
+My 2022 implementation of the Matrix rain had too many loose ends. Unicode
+support was inflexible: the charset had to be a single contiguous block with no
+way to mix ASCII with something like Katakana; Phosphor decay level was stored
+in a dedicated array--still don't understand why I did that when I had already
+used bit-packing for the RGB channels; The algorithm was difficult to decipher.
+The 2022 version worked, but that’s not the same thing as being correct.
-<video style="max-width:100%;" controls="" poster="poster.png">
- <source src="matrix.mp4" type="video/mp4">
-</video>
+I began by placing the decay factor in the LSB of the 4-byte RGB value. Let's
+call that RGB-PD. PD plays a somewhat analogous role to an alpha channel; I
+avoided labelling it A so as not to cause confusion:
+
+```
+enum {
+ R, /* Red */
+ G, /* Green */
+ B, /* Blue */
+ PD /* Phosphor decay level */
+};
-This is a fork of Domsson's unique rendition of the Matrix rain: <a
-href="https://github.com/domsson/fakesteak" class="external" target="_blank"
-rel="noopener noreferrer">Fakesteak</a>. Three years ago, I forked his project
-and added truecolor and Unicode support. I also drastically modified the
-algorithm to produce a rain that resembled the original aesthetic with high
-visual fidelity.
+typedef union color_tag {
+ uint32_t value;
+ unsigned char color[4];
+} color;
+```
-## Unicode support
+The decision to use union over more portable bit twiddling was made three years
+ago, as I recall, for readability. Seeing as all my systems are little-endian,
+this is unlikely to cause me trouble. Besides, if union is never to be used,
+why is it in the language anyway?
-Unicode support in the 2022 version lacked flexibility. The charset used in the
-rain had to be a single contiguous block defined by `UNICODE_MIN` and
-`UNICODE_MAX` settings:
+The blend() function, which emulates the dim afterglow of Phosphor by eroding
+the RGB channels towards the background, remains as elegant as it did three
+years ago:
```
-#define UNICODE_MIN 0x0021
-#define UNICODE_MAX 0x007E
+#define DECAY_MPLIER 2
-static inline void insert_code(matrix *mat,
- size_t row, size_t col)
+static inline void blend(matrix *mat,
+ size_t row, size_t col)
{
- mat->code[index(mat, row, col)] = rand()
- % (UNICODE_MAX - UNICODE_MIN)
- + UNICODE_MIN;
+ unsigned char *color;
+
+ color = mat->rgb[index(mat, row, col)].color;
+ color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
+ color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
+ color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
}
```
-There was no way, for instance, to use both ASCII and Katakana at the same
-time. The user had to pick one. In the new version, the user can use any number
-of Unicode blocks using `glyphs` array. In fact, the default rain now includes
-both ASCII and half-width Katakana characters:
+While the memory inefficiency of Phosphor decay tracking was a technical
+oversight I hadn't noticed, the limitation around mixing nonadjacent Unicode
+blocks was a nagging concern even three years ago. So, a fix was long overdue.
+
+In the new version, I introduced a glyphs array that enables a user to add as
+many Unicode blocks as they want. The insert_code() function picks a block
+from the array at random, and then picks a character from that block at random:
```
#define UNICODE(min, max) (((uint64_t)max << 32) | min)
@@ -70,50 +89,30 @@ static inline void insert_code(matrix *mat,
}
```
-Entries in the `glyphs` array are Unicode blocks bit-packed in an 8-byte
-container: the four low bytes forms the first codepoint and the four high bytes
-the last.
-
-## Phosphor decay
+The Unicode blocks are stored in 8-byte containers: the low four bytes form the
+first codepoint and the high four bytes the last. Here, I chose bitwise
+operations over unions because, first and foremost, the operations themselves
+are trivial and idiomatic, and the UNICODE() macro simplifies the management of
+charsets. The insert_code() function is now ready to take its rightful place
+next to blend().
-The dim afterglow of monochrome CRT displays is achieved by carefully scaling
-the RGB channels individually and mixing them:
-
-```
-#define DECAY_MPLIER 2
-
-static inline void blend(matrix *mat,
- size_t row, size_t col)
-{
- unsigned char *color;
-
- color = mat->rgb[index(mat, row, col)].color;
- color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
- color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
- color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
-}
-```
-
-The blending function emulates the phosphor decay by gradually transitioning
-each raindrop's color towards the background color. The multiplier is the
-number of passes over the rain track needed before the afterglow disappears.
-
-## The algorithm
-
-Nonetheless, the rain resembles the original with high visual fidelity. It's
-highly customizable and gentle on the CPU. On my 14" ThinkPad T490, which has a
-resolution of 1920x1080 and 4GHz CPU, it uses 2-3% of the CPU with occasional
-jumps of up to about 8%. Not too bad for a weekend project. The program has
-been tested with xterm and urxvt terminal emulators on OpenBSD and Arch Linux
-systems. Someone has managed to get it moving on a Raspberry Pi as well.
-
-Lastly, to compile and run:
+The result is a digital rain that captures the original Matrix aesthetic with
+high visual fidelity:
```
$ cc -O3 main.c -o matrix
$ ./matrix
```
-"All I see is blonde, brunette, red head."
+<video style="max-width:100%;" controls="" poster="poster.png">
+ <source src="matrix.mp4" type="video/mp4">
+</video>
+
+There was no cause to measure the program's performance characteristics
+precisely; it's gentle on the CPU. On my ThinkPad T490 running OpenBSD, which
+has a resolution of 1920x1080, it uses about 2-3% of the CPU, with occasional
+jumps of up to about 8%; the cores remain silent, the fans don't whir, the rain
+falls in quiet.
Files: [source.tar.gz](source.tar.gz)
+
diff --git a/_log/my-first-pcb.md b/_log/my-first-pcb.md
index c380ea0..eced7e4 100644
--- a/_log/my-first-pcb.md
+++ b/_log/my-first-pcb.md
@@ -2,7 +2,6 @@
title: My first PCB
date: 2025-04-26
layout: post
-thumbnail: thumb_sm.jpeg
---
In 2023, I started tinkering with DIY electronics as a hobby. Until now, I've
diff --git a/_log/neo4j-a-star-search.md b/_log/neo4j-a-star-search.md
new file mode 100644
index 0000000..8c7e26e
--- /dev/null
+++ b/_log/neo4j-a-star-search.md
@@ -0,0 +1,317 @@
+---
+title: Neo4J A* search
+date: 2018-03-06
+layout: post
+---
+
+Back in 2018, we used <a href="https://neo4j.com/" class="external"
+target="_blank" rel="noopener noreferrer">Neo4J</a> graph database to track the
+movement of marine vessels. We were interested in the shortest path a ship
+could take through a network of about 13,000 route points. Graph theoretic
+algorithms provide optimal solutions to such problems, and the set of route
+points lends itself well to graph-based modelling.
+
+A graph is a finite set of vertices, and a subset of vertex pairs (edges).
+Edges can have weights. In the case of vessel tracking, the route points form
+the vertices of a graph; the routes between them the edges; and the distances
+between them the weights. For various reasons, people are interested in
+minimizing (or maximizing) the weight of a path through a set of vertices, such
+as the shortest path between two ports to predict a vessel's arrival time.
+
+Given a graph, an algorithm like Dijkstra's search could compute the shortest
+path between two vertices. In fact, this was the algorithm Neo4J shipped with
+at the time. One drawback of Dijkstra's algorithm is that it computes all the
+shortest paths from the source to all other vertices before terminating at the
+destination vertex. The time complexity of this exhaustive search prevented our
+database from scaling beyond 4,000 route points.
+
+The following enhancement to Dijkstra's search, also known as the A* search,
+employs a heuristic to steer the search in the direction of the destination
+more quickly. In the case of our network of vessels, which are on the earth's
+surface, spherical distance is a good candidate for a heuristic:
+
+```
+package org.neo4j.graphalgo.impl;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.neo4j.graphalgo.api.Graph;
+import org.neo4j.graphalgo.core.utils.ProgressLogger;
+import org.neo4j.graphalgo.core.utils.queue.IntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.queue.SharedIntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.traverse.SimpleBitSet;
+import org.neo4j.graphdb.Direction;
+import org.neo4j.graphdb.Node;
+import org.neo4j.kernel.internal.GraphDatabaseAPI;
+
+import com.carrotsearch.hppc.IntArrayDeque;
+import com.carrotsearch.hppc.IntDoubleMap;
+import com.carrotsearch.hppc.IntDoubleScatterMap;
+import com.carrotsearch.hppc.IntIntMap;
+import com.carrotsearch.hppc.IntIntScatterMap;
+
+public class ShortestPathAStar extends Algorithm<ShortestPathAStar> {
+
+ private final GraphDatabaseAPI dbService;
+ private static final int PATH_END = -1;
+
+ private Graph graph;
+ private final int nodeCount;
+ private IntDoubleMap gCosts;
+ private IntDoubleMap fCosts;
+ private double totalCost;
+ private IntPriorityQueue openNodes;
+ private IntIntMap path;
+ private IntArrayDeque shortestPath;
+ private SimpleBitSet closedNodes;
+ private final ProgressLogger progressLogger;
+
+ public static final double NO_PATH_FOUND = -1.0;
+
+ public ShortestPathAStar(
+ final Graph graph,
+ final GraphDatabaseAPI dbService) {
+
+ this.graph = graph;
+ this.dbService = dbService;
+
+ nodeCount = Math.toIntExact(graph.nodeCount());
+ gCosts = new IntDoubleScatterMap(nodeCount);
+ fCosts = new IntDoubleScatterMap(nodeCount);
+ openNodes = SharedIntPriorityQueue.min(
+ nodeCount,
+ fCosts,
+ Double.MAX_VALUE);
+ path = new IntIntScatterMap(nodeCount);
+ closedNodes = new SimpleBitSet(nodeCount);
+ shortestPath = new IntArrayDeque();
+ progressLogger = getProgressLogger();
+ }
+
+ public ShortestPathAStar compute(
+ final long startNode,
+ final long goalNode,
+ final String propertyKeyLat,
+ final String propertyKeyLon,
+ final Direction direction) {
+
+ reset();
+
+ final int startNodeInternal =
+ graph.toMappedNodeId(startNode);
+ final double startNodeLat =
+ getNodeCoordinate(startNodeInternal, propertyKeyLat);
+ final double startNodeLon =
+ getNodeCoordinate(startNodeInternal, propertyKeyLon);
+
+ final int goalNodeInternal =
+ graph.toMappedNodeId(goalNode);
+ final double goalNodeLat =
+ getNodeCoordinate(goalNodeInternal, propertyKeyLat);
+ final double goalNodeLon =
+ getNodeCoordinate(goalNodeInternal, propertyKeyLon);
+
+ final double initialHeuristic =
+ computeHeuristic(startNodeLat,
+ startNodeLon,
+ goalNodeLat,
+ goalNodeLon);
+
+ gCosts.put(startNodeInternal, 0.0);
+ fCosts.put(startNodeInternal, initialHeuristic);
+ openNodes.add(startNodeInternal, 0.0);
+
+ run(goalNodeInternal,
+ propertyKeyLat,
+ propertyKeyLon,
+ direction);
+
+ if (path.containsKey(goalNodeInternal)) {
+ totalCost = gCosts.get(goalNodeInternal);
+ int node = goalNodeInternal;
+ while (node != PATH_END) {
+ shortestPath.addFirst(node);
+ node = path.getOrDefault(node, PATH_END);
+ }
+ }
+ return this;
+ }
+
+ private void run(
+ final int goalNodeId,
+ final String propertyKeyLat,
+ final String propertyKeyLon,
+ final Direction direction) {
+
+ final double goalLat =
+ getNodeCoordinate(goalNodeId, propertyKeyLat);
+ final double goalLon =
+ getNodeCoordinate(goalNodeId, propertyKeyLon);
+
+ while (!openNodes.isEmpty() && running()) {
+ int currentNodeId = openNodes.pop();
+ if (currentNodeId == goalNodeId) {
+ return;
+ }
+
+ closedNodes.put(currentNodeId);
+
+ double currentNodeCost =
+ this.gCosts.getOrDefault(
+ currentNodeId,
+ Double.MAX_VALUE);
+
+ graph.forEachRelationship(
+ currentNodeId,
+ direction,
+ (source, target, relationshipId, weight) -> {
+ double neighbourLat =
+ getNodeCoordinate(target, propertyKeyLat);
+ double neighbourLon =
+ getNodeCoordinate(target, propertyKeyLon);
+ double heuristic =
+ computeHeuristic(
+ neighbourLat,
+ neighbourLon,
+ goalLat,
+ goalLon);
+
+ updateCosts(
+ source,
+ target,
+ weight + currentNodeCost,
+ heuristic);
+
+ if (!closedNodes.contains(target)) {
+ openNodes.add(target, 0);
+ }
+ return true;
+ });
+
+ progressLogger.logProgress(
+ (double) currentNodeId / (nodeCount - 1));
+ }
+ }
+
+ private double computeHeuristic(
+ final double lat1,
+ final double lon1,
+ final double lat2,
+ final double lon2) {
+
+ final int earthRadius = 6371;
+ final double kmToNM = 0.539957;
+ final double latDistance = Math.toRadians(lat2 - lat1);
+ final double lonDistance = Math.toRadians(lon2 - lon1);
+ final double a = Math.sin(latDistance / 2)
+ * Math.sin(latDistance / 2)
+ + Math.cos(Math.toRadians(lat1))
+ * Math.cos(Math.toRadians(lat2))
+ * Math.sin(lonDistance / 2)
+ * Math.sin(lonDistance / 2);
+ final double c = 2
+ * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ final double distance = earthRadius * c * kmToNM;
+ return distance;
+ }
+
+ private double getNodeCoordinate(
+ final int nodeId,
+ final String coordinateType) {
+
+ final long neo4jId = graph.toOriginalNodeId(nodeId);
+ final Node node = dbService.getNodeById(neo4jId);
+ return (double) node.getProperty(coordinateType);
+ }
+
+ private void updateCosts(
+ final int source,
+ final int target,
+ final double newCost,
+ final double heuristic) {
+
+ final double oldCost =
+ gCosts.getOrDefault(target, Double.MAX_VALUE);
+
+ if (newCost < oldCost) {
+ gCosts.put(target, newCost);
+ fCosts.put(target, newCost + heuristic);
+ path.put(target, source);
+ }
+ }
+
+ private void reset() {
+ closedNodes.clear();
+ openNodes.clear();
+ gCosts.clear();
+ fCosts.clear();
+ path.clear();
+ shortestPath.clear();
+ totalCost = NO_PATH_FOUND;
+ }
+
+ public Stream<Result> resultStream() {
+ return StreamSupport.stream(
+ shortestPath.spliterator(), false)
+ .map(cursor -> new Result(
+ graph.toOriginalNodeId(cursor.value),
+ gCosts.get(cursor.value)));
+ }
+
+ public IntArrayDeque getFinalPath() {
+ return shortestPath;
+ }
+
+ public double getTotalCost() {
+ return totalCost;
+ }
+
+ public int getPathLength() {
+ return shortestPath.size();
+ }
+
+ @Override
+ public ShortestPathAStar me() {
+ return this;
+ }
+
+ @Override
+ public ShortestPathAStar release() {
+ graph = null;
+ gCosts = null;
+ fCosts = null;
+ openNodes = null;
+ path = null;
+ shortestPath = null;
+ closedNodes = null;
+ return this;
+ }
+
+ public static class Result {
+
+ /**
+ * the neo4j node id
+ */
+ public final Long nodeId;
+
+ /**
+ * cost to reach the node from startNode
+ */
+ public final Double cost;
+
+ public Result(Long nodeId, Double cost) {
+ this.nodeId = nodeId;
+ this.cost = cost;
+ }
+ }
+}
+```
+
+The heuristic function is domain-specific. If chosen wisely, it can
+significantly speed up the search. In our case, we achieved a 300x speedup,
+enabling us to expand our search from 4,000 to 13,000 route points. The <a
+href="https://github.com/neo4j-contrib/neo4j-graph-algorithms/releases/tag/3.4.0.0"
+class="external" target="_blank" rel="noopener noreferrer">v3.4.0</a> of the
+Neo4J graph algorithms shipped with our A* search algorithm.
+
diff --git a/_site/feed.xml b/_site/feed.xml
index 4d5229e..c7b21b4 100644
--- a/_site/feed.xml
+++ b/_site/feed.xml
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="http://localhost:4000/feed.xml" rel="self" type="application/atom+xml" /><link href="http://localhost:4000/" rel="alternate" type="text/html" /><updated>2025-12-22T23:37:38+08:00</updated><id>http://localhost:4000/feed.xml</id><title type="html">ASCIIMX | Log</title><author><name>W. D. Sadeep Madurange</name></author><entry><title type="html">Recreating the Matrix rain with ANSI escape sequences</title><link href="http://localhost:4000/log/matrix-digital-rain/" rel="alternate" type="text/html" title="Recreating the Matrix rain with ANSI escape sequences" /><published>2025-12-21T00:00:00+08:00</published><updated>2025-12-21T00:00:00+08:00</updated><id>http://localhost:4000/log/matrix-digital-rain</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[The Matrix digital rain implemented in raw C using ANSI escape sequences with zero dependencies—not even ncurses.]]></summary></entry><entry><title type="html">How to manage Suckless software installations</title><link href="http://localhost:4000/log/suckless-software/" rel="alternate" type="text/html" title="How to manage Suckless software installations" /><published>2025-11-30T00:00:00+08:00</published><updated>2025-11-30T00:00:00+08:00</updated><id>http://localhost:4000/log/suckless-software</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Since suckless software requires users to modify the source code and recompile to customize, I need a way to maintain patches over the long term while retaining the ability to upgrade the software as new versions are released.]]></summary></entry><entry><title type="html">Fingerprint door lock</title><link href="http://localhost:4000/log/fpm-door-lock/" rel="alternate" type="text/html" title="Fingerprint door lock" /><published>2025-08-18T00:00:00+08:00</published><updated>2025-08-18T00:00:00+08:00</updated><id>http://localhost:4000/log/fpm-door-lock</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This project features a fingerprint door lock powered by an ATmega328P microcontroller.]]></summary></entry><entry><title type="html">On the use of MOSFETs as electronic switches</title><link href="http://localhost:4000/log/mosfet-switches/" rel="alternate" type="text/html" title="On the use of MOSFETs as electronic switches" /><published>2025-06-22T00:00:00+08:00</published><updated>2025-06-22T00:00:00+08:00</updated><id>http://localhost:4000/log/mosfet-switches</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Recently, I needed a low-power circuit for one of my battery-operated projects. Much of the system’s power savings depended on its ability to electronically switch off components, such as servos, that draw high levels of quiescent currents. My search for a solution led me to MOSFETs, transistors capable of controlling circuits operating at voltages far above their own.]]></summary></entry><entry><title type="html">How to configure ATmega328P microcontrollers to run at 3.3V and 5V</title><link href="http://localhost:4000/log/arduino-uno/" rel="alternate" type="text/html" title="How to configure ATmega328P microcontrollers to run at 3.3V and 5V" /><published>2025-06-10T00:00:00+08:00</published><updated>2025-06-10T00:00:00+08:00</updated><id>http://localhost:4000/log/arduino-uno</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This is a quick reference for wiring up ATmega328P ICs to run at 5V and 3.3V. While the 5V configuration is common, the 3.3V configuration can be useful in low-power applications and when interfacing with parts that themselves run at 3.3V. In this guide, the 5V setup is configured with a 16MHz crystal oscillator, while the 3.3V configuration makes use of an 8MHz crystal oscillator.]]></summary></entry><entry><title type="html">My first PCB</title><link href="http://localhost:4000/log/my-first-pcb/" rel="alternate" type="text/html" title="My first PCB" /><published>2025-04-26T00:00:00+08:00</published><updated>2025-04-26T00:00:00+08:00</updated><id>http://localhost:4000/log/my-first-pcb</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[In 2023, I started tinkering with DIY electronics as a hobby. Until now, I’ve been using development boards like the Arduino Uno and ESP-32-WROOM so that I can focus on the software. Recently, I decided to step outside of my comfort zone and design a PCB from scratch for a door lock I’m working on.]]></summary></entry><entry><title type="html">Bumblebee: browser automation</title><link href="http://localhost:4000/log/bumblebee/" rel="alternate" type="text/html" title="Bumblebee: browser automation" /><published>2025-04-02T00:00:00+08:00</published><updated>2025-04-02T00:00:00+08:00</updated><id>http://localhost:4000/log/bumblebee</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Bumblebee is a tool I built for one of my employers to automate the generation of web scraping scripts.]]></summary></entry><entry><title type="html">How to set up ATSAM3X8E microcontrollers for bare-metal programming in C</title><link href="http://localhost:4000/log/arduino-due/" rel="alternate" type="text/html" title="How to set up ATSAM3X8E microcontrollers for bare-metal programming in C" /><published>2024-09-16T00:00:00+08:00</published><updated>2024-09-16T00:00:00+08:00</updated><id>http://localhost:4000/log/arduino-due</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This article is a step-by-step guide for programming bare-metal ATSAM3X8E chips found on Arduino Due boards. It also includes notes on the chip’s memory layout relevant for writing linker scripts. The steps described in this article were tested on an OpenBSD workstation.]]></summary></entry><entry><title type="html">Etlas: e-paper dashboard</title><link href="http://localhost:4000/log/etlas/" rel="alternate" type="text/html" title="Etlas: e-paper dashboard" /><published>2024-09-05T00:00:00+08:00</published><updated>2024-09-05T00:00:00+08:00</updated><id>http://localhost:4000/log/etlas</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Etlas is a news, stock market, and weather tracker powered by an ESP32 NodeMCU D1, featuring a 7.5-inch Waveshare e-paper display and a DHT22 sensor module.]]></summary></entry><entry><title type="html">Experimental e-reader</title><link href="http://localhost:4000/log/e-reader/" rel="alternate" type="text/html" title="Experimental e-reader" /><published>2023-10-24T00:00:00+08:00</published><updated>2023-10-24T00:00:00+08:00</updated><id>http://localhost:4000/log/e-reader</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This project features an experimental e-reader powered by an ESP-WROOM-32 development board and a 7.5-inch Waveshare e-paper display built with the intention of learning about e-paper displays.]]></summary></entry></feed> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-12-24T16:28:25+08:00</updated><id>/feed.xml</id><title type="html">ASCIIMX | Log</title><author><name>W. D. Sadeep Madurange</name></author><entry><title type="html">Recreating the Matrix rain with ANSI escape sequences</title><link href="/log/matrix-digital-rain/" rel="alternate" type="text/html" title="Recreating the Matrix rain with ANSI escape sequences" /><published>2025-12-21T00:00:00+08:00</published><updated>2025-12-21T00:00:00+08:00</updated><id>/log/matrix-digital-rain</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[My 2022 implementation of the Matrix rain had too many loose ends. Unicode support was inflexible: the charset had to be a single contiguous block with no way to mix ASCII with something like Katakana; Phosphor decay level was stored in a dedicated array–still don’t understand why I did that when I had already used bit-packing for the RGB channels; The algorithm was difficult to decipher. The 2022 version worked, but that’s not the same thing as being correct.]]></summary></entry><entry><title type="html">How to manage Suckless software installations</title><link href="/log/suckless-software/" rel="alternate" type="text/html" title="How to manage Suckless software installations" /><published>2025-11-30T00:00:00+08:00</published><updated>2025-11-30T00:00:00+08:00</updated><id>/log/suckless-software</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Since suckless software requires users to modify the source code and recompile to customize, I need a way to maintain patches over the long term while retaining the ability to upgrade the software as new versions are released.]]></summary></entry><entry><title type="html">Fingerprint door lock</title><link href="/log/fpm-door-lock/" rel="alternate" type="text/html" title="Fingerprint door lock" /><published>2025-08-18T00:00:00+08:00</published><updated>2025-08-18T00:00:00+08:00</updated><id>/log/fpm-door-lock</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This project features a fingerprint door lock powered by an ATmega328P microcontroller.]]></summary></entry><entry><title type="html">On the use of MOSFETs as electronic switches</title><link href="/log/mosfet-switches/" rel="alternate" type="text/html" title="On the use of MOSFETs as electronic switches" /><published>2025-06-22T00:00:00+08:00</published><updated>2025-06-22T00:00:00+08:00</updated><id>/log/mosfet-switches</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Recently, I needed a low-power circuit for one of my battery-operated projects. Much of the system’s power savings depended on its ability to electronically switch off components, such as servos, that draw high levels of quiescent currents. My search for a solution led me to MOSFETs, transistors capable of controlling circuits operating at voltages far above their own.]]></summary></entry><entry><title type="html">How to configure ATmega328P microcontrollers to run at 3.3V and 5V</title><link href="/log/arduino-uno/" rel="alternate" type="text/html" title="How to configure ATmega328P microcontrollers to run at 3.3V and 5V" /><published>2025-06-10T00:00:00+08:00</published><updated>2025-06-10T00:00:00+08:00</updated><id>/log/arduino-uno</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This is a quick reference for wiring up ATmega328P ICs to run at 5V and 3.3V. While the 5V configuration is common, the 3.3V configuration can be useful in low-power applications and when interfacing with parts that themselves run at 3.3V. In this guide, the 5V setup is configured with a 16MHz crystal oscillator, while the 3.3V configuration makes use of an 8MHz crystal oscillator.]]></summary></entry><entry><title type="html">My first PCB</title><link href="/log/my-first-pcb/" rel="alternate" type="text/html" title="My first PCB" /><published>2025-04-26T00:00:00+08:00</published><updated>2025-04-26T00:00:00+08:00</updated><id>/log/my-first-pcb</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[In 2023, I started tinkering with DIY electronics as a hobby. Until now, I’ve been using development boards like the Arduino Uno and ESP-32-WROOM so that I can focus on the software. Recently, I decided to step outside of my comfort zone and design a PCB from scratch for a door lock I’m working on.]]></summary></entry><entry><title type="html">Bumblebee: browser automation</title><link href="/log/bumblebee/" rel="alternate" type="text/html" title="Bumblebee: browser automation" /><published>2025-04-02T00:00:00+08:00</published><updated>2025-04-02T00:00:00+08:00</updated><id>/log/bumblebee</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Bumblebee is a tool I built for one of my employers to automate the generation of web scraping scripts.]]></summary></entry><entry><title type="html">How to set up ATSAM3X8E microcontrollers for bare-metal programming in C</title><link href="/log/arduino-due/" rel="alternate" type="text/html" title="How to set up ATSAM3X8E microcontrollers for bare-metal programming in C" /><published>2024-09-16T00:00:00+08:00</published><updated>2024-09-16T00:00:00+08:00</updated><id>/log/arduino-due</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This article is a step-by-step guide for programming bare-metal ATSAM3X8E chips found on Arduino Due boards. It also includes notes on the chip’s memory layout relevant for writing linker scripts. The steps described in this article were tested on an OpenBSD workstation.]]></summary></entry><entry><title type="html">Etlas: e-paper dashboard</title><link href="/log/etlas/" rel="alternate" type="text/html" title="Etlas: e-paper dashboard" /><published>2024-09-05T00:00:00+08:00</published><updated>2024-09-05T00:00:00+08:00</updated><id>/log/etlas</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[Etlas is a news, stock market, and weather tracker powered by an ESP32 NodeMCU D1, featuring a 7.5-inch Waveshare e-paper display and a DHT22 sensor module.]]></summary></entry><entry><title type="html">Experimental e-reader</title><link href="/log/e-reader/" rel="alternate" type="text/html" title="Experimental e-reader" /><published>2023-10-24T00:00:00+08:00</published><updated>2023-10-24T00:00:00+08:00</updated><id>/log/e-reader</id><author><name>W. D. Sadeep Madurange</name></author><summary type="html"><![CDATA[This project features an experimental e-reader powered by an ESP-WROOM-32 development board and a 7.5-inch Waveshare e-paper display built with the intention of learning about e-paper displays.]]></summary></entry></feed> \ No newline at end of file
diff --git a/_site/log/index.html b/_site/log/index.html
index 32f0ff2..4296fb7 100644
--- a/_site/log/index.html
+++ b/_site/log/index.html
@@ -174,6 +174,19 @@
+ <tr>
+ <td class="posts-td posts-td-link">
+ <a href="/log/neo4j-a-star-search/" class="link-decor-none">Neo4J A* search</a>
+ </td>
+ <td class="posts-td posts-td-time">
+ <span class="post-meta">
+ <time datetime="2018-03-06 00:00:00 +0800">2018-03-06</time>
+ </span>
+ </td>
+ </tr>
+
+
+
</table>
</div>
diff --git a/_site/log/matrix-digital-rain/index.html b/_site/log/matrix-digital-rain/index.html
index 6007da5..4a23d29 100644
--- a/_site/log/matrix-digital-rain/index.html
+++ b/_site/log/matrix-digital-rain/index.html
@@ -44,40 +44,60 @@
<h2 class="center" id="title">RECREATING THE MATRIX RAIN WITH ANSI ESCAPE SEQUENCES</h2>
<h6 class="center">21 DECEMBER 2025</h5>
<br>
- <div class="twocol justify"><p>The Matrix digital rain implemented in raw C using ANSI escape sequences with
-zero dependencies—not even ncurses.</p>
-
-<video style="max-width:100%;" controls="" poster="poster.png">
- <source src="matrix.mp4" type="video/mp4" />
-</video>
+ <div class="twocol justify"><p>My 2022 implementation of the Matrix rain had too many loose ends. Unicode
+support was inflexible: the charset had to be a single contiguous block with no
+way to mix ASCII with something like Katakana; Phosphor decay level was stored
+in a dedicated array–still don’t understand why I did that when I had already
+used bit-packing for the RGB channels; The algorithm was difficult to decipher.
+The 2022 version worked, but that’s not the same thing as being correct.</p>
+
+<p>I began by placing the decay factor in the LSB of the 4-byte RGB value. Let’s
+call that RGB-PD. PD plays a somewhat analogous role to an alpha channel; I
+avoided labelling it A so as not to cause confusion:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>enum {
+ R, /* Red */
+ G, /* Green */
+ B, /* Blue */
+ PD /* Phosphor decay level */
+};
-<p>This is a fork of Domsson’s unique rendition of the Matrix rain: <a href="https://github.com/domsson/fakesteak" class="external" target="_blank" rel="noopener noreferrer">Fakesteak</a>. Three years ago, I forked his project
-and added truecolor and Unicode support. I also drastically modified the
-algorithm to produce a rain that resembled the original aesthetic with high
-visual fidelity.</p>
+typedef union color_tag {
+ uint32_t value;
+ unsigned char color[4];
+} color;
+</code></pre></div></div>
-<h2 id="unicode-support">Unicode support</h2>
+<p>The decision to use union over more portable bit twiddling was made three years
+ago, as I recall, for readability. Seeing as all my systems are little-endian,
+this is unlikely to cause me trouble. Besides, if union is never to be used,
+why is it in the language anyway?</p>
-<p>Unicode support in the 2022 version lacked flexibility. The charset used in the
-rain had to be a single contiguous block defined by <code class="language-plaintext highlighter-rouge">UNICODE_MIN</code> and
-<code class="language-plaintext highlighter-rouge">UNICODE_MAX</code> settings:</p>
+<p>The blend() function, which emulates the dim afterglow of Phosphor by eroding
+the RGB channels towards the background, remains as elegant as it did three
+years ago:</p>
-<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define UNICODE_MIN 0x0021
-#define UNICODE_MAX 0x007E
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define DECAY_MPLIER 2
-static inline void insert_code(matrix *mat,
- size_t row, size_t col)
+static inline void blend(matrix *mat,
+ size_t row, size_t col)
{
- mat-&gt;code[index(mat, row, col)] = rand()
- % (UNICODE_MAX - UNICODE_MIN)
- + UNICODE_MIN;
+ unsigned char *color;
+
+ color = mat-&gt;rgb[index(mat, row, col)].color;
+ color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
+ color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
+ color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
}
</code></pre></div></div>
-<p>There was no way, for instance, to use both ASCII and Katakana at the same
-time. The user had to pick one. In the new version, the user can use any number
-of Unicode blocks using <code class="language-plaintext highlighter-rouge">glyphs</code> array. In fact, the default rain now includes
-both ASCII and half-width Katakana characters:</p>
+<p>While the memory inefficiency of Phosphor decay tracking was a technical
+oversight I hadn’t noticed, the limitation around mixing nonadjacent Unicode
+blocks was a nagging concern even three years ago. So, a fix was long overdue.</p>
+
+<p>In the new version, I introduced a glyphs array that enables a user to add as
+many Unicode blocks as they want. The insert_code() function picks a block
+from the array at random, and then picks a character from that block at random:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define UNICODE(min, max) (((uint64_t)max &lt;&lt; 32) | min)
@@ -104,51 +124,32 @@ static inline void insert_code(matrix *mat,
}
</code></pre></div></div>
-<p>Entries in the <code class="language-plaintext highlighter-rouge">glyphs</code> array are Unicode blocks bit-packed in an 8-byte
-container: the four low bytes forms the first codepoint and the four high bytes
-the last.</p>
-
-<h2 id="phosphor-decay">Phosphor decay</h2>
+<p>The Unicode blocks are stored in 8-byte containers: the low four bytes form the
+first codepoint and the high four bytes the last. Here, I chose bitwise
+operations over unions because, first and foremost, the operations themselves
+are trivial and idiomatic, and the UNICODE() macro simplifies the management of
+charsets. The insert_code() function is now ready to take its rightful place
+next to blend().</p>
-<p>The dim afterglow of monochrome CRT displays is achieved by carefully scaling
-the RGB channels individually and mixing them:</p>
-
-<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#define DECAY_MPLIER 2
-
-static inline void blend(matrix *mat,
- size_t row, size_t col)
-{
- unsigned char *color;
-
- color = mat-&gt;rgb[index(mat, row, col)].color;
- color[R] = color[R] - (color[R] - RGB_BG_RED) / DECAY_MPLIER;
- color[G] = color[G] - (color[G] - RGB_BG_GRN) / DECAY_MPLIER;
- color[B] = color[B] - (color[B] - RGB_BG_BLU) / DECAY_MPLIER;
-}
-</code></pre></div></div>
-
-<p>The blending function emulates the phosphor decay by gradually transitioning
-each raindrop’s color towards the background color. The multiplier is the
-number of passes over the rain track needed before the afterglow disappears.</p>
-
-<h2 id="the-algorithm">The algorithm</h2>
-
-<p>Nonetheless, the rain resembles the original with high visual fidelity. It’s
-highly customizable and gentle on the CPU. On my 14” ThinkPad T490, which has a
-resolution of 1920x1080 and 4GHz CPU, it uses 2-3% of the CPU with occasional
-jumps of up to about 8%. Not too bad for a weekend project. The program has
-been tested with xterm and urxvt terminal emulators on OpenBSD and Arch Linux
-systems. Someone has managed to get it moving on a Raspberry Pi as well.</p>
-
-<p>Lastly, to compile and run:</p>
+<p>The result is a digital rain that captures the original Matrix aesthetic with
+high visual fidelity:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cc -O3 main.c -o matrix
$ ./matrix
</code></pre></div></div>
-<p>“All I see is blonde, brunette, red head.”</p>
+<video style="max-width:100%;" controls="" poster="poster.png">
+ <source src="matrix.mp4" type="video/mp4" />
+</video>
+
+<p>There was no cause to measure the program’s performance characteristics
+precisely; it’s gentle on the CPU. On my ThinkPad T490 running OpenBSD, which
+has a resolution of 1920x1080, it uses about 2-3% of the CPU, with occasional
+jumps of up to about 8%; the cores remain silent, the fans don’t whir, the rain
+falls in quiet.</p>
<p>Files: <a href="source.tar.gz">source.tar.gz</a></p>
+
</div>
<p class="post-author right">by W. D. Sadeep Madurange</p>
</div>
diff --git a/_site/log/neo4j-a-star-search/index.html b/_site/log/neo4j-a-star-search/index.html
new file mode 100644
index 0000000..9473dc6
--- /dev/null
+++ b/_site/log/neo4j-a-star-search/index.html
@@ -0,0 +1,370 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Neo4J A* search</title>
+
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Neo4J A* search</title>
+ <link rel="stylesheet" href="/assets/css/main.css">
+ <link rel="stylesheet" href="/assets/css/skeleton.css">
+</head>
+
+
+
+ </head>
+ <body>
+
+ <div id="nav-container" class="container">
+ <ul id="navlist" class="left">
+
+ <li >
+ <a href="/" class="link-decor-none">hme</a>
+ </li>
+ <li class="active">
+ <a href="/log/" class="link-decor-none">log</a>
+ </li>
+ <li >
+ <a href="/projects/" class="link-decor-none">poc</a>
+ </li>
+ <li >
+ <a href="/about/" class="link-decor-none">abt</a>
+ </li>
+ <li><a href="/feed.xml" class="link-decor-none">rss</a></li>
+ </ul>
+</div>
+
+
+
+ <main>
+ <div class="container">
+ <div class="container-2">
+ <h2 class="center" id="title">NEO4J A* SEARCH</h2>
+ <h6 class="center">06 MARCH 2018</h5>
+ <br>
+ <div class="twocol justify"><p>Back in 2018, we used <a href="https://neo4j.com/" class="external" target="_blank" rel="noopener noreferrer">Neo4J</a> graph database to track the
+movement of marine vessels. We were interested in the shortest path a ship
+could take through a network of about 13,000 route points. Graph theoretic
+algorithms provide optimal solutions to such problems, and the set of route
+points lends itself well to graph-based modelling.</p>
+
+<p>A graph is a finite set of vertices, and a subset of vertex pairs (edges).
+Edges can have weights. In the case of vessel tracking, the route points form
+the vertices of a graph; the routes between them the edges; and the distances
+between them the weights. For various reasons, people are interested in
+minimizing (or maximizing) the weight of a path through a set of vertices, such
+as the shortest path between two ports to predict a vessel’s arrival time.</p>
+
+<p>Given a graph, an algorithm like Dijkstra’s search could compute the shortest
+path between two vertices. In fact, this was the algorithm Neo4J shipped with
+at the time. One drawback of Dijkstra’s algorithm is that it computes all the
+shortest paths from the source to all other vertices before terminating at the
+destination vertex. The time complexity of this exhaustive search prevented our
+database from scaling beyond 4,000 route points.</p>
+
+<p>The following enhancement to Dijkstra’s search, also known as the A* search,
+employs a heuristic to steer the search in the direction of the destination
+more quickly. In the case of our network of vessels, which are on the earth’s
+surface, spherical distance is a good candidate for a heuristic:</p>
+
+<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package org.neo4j.graphalgo.impl;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.neo4j.graphalgo.api.Graph;
+import org.neo4j.graphalgo.core.utils.ProgressLogger;
+import org.neo4j.graphalgo.core.utils.queue.IntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.queue.SharedIntPriorityQueue;
+import org.neo4j.graphalgo.core.utils.traverse.SimpleBitSet;
+import org.neo4j.graphdb.Direction;
+import org.neo4j.graphdb.Node;
+import org.neo4j.kernel.internal.GraphDatabaseAPI;
+
+import com.carrotsearch.hppc.IntArrayDeque;
+import com.carrotsearch.hppc.IntDoubleMap;
+import com.carrotsearch.hppc.IntDoubleScatterMap;
+import com.carrotsearch.hppc.IntIntMap;
+import com.carrotsearch.hppc.IntIntScatterMap;
+
+public class ShortestPathAStar extends Algorithm&lt;ShortestPathAStar&gt; {
+
+ private final GraphDatabaseAPI dbService;
+ private static final int PATH_END = -1;
+
+ private Graph graph;
+ private final int nodeCount;
+ private IntDoubleMap gCosts;
+ private IntDoubleMap fCosts;
+ private double totalCost;
+ private IntPriorityQueue openNodes;
+ private IntIntMap path;
+ private IntArrayDeque shortestPath;
+ private SimpleBitSet closedNodes;
+ private final ProgressLogger progressLogger;
+
+ public static final double NO_PATH_FOUND = -1.0;
+
+ public ShortestPathAStar(
+ final Graph graph,
+ final GraphDatabaseAPI dbService) {
+
+ this.graph = graph;
+ this.dbService = dbService;
+
+ nodeCount = Math.toIntExact(graph.nodeCount());
+ gCosts = new IntDoubleScatterMap(nodeCount);
+ fCosts = new IntDoubleScatterMap(nodeCount);
+ openNodes = SharedIntPriorityQueue.min(
+ nodeCount,
+ fCosts,
+ Double.MAX_VALUE);
+ path = new IntIntScatterMap(nodeCount);
+ closedNodes = new SimpleBitSet(nodeCount);
+ shortestPath = new IntArrayDeque();
+ progressLogger = getProgressLogger();
+ }
+
+ public ShortestPathAStar compute(
+ final long startNode,
+ final long goalNode,
+ final String propertyKeyLat,
+ final String propertyKeyLon,
+ final Direction direction) {
+
+ reset();
+
+ final int startNodeInternal =
+ graph.toMappedNodeId(startNode);
+ final double startNodeLat =
+ getNodeCoordinate(startNodeInternal, propertyKeyLat);
+ final double startNodeLon =
+ getNodeCoordinate(startNodeInternal, propertyKeyLon);
+
+ final int goalNodeInternal =
+ graph.toMappedNodeId(goalNode);
+ final double goalNodeLat =
+ getNodeCoordinate(goalNodeInternal, propertyKeyLat);
+ final double goalNodeLon =
+ getNodeCoordinate(goalNodeInternal, propertyKeyLon);
+
+ final double initialHeuristic =
+ computeHeuristic(startNodeLat,
+ startNodeLon,
+ goalNodeLat,
+ goalNodeLon);
+
+ gCosts.put(startNodeInternal, 0.0);
+ fCosts.put(startNodeInternal, initialHeuristic);
+ openNodes.add(startNodeInternal, 0.0);
+
+ run(goalNodeInternal,
+ propertyKeyLat,
+ propertyKeyLon,
+ direction);
+
+ if (path.containsKey(goalNodeInternal)) {
+ totalCost = gCosts.get(goalNodeInternal);
+ int node = goalNodeInternal;
+ while (node != PATH_END) {
+ shortestPath.addFirst(node);
+ node = path.getOrDefault(node, PATH_END);
+ }
+ }
+ return this;
+ }
+
+ private void run(
+ final int goalNodeId,
+ final String propertyKeyLat,
+ final String propertyKeyLon,
+ final Direction direction) {
+
+ final double goalLat =
+ getNodeCoordinate(goalNodeId, propertyKeyLat);
+ final double goalLon =
+ getNodeCoordinate(goalNodeId, propertyKeyLon);
+
+ while (!openNodes.isEmpty() &amp;&amp; running()) {
+ int currentNodeId = openNodes.pop();
+ if (currentNodeId == goalNodeId) {
+ return;
+ }
+
+ closedNodes.put(currentNodeId);
+
+ double currentNodeCost =
+ this.gCosts.getOrDefault(
+ currentNodeId,
+ Double.MAX_VALUE);
+
+ graph.forEachRelationship(
+ currentNodeId,
+ direction,
+ (source, target, relationshipId, weight) -&gt; {
+ double neighbourLat =
+ getNodeCoordinate(target, propertyKeyLat);
+ double neighbourLon =
+ getNodeCoordinate(target, propertyKeyLon);
+ double heuristic =
+ computeHeuristic(
+ neighbourLat,
+ neighbourLon,
+ goalLat,
+ goalLon);
+
+ updateCosts(
+ source,
+ target,
+ weight + currentNodeCost,
+ heuristic);
+
+ if (!closedNodes.contains(target)) {
+ openNodes.add(target, 0);
+ }
+ return true;
+ });
+
+ progressLogger.logProgress(
+ (double) currentNodeId / (nodeCount - 1));
+ }
+ }
+
+ private double computeHeuristic(
+ final double lat1,
+ final double lon1,
+ final double lat2,
+ final double lon2) {
+
+ final int earthRadius = 6371;
+ final double kmToNM = 0.539957;
+ final double latDistance = Math.toRadians(lat2 - lat1);
+ final double lonDistance = Math.toRadians(lon2 - lon1);
+ final double a = Math.sin(latDistance / 2)
+ * Math.sin(latDistance / 2)
+ + Math.cos(Math.toRadians(lat1))
+ * Math.cos(Math.toRadians(lat2))
+ * Math.sin(lonDistance / 2)
+ * Math.sin(lonDistance / 2);
+ final double c = 2
+ * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ final double distance = earthRadius * c * kmToNM;
+ return distance;
+ }
+
+ private double getNodeCoordinate(
+ final int nodeId,
+ final String coordinateType) {
+
+ final long neo4jId = graph.toOriginalNodeId(nodeId);
+ final Node node = dbService.getNodeById(neo4jId);
+ return (double) node.getProperty(coordinateType);
+ }
+
+ private void updateCosts(
+ final int source,
+ final int target,
+ final double newCost,
+ final double heuristic) {
+
+ final double oldCost =
+ gCosts.getOrDefault(target, Double.MAX_VALUE);
+
+ if (newCost &lt; oldCost) {
+ gCosts.put(target, newCost);
+ fCosts.put(target, newCost + heuristic);
+ path.put(target, source);
+ }
+ }
+
+ private void reset() {
+ closedNodes.clear();
+ openNodes.clear();
+ gCosts.clear();
+ fCosts.clear();
+ path.clear();
+ shortestPath.clear();
+ totalCost = NO_PATH_FOUND;
+ }
+
+ public Stream&lt;Result&gt; resultStream() {
+ return StreamSupport.stream(
+ shortestPath.spliterator(), false)
+ .map(cursor -&gt; new Result(
+ graph.toOriginalNodeId(cursor.value),
+ gCosts.get(cursor.value)));
+ }
+
+ public IntArrayDeque getFinalPath() {
+ return shortestPath;
+ }
+
+ public double getTotalCost() {
+ return totalCost;
+ }
+
+ public int getPathLength() {
+ return shortestPath.size();
+ }
+
+ @Override
+ public ShortestPathAStar me() {
+ return this;
+ }
+
+ @Override
+ public ShortestPathAStar release() {
+ graph = null;
+ gCosts = null;
+ fCosts = null;
+ openNodes = null;
+ path = null;
+ shortestPath = null;
+ closedNodes = null;
+ return this;
+ }
+
+ public static class Result {
+
+ /**
+ * the neo4j node id
+ */
+ public final Long nodeId;
+
+ /**
+ * cost to reach the node from startNode
+ */
+ public final Double cost;
+
+ public Result(Long nodeId, Double cost) {
+ this.nodeId = nodeId;
+ this.cost = cost;
+ }
+ }
+}
+</code></pre></div></div>
+
+<p>The heuristic function is domain-specific. If chosen wisely, it can
+significantly speed up the search. In our case, we achieved a 300x speedup,
+enabling us to expand our search from 4,000 to 13,000 route points. The <a href="https://github.com/neo4j-contrib/neo4j-graph-algorithms/releases/tag/3.4.0.0" class="external" target="_blank" rel="noopener noreferrer">v3.4.0</a> of the
+Neo4J graph algorithms shipped with our A* search algorithm.</p>
+
+</div>
+ <p class="post-author right">by W. D. Sadeep Madurange</p>
+ </div>
+ </div>
+ </main>
+
+ <div class="footer">
+ <div class="container">
+ <div class="twelve columns right container-2">
+ <p id="footer-text">&copy; ASCIIMX - 2025</p>
+ </div>
+ </div>
+</div>
+
+
+ </body>
+</html>
diff --git a/_site/posts.xml b/_site/posts.xml
index 1d3402d..37970da 100644
--- a/_site/posts.xml
+++ b/_site/posts.xml
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="http://localhost:4000/posts.xml" rel="self" type="application/atom+xml" /><link href="http://localhost:4000/" rel="alternate" type="text/html" /><updated>2025-12-22T23:37:38+08:00</updated><id>http://localhost:4000/posts.xml</id><title type="html">ASCIIMX</title><author><name>W. D. Sadeep Madurange</name></author></feed> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/posts.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2025-12-24T16:28:25+08:00</updated><id>/posts.xml</id><title type="html">ASCIIMX</title><author><name>W. D. Sadeep Madurange</name></author></feed> \ No newline at end of file
diff --git a/_site/robots.txt b/_site/robots.txt
index d297064..e087884 100644
--- a/_site/robots.txt
+++ b/_site/robots.txt
@@ -1 +1 @@
-Sitemap: http://localhost:4000/sitemap.xml
+Sitemap: /sitemap.xml
diff --git a/_site/sitemap.xml b/_site/sitemap.xml
index 7a775fd..e8e9d7b 100644
--- a/_site/sitemap.xml
+++ b/_site/sitemap.xml
@@ -1,55 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
-<loc>http://localhost:4000/log/e-reader/</loc>
+<loc>/log/neo4j-a-star-search/</loc>
+<lastmod>2018-03-06T00:00:00+08:00</lastmod>
+</url>
+<url>
+<loc>/log/e-reader/</loc>
<lastmod>2023-10-24T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/etlas/</loc>
+<loc>/log/etlas/</loc>
<lastmod>2024-09-05T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/arduino-due/</loc>
+<loc>/log/arduino-due/</loc>
<lastmod>2024-09-16T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/bumblebee/</loc>
+<loc>/log/bumblebee/</loc>
<lastmod>2025-04-02T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/my-first-pcb/</loc>
+<loc>/log/my-first-pcb/</loc>
<lastmod>2025-04-26T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/arduino-uno/</loc>
+<loc>/log/arduino-uno/</loc>
<lastmod>2025-06-10T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/mosfet-switches/</loc>
+<loc>/log/mosfet-switches/</loc>
<lastmod>2025-06-22T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/fpm-door-lock/</loc>
+<loc>/log/fpm-door-lock/</loc>
<lastmod>2025-08-18T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/suckless-software/</loc>
+<loc>/log/suckless-software/</loc>
<lastmod>2025-11-30T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/log/matrix-digital-rain/</loc>
+<loc>/log/matrix-digital-rain/</loc>
<lastmod>2025-12-21T00:00:00+08:00</lastmod>
</url>
<url>
-<loc>http://localhost:4000/about/</loc>
+<loc>/about/</loc>
</url>
<url>
-<loc>http://localhost:4000/</loc>
+<loc>/</loc>
</url>
<url>
-<loc>http://localhost:4000/log/</loc>
+<loc>/log/</loc>
</url>
<url>
-<loc>http://localhost:4000/projects/</loc>
+<loc>/projects/</loc>
</url>
</urlset>