From 1a4a6cb6d2aa2c8512e9637dc5dd95997321c444 Mon Sep 17 00:00:00 2001 From: Sadeep Madurange Date: Sun, 4 Jan 2026 17:57:39 +0800 Subject: Fix the search engine post. --- _log/_site/arduino-due.html | 109 ---------- _log/_site/arduino-due/connections.jpeg | Bin 29090 -> 0 bytes _log/_site/arduino-due/schematic.png | Bin 68688 -> 0 bytes _log/_site/arduino-due/source.tar.gz | Bin 1174 -> 0 bytes _log/_site/arduino-uno.html | 76 ------- _log/_site/arduino-uno/3v3.Makefile | 46 ---- _log/_site/arduino-uno/Makefile | 43 ---- _log/_site/arduino-uno/breadboard.jpeg | Bin 54319 -> 0 bytes _log/_site/arduino-uno/pinout.png | Bin 247197 -> 0 bytes _log/_site/bumblebee.html | 40 ---- _log/_site/bumblebee/bee.mp4 | Bin 2352029 -> 0 bytes _log/_site/bumblebee/poster.png | Bin 18024 -> 0 bytes _log/_site/bumblebee/thumb_sm.png | Bin 6189 -> 0 bytes _log/_site/e-reader.html | 85 -------- _log/_site/e-reader/circuit.svg | 145 ------------- _log/_site/e-reader/ereader.mp4 | Bin 3101166 -> 0 bytes _log/_site/e-reader/poster.png | Bin 674187 -> 0 bytes _log/_site/e-reader/source.tar.gz | Bin 14304 -> 0 bytes _log/_site/e-reader/thumb_sm.png | Bin 240117 -> 0 bytes _log/_site/etlas.html | 103 --------- _log/_site/etlas/circuit.svg | 105 --------- _log/_site/etlas/dash.jpg | Bin 85874 -> 0 bytes _log/_site/etlas/etlas_arch.png | Bin 47732 -> 0 bytes _log/_site/etlas/pcb.jpg | Bin 75769 -> 0 bytes _log/_site/etlas/schematic.svg | 4 - _log/_site/etlas/source.tar.gz | Bin 46871 -> 0 bytes _log/_site/etlas/thumb_sm.jpg | Bin 55678 -> 0 bytes _log/_site/feed.xml | 1 - _log/_site/fpm-door-lock.html | 105 --------- _log/_site/fpm-door-lock/breadboard.jpg | Bin 46771 -> 0 bytes _log/_site/fpm-door-lock/footprint.png | Bin 198127 -> 0 bytes _log/_site/fpm-door-lock/gerber.zip | Bin 89431 -> 0 bytes _log/_site/fpm-door-lock/pcb.jpg | Bin 68237 -> 0 bytes _log/_site/fpm-door-lock/pcb1.jpg | Bin 37068 -> 0 bytes _log/_site/fpm-door-lock/source.tar.gz | Bin 29473 -> 0 bytes _log/_site/fpm-door-lock/thumb_sm.jpg | Bin 18380 -> 0 bytes _log/_site/fpm-door-lock/video.mp4 | Bin 13264594 -> 0 bytes _log/_site/matrix-digital-rain.html | 103 --------- _log/_site/matrix-digital-rain/katakana.png | Bin 133709 -> 0 bytes _log/_site/matrix-digital-rain/matrix.mp4 | Bin 696574 -> 0 bytes _log/_site/matrix-digital-rain/poster.png | Bin 233077 -> 0 bytes _log/_site/matrix-digital-rain/source.tar.gz | Bin 3602 -> 0 bytes _log/_site/matrix-digital-rain/thumb_sm.png | Bin 52762 -> 0 bytes _log/_site/mosfet-switches.html | 110 ---------- _log/_site/mosfet-switches/bjt.png | Bin 12838 -> 0 bytes _log/_site/mosfet-switches/n_high_side.png | Bin 10825 -> 0 bytes _log/_site/mosfet-switches/p_high_side.png | Bin 10724 -> 0 bytes _log/_site/my-first-pcb.html | 57 ----- _log/_site/my-first-pcb/back.jpeg | Bin 34023 -> 0 bytes _log/_site/my-first-pcb/back_design.jpeg | Bin 31946 -> 0 bytes _log/_site/my-first-pcb/front.jpeg | Bin 28997 -> 0 bytes _log/_site/my-first-pcb/front_design.jpeg | Bin 32174 -> 0 bytes _log/_site/my-first-pcb/gerber_back.zip | Bin 48217 -> 0 bytes _log/_site/my-first-pcb/gerber_front.zip | Bin 49605 -> 0 bytes _log/_site/my-first-pcb/source.tar.gz | Bin 6660 -> 0 bytes _log/_site/my-first-pcb/thumb_sm.jpeg | Bin 6181 -> 0 bytes _log/_site/neo4j-a-star-search.html | 307 --------------------------- _log/_site/robots.txt | 1 - _log/_site/sitemap.xml | 36 ---- _log/_site/suckless-software.html | 78 ------- _log/site-search.md | 120 ++++++++--- _site/feed.xml | 2 +- _site/index.html | 4 +- _site/log/site-search/index.html | 130 +++++++++--- _site/posts.xml | 2 +- _site/sitemap.xml | 2 +- cgi-bin/_site/feed.xml | 1 - cgi-bin/_site/find.cgi | 247 --------------------- cgi-bin/_site/indexer.pl | 116 ---------- cgi-bin/_site/robots.txt | 1 - cgi-bin/_site/sitemap.xml | 3 - 71 files changed, 198 insertions(+), 1984 deletions(-) delete mode 100644 _log/_site/arduino-due.html delete mode 100644 _log/_site/arduino-due/connections.jpeg delete mode 100644 _log/_site/arduino-due/schematic.png delete mode 100644 _log/_site/arduino-due/source.tar.gz delete mode 100644 _log/_site/arduino-uno.html delete mode 100644 _log/_site/arduino-uno/3v3.Makefile delete mode 100644 _log/_site/arduino-uno/Makefile delete mode 100644 _log/_site/arduino-uno/breadboard.jpeg delete mode 100644 _log/_site/arduino-uno/pinout.png delete mode 100644 _log/_site/bumblebee.html delete mode 100644 _log/_site/bumblebee/bee.mp4 delete mode 100644 _log/_site/bumblebee/poster.png delete mode 100644 _log/_site/bumblebee/thumb_sm.png delete mode 100644 _log/_site/e-reader.html delete mode 100644 _log/_site/e-reader/circuit.svg delete mode 100644 _log/_site/e-reader/ereader.mp4 delete mode 100644 _log/_site/e-reader/poster.png delete mode 100644 _log/_site/e-reader/source.tar.gz delete mode 100644 _log/_site/e-reader/thumb_sm.png delete mode 100644 _log/_site/etlas.html delete mode 100644 _log/_site/etlas/circuit.svg delete mode 100644 _log/_site/etlas/dash.jpg delete mode 100644 _log/_site/etlas/etlas_arch.png delete mode 100644 _log/_site/etlas/pcb.jpg delete mode 100644 _log/_site/etlas/schematic.svg delete mode 100644 _log/_site/etlas/source.tar.gz delete mode 100644 _log/_site/etlas/thumb_sm.jpg delete mode 100644 _log/_site/feed.xml delete mode 100644 _log/_site/fpm-door-lock.html delete mode 100644 _log/_site/fpm-door-lock/breadboard.jpg delete mode 100644 _log/_site/fpm-door-lock/footprint.png delete mode 100644 _log/_site/fpm-door-lock/gerber.zip delete mode 100644 _log/_site/fpm-door-lock/pcb.jpg delete mode 100644 _log/_site/fpm-door-lock/pcb1.jpg delete mode 100644 _log/_site/fpm-door-lock/source.tar.gz delete mode 100644 _log/_site/fpm-door-lock/thumb_sm.jpg delete mode 100644 _log/_site/fpm-door-lock/video.mp4 delete mode 100644 _log/_site/matrix-digital-rain.html delete mode 100644 _log/_site/matrix-digital-rain/katakana.png delete mode 100644 _log/_site/matrix-digital-rain/matrix.mp4 delete mode 100644 _log/_site/matrix-digital-rain/poster.png delete mode 100644 _log/_site/matrix-digital-rain/source.tar.gz delete mode 100644 _log/_site/matrix-digital-rain/thumb_sm.png delete mode 100644 _log/_site/mosfet-switches.html delete mode 100644 _log/_site/mosfet-switches/bjt.png delete mode 100644 _log/_site/mosfet-switches/n_high_side.png delete mode 100644 _log/_site/mosfet-switches/p_high_side.png delete mode 100644 _log/_site/my-first-pcb.html delete mode 100644 _log/_site/my-first-pcb/back.jpeg delete mode 100644 _log/_site/my-first-pcb/back_design.jpeg delete mode 100644 _log/_site/my-first-pcb/front.jpeg delete mode 100644 _log/_site/my-first-pcb/front_design.jpeg delete mode 100644 _log/_site/my-first-pcb/gerber_back.zip delete mode 100644 _log/_site/my-first-pcb/gerber_front.zip delete mode 100644 _log/_site/my-first-pcb/source.tar.gz delete mode 100644 _log/_site/my-first-pcb/thumb_sm.jpeg delete mode 100644 _log/_site/neo4j-a-star-search.html delete mode 100644 _log/_site/robots.txt delete mode 100644 _log/_site/sitemap.xml delete mode 100644 _log/_site/suckless-software.html delete mode 100644 cgi-bin/_site/feed.xml delete mode 100644 cgi-bin/_site/find.cgi delete mode 100644 cgi-bin/_site/indexer.pl delete mode 100644 cgi-bin/_site/robots.txt delete mode 100644 cgi-bin/_site/sitemap.xml diff --git a/_log/_site/arduino-due.html b/_log/_site/arduino-due.html deleted file mode 100644 index 172ee1a..0000000 --- a/_log/_site/arduino-due.html +++ /dev/null @@ -1,109 +0,0 @@ -

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.

