diff options
Diffstat (limited to '_log')
63 files changed, 1934 insertions, 65 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 + > halt + > at91sam3 gpnvm show + > at91sam3 gpnvm set 1 + > 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 Binary files differnew file mode 100644 index 0000000..081e6d4 --- /dev/null +++ b/_log/_site/arduino-due/connections.jpeg diff --git a/_log/_site/arduino-due/schematic.png b/_log/_site/arduino-due/schematic.png Binary files differnew file mode 100644 index 0000000..62ddadd --- /dev/null +++ b/_log/_site/arduino-due/schematic.png diff --git a/_log/_site/arduino-due/source.tar.gz b/_log/_site/arduino-due/source.tar.gz Binary files differnew file mode 100644 index 0000000..496567b --- /dev/null +++ b/_log/_site/arduino-due/source.tar.gz 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 Binary files differnew file mode 100644 index 0000000..bd74907 --- /dev/null +++ b/_log/_site/arduino-uno/breadboard.jpeg diff --git a/_log/_site/arduino-uno/pinout.png b/_log/_site/arduino-uno/pinout.png Binary files differnew file mode 100644 index 0000000..59acfbc --- /dev/null +++ b/_log/_site/arduino-uno/pinout.png 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 Binary files differnew file mode 100644 index 0000000..835600d --- /dev/null +++ b/_log/_site/bumblebee/bee.mp4 diff --git a/_log/_site/bumblebee/poster.png b/_log/_site/bumblebee/poster.png Binary files differnew file mode 100644 index 0000000..6dc955e --- /dev/null +++ b/_log/_site/bumblebee/poster.png diff --git a/_log/_site/bumblebee/thumb_sm.png b/_log/_site/bumblebee/thumb_sm.png Binary files differnew file mode 100644 index 0000000..f7cfbf3 --- /dev/null +++ b/_log/_site/bumblebee/thumb_sm.png 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 Binary files differnew file mode 100644 index 0000000..89e05eb --- /dev/null +++ b/_log/_site/e-reader/ereader.mp4 diff --git a/_log/_site/e-reader/poster.png b/_log/_site/e-reader/poster.png Binary files differnew file mode 100644 index 0000000..1e222d2 --- /dev/null +++ b/_log/_site/e-reader/poster.png diff --git a/_log/_site/e-reader/source.tar.gz b/_log/_site/e-reader/source.tar.gz Binary files differnew file mode 100644 index 0000000..3e343a7 --- /dev/null +++ b/_log/_site/e-reader/source.tar.gz diff --git a/_log/_site/e-reader/thumb_sm.png b/_log/_site/e-reader/thumb_sm.png Binary files differnew file mode 100644 index 0000000..7c971e8 --- /dev/null +++ b/_log/_site/e-reader/thumb_sm.png 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 < 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, &rc, (TickType_t) 0); + return 0; + } + if (!dht_await_pin_state(1, 80)) { + rc = 2; + xQueueSend(dht_evt_queue, &rc, (TickType_t) 0); + return 0; + } + if (!dht_await_pin_state(0, 80)) { + rc = 3; + xQueueSend(dht_evt_queue, &rc, (TickType_t) 0); + return 0; + } + + for (i = 0; i < DHT_DATA_LEN; i++) { + if (!(pwl = dht_await_pin_state(1, 50))) { + rc = 4; + xQueueSend(dht_evt_queue, &rc, (TickType_t) 0); + return 0; + } + if (!(pwh = dht_await_pin_state(0, 70))) { + rc = 5; + xQueueSend(dht_evt_queue, &rc, (TickType_t) 0); + return 0; + } + buf[i] = pwh > 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 Binary files differnew file mode 100644 index 0000000..cf4efc6 --- /dev/null +++ b/_log/_site/etlas/dash.jpg diff --git a/_log/_site/etlas/etlas_arch.png b/_log/_site/etlas/etlas_arch.png Binary files differnew file mode 100644 index 0000000..241e9f1 --- /dev/null +++ b/_log/_site/etlas/etlas_arch.png diff --git a/_log/_site/etlas/pcb.jpg b/_log/_site/etlas/pcb.jpg Binary files differnew file mode 100644 index 0000000..fcb40fa --- /dev/null +++ b/_log/_site/etlas/pcb.jpg 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="<mxfile host="app.diagrams.net" modified="2024-01-04T10:11:48.655Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0" etag="QhztmAZcJcNMJFkbNl2E" version="21.2.9" type="device"><diagram name="Page-1" id="Hvsc3-8LFRnBvTan2Q2q">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==</diagram></mxfile>"><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 Binary files differnew file mode 100644 index 0000000..8b12cf6 --- /dev/null +++ b/_log/_site/etlas/source.tar.gz diff --git a/_log/_site/etlas/thumb_sm.jpg b/_log/_site/etlas/thumb_sm.jpg Binary files differnew file mode 100644 index 0000000..a374879 --- /dev/null +++ b/_log/_site/etlas/thumb_sm.jpg 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 Binary files differnew file mode 100644 index 0000000..2bf47a9 --- /dev/null +++ b/_log/_site/fpm-door-lock/breadboard.jpg diff --git a/_log/_site/fpm-door-lock/footprint.png b/_log/_site/fpm-door-lock/footprint.png Binary files differnew file mode 100644 index 0000000..5511bf1 --- /dev/null +++ b/_log/_site/fpm-door-lock/footprint.png diff --git a/_log/_site/fpm-door-lock/gerber.zip b/_log/_site/fpm-door-lock/gerber.zip Binary files differnew file mode 100644 index 0000000..19a9d19 --- /dev/null +++ b/_log/_site/fpm-door-lock/gerber.zip diff --git a/_log/_site/fpm-door-lock/pcb.jpg b/_log/_site/fpm-door-lock/pcb.jpg Binary files differnew file mode 100644 index 0000000..fbd800b --- /dev/null +++ b/_log/_site/fpm-door-lock/pcb.jpg diff --git a/_log/_site/fpm-door-lock/pcb1.jpg b/_log/_site/fpm-door-lock/pcb1.jpg Binary files differnew file mode 100644 index 0000000..367187d --- /dev/null +++ b/_log/_site/fpm-door-lock/pcb1.jpg diff --git a/_log/_site/fpm-door-lock/source.tar.gz b/_log/_site/fpm-door-lock/source.tar.gz Binary files differnew file mode 100644 index 0000000..ef23422 --- /dev/null +++ b/_log/_site/fpm-door-lock/source.tar.gz diff --git a/_log/_site/fpm-door-lock/thumb_sm.jpg b/_log/_site/fpm-door-lock/thumb_sm.jpg Binary files differnew file mode 100644 index 0000000..a8fa534 --- /dev/null +++ b/_log/_site/fpm-door-lock/thumb_sm.jpg diff --git a/_log/_site/fpm-door-lock/video.mp4 b/_log/_site/fpm-door-lock/video.mp4 Binary files differnew file mode 100644 index 0000000..a907a9b --- /dev/null +++ b/_log/_site/fpm-door-lock/video.mp4 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->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 << 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 >> 32); + + mat->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->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 Binary files differnew file mode 100644 index 0000000..b9df873 --- /dev/null +++ b/_log/_site/matrix-digital-rain/katakana.png diff --git a/_log/_site/matrix-digital-rain/matrix.mp4 b/_log/_site/matrix-digital-rain/matrix.mp4 Binary files differnew file mode 100644 index 0000000..7edf5d6 --- /dev/null +++ b/_log/_site/matrix-digital-rain/matrix.mp4 diff --git a/_log/_site/matrix-digital-rain/poster.png b/_log/_site/matrix-digital-rain/poster.png Binary files differnew file mode 100644 index 0000000..1f68ca4 --- /dev/null +++ b/_log/_site/matrix-digital-rain/poster.png diff --git a/_log/_site/matrix-digital-rain/source.tar.gz b/_log/_site/matrix-digital-rain/source.tar.gz Binary files differnew file mode 100644 index 0000000..5a69236 --- /dev/null +++ b/_log/_site/matrix-digital-rain/source.tar.gz diff --git a/_log/_site/matrix-digital-rain/thumb_sm.png b/_log/_site/matrix-digital-rain/thumb_sm.png Binary files differnew file mode 100644 index 0000000..940965a --- /dev/null +++ b/_log/_site/matrix-digital-rain/thumb_sm.png 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 Binary files differnew file mode 100644 index 0000000..9858fa7 --- /dev/null +++ b/_log/_site/mosfet-switches/bjt.png diff --git a/_log/_site/mosfet-switches/n_high_side.png b/_log/_site/mosfet-switches/n_high_side.png Binary files differnew file mode 100644 index 0000000..c851768 --- /dev/null +++ b/_log/_site/mosfet-switches/n_high_side.png diff --git a/_log/_site/mosfet-switches/p_high_side.png b/_log/_site/mosfet-switches/p_high_side.png Binary files differnew file mode 100644 index 0000000..9f5397a --- /dev/null +++ b/_log/_site/mosfet-switches/p_high_side.png 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 Binary files differnew file mode 100644 index 0000000..f458e69 --- /dev/null +++ b/_log/_site/my-first-pcb/back.jpeg diff --git a/_log/_site/my-first-pcb/back_design.jpeg b/_log/_site/my-first-pcb/back_design.jpeg Binary files differnew file mode 100644 index 0000000..b6c0f5d --- /dev/null +++ b/_log/_site/my-first-pcb/back_design.jpeg diff --git a/_log/_site/my-first-pcb/front.jpeg b/_log/_site/my-first-pcb/front.jpeg Binary files differnew file mode 100644 index 0000000..2b2931f --- /dev/null +++ b/_log/_site/my-first-pcb/front.jpeg diff --git a/_log/_site/my-first-pcb/front_design.jpeg b/_log/_site/my-first-pcb/front_design.jpeg Binary files differnew file mode 100644 index 0000000..f81f09c --- /dev/null +++ b/_log/_site/my-first-pcb/front_design.jpeg diff --git a/_log/_site/my-first-pcb/gerber_back.zip b/_log/_site/my-first-pcb/gerber_back.zip Binary files differnew file mode 100644 index 0000000..26659ad --- /dev/null +++ b/_log/_site/my-first-pcb/gerber_back.zip diff --git a/_log/_site/my-first-pcb/gerber_front.zip b/_log/_site/my-first-pcb/gerber_front.zip Binary files differnew file mode 100644 index 0000000..864334e --- /dev/null +++ b/_log/_site/my-first-pcb/gerber_front.zip diff --git a/_log/_site/my-first-pcb/source.tar.gz b/_log/_site/my-first-pcb/source.tar.gz Binary files differnew file mode 100644 index 0000000..c31aa22 --- /dev/null +++ b/_log/_site/my-first-pcb/source.tar.gz diff --git a/_log/_site/my-first-pcb/thumb_sm.jpeg b/_log/_site/my-first-pcb/thumb_sm.jpeg Binary files differnew file mode 100644 index 0000000..c275b12 --- /dev/null +++ b/_log/_site/my-first-pcb/thumb_sm.jpeg 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<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; + } + } +} +</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 <tag> +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 <target></code> command will generate it from the defaults and compile +the software. The <code class="language-plaintext highlighter-rouge"><target></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. + |