- -

Toolchain

- -

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 ST-LINK/V2 programmer fits this -bill.

- -

The OpenOCD 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.

- -

Finally, we need the ARM GNU Compiler -Toolchain 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.

- -

Electrical connections

- -

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.

- - - - - - -
- Pinout -

Wiring

-
- Circuit -

Arduino Due

-
- -

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.

- -

Uploading the program

- -

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:

- -
$ arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -T script.ld \
-    -nostartfiles \
-    -nostdlib \
-    -o a.elf main.c
-
- -

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:

- -
$ 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"
-
- -

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.

- -

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.

- -

GPNVM bits

- -

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.

- -

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.

- -

Linker script

- -

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.

- -

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.

- -

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.

- -

Files: source.tar.gz

diff --git a/_log/_site/arduino-due/connections.jpeg b/_log/_site/arduino-due/connections.jpeg deleted file mode 100644 index 081e6d4..0000000 Binary files a/_log/_site/arduino-due/connections.jpeg and /dev/null differ diff --git a/_log/_site/arduino-due/schematic.png b/_log/_site/arduino-due/schematic.png deleted file mode 100644 index 62ddadd..0000000 Binary files a/_log/_site/arduino-due/schematic.png and /dev/null differ diff --git a/_log/_site/arduino-due/source.tar.gz b/_log/_site/arduino-due/source.tar.gz deleted file mode 100644 index 496567b..0000000 Binary files a/_log/_site/arduino-due/source.tar.gz and /dev/null differ diff --git a/_log/_site/arduino-uno.html b/_log/_site/arduino-uno.html deleted file mode 100644 index d8ffac4..0000000 --- a/_log/_site/arduino-uno.html +++ /dev/null @@ -1,76 +0,0 @@ -

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.

- -

The steps that follow refer to the following pinout.

- - - - - - -
- Pinout -

Pinout

-
- Circuit -

Breadboard

-
- -

5V-16MHz configuration

- -

Powering ATmega328P microcontrollers with 5V is the most common setup. This is -also how Arduino Uno boards are wired.

- -

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.

- -

Here’s a sample Makefile for compiling C programs for ATmega328P -microcontrollers using avr-gcc/avrdude toolchain.

- -

3.3V-8MHz configuration

- -

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.

- -

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).

- -

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.

- -

Power up the the ATmega328P by connecting its VCC 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.

- -

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. -Here’s a sample Makefile with adjusted parameters (e.g., baud -rate) for an 8MHz clock.

- -

Remarks

- -

In both configurations, if you intend to use the ATmega328P’s analog-to-digital -converter with the internal 1.1V or AVcc voltage as reference, do -not connect AREF (pin 21) to Vcc. Refer to section 23.5.2 in the -datasheet for more information.

- diff --git a/_log/_site/arduino-uno/3v3.Makefile b/_log/_site/arduino-uno/3v3.Makefile deleted file mode 100644 index 4ca89d4..0000000 --- a/_log/_site/arduino-uno/3v3.Makefile +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 9db7b09..0000000 --- a/_log/_site/arduino-uno/Makefile +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index bd74907..0000000 Binary files a/_log/_site/arduino-uno/breadboard.jpeg and /dev/null differ diff --git a/_log/_site/arduino-uno/pinout.png b/_log/_site/arduino-uno/pinout.png deleted file mode 100644 index 59acfbc..0000000 Binary files a/_log/_site/arduino-uno/pinout.png and /dev/null differ diff --git a/_log/_site/bumblebee.html b/_log/_site/bumblebee.html deleted file mode 100644 index a466d0b..0000000 --- a/_log/_site/bumblebee.html +++ /dev/null @@ -1,40 +0,0 @@ -

Bumblebee is a tool I built for one of my employers to automate the generation -of web scraping scripts.

- - - -

In 2024, we were tasked with collecting market data using various methods, -including scraping data from authorized websites for traders’ use.

- -

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.

- -

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 WebView2, and -the Scintilla.NET text editor.

- -

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.

- -

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.

- -

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.

- diff --git a/_log/_site/bumblebee/bee.mp4 b/_log/_site/bumblebee/bee.mp4 deleted file mode 100644 index 835600d..0000000 Binary files a/_log/_site/bumblebee/bee.mp4 and /dev/null differ diff --git a/_log/_site/bumblebee/poster.png b/_log/_site/bumblebee/poster.png deleted file mode 100644 index 6dc955e..0000000 Binary files a/_log/_site/bumblebee/poster.png and /dev/null differ diff --git a/_log/_site/bumblebee/thumb_sm.png b/_log/_site/bumblebee/thumb_sm.png deleted file mode 100644 index f7cfbf3..0000000 Binary files a/_log/_site/bumblebee/thumb_sm.png and /dev/null differ diff --git a/_log/_site/e-reader.html b/_log/_site/e-reader.html deleted file mode 100644 index f60c485..0000000 --- a/_log/_site/e-reader.html +++ /dev/null @@ -1,85 +0,0 @@ -

This project features an experimental e-reader powered by an ESP-WROOM-32 -development board and a 7.5-inch Waveshare -e-paper display built with the intention of learning about e-paper displays.

- - - -

Introduction

- -

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.

- -

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.

- -

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.

- -

The file format

- -

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).

- -

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.

- -

The pdftoebm.py script enclosed in the tarball at the end of the page converts -PDF documents to EBM files.

- -

How does it work?

- -

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 -EBM_ARCH_URL 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 -EBM_ARCH_URL value. The latter requires us to recompile the embedded -software.

- -

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.

- -

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.

- -

Epilogue

- -

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.

- -

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.

- -

Files: source.tar.gz

diff --git a/_log/_site/e-reader/circuit.svg b/_log/_site/e-reader/circuit.svg deleted file mode 100644 index fd7508b..0000000 --- a/_log/_site/e-reader/circuit.svg +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - 10 kΩ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 2 - 3 - 4 - - - - - - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - - - - - 5 - 6 - 7 - 8 - BUS - E-Paper Display HAT - - - CS - - DC - - DIN - - CLK - - BUSY - - RST - - GND - - VCC - - ESP-WROOM-32 - - IO21 - - - IO5 - - - IO16 - - - IO23 - - - IO18 - - - IO22 - - - IO4 - - - IO2 - - - GND - - - 3V3 - - - GND - - - IO15 - - - - \ No newline at end of file diff --git a/_log/_site/e-reader/ereader.mp4 b/_log/_site/e-reader/ereader.mp4 deleted file mode 100644 index 89e05eb..0000000 Binary files a/_log/_site/e-reader/ereader.mp4 and /dev/null differ diff --git a/_log/_site/e-reader/poster.png b/_log/_site/e-reader/poster.png deleted file mode 100644 index 1e222d2..0000000 Binary files a/_log/_site/e-reader/poster.png and /dev/null differ diff --git a/_log/_site/e-reader/source.tar.gz b/_log/_site/e-reader/source.tar.gz deleted file mode 100644 index 3e343a7..0000000 Binary files a/_log/_site/e-reader/source.tar.gz and /dev/null differ diff --git a/_log/_site/e-reader/thumb_sm.png b/_log/_site/e-reader/thumb_sm.png deleted file mode 100644 index 7c971e8..0000000 Binary files a/_log/_site/e-reader/thumb_sm.png and /dev/null differ diff --git a/_log/_site/etlas.html b/_log/_site/etlas.html deleted file mode 100644 index 06e3ce7..0000000 --- a/_log/_site/etlas.html +++ /dev/null @@ -1,103 +0,0 @@ -

Etlas is a news, stock market, and weather tracker powered by an ESP32 NodeMCU -D1, featuring a 7.5-inch Waveshare e-paper display and a -DHT22 sensor module.

- - - - - - -
frontback
- -

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.

- -

The following diagram outlines the Etlas’s overall system architecture.

- -

architecture

- -

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.

- -

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:

- -
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;
-}
-
- -

I ported this implementation from ESP8266 -to ESP32—all credit for the algorithm belongs to them.

- -

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 -examples for Arduino and STM32 -platforms.

- -

Etlas has been running reliably for over a year since August 2024.

- -

Files: source.tar.gz

diff --git a/_log/_site/etlas/circuit.svg b/_log/_site/etlas/circuit.svg deleted file mode 100644 index 6255045..0000000 --- a/_log/_site/etlas/circuit.svg +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - AM2302 - - - DATA - - GND - - VCC - E-Paper display HAT - - - CS - - DC - - DIN - - CLK - - BUSY - - RST - - PWR - - GND - - VCC - - - - - - - - - - - - - - 1 - 2 - 3 - 4 - - - - - 1 - 2 - 3 - 4 - - ESP32 Mini NodeMCU D1 - - IO19 - - - IO15 - - - GND - - - IO27 - - - IO14 - - - IO13 - - - IO25 - - - IO26 - - - IO16 - - - GND - - - 3V3 - - - \ No newline at end of file diff --git a/_log/_site/etlas/dash.jpg b/_log/_site/etlas/dash.jpg deleted file mode 100644 index cf4efc6..0000000 Binary files a/_log/_site/etlas/dash.jpg and /dev/null differ diff --git a/_log/_site/etlas/etlas_arch.png b/_log/_site/etlas/etlas_arch.png deleted file mode 100644 index 241e9f1..0000000 Binary files a/_log/_site/etlas/etlas_arch.png and /dev/null differ diff --git a/_log/_site/etlas/pcb.jpg b/_log/_site/etlas/pcb.jpg deleted file mode 100644 index fcb40fa..0000000 Binary files a/_log/_site/etlas/pcb.jpg and /dev/null differ diff --git a/_log/_site/etlas/schematic.svg b/_log/_site/etlas/schematic.svg deleted file mode 100644 index 3070dd1..0000000 --- a/_log/_site/etlas/schematic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -152726131425193V3GND
ESP32 Mini NodeMCU D1
ESP32 Mini NodeMCU D1
DHT22
DHT22
E-paper HAT
E-paper HAT
3.3V
3.3V
3.3V
3.3V
3.3V
3.3V
CS
CS
DC
DC
RST
RST
CLK
CLK
MOSY
MOSY
BUSY
BUSY
VCC
VCC
GND
GND
VCC
VCC
GND
GND
DATA
DATA
Text is not SVG - cannot display
\ No newline at end of file diff --git a/_log/_site/etlas/source.tar.gz b/_log/_site/etlas/source.tar.gz deleted file mode 100644 index 8b12cf6..0000000 Binary files a/_log/_site/etlas/source.tar.gz and /dev/null differ diff --git a/_log/_site/etlas/thumb_sm.jpg b/_log/_site/etlas/thumb_sm.jpg deleted file mode 100644 index a374879..0000000 Binary files a/_log/_site/etlas/thumb_sm.jpg and /dev/null differ diff --git a/_log/_site/feed.xml b/_log/_site/feed.xml deleted file mode 100644 index ab90c82..0000000 --- a/_log/_site/feed.xml +++ /dev/null @@ -1 +0,0 @@ -Jekyll2025-12-24T08:01:11+08:00/feed.xml \ No newline at end of file diff --git a/_log/_site/fpm-door-lock.html b/_log/_site/fpm-door-lock.html deleted file mode 100644 index 820af3f..0000000 --- a/_log/_site/fpm-door-lock.html +++ /dev/null @@ -1,105 +0,0 @@ -

This project features a fingerprint door lock powered by an ATmega328P -microcontroller.

- - - -

Overview

- -

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.

- -

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.

- -

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.

- -

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.

- -

Embedded software

- -

The embedded software, written in C, includes a driver for the sensor, servo -control routines, and a battery monitoring system.

- -

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.

- -

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.

- -

The PCB

- -

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.

- - - - - - - - - -
- PCB - - Design -
- PCB footprint -
- -

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.

- -

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.

- -

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.

- -

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.

- -

Epilogue

- -

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.

- -

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.

- -

Files: source.tar.gz, gerber.zip

diff --git a/_log/_site/fpm-door-lock/breadboard.jpg b/_log/_site/fpm-door-lock/breadboard.jpg deleted file mode 100644 index 2bf47a9..0000000 Binary files a/_log/_site/fpm-door-lock/breadboard.jpg and /dev/null differ diff --git a/_log/_site/fpm-door-lock/footprint.png b/_log/_site/fpm-door-lock/footprint.png deleted file mode 100644 index 5511bf1..0000000 Binary files a/_log/_site/fpm-door-lock/footprint.png and /dev/null differ diff --git a/_log/_site/fpm-door-lock/gerber.zip b/_log/_site/fpm-door-lock/gerber.zip deleted file mode 100644 index 19a9d19..0000000 Binary files a/_log/_site/fpm-door-lock/gerber.zip and /dev/null differ diff --git a/_log/_site/fpm-door-lock/pcb.jpg b/_log/_site/fpm-door-lock/pcb.jpg deleted file mode 100644 index fbd800b..0000000 Binary files a/_log/_site/fpm-door-lock/pcb.jpg and /dev/null differ diff --git a/_log/_site/fpm-door-lock/pcb1.jpg b/_log/_site/fpm-door-lock/pcb1.jpg deleted file mode 100644 index 367187d..0000000 Binary files a/_log/_site/fpm-door-lock/pcb1.jpg and /dev/null differ diff --git a/_log/_site/fpm-door-lock/source.tar.gz b/_log/_site/fpm-door-lock/source.tar.gz deleted file mode 100644 index ef23422..0000000 Binary files a/_log/_site/fpm-door-lock/source.tar.gz and /dev/null differ diff --git a/_log/_site/fpm-door-lock/thumb_sm.jpg b/_log/_site/fpm-door-lock/thumb_sm.jpg deleted file mode 100644 index a8fa534..0000000 Binary files a/_log/_site/fpm-door-lock/thumb_sm.jpg and /dev/null differ diff --git a/_log/_site/fpm-door-lock/video.mp4 b/_log/_site/fpm-door-lock/video.mp4 deleted file mode 100644 index a907a9b..0000000 Binary files a/_log/_site/fpm-door-lock/video.mp4 and /dev/null differ diff --git a/_log/_site/matrix-digital-rain.html b/_log/_site/matrix-digital-rain.html deleted file mode 100644 index 85bac5d..0000000 --- a/_log/_site/matrix-digital-rain.html +++ /dev/null @@ -1,103 +0,0 @@ -

The Matrix digital rain implemented in raw C using ANSI escape sequences with -zero dependencies—not even ncurses.

- - - -

This project began over three years ago as a fork of Domsson’s unique rendition -of the Matrix rain: Fakesteak. I -aimed to modify the algorithm to produce a rain that resembled the original -with high visual fidelity.

- -

Unicode support

- -

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:

- -
#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;
-}
-
- -

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:

- -
#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;
-}
-
- -

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 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.

- -

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:

- -
$ cc -O3 main.c -o matrix
-$ ./matrix
-
- -

“All I see is blonde, brunette, red head.”

- -

Files: source.tar.gz

diff --git a/_log/_site/matrix-digital-rain/katakana.png b/_log/_site/matrix-digital-rain/katakana.png deleted file mode 100644 index b9df873..0000000 Binary files a/_log/_site/matrix-digital-rain/katakana.png and /dev/null differ diff --git a/_log/_site/matrix-digital-rain/matrix.mp4 b/_log/_site/matrix-digital-rain/matrix.mp4 deleted file mode 100644 index 7edf5d6..0000000 Binary files a/_log/_site/matrix-digital-rain/matrix.mp4 and /dev/null differ diff --git a/_log/_site/matrix-digital-rain/poster.png b/_log/_site/matrix-digital-rain/poster.png deleted file mode 100644 index 1f68ca4..0000000 Binary files a/_log/_site/matrix-digital-rain/poster.png and /dev/null differ diff --git a/_log/_site/matrix-digital-rain/source.tar.gz b/_log/_site/matrix-digital-rain/source.tar.gz deleted file mode 100644 index 5a69236..0000000 Binary files a/_log/_site/matrix-digital-rain/source.tar.gz and /dev/null differ diff --git a/_log/_site/matrix-digital-rain/thumb_sm.png b/_log/_site/matrix-digital-rain/thumb_sm.png deleted file mode 100644 index 940965a..0000000 Binary files a/_log/_site/matrix-digital-rain/thumb_sm.png and /dev/null differ diff --git a/_log/_site/mosfet-switches.html b/_log/_site/mosfet-switches.html deleted file mode 100644 index c1a7b52..0000000 --- a/_log/_site/mosfet-switches.html +++ /dev/null @@ -1,110 +0,0 @@ -

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.

- -

Acknowledgments

- -

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 Simon Fitch.

- -

Preamble

- -

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).

- -

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-channel high-side switch

- -

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-channel high-side switching circuit

- -

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.

- -

N-channel high-side switch

- -

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:

- -

N-channel high-side switching circuit

- -

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.

- -

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.

- -

The D1 flyback diodes used in the two topologies safeguard the MOSFET from -voltage spikes caused by inductive loads such as servos.

- -

A BJT alternative

- -

A Bipolar Junction Transistor (BJT) is a simpler, cheaper, and more widely -available type of transistor that can be used as a switch.

- -

BJT architecture

- -

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. “How to choose a -transistor as a switch” is an excellent guide on using BJTs as electronic -switches.

- -

Which topology to choose?

- -

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 VGS potentials -required to achieve specified RDS(on) values (i.e., to turn them on -fully).

- -

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.

- -

Further reading

- - diff --git a/_log/_site/mosfet-switches/bjt.png b/_log/_site/mosfet-switches/bjt.png deleted file mode 100644 index 9858fa7..0000000 Binary files a/_log/_site/mosfet-switches/bjt.png and /dev/null differ diff --git a/_log/_site/mosfet-switches/n_high_side.png b/_log/_site/mosfet-switches/n_high_side.png deleted file mode 100644 index c851768..0000000 Binary files a/_log/_site/mosfet-switches/n_high_side.png and /dev/null differ diff --git a/_log/_site/mosfet-switches/p_high_side.png b/_log/_site/mosfet-switches/p_high_side.png deleted file mode 100644 index 9f5397a..0000000 Binary files a/_log/_site/mosfet-switches/p_high_side.png and /dev/null differ diff --git a/_log/_site/my-first-pcb.html b/_log/_site/my-first-pcb.html deleted file mode 100644 index d5e886b..0000000 --- a/_log/_site/my-first-pcb.html +++ /dev/null @@ -1,57 +0,0 @@ -

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.

- -

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.

- - - - - - - - - - -
- Design (front) -

Footprint (front)

-
- PCB (front) -

PCB (front)

-
- Design (back) -

Footprint (back)

-
- PCB (back) -

PCB (back)

-
- -

The PCBs have two layers. A copper region serves as the ground plane. The 0.3mm -wide 1oz/ft2 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.

- -

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.

- -

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/ft2 traces would have been -safer.

- -

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.

- -

Files: gerber_back.zip, gerber_front.zip, - source.tar.gz

diff --git a/_log/_site/my-first-pcb/back.jpeg b/_log/_site/my-first-pcb/back.jpeg deleted file mode 100644 index f458e69..0000000 Binary files a/_log/_site/my-first-pcb/back.jpeg and /dev/null differ diff --git a/_log/_site/my-first-pcb/back_design.jpeg b/_log/_site/my-first-pcb/back_design.jpeg deleted file mode 100644 index b6c0f5d..0000000 Binary files a/_log/_site/my-first-pcb/back_design.jpeg and /dev/null differ diff --git a/_log/_site/my-first-pcb/front.jpeg b/_log/_site/my-first-pcb/front.jpeg deleted file mode 100644 index 2b2931f..0000000 Binary files a/_log/_site/my-first-pcb/front.jpeg and /dev/null differ diff --git a/_log/_site/my-first-pcb/front_design.jpeg b/_log/_site/my-first-pcb/front_design.jpeg deleted file mode 100644 index f81f09c..0000000 Binary files a/_log/_site/my-first-pcb/front_design.jpeg and /dev/null differ diff --git a/_log/_site/my-first-pcb/gerber_back.zip b/_log/_site/my-first-pcb/gerber_back.zip deleted file mode 100644 index 26659ad..0000000 Binary files a/_log/_site/my-first-pcb/gerber_back.zip and /dev/null differ diff --git a/_log/_site/my-first-pcb/gerber_front.zip b/_log/_site/my-first-pcb/gerber_front.zip deleted file mode 100644 index 864334e..0000000 Binary files a/_log/_site/my-first-pcb/gerber_front.zip and /dev/null differ diff --git a/_log/_site/my-first-pcb/source.tar.gz b/_log/_site/my-first-pcb/source.tar.gz deleted file mode 100644 index c31aa22..0000000 Binary files a/_log/_site/my-first-pcb/source.tar.gz and /dev/null differ diff --git a/_log/_site/my-first-pcb/thumb_sm.jpeg b/_log/_site/my-first-pcb/thumb_sm.jpeg deleted file mode 100644 index c275b12..0000000 Binary files a/_log/_site/my-first-pcb/thumb_sm.jpeg and /dev/null differ diff --git a/_log/_site/neo4j-a-star-search.html b/_log/_site/neo4j-a-star-search.html deleted file mode 100644 index 9903abc..0000000 --- a/_log/_site/neo4j-a-star-search.html +++ /dev/null @@ -1,307 +0,0 @@ -

Back in 2018, we used Neo4J 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 v3.4.0 of the -Neo4J graph algorithms shipped with our A* search algorithm.

- diff --git a/_log/_site/robots.txt b/_log/_site/robots.txt deleted file mode 100644 index e087884..0000000 --- a/_log/_site/robots.txt +++ /dev/null @@ -1 +0,0 @@ -Sitemap: /sitemap.xml diff --git a/_log/_site/sitemap.xml b/_log/_site/sitemap.xml deleted file mode 100644 index 8c167bd..0000000 --- a/_log/_site/sitemap.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - -/arduino-due.html - - -/arduino-uno.html - - -/bumblebee.html - - -/e-reader.html - - -/etlas.html - - -/fpm-door-lock.html - - -/matrix-digital-rain.html - - -/mosfet-switches.html - - -/my-first-pcb.html - - -/neo4j-a-star-search.html - - -/suckless-software.html - - diff --git a/_log/_site/suckless-software.html b/_log/_site/suckless-software.html deleted file mode 100644 index d91ed16..0000000 --- a/_log/_site/suckless-software.html +++ /dev/null @@ -1,78 +0,0 @@ -

Since suckless software requires users to modify the -source code and recompile to customize, I need a way to maintain patches over -the long term while retaining the ability to upgrade the software as new -versions are released.

- -

Initial setup

- -

When using a suckless program, I usually begin by cloning the project and -setting the remote push URL to my own git repository:

- -
git clone git://git.suckless.org/dwm
-git reset --hard <tag>
-git remote set-url --push origin git@git.asciimx.com:/repos/dwm
-
- -

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.

- -

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 -make clean <target> command will generate it from the defaults and compile -the software. The <target> is the name of the application (e.g., dwm) found -in the Makefile. I modify the resulting config.h file and run make clean -install to install the software before committing and pushing my changes to -the git repo.

- -

dwm and slstatus

- -

Since dwm and slstatus are always running, make install 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 make install to install the software and switch back to the -graphical session (Ctrl + Alt + F5).

- -

The key combinations for switching to the tty and back may differ across -systems. The ones listed above are for OpenBSD.

- -

Subsequent upgrades

- -

When suckless releases a new version, I run git pull --rebase 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.

- -

Commit log before upgrading:

- -
dt236  My patch.
-3fkdf  Version 6.5.
-
- -

Commit log after pulling:

- -
w467d  My patch.
-gh25g  A commit.
-g525g  Another commit.
-3fkdf  Version 6.6.
-vd425  Old commit.
-q12vu  Another old commit.
-3fkdf  Version 6.5.
-
- -

Commit log after the interactive rebase:

- -
h57jh  My patch.
-3fkdf  Version 6.6.
-vd425  Old commit.
-q12vu  Another old commit.
-3fkdf  Version 6.5.
-
- -

And finally, I commit and push all the changes to my git repository.

- diff --git a/_log/site-search.md b/_log/site-search.md index 6af9f63..d178d07 100644 --- a/_log/site-search.md +++ b/_log/site-search.md @@ -1,25 +1,49 @@ --- -title: Perl + FastCGI + SA search engine -date: 2026-01-02 +title: Search engine (Perl + FastCGI + SA) +date: 2026-01-03 layout: post --- -Number of articles growing. Need search. +Article count on the website is growing. Need a way to search. -Requirements: substring match, case-insensitive, fast, secure. No JavaScript. +Requirements: matches substrings, case-insensitive, fast, secure. No +JavaScript. -Architecture: OpenBSD httpd → slowcgi (FastCGI) → Perl script. +Architecture: browser → httpd → slowcgi → Perl CGI script. -Data structure: suffix array. Three files: corpus.bin (articles), sa.bin -(sorted byte offsets), file_map.dat (metadata). +httpd, slowcgi, Perl are in the OpenBSD base system. No dependencies. Access +governed by file system permissions--no secrets to manage. Secure by default. -Indexer crawls posts, extracts HTML with regex, lowercases, concatenates. Null -byte sentinel for document boundaries. Sort lexicographically:: +2025-12-30: Regex search. Wrote a 140-line Perl script that searches HTML files +using Regex. Script slurps 500 files (16 KB each) in 40 milliseconds. Fast +enough. Start to feel the O(N) pull at higher file counts. + +Regex and the site crawl introduce ReDoS and symlink attack vectors. Both can +be mitigated. Tempted to stop here. + +2026-01-03: Suffix array (SA) based index lookup. + +Inefficiency of scanning every file on each request troubles me. Regex search +depends almost entirely on hardware for speed. + +Built SA index comprising three files: corpus.bin (articles), sa.bin (sorted +byte offsets), file_map.dat (metadata). Index created with the site: + +``` +$ JEKYLL_ENV=production bundle exec jekyll build +$ cd cgi-bin/ +$ perl indexer.pl +``` + +Indexer extracts HTML, lowercases, encodes text as UTF-8 binary sequences, and +saves to corpus.bin. Null byte sentinel marks the document boundaries. Suffix +array stores offsets of all suffixes as 32-bit unsigned integers, sorted by the +lexicographical order: ``` -# Use a block that forces byte-level comparison +my @sa = 0 .. (length($corpus) - 1); { - use bytes; + use bytes; # Force compare 8-bit Unicode value comparisons @sa = sort { # First 64 bytes check (fast path) (substr($corpus, $a, 64) cmp substr($corpus, $b, 64)) || @@ -27,31 +51,73 @@ byte sentinel for document boundaries. Sort lexicographically:: (substr($corpus, $a) cmp substr($corpus, $b)) } @sa; } + +CORPUS: a s c i \0 i m x +OFFSET: 0 1 2 3 4 5 6 7 + +SORTED SUFFIXES: + +[4] \0imx +[0] asci\0imx +[2] ci\0imx +[3] i\0imx <-- "i" from "asci" +[5] imx <-- "i" from "imx" +[6] mx +[1] sci\0imx +[7] x +``` + +Algorithmic complexity: O(L⋅N log N). Fast path caps L at 64 bytes (length of a +cache line), reducing complexity to O(N log N). + +Search: Textbook range query with two binary searches. Random access to offsets +and the text is possible via the fixed-width offsets: + +``` +seek($fh_sa, $mid * 4, 0); +read($fh_sa, my $bin_off, 4); +my $off = unpack("L", $bin_off); +seek($fh_cp, $off, 0); +read($fh_cp, my $text, $query_len); ``` -Slow path: O(L⋅N log N). Fast path caps L at 64 bytes → O(N log N). 64-byte -length targets cache lines. +Small seek/reads are fast on modern SSDs. Keeps memory usage low, and was easy +to implement. Note to self: mmap. -Search: binary search for range query. Cap at 20 results--define limits or be -surprised by them. +Benchmarks on T490 (i7-10510U, OpenBSD 7.8, article size: 16 KB). -File IO and memory: many seek/read small chunks beat one large allocation (see -benchmarks for find_one_file.cgi). +500 files: + - Index size: 204.94 KB + - Indexing time: 0.1475 s + - Peak RAM (SA): 8828 KB + - Peak RAM (Regex): 9136 KB + - Search (SA): 0.0012 s + - Search (Regex): 0.0407 s -Benchmarks on T490 (i7-10510U, OpenBSD 7.8, 16KB articles): +1,000 files: + - Index size: 410.51 KB + - Indexing time: 0.3101 s + - Peak RAM (SA): 8980 KB + - Peak RAM (Regex): 9460 KB + - Search (SA): 0.0019 s + - Search (Regex): 0.0795 s -1,000 files: 0.31s indexing, 410 KB total index. -10,000 files: 10.97s indexing, 4.16 MB total index. +10,000 files: + - Index size: 4163.44 KB + - Indexing time: 10.9661 s + - Peak RAM (SA): 12504 KB + - Peak RAM (Regex): 12804 KB + - Search (SA): 0.0161 s + - Search (Regex): 0.9120 s -Search 'arduino' (0 matches): -1,000 files: 0.002s (SA) vs 0.080s (naive regex). -10,000 files: 0.016s (SA) vs 0.912s (naive regex). +Security: Much of it comes from its architectural simplicity. No dependencies +to manage, no secrets to hide, no assets for lateral movement. Runs in chroot. -Security. Semaphore (lock files) limits parallel queries. Escape HTML (XSS). -Sanitize input--strip non-printables, limit length, and quote metacharacters -(ReDOS). No exec/system (command injection). Chroot. +Resource exhaustion and XSS attacks are inherent. The former is mitigated by +limiting concurrent searches via lock-file semaphores and capping query length +(64B) and result sets (20). All output is HTML-escaped to prevent XSS. -Verdict: Fast SA lookup. Primary attack vectors mitigated. No dependencies. +Verdict: Fast SA lookup; Works on every conceivable web browser. Commit: [6da102d](https://git.asciimx.com/www/commit/?h=term&id=6da102d6e0494a3eac3f05fa3b2cdcc25ba2754e) diff --git a/_site/feed.xml b/_site/feed.xml index 4070b39..cb9e91e 100644 --- a/_site/feed.xml +++ b/_site/feed.xml @@ -1 +1 @@ -Jekyll2026-01-03T19:18:57+08:00/feed.xmlASCIIMX | LogW. D. Sadeep MadurangePerl + FastCGI + SA search engine2026-01-02T00:00:00+08:002026-01-02T00:00:00+08:00/log/site-searchW. D. Sadeep MadurangeMatrix Rain: 2025 refactor2025-12-21T00:00:00+08:002025-12-21T00:00:00+08:00/log/matrix-digital-rainW. D. Sadeep MadurangeFingerprint door lock (LP)2025-08-18T00:00:00+08:002025-08-18T00:00:00+08:00/log/fpm-door-lock-lpW. D. Sadeep MadurangeHigh-side MOSFET switching2025-06-22T00:00:00+08:002025-06-22T00:00:00+08:00/log/mosfet-switchesW. D. Sadeep MadurangeATmega328P at 3.3V and 5V2025-06-10T00:00:00+08:002025-06-10T00:00:00+08:00/log/arduino-unoW. D. Sadeep MadurangeFingerprint door lock (RF)2025-06-05T00:00:00+08:002025-06-05T00:00:00+08:00/log/fpm-door-lock-rfW. D. Sadeep MadurangeBumblebee: browser automation2025-04-02T00:00:00+08:002025-04-02T00:00:00+08:00/log/bumblebeeW. D. Sadeep MadurangeBare-metal ATSAM3X8E2024-09-16T00:00:00+08:002024-09-16T00:00:00+08:00/log/arduino-dueW. D. Sadeep MadurangeEtlas: e-paper dashboard2024-09-05T00:00:00+08:002024-09-05T00:00:00+08:00/log/etlasW. D. Sadeep MadurangeESP32 e-reader prototype2023-10-24T00:00:00+08:002023-10-24T00:00:00+08:00/log/e-readerW. D. Sadeep Madurange \ No newline at end of file +Jekyll2026-01-04T17:31:30+08:00/feed.xmlASCIIMX | LogW. D. Sadeep MadurangeSearch engine (Perl + FastCGI + SA)2026-01-03T00:00:00+08:002026-01-03T00:00:00+08:00/log/site-searchW. D. Sadeep MadurangeMatrix Rain: 2025 refactor2025-12-21T00:00:00+08:002025-12-21T00:00:00+08:00/log/matrix-digital-rainW. D. Sadeep MadurangeFingerprint door lock (LP)2025-08-18T00:00:00+08:002025-08-18T00:00:00+08:00/log/fpm-door-lock-lpW. D. Sadeep MadurangeHigh-side MOSFET switching2025-06-22T00:00:00+08:002025-06-22T00:00:00+08:00/log/mosfet-switchesW. D. Sadeep MadurangeATmega328P at 3.3V and 5V2025-06-10T00:00:00+08:002025-06-10T00:00:00+08:00/log/arduino-unoW. D. Sadeep MadurangeFingerprint door lock (RF)2025-06-05T00:00:00+08:002025-06-05T00:00:00+08:00/log/fpm-door-lock-rfW. D. Sadeep MadurangeBumblebee: browser automation2025-04-02T00:00:00+08:002025-04-02T00:00:00+08:00/log/bumblebeeW. D. Sadeep MadurangeBare-metal ATSAM3X8E2024-09-16T00:00:00+08:002024-09-16T00:00:00+08:00/log/arduino-dueW. D. Sadeep MadurangeEtlas: e-paper dashboard2024-09-05T00:00:00+08:002024-09-05T00:00:00+08:00/log/etlasW. D. Sadeep MadurangeESP32 e-reader prototype2023-10-24T00:00:00+08:002023-10-24T00:00:00+08:00/log/e-readerW. D. Sadeep Madurange \ No newline at end of file diff --git a/_site/index.html b/_site/index.html index 39a51a5..ef782ee 100644 --- a/_site/index.html +++ b/_site/index.html @@ -58,11 +58,11 @@ - Perl + FastCGI + SA search engine + Search engine (Perl + FastCGI + SA) - + diff --git a/_site/log/site-search/index.html b/_site/log/site-search/index.html index 04f98e9..9170002 100644 --- a/_site/log/site-search/index.html +++ b/_site/log/site-search/index.html @@ -3,7 +3,7 @@ - Perl + FastCGI + SA search engine + Search engine (Perl + FastCGI + SA) @@ -37,24 +37,47 @@
-

PERL + FASTCGI + SA SEARCH ENGINE

-
02 JANUARY 2026
+

SEARCH ENGINE (PERL + FASTCGI + SA)

+
03 JANUARY 2026

-

Number of articles growing. Need search.

+

Article count on the website is growing. Need a way to search.

-

Requirements: substring match, case-insensitive, fast, secure. No JavaScript.

+

Requirements: matches substrings, case-insensitive, fast, secure. No +JavaScript.

-

Architecture: OpenBSD httpd → slowcgi (FastCGI) → Perl script.

+

Architecture: browser → httpd → slowcgi → Perl CGI script.

-

Data structure: suffix array. Three files: corpus.bin (articles), sa.bin -(sorted byte offsets), file_map.dat (metadata).

+

httpd, slowcgi, Perl are in the OpenBSD base system. No dependencies. Access +governed by file system permissions–no secrets to manage. Secure by default.

-

Indexer crawls posts, extracts HTML with regex, lowercases, concatenates. Null -byte sentinel for document boundaries. Sort lexicographically::

+

2025-12-30: Regex search. Wrote a 140-line Perl script that searches HTML files +using Regex. Script slurps 500 files (16 KB each) in 40 milliseconds. Fast +enough. Start to feel the O(N) pull at higher file counts.

-
# Use a block that forces byte-level comparison
+

Regex and the site crawl introduce ReDoS and symlink attack vectors. Both can +be mitigated. Tempted to stop here.

+ +

2026-01-03: Suffix array (SA) based index lookup.

+ +

Inefficiency of scanning every file on each request troubles me. Regex search +depends almost entirely on hardware for speed.

+ +

Built SA index comprising three files: corpus.bin (articles), sa.bin (sorted +byte offsets), file_map.dat (metadata). Index created with the site:

+ +
$ JEKYLL_ENV=production bundle exec jekyll build
+$ cd cgi-bin/
+$ perl indexer.pl
+
+ +

Indexer extracts HTML, lowercases, encodes text as UTF-8 binary sequences, and +saves to corpus.bin. Null byte sentinel marks the document boundaries. Suffix +array stores offsets of all suffixes as 32-bit unsigned integers, sorted by the +lexicographical order:

+ +
my @sa = 0 .. (length($corpus) - 1);
 {
-    use bytes; 
+    use bytes; # Force compare 8-bit Unicode value comparisons
     @sa = sort { 
         # First 64 bytes check (fast path)
         (substr($corpus, $a, 64) cmp substr($corpus, $b, 64)) || 
@@ -62,31 +85,78 @@ byte sentinel for document boundaries. Sort lexicographically::

(substr($corpus, $a) cmp substr($corpus, $b)) } @sa; } -
- -

Slow path: O(L⋅N log N). Fast path caps L at 64 bytes → O(N log N). 64-byte -length targets cache lines.

-

Search: binary search for range query. Cap at 20 results–define limits or be -surprised by them.

+CORPUS: a s c i \0 i m x +OFFSET: 0 1 2 3 4 5 6 7 -

File IO and memory: many seek/read small chunks beat one large allocation (see -benchmarks for find_one_file.cgi).

+SORTED SUFFIXES: -

Benchmarks on T490 (i7-10510U, OpenBSD 7.8, 16KB articles):

+[4] \0imx +[0] asci\0imx +[2] ci\0imx +[3] i\0imx <-- "i" from "asci" +[5] imx <-- "i" from "imx" +[6] mx +[1] sci\0imx +[7] x +
-

1,000 files: 0.31s indexing, 410 KB total index.
-10,000 files: 10.97s indexing, 4.16 MB total index.

+

Algorithmic complexity: O(L⋅N log N). Fast path caps L at 64 bytes (length of a +cache line), reducing complexity to O(N log N).

-

Search ‘arduino’ (0 matches):
-1,000 files: 0.002s (SA) vs 0.080s (naive regex).
-10,000 files: 0.016s (SA) vs 0.912s (naive regex).

+

Search: Textbook range query with two binary searches. Random access to offsets +and the text is possible via the fixed-width offsets:

-

Security. Semaphore (lock files) limits parallel queries. Escape HTML (XSS). -Sanitize input–strip non-printables, limit length, and quote metacharacters -(ReDOS). No exec/system (command injection). Chroot.

+
seek($fh_sa, $mid * 4, 0);
+read($fh_sa, my $bin_off, 4);
+my $off = unpack("L", $bin_off);
+seek($fh_cp, $off, 0);
+read($fh_cp, my $text, $query_len);
+
-

Verdict: Fast SA lookup. Primary attack vectors mitigated. No dependencies.

+

Small seek/reads are fast on modern SSDs. Keeps memory usage low, and was easy +to implement. Note to self: mmap.

+ +

Benchmarks on T490 (i7-10510U, OpenBSD 7.8, article size: 16 KB).

+ +

500 files:

+
    +
  • Index size: 204.94 KB
  • +
  • Indexing time: 0.1475 s
  • +
  • Peak RAM (SA): 8828 KB
  • +
  • Peak RAM (Regex): 9136 KB
  • +
  • Search (SA): 0.0012 s
  • +
  • Search (Regex): 0.0407 s
  • +
+ +

1,000 files:

+
    +
  • Index size: 410.51 KB
  • +
  • Indexing time: 0.3101 s
  • +
  • Peak RAM (SA): 8980 KB
  • +
  • Peak RAM (Regex): 9460 KB
  • +
  • Search (SA): 0.0019 s
  • +
  • Search (Regex): 0.0795 s
  • +
+ +

10,000 files:

+
    +
  • Index size: 4163.44 KB
  • +
  • Indexing time: 10.9661 s
  • +
  • Peak RAM (SA): 12504 KB
  • +
  • Peak RAM (Regex): 12804 KB
  • +
  • Search (SA): 0.0161 s
  • +
  • Search (Regex): 0.9120 s
  • +
+ +

Security: Much of it comes from its architectural simplicity. No dependencies +to manage, no secrets to hide, no assets for lateral movement. Runs in chroot.

+ +

Resource exhaustion and XSS attacks are inherent. The former is mitigated by +limiting concurrent searches via lock-file semaphores and capping query length +(64B) and result sets (20). All output is HTML-escaped to prevent XSS.

+ +

Verdict: Fast SA lookup; Works on every conceivable web browser.

Commit: 6da102d diff --git a/_site/posts.xml b/_site/posts.xml index 02e7149..b7913a1 100644 --- a/_site/posts.xml +++ b/_site/posts.xml @@ -1 +1 @@ -Jekyll2026-01-03T19:18:57+08:00/posts.xmlASCIIMXW. D. Sadeep Madurange \ No newline at end of file +Jekyll2026-01-04T17:31:30+08:00/posts.xmlASCIIMXW. D. Sadeep Madurange \ No newline at end of file diff --git a/_site/sitemap.xml b/_site/sitemap.xml index 3a7874c..ad3bdcd 100644 --- a/_site/sitemap.xml +++ b/_site/sitemap.xml @@ -42,7 +42,7 @@ /log/site-search/ -2026-01-02T00:00:00+08:00 +2026-01-03T00:00:00+08:00 /about/ diff --git a/cgi-bin/_site/feed.xml b/cgi-bin/_site/feed.xml deleted file mode 100644 index 66f8d32..0000000 --- a/cgi-bin/_site/feed.xml +++ /dev/null @@ -1 +0,0 @@ -Jekyll2026-01-03T14:43:24+08:00http://localhost:4000/feed.xml \ No newline at end of file diff --git a/cgi-bin/_site/find.cgi b/cgi-bin/_site/find.cgi deleted file mode 100644 index ab066dd..0000000 --- a/cgi-bin/_site/find.cgi +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use Storable qw(retrieve); -use Encode qw(decode_utf8 encode_utf8); -use URI::Escape qw(uri_unescape); -use HTML::Escape qw(escape_html); - -# Configuration -my $max_parallel = 50; # Max parallel search requests -my $lock_timeout = 30; # Seconds before dropping stale locks -my $max_results = 20; # Max search results to display -my $sa_file = 'sa.bin'; # Suffix Array index -my $cp_file = 'corpus.bin'; # Raw text corpus -my $map_file = 'file_map.dat'; # File metadata -my $lock_dir = '/tmp/search_locks'; # Semaphore directory - -# Concurrency control -mkdir $lock_dir, 0777 unless -d $lock_dir; -my $active_count = 0; -my $now = time(); - -opendir(my $dh, $lock_dir); -while (my $file = readdir($dh)) { - next unless $file =~ /\.lock$/; - my $path = "$lock_dir/$file"; - my $mtime = (stat($path))[9] || 0; - ($now - $mtime > $lock_timeout) ? unlink($path) : $active_count++; -} -closedir($dh); - -# Template variables -my $year = (localtime)[5] + 1900; -my $search_text = ''; - -# Busy check -if ($active_count >= $max_parallel) { - print "Content-Type: text/html\n\n"; - render_html("

Server busy. Please try again in a few seconds.

", "", $year); - exit; -} - -# Create semaphore lock -my $lock_file = "$lock_dir/$$.lock"; -open(my $fh_lock, '>', $lock_file); - -# Query decoding -if (($ENV{QUERY_STRING} || '') =~ /^q=([^&]*)/) { - my $raw_q = $1; - $raw_q =~ tr/+/ /; - $search_text = uri_unescape($raw_q); - $search_text = decode_utf8($search_text // ""); - $search_text =~ s/\P{Print}//g; - $search_text = substr($search_text, 0, 64); - $search_text =~ s/^\s+|\s+$//g; -} - -my $safe_search_text = escape_html($search_text); - -print "Content-Type: text/html\n\n"; - -if ($search_text eq '') { - final_output("

Please enter a search term above.

"); -} - -# Binary search -my @results; -my $query = encode_utf8(lc($search_text)); -my $query_len = length($query); - -if (-f $sa_file && -f $cp_file) { - open(my $fh_sa, '<', $sa_file) or die $!; - open(my $fh_cp, '<', $cp_file) or die $!; - binmode($fh_sa); - binmode($fh_cp); - - my $file_map = retrieve($map_file); - my $total_suffixes = (-s $sa_file) / 4; - - # Find left boundary - my ($low, $high) = (0, $total_suffixes - 1); - my $first_hit = -1; - - while ($low <= $high) { - my $mid = int(($low + $high) / 2); - seek($fh_sa, $mid * 4, 0); - read($fh_sa, my $bin_off, 4); - my $off = unpack("L", $bin_off); - seek($fh_cp, $off, 0); - read($fh_cp, my $text, $query_len); - - my $cmp = $text cmp $query; - if ($cmp >= 0) { - $first_hit = $mid if $cmp == 0; - $high = $mid - 1; - } else { - $low = $mid + 1; - } - } - - # Collect results if found - if ($first_hit != -1) { - my $last_hit = $first_hit; - ($low, $high) = ($first_hit, $total_suffixes - 1); - - # Find right boundary - while ($low <= $high) { - my $mid = int(($low + $high) / 2); - seek($fh_sa, $mid * 4, 0); - read($fh_sa, my $bin_off, 4); - my $off = unpack("L", $bin_off); - seek($fh_cp, $off, 0); - read($fh_cp, my $text, $query_len); - - if (($text cmp $query) <= 0) { - $last_hit = $mid if $text eq $query; - $low = $mid + 1; - } else { - $high = $mid - 1; - } - } - - my %seen; - for my $i ($first_hit .. $last_hit) { - seek($fh_sa, $i * 4, 0); - read($fh_sa, my $bin_off, 4); - my $offset = unpack("L", $bin_off); - - foreach my $m (@$file_map) { - if ($offset >= $m->{start} && $offset < $m->{end}) { - if (!$seen{$m->{path}}++) { - # Capture more than 50 chars for trimming - my $snip_start = ($offset - 30 < $m->{start}) ? $m->{start} : $offset - 30; - my $max_len = $m->{end} - $snip_start; - my $read_len = ($max_len > 120) ? 120 : $max_len; - seek($fh_cp, $snip_start, 0); - read($fh_cp, my $raw_snip, $read_len); - - my $snippet = decode_utf8($raw_snip, Encode::FB_QUIET) // $raw_snip; - $snippet =~ s/\s+/ /g; # Normalize whitespace - - # Trim start: Partial word removal - if ($snip_start > $m->{start}) { - $snippet =~ s/^[^\s]*\s//; - } - - # Trim end: Length limit and partial word removal - my $has_more = 0; - if (length($snippet) > 50) { - $snippet = substr($snippet, 0, 50); - $has_more = 1 if $snippet =~ s/\s+[^\s]*$//; - } - elsif ($snip_start + $read_len < $m->{end}) { - # This check handles snippets that are naturally short but - # there's still more text in the article we didn't read - $has_more = 1; - } - - # Cleanup & capitalize - $snippet = ucfirst($snippet); - $snippet = escape_html($snippet) . ($has_more ? "..." : ""); - - my $clean_path = $m->{path}; - $clean_path =~ s|^\.\./_site/||; - - push @results, { - path => $clean_path, - title => $m->{title},, - snippet => $snippet - }; - } - last; - } - } - last if scalar @results >= $max_results; - } - } - close($fh_sa); - close($fh_cp); -} - -# --- Formatting & Output --- -my $list_html = ""; -if (@results == 0) { - $list_html = "

No results found for \"$safe_search_text\".

"; -} else { - $list_html = ""; -} - -final_output($list_html); - -# --- Helpers --- -sub final_output { - my ($content) = @_; - render_html($content, $safe_search_text, $year); - if ($fh_lock) { close($fh_lock); unlink($lock_file); } - exit; -} - -sub render_html { - my ($content, $q_val, $yr) = @_; - print <<"HTML"; - - - - - - Search - - - - - -
-
-

Search

-
- - -
- $content -
-
- - - -HTML -} - diff --git a/cgi-bin/_site/indexer.pl b/cgi-bin/_site/indexer.pl deleted file mode 100644 index 69f6838..0000000 --- a/cgi-bin/_site/indexer.pl +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use File::Find; -use Storable qw(store); -use Encode qw(encode_utf8); -use HTML::Entities qw(decode_entities); -use Time::HiRes qw(gettimeofday tv_interval); - -my $dir = '../_site/log'; -my $cgi_dir = '../_site/cgi-bin/'; -my $corpus_file = "${cgi_dir}corpus.bin"; -my $sa_file = "${cgi_dir}sa.bin"; -my $map_file = "${cgi_dir}file_map.dat"; - -my %excluded_files = ( - 'index.html' => 1, # /log/index.html -); - -# Start timing -my $t0 = [gettimeofday]; - -my $corpus = ""; -my @file_map; - -print "Building corpus...\n"; - -find({ - wanted => sub { - # Only index index.html files - return unless -f $_ && $_ eq 'index.html'; - - my $rel_path = $File::Find::name; - $rel_path =~ s|^\Q$dir\E/?||; - return if $excluded_files{$rel_path}; - - if (open my $fh, '<:encoding(UTF-8)', $_) { - my $content = do { local $/; <$fh> }; - close $fh; - - my ($title) = $content =~ m|(.*?)|is; - $title //= (split('/', $File::Find::name))[-2]; # Fallback to folder name - $title =~ s/^\s+|\s+$//g; - - # Extract content from
or use whole file - my ($text) = $content =~ m|
(.*?)
|is; - $text //= $content; - - # Strip tags and normalize whitespace - $text =~ s|]*>.*?| |gs; - $text =~ s|]*>.*?| |gs; - $text =~ s|<[^>]+>| |g; - $text = decode_entities($text); - $text =~ s|\s+| |g; - $text =~ s/^\s+|\s+$//g; - - # CRITICAL: Convert to lowercase and then to raw bytes - # This ensures length() and substr() work on byte offsets for seek() - my $raw_entry = encode_utf8(lc($text) . "\0"); - - my $start = length($corpus); - $corpus .= $raw_entry; - - push @file_map, { - start => $start, - end => length($corpus), - title => $title, - path => $File::Find::name - }; - } - }, - no_chdir => 0, -}, $dir); - -print "Sorting suffixes...\n"; - -# Initialize the array of indices -my @sa = 0 .. (length($corpus) - 1); - -# Use a block that forces byte-level comparison -{ - use bytes; - @sa = sort { - # First 64 bytes check (fast path) - (substr($corpus, $a, 64) cmp substr($corpus, $b, 64)) || - # Full string fallback (required for correctness) - (substr($corpus, $a) cmp substr($corpus, $b)) - } @sa; -} - -print "Writing index files to disk...\n"; - -open my $cfh, '>', $corpus_file or die "Cannot write $corpus_file: $!"; -binmode($cfh); # Raw byte mode -print $cfh $corpus; -close $cfh; - -open my $sfh, '>', $sa_file or die "Cannot write $sa_file: $!"; -binmode($sfh); -# Pack as 32-bit unsigned integers (standard 'L') -print $sfh pack("L*", @sa); -close $sfh; - -store \@file_map, $map_file; - -my $elapsed = tv_interval($t0); -my $c_size = -s $corpus_file; -my $s_size = -s $sa_file; - -printf "\nIndexing Complete!\n"; -printf "Total Time: %.4f seconds\n", $elapsed; -printf "Corpus Size: %.2f KB\n", $c_size / 1024; -printf "Suffix Array: %.2f KB\n", $s_size / 1024; -printf "Files Processed: %d\n", scalar(@file_map); - diff --git a/cgi-bin/_site/robots.txt b/cgi-bin/_site/robots.txt deleted file mode 100644 index d297064..0000000 --- a/cgi-bin/_site/robots.txt +++ /dev/null @@ -1 +0,0 @@ -Sitemap: http://localhost:4000/sitemap.xml diff --git a/cgi-bin/_site/sitemap.xml b/cgi-bin/_site/sitemap.xml deleted file mode 100644 index 9bf9de2..0000000 --- a/cgi-bin/_site/sitemap.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - -- cgit v1.2.3