[{"content":"I started running a little after 6 a.m., when Shenzhen was still half asleep. From Nanshan, I headed south along familiar city roads toward the coastal area, letting a 16.33 km morning run gently set the rhythm for the day.\nThe route ran almost straight north to south, cutting through the quiet early-morning city and gradually opening up toward the coastline. Traffic was minimal, the air was cool, and the rhythm of footsteps echoed against empty sidewalks.\nAs the city thinned out, the scenery changed. The sky softened, the water lay completely still, and a thin crescent moon lingered above the horizon. The reflection of trees and sky on the bay made everything feel slower and calmer — a rare contrast to Shenzhen’s usual pace.\nIt was a steady, controlled effort. The pace stayed comfortably under 5 minutes per kilometer—fast enough to feel strong, but relaxed enough to remain sustainable throughout.\nDistance: 16.33 km Time: 01:20:48 Average Pace: 4'57\u0026quot;/km Fastest Pace: 4'49\u0026quot;/km Calories Burned: 898 kcal Long runs like this are less about speed and more about mindset. No music, no rush — just breathing, cadence, and a quiet conversation with the road.\nSixteen kilometers later, legs felt solid, mind felt clear, and the day hadn’t even properly begun yet. Sometimes, the best way to get ahead of a busy day is to start it before the city wakes up.\n","permalink":"https://luoyao.info/posts/shenzhen-morning-run/","summary":"A 16.33 km dawn run from Nanshan to Shenzhen Bay — empty streets, still water, and a steady sub-5 pace.","title":"16.33 km at Dawn | A Long Morning Run in Shenzhen"},{"content":"Preface Looking back at my 2025 fitness record, it was not dramatic or extreme — but it was consistent, intentional, and honest.\nThere were no sudden breakthroughs or short-lived bursts of motivation.\nInstead, there was a repeated decision made on many ordinary days: go outside and move.\nThe numbers are only outcomes. What matters more is that the time itself was used well.\n1. Walking: The Base Layer of the Year Total steps in 2025:\n3,971,726 steps\nDays with over 10,000 steps: 152 Increase compared to last year: +568,485 steps Estimated carbon reduction equivalent: 56 trees Peak daily step counts ranged from 28k to 34k, mostly driven by travel days, long outdoor walks, or intentionally extended routes.\nWalking does not require mental preparation like running does, yet it proved to be the most reliable form of movement throughout the year.\nIt quietly formed the foundation of all other activities.\n2. Running: From Habit to Rhythm Total running distance:\n1,399.2 km\nRunning days: 127 Ahead of 96% of users Increase compared to last year: +767.5 km On average:\n2–3 runs per week 10–12 km per session There was no obsession with pace. What gradually emerged was rhythm and sustainability.\nBeing able to keep running mattered more than running fast.\n3. Half Marathons: Milestones Along the Way Total completed in 2025:\n9 half marathons\nBest half marathon time:\n1 hour 40 minutes Achieved on:\nOctober 8 This was not a result of talent, but of accumulation.\nNo aggressive starts, no dramatic collapses — just steady execution.\nOn race day, the experience was surprisingly calm.\nThe body already knew what to do.\n4. A Few Takeaways Several conclusions stand out clearly after reviewing the year:\nConsistency beats intensity\nLong-term frequency matters far more than occasional extremes.\nWalking is underrated\nIt supports recovery, stability, and mental clarity.\nPerformance is a byproduct\nWhen movement becomes part of daily life, results follow naturally.\nClosing Thoughts There were no record-breaking moments in 2025.\nBut there were many quiet confirmations of effort.\nSteps do not lie.\nThey mark where time has truly gone.\n2025 was walked and run — honestly, steadily, and fully.\nOn to the next year.\n","permalink":"https://luoyao.info/posts/2025-sports-review/","summary":"A review of my 2025 fitness record — walking, running, and half marathons, measured one step at a time.","title":"2025 | A Year Measured in Steps"},{"content":"Overview This article introduces Driver-Hub, a Feishu (Lark) Web Application I built to connect with our internal GitLab server.\nThe app allows searching projects, browsing branches, viewing commits, and downloading code archives directly from Feishu’s workplace UI.\nThe implementation is purely web-based.\nTraditional bot features were removed — users open Driver-Hub in Feishu, interact with form controls, and the app communicates with GitLab APIs through FastAPI.\n1. Feishu Developer Platform Setup Before writing any code, configure a Feishu developer app to host the Driver-Hub UI.\nStep 1. Create a New Application Go to your organization’s Feishu Developer Console. Click Create App → select Internal Application. Enter the name Driver-Hub, add an icon and description. Save and note your App ID and App Secret. Step 2. Enable Web Application Left sidebar → Functional Configuration → Web Application. Enable “Workplace Web Application”. Fill homepage URLs (desktop \u0026amp; mobile): https://\u0026lt;your-ngrok-domain\u0026gt;/ Save the configuration. Step 3. Publish the Application After setup, go to Version Management \u0026amp; Release → Release → “Only visible to me”.\nThen open Feishu → “Workplace” → find Driver-Hub → open the embedded page.\n2. Running a Local HTTPS Endpoint with ngrok Feishu requires a public HTTPS endpoint for embedded web pages.\nInstead of deploying to the cloud, we use ngrok to expose FastAPI (localhost:3000) securely.\nStep 1. Install ngrok Using Scoop on Windows:\nscoop install ngrok Or macOS Homebrew:\nbrew install ngrok/ngrok/ngrok Step 2. Connect Your Account After signing up at https://ngrok.com:\nngrok config add-authtoken \u0026lt;YOUR_AUTHTOKEN\u0026gt; Step 3. Start the Tunnel ngrok http 3000 Output example:\nForwarding https://driverhub.ngrok-free.dev -\u0026gt; http://localhost:3000 Use this HTTPS address in Feishu’s web app config.\n⚠️ Free-tier ngrok adds a browser warning page that blocks Feishu WebView.\nFor stable use, upgrade ngrok or deploy Driver-Hub on an internal HTTPS server.\n3. GitLab Personal Access Token Driver-Hub communicates with GitLab via its REST API, requiring a Personal Access Token (PAT).\nStep 1. Create Token Log in to your GitLab (e.g., https://gitlab.xxx.net). Go to Preferences → Access Tokens. Set: Name: driver-hub Scopes: api, read_repository Click Create Token, then copy it (only shown once). Step 2. Test the Token curl --header \u0026#34;PRIVATE-TOKEN: \u0026lt;your_token\u0026gt;\u0026#34; https://gitlab.xxx.net/api/v4/user Should return user info JSON.\n4. FastAPI Backend main.py serves both UI and REST endpoints.\nEndpoint Method Description / GET Serve main HTML UI /api/config/test POST Validate GitLab base URL and token /api/projects GET Search GitLab projects /api/projects/{id}/branches GET List branches /api/projects/{id}/commits GET List commits /api/projects/{id}/branches/{branch}/archive GET Download ZIP Example route:\n@app.get(\u0026#34;/\u0026#34;) def index(): return \u0026#34;\u0026lt;h2\u0026gt;Driver-Hub Web UI is running\u0026lt;/h2\u0026gt;\u0026#34; 5. Run the Application Start FastAPI locally:\npython main.py Then start ngrok:\nngrok http 3000 Open Feishu → “Workplace” → Driver-Hub → view the web UI.\n6. Summary Driver-Hub is a lightweight internal GitLab management console embedded directly inside Feishu.\nIt uses Python + FastAPI for backend logic, ngrok for HTTPS tunneling, and GitLab’s REST API for project operations.\nFor production, host Driver-Hub on an internal server with HTTPS, e.g. https://driver-hub.xxx.net.\nProject Structure\n. ├── main.py ├── gitlab_client.py ├── favicon.ico └── .env ","permalink":"https://luoyao.info/posts/driver-hub-feishu-webapp/","summary":"How I built a focused Feishu web app for searching GitLab projects, browsing branches and commits, and downloading source archives.","title":"Building Driver-Hub: A Feishu Web App for GitLab Project Management"},{"content":"When running Doom Emacs under Mingw64 inside Windows Terminal, you may find the UI symbols (like those from Nerd Font) showing up as random blocks or question marks.\nThis issue is almost always caused by Windows console encoding not using UTF-8, even though your Emacs and fonts are already configured correctly.\nThis article explains why it happens and how to fix it completely — including installing Nerd Fonts via Scoop, adjusting Windows Terminal profiles, and ensuring UTF-8 consistency across all layers.\n1. Root Cause On Windows, every console session has a code page.\nIf it’s not set to UTF-8 (code page 65001), the shell misinterprets UTF-8 bytes as ANSI text before passing them to the terminal renderer.\nSo even with the correct font, you’ll still see garbled icons.\nDoom Emacs and Nerd Fonts use Unicode glyphs in UTF-8.\nWe simply need to ensure the console also interprets everything as UTF-8.\n2. Install a Nerd Font via Scoop Open PowerShell and make sure Scoop is installed:\niwr -useb get.scoop.sh | iex Add the nerd-fonts bucket:\nscoop bucket add nerd-fonts Install one of the Nerd Fonts you like, for example JetBrainsMono Nerd Font:\nscoop install JetBrainsMono-NF Once installed, the font appears in your Windows system fonts and can be selected in Windows Terminal.\n3. Configure Windows Terminal Profile for Mingw64 Open Windows Terminal → Settings → Profiles → Mingw64 (or MSYS2 if you named it differently).\nUpdate the Command line to explicitly set UTF-8 before starting Bash:\n\u0026#34;commandline\u0026#34;: \u0026#34;C:\\\\Windows\\\\System32\\\\cmd.exe /c chcp 65001 \u0026gt;NUL \u0026amp; C:\\\\msys64\\\\usr\\\\bin\\\\bash.exe -li\u0026#34; Explanation:\ncmd /c chcp 65001 \u0026gt;NUL sets the active console code page to UTF-8 quietly. bash.exe -li launches your usual interactive shell. \u0026amp; chains the commands so the shell inherits UTF-8 from the console layer. This approach is cleaner than putting chcp.com 65001 inside .bashrc, since it affects only terminal sessions and leaves non-interactive shells untouched.\n4. Set the Font in Windows Terminal Under the same profile:\nGo to Appearance → Font face\nChoose your installed font, e.g.:\nJetBrainsMono Nerd Font Save settings and restart the terminal.\nIf you use other profiles (PowerShell, WSL, etc.), you can optionally apply the same font to keep icons consistent.\n5. Verify Encoding Inside your Mingw64 shell:\necho $LANG It should show something like:\nen_US.UTF-8 Then run:\nchcp Expected output:\nActive code page: 65001 If both are correct, you now have a fully UTF-8 environment.\n6. Launch Doom Emacs Now simply run:\nemacs -nw or open GUI Emacs normally.\nAll those lovely Nerd Font icons (from mode line, dashboard, etc.) will render properly in Windows Terminal.\n7. Optional: System-Wide UTF-8 Setting If you want Windows to always default to UTF-8, enable this option:\nControl Panel → Region → Administrative → Change system locale → Check “Use Unicode UTF-8 for worldwide language support.”\nThis ensures all console sessions start in UTF-8 by default.\nHowever, beware: some legacy software may not handle UTF-8 well.\n8. Summary Layer What to Check Correct Setting Windows Code Page chcp output 65001 Bash Locale $LANG en_US.UTF-8 Terminal Font Windows Terminal → Appearance Nerd Font Emacs UTF-8 internal encoding default Once these are aligned, Doom Emacs in Mingw64 becomes just as beautiful and functional as on Linux or macOS — with perfect icons and smooth input.\nReferences MSYS2 UTF-8 Locale Documentation Nerd Fonts Project Windows Terminal JSON Configuration Enjoy your perfectly rendered Doom Emacs on Windows!\n","permalink":"https://luoyao.info/posts/doom-emacs-windows-terminal-utf8/","summary":"Why Nerd Font icons garble in Doom Emacs under MSYS2/MinGW64 in Windows Terminal, and how to fix it with UTF-8 code pages and font setup.","title":"Fixing Doom Emacs Icon Garbling in Windows Terminal (MSYS2 + Mingw64)"},{"content":"\nIntroduction A recent system crash forced me to wipe years of accumulated clutter—an overstuffed 1TB SSD, conflicting software, and tangled environment variables. I rebuilt the machine from scratch by preparing a Ventoy USB stick and installing Windows 10 Pro 22H2. This article captures the resulting development environment so I can reproduce it quickly or share the recipe with teammates.\nThe guide covers:\nShell tooling with zsh/oh-my-zsh and PowerShell/oh-my-posh Doom Emacs across WSL Debian, VirtualBox Debian, and MSYS2/MinGW64 A reproducible MicroPython build toolchain Shared V2RayN proxy settings plus Debian and pip mirror configuration Scoop and Pacman package manager tuning Consistent Windows Terminal profiles Hardware Baseline Model: HP ZHAN 66 Pro 14 G4 mobile workstation CPU: 11th Gen Intel® Core™ i7-1165G7 @ 2.80 GHz GPU: Intel® Iris® Xe Graphics (shared memory) RAM: 16 GB DDR4 (dual channel) Storage: 1TB Samsung 980 Pro NVMe SSD OS: Windows 10 Pro 22H2, clean installation (November 2025) Input devices: FLICO · Majestouch 2 (FILCKF16) + Logitech GPW3 All instructions below are validated on this configuration. Comparable 11th-gen Intel laptops should deliver nearly identical results.\nNetworking and Mirrors Sharing the Windows V2RayN Proxy Locate the host IP from WSL:\ncat /etc/resolv.conf | grep nameserver This typically reveals 192.168.x.x for VirtualBox bridged networking or 172.30.x.x for WSL.\nSet proxy variables:\nexport http_proxy=socks://\u0026lt;windows-ip\u0026gt;:10808 export https_proxy=socks://\u0026lt;windows-ip\u0026gt;:10808 export all_proxy=socks://\u0026lt;windows-ip\u0026gt;:10808 Persist the variables: append the three lines above to ~/.zshrc (or ~/.bashrc) in both WSL Debian and VirtualBox Debian. VirtualBox should use a Bridged Adapter so the guest reaches the Windows host.\nConfigure apt: create /etc/apt/apt.conf.d/proxy.conf with\nAcquire::http::Proxy \u0026#34;socks://\u0026lt;windows-ip\u0026gt;:10808\u0026#34;; Acquire::https::Proxy \u0026#34;socks://\u0026lt;windows-ip\u0026gt;:10808\u0026#34;; Debian mirror (Tsinghua University) Update /etc/apt/sources.list in both Debian environments:\ndeb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware Then refresh:\nsudo apt update Python pip mirror (Tsinghua University) Create ~/.pip/pip.conf on Debian and %APPDATA%\\pip\\pip.ini on Windows:\n[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple timeout = 6000 Shell Tooling zsh + oh-my-zsh Install and standardize zsh across WSL Debian and MSYS2 so aliases and prompts behave the same everywhere.\nWSL or VirtualBox Debian:\nsudo apt install zsh git curl -y chsh -s $(which zsh) Install oh-my-zsh:\nsh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; Configure ~/.zshrc (ideally checked into dotfiles):\nplugins=(git z sudo extract zsh-autosuggestions zsh-syntax-highlighting) ZSH_THEME=\u0026#34;powerlevel10k/powerlevel10k\u0026#34; source $ZSH/oh-my-zsh.sh Clone Powerlevel10k once and let p10k configure tailor the prompt per environment:\ngit clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k MSYS2 MinGW64:\npacman -S zsh git Make zsh the default shell by editing C:\\msys64\\msys2_shell.cmd:\nset \u0026#34;MSYS2_PATH_TYPE=inherit\u0026#34; set \u0026#34;MSYSTEM=MINGW64\u0026#34; set \u0026#34;SHELL=/usr/bin/zsh\u0026#34; Symlink shared dotfiles from Windows storage (example):\nln -s /mnt/c/Users/\u0026lt;USERNAME\u0026gt;/dotfiles/.zshrc ~/.zshrc ln -s /mnt/c/Users/\u0026lt;USERNAME\u0026gt;/dotfiles/.p10k.zsh ~/.p10k.zsh PowerShell + oh-my-posh Install PowerShell 7 and enhance it with oh-my-posh and PSReadLine:\nwinget install --id Microsoft.PowerShell winget install JanDeDobbeleer.OhMyPosh Install-Module -Name PSReadLine -Scope CurrentUser -Force Bootstrap your $PROFILE (create it if missing):\nif (!(Test-Path -Path $PROFILE)) { New-Item -ItemType File -Force -Path $PROFILE | Out-Null } @\u0026#39; Import-Module PSReadLine Set-PSReadLineOption -PredictionSource HistoryAndPlugin Set-PSReadLineOption -PredictionViewStyle ListView oh-my-posh init pwsh --config \u0026#34;$env:POSH_THEMES_PATH\\paradox.omp.json\u0026#34; | Invoke-Expression \u0026#39;@ | Set-Content -Path $PROFILE -Encoding UTF8 Install JetBrainsMono Nerd Font (from https://www.nerdfonts.com/font-downloads) and set it in Windows Terminal so glyphs render correctly in both shells.\nDoom Emacs Across Platforms Shared installation steps git clone --depth 1 https://github.com/doomemacs/doomemacs ~/.emacs.d ~/.emacs.d/bin/doom install Symlink your Doom configuration repository:\ngit clone git@github.com:yourname/dotfiles.git ~/dotfiles ln -s ~/dotfiles/doom ~/.doom.d Example font configuration inside config.el:\n(cond ((eq system-type \u0026#39;windows-nt) (setq doom-font (font-spec :family \u0026#34;JetBrainsMono Nerd Font\u0026#34; :size 12))) ((eq system-type \u0026#39;gnu/linux) (setq doom-font (font-spec :family \u0026#34;monospace\u0026#34; :size 14)))) WSL Debian sudo apt install build-essential ripgrep fd-find imagemagick -y fc-cache -fv ln -s /mnt/c/Windows/Fonts/\u0026#34;JetBrainsMonoNLNerdFont-Regular.ttf\u0026#34; ~/.local/share/fonts/ VirtualBox Debian Use Bridged Adapter networking for proxy access. Install the same dependencies as WSL. Copy Nerd Fonts into /usr/local/share/fonts (or mount a shared folder) and run sudo fc-cache -fv. MSYS2 MinGW64 (inside Windows Terminal) pacman -S --needed git mingw-w64-x86_64-emacs ripgrep fd tree-sitter Apply these tweaks so Emacs behaves nicely under Windows Terminal:\nFonts: ensure doom-font uses “JetBrainsMono Nerd Font” and set the same face in the Windows Terminal profile. Cursor shape: add (setq-default cursor-type 'bar) to keep the caret thin instead of a block. Eshell aliases: populate ~/.emacs.d/eshell/alias with quick shortcuts: ll ls -l g git status v nvim $1 e emacs $1 Highlighting and UI polish: (setq doom-theme \u0026#39;tango-dark) (global-font-lock-mode 1) (global-hl-line-mode 1) (setq doom-themes-enable-bold t doom-themes-enable-italic t) (add-hook \u0026#39;eshell-first-time-mode-hook #\u0026#39;doom-themes-visual-bell-config) MicroPython Build Toolchain Prerequisites (WSL and VirtualBox Debian) sudo apt install git build-essential ninja-build cmake python3 python3-pip python3-venv pkg-config -y python3 -m pip install --user pyserial esptool sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi Prerequisites (MSYS2 MinGW64) pacman -S --needed git base-devel mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-python pip install --upgrade pyserial esptool Add MAKEFLAGS=\u0026quot;-j$(nproc)\u0026quot; to ~/.zshrc for parallel builds on Windows.\nClone and build git clone https://github.com/micropython/micropython.git ~/Dev/micropython cd ~/Dev/micropython make -C mpy-cross make -C ports/stm32 BOARD=PYBD_SF2 For ESP32 targets:\nmake -C ports/esp32 submodules make -C ports/esp32 BOARD=GENERIC_SPIRAM Flashing helper Create ~/bin/micropython-flash (remember to chmod +x):\n#!/usr/bin/env bash esptool.py --chip esp32 --port \u0026#34;$1\u0026#34; --baud 460800 write_flash -z 0x1000 build-GENERIC_SPIRAM/firmware.bin Use udev rules on Linux or Device Manager on Windows to maintain consistent serial port naming.\nPackage Managers Scoop (Windows) Set-ExecutionPolicy RemoteSigned -Scope CurrentUser iwr -useb get.scoop.sh | iex scoop install git curl wget neovim Point Scoop at the Tsinghua mirror and enable aria2 downloads:\nscoop config SCOOP_REPO https://mirrors.tuna.tsinghua.edu.cn/git/scoopinstaller/scoop.git scoop bucket add extras https://mirrors.tuna.tsinghua.edu.cn/git/scoop-bucket/extras.git scoop config aria2-enabled true scoop config aria2-warning-enabled false scoop config proxy 172.30.0.1:10808 Pacman (MSYS2) Edit /etc/pacman.d/mirrorlist.msys and /etc/pacman.d/mirrorlist.mingw64:\nServer = https://mirrors.tuna.tsinghua.edu.cn/msys2/msys/$arch Server = https://mirrors.tuna.tsinghua.edu.cn/msys2/mingw/x86_64 Update and stay current:\npacman -Syyu pacman -Syu --disable-download-timeout Windows Terminal Profiles Install Windows Terminal via the Microsoft Store or Winget:\nwinget install --id Microsoft.WindowsTerminal -e Append the following to settings.json (Ctrl+, → “Open JSON file”):\n{ \u0026#34;defaultProfile\u0026#34;: \u0026#34;{58ad8b0c-3ef8-5f4d-bc6f-13e4c00f2530}\u0026#34;, \u0026#34;profiles\u0026#34;: { \u0026#34;list\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;Debian\u0026#34;, \u0026#34;source\u0026#34;: \u0026#34;Windows.Terminal.Wsl\u0026#34;, \u0026#34;font\u0026#34;: { \u0026#34;face\u0026#34;: \u0026#34;JetBrainsMono Nerd Font\u0026#34;, \u0026#34;size\u0026#34;: 11 }, \u0026#34;cursorShape\u0026#34;: \u0026#34;bar\u0026#34;, \u0026#34;colorScheme\u0026#34;: \u0026#34;One Half Dark\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;MinGW64\u0026#34;, \u0026#34;commandline\u0026#34;: \u0026#34;C:/msys64/msys2_shell.cmd -mingw64 -shell zsh\u0026#34;, \u0026#34;icon\u0026#34;: \u0026#34;C:\\\\msys64\\\\mingw64.ico\u0026#34;, \u0026#34;font\u0026#34;: { \u0026#34;face\u0026#34;: \u0026#34;JetBrainsMono Nerd Font\u0026#34;, \u0026#34;size\u0026#34;: 11 }, \u0026#34;cursorShape\u0026#34;: \u0026#34;bar\u0026#34;, \u0026#34;colorScheme\u0026#34;: \u0026#34;One Half Dark\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;PowerShell\u0026#34;, \u0026#34;source\u0026#34;: \u0026#34;Windows.Terminal.PowershellCore\u0026#34;, \u0026#34;font\u0026#34;: { \u0026#34;face\u0026#34;: \u0026#34;JetBrainsMono Nerd Font\u0026#34;, \u0026#34;size\u0026#34;: 11 }, \u0026#34;cursorShape\u0026#34;: \u0026#34;bar\u0026#34;, \u0026#34;colorScheme\u0026#34;: \u0026#34;One Half Dark\u0026#34; } ] } } Use the profile Appearance tab or oh-my-posh font install to verify the Nerd Font is active; otherwise icons and prompt glyphs will appear garbled.\nConclusion This rebuild delivers a portable workflow:\nzsh + oh-my-zsh for consistent shell ergonomics PowerShell + oh-my-posh for modern Windows scripting Doom Emacs as the cross-platform editor Shared V2RayN proxy and Tsinghua mirrors for predictable networking Scoop and Pacman for fast package management Curated Windows Terminal profiles as the single launchpad With everything documented, spinning up a fresh Windows or Linux guest now takes minutes instead of days.\nReference links\noh-my-zsh oh-my-posh Doom Emacs MicroPython Windows Terminal Scoop ","permalink":"https://luoyao.info/posts/windows-dev-setup/","summary":"A reproducible Windows 10 Pro 22H2 workstation setup for cross-platform development: zsh, PowerShell, Doom Emacs, MicroPython, Windows Terminal, and proxy-friendly package mirrors.","title":"Rebuilding My Windows 10 Development Environment"},{"content":"1. Along the River, Starting from Wanguo Stadium Golden Week turns Shanghai into a softer city. Traffic thins, the air cools, and the river path near Wanguo Stadium feels like it belongs to the runners again. On the first morning of the holiday I slipped onto the trail, followed the water south until the path ran out of concrete, then traced the same route home.\nThe pace settled into something meditative—steady breathing, steady cadence, just the splash of waves against the embankment. The only real obstacles were the holiday strollers drifting three abreast, chatting as if the path were their living room.\nDistance: 15.02 km Time: 1:12:58 Average pace: 4'51\u0026rsquo;\u0026rsquo;/km Fastest split: 4'35'' Calories burned: ~892 kcal 2. The Stadium Grind A few days later the city snapped back to life, so the only safe flat stretch I could find was the neighborhood stadium. Not exactly inspiring, but I had a half-marathon personal record to chase. So I lined up with the lane markers, set the watch, and surrendered to 21 kilometers of monotony.\nThe first ten laps felt fine. After that, each turn became a negotiation. Lap after lap, the view never changed, but the discipline sharpened. Eventually the numbers on the watch dropped below my previous best. The finish felt less like triumph and more like a long exhale of relief.\n3. Reflection Both runs told the same story: location matters less than rhythm. Give the mind a pattern to hold and the body will follow, whether you’re tracing the edge of the river or circling a rubber track. Shanghai’s skyline faded into a backdrop while I listened to footsteps and breathing, and that turned out to be the view worth keeping.\nNext goal: sub-1:40 half marathon before winter. Preferably not in circles.\n","permalink":"https://luoyao.info/posts/golden-week-run/","summary":"Golden Week running in Shanghai: a 15 km riverside cruise from Wanguo Stadium and a grinding session of stadium laps.","title":"Golden Week Runs: From River Breeze to Endless Laps"},{"content":"Growing up in Sichuan means measuring seasons by the peppers in the market. At home, the wok was rarely quiet, and a handful of chilies was the quickest way to pull everyone into the kitchen. My wife and kid used to protest, waving chopsticks like white flags, so I dialed down the heat until they got hooked. These days they request this stir-fry by name. The dish is officially Hunan, but in practice it’s a friendly handshake between Sichuan’s smoky depth and Hunan’s clean heat. The heart of it is simple: bright fire, fragrant oil, and a flash of sweetness from good pork.\nIngredients (2–3 servings) 300g pork leg with skin 150g green chili peppers (mild to medium hot) 50g pickled red Erjingtiao chili (adds tang and color) 2 cloves garlic A thumb-sized piece of ginger 2 scallions 1 tbsp light soy sauce 1 tbsp oyster sauce 1 tbsp cooking wine 2 tbsp vegetable oil Salt and chicken bouillon, to taste A spoonful of Pixian doubanjiang (fermented broad bean chili paste) Cooking Steps Prep the ingredients\nSeparate the pork skin and fat, slicing them into thin pieces so they render easily. Cut the lean meat into sturdy strips—if they’re too thin they’ll turn stringy. Season the lean pork with a pinch of salt, a trickle of water, and a veil of starch so it stays juicy.\nTrim the chilies on a bias, removing seeds if you prefer a gentler heat. Smash the garlic, slice the ginger and pickled chili, and divide the scallions: chunky pieces for the wok, thin slivers for a final flourish.\nSear the pork\nBring the wok to smoking point, then slick it with oil and slide in the fatty pork. Stir until it turns translucent and renders a fragrant base. Add the marinated lean pork and toss quickly until just shy of cooked; scoop everything out.\nIn the remaining oil, fry the Pixian doubanjiang until it stains the wok brick-red. Follow with garlic, ginger, scallion chunks, and the pickled chili. When the aroma blooms, return the pork and deglaze with soy sauce and cooking wine. The kitchen should smell like a late-night street stall at this point.\nBring in the chili\nThrow in the green chilies and keep the fire fierce. Stir-fry quickly so they stay bright but lose their raw bite. Brace yourself for the peppery plume that hits the air.\nSeason and finish\nAdd oyster sauce, adjust with salt and a pinch of chicken bouillon if you like that restaurant gloss. A drizzle of fresh oil gives the dish a lacquered sheen. Toss in the scallion slivers, flip twice, and turn off the heat.\nServe and devour\nPile everything onto a warm plate and surround it with white rice. Blink and it will be gone—the best compliment any cook can ask for.\nTips Adjust chili type based on your spice tolerance. Always fry Pixian doubanjiang until aromatic—never use it raw. Have a cold beer or iced tea on standby. This is sweat, spice, and satisfaction on a plate. Simple, fiery, and absolutely addictive. Try it once, and you’ll understand why it’s a classic.\n","permalink":"https://luoyao.info/posts/chili-papper-stir-fried-pork/","summary":"A Hunan-style chili pepper stir-fried pork recipe with Sichuan roots — ingredients, step-by-step method, and tips for balancing heat and sweetness.","title":"Chili Pepper Stir-Fried Pork: A Spicy Symphony"},{"content":"Weekdays lately feel like a sprint between meetings, so the only way to keep training is to embrace the office treadmill. Surprisingly, I don’t mind it. I leave the headphones in my locker and listen to the belt hum in rhythm with my footsteps. It’s a stripped-down kind of focus.\nOn September 25, I logged a solid session before the day fully kicked in. Because treadmills can feel forgiving, I nudged the incline up a notch to mimic pavement.\nDistance: 11.11 km Time: 55:31 Average Pace: 5'00\u0026rsquo;\u0026rsquo;/km Calories: ~689 kcal Pre- and post-run weigh-ins showed a 1 kg drop—not fat loss, just sweat turning my shirt into a towel. Still, it’s a satisfying little data point.\nThe view helps. Through the big window in front of the treadmill, the traffic lights of Shanghai’s Middle Ring Road stream by like a neon river. It’s oddly soothing and far better than facing a blank wall.\nSome people say logging double digits on a treadmill is madness; I call it practice. With city lights for company, even an indoor run feels like a small adventure. Next up: an outdoor half marathon this weekend, weather permitting.\n","permalink":"https://luoyao.info/posts/treadmill-sep26-2025/","summary":"An 11.11 km treadmill session with the incline nudged up and Shanghai\u0026rsquo;s Middle Ring Road streaming past the window.","title":"11.11 km on the Corporate Treadmill"},{"content":"Essential Knowledge of Chinese Cooking Chinese cooking is a language of flame and patience. Recipes are useful, but the real fluency comes from understanding heat, timing, and balance—the grammar behind every stir-fry, braise, or steamed dish. The notes below are my crib sheet when I need to reset my instincts.\nThe Magic of Beer in Braised Dishes When a chef tips beer into red-braised pork, it’s not a whim. Beer plays three roles at once:\nOdor slayer – Alcohol and hops lift away the raw smell of meat. Meat softener – Carbonation and alcohol loosen muscle fibers, so the meat relaxes. Flavor booster – Malt sugars darken and sweeten as they cook, deepening the sauce. One pour and the kitchen changes character—suddenly it smells like a holiday meal is on the way.\nWhy Drizzle Finishing Oil? The final drizzle of finishing oil isn’t just for show.\nGloss: It makes a dish look alive, catching light on the way to the table. Seal: It forms a thin film that traps aroma, keeping each bite fragrant. When to Add What: The Golden Rules Stir-Fry (Quick Fireworks) Opening act: Garlic, ginger, chili—aromatics must hit hot oil to release their perfume. Middle section: Soy sauce, sugar, bean pastes—the body of the dish. Curtain call: Vinegar, sesame oil, pepper—light touches that evaporate if added too soon. Braise \u0026amp; Stew (Slow Drama) Opening act: Spices such as star anise, cinnamon, bay leaves—they need time to bloom. Middle section: Soy sauce, cooking wine, sugar—the foundation of the broth. Curtain call: Salt, vinegar, sesame oil—they lose sparkle if simmered for hours. Rule of thumb: Let the sturdy ingredients take the heat; treat the delicate ones gently.\nMeet the Condiments Vinegar: Provides snap and cuts odors—save it for the end. Cooking wine: Goes in early to tame raw smells. Oyster sauce: A sweet-salty umami hug, best added midstream or near the finish. Sesame oil: A nutty perfume—always drizzle at the end. Light soy sauce: Adds salt and a gentle amber color. Dark soy sauce: Stains dishes mahogany without adding much salt. Doubanjiang: Sichuan’s heartbeat; fry it early to release its red oil. MSG vs Chicken Powder MSG: The pure scientist \u0026ndash; just umami, no nonsense. Chicken Powder: The extrovert \u0026ndash; umami plus salt, sugar, and chicken notes. Rule: Don\u0026rsquo;t use both; they fight for attention. Always sprinkle near the end. Pickled Chili vs Roasted Ginger Pickled Chili (Erjingtiao): Sour, spicy, and bold \u0026ndash; the rock star of Sichuan cold and hot dishes. Roasted Ginger: Warm, mellow heat \u0026ndash; the wise old monk, perfect for braises and herbal soups. Spice Cabinet Essentials Bay leaf: Gentle and green, especially good with beef or lamb. Star anise: Sweetly floral, the soul of red-braised pork. Cinnamon bark: Warm, slightly sweet, excellent at taming “muttony” aromas. Tsaoko: Bitter-spicy and bold; the heavy artillery for beef and lamb. Fennel seed: Licorice notes that make dumplings and lamb fillings sing. Clove: Powerful; use a pinch or it steals the show. Amomum: Peppery freshness for soups and slow stews. Dried tangerine peel: Citrus lift that cuts through rich sauces, a Cantonese favorite. Cold Dishes: The Zen of Simplicity Take cucumber salad as the ultimate example:\nSmash cucumbers, salt them, and let the water weep out. Stir together garlic, soy sauce, sugar, and vinegar. Heat oil until it shimmers, pour it over chili and pepper flakes, then mix everything. Serve immediately—crispness doesn’t linger. General law: Sour + sweet + salty + spicy + fresh + fragrant = harmony.\nHow to Master Chinese Cooking Knife skills: Precision cutting sets the texture before heat touches the food. Heat control: Wok hei is equal parts flame and timing. Kitchen chemistry: Maillard browning, caramelization, protein denaturing—know what’s happening in the pan. Style fluency: Stir-fry, braise, steam, roast—each has its own rhythm. Read the masters: Harold McGee, On Food and Cooking J. Kenji López-Alt, The Food Lab Practice, taste, adjust: Every attempt teaches something, especially the flawed ones. Chinese cooking is a game of balance: hot with cold, sour with sweet, richness with lightness. Once those instincts settle in, the wok feels less like equipment and more like an orchestra waiting for its cue.\n","permalink":"https://luoyao.info/posts/essential_knowledge_chinese_cooking/","summary":"Core principles behind Chinese cooking — heat control, timing, and flavor balance — distilled into a practical crib sheet.","title":"Essential Knowledge of Chinese Cooking"},{"content":" “Premature optimization is the root of all evil.” — Donald Knuth\nOverview Goal: Package low-level firmware as a ROM image plus symbol export. The application imports that symbol table during its own link stage, so it can call into ROM code without ever touching the original sources. ROM stays frozen and reusable; the APP continues to evolve.\nKey techniques:\nld --just-symbols to reuse symbol addresses Exporting stable symbols __weak rewriting for patches Shared RAM segments Interface consistency constraints Equivalent implementation in Keil using --symdefs I. ROM Packaging with gcc --just-symbols Gather ROM code into a dedicated section\nTag the reusable functions with __attribute__((section(\u0026quot;.rom\u0026quot;))) so the object files collect them neatly.\nPin the section in the linker script\nIn rom.ld, add KEEP(*(.rom*)) and place it squarely inside the ROM region. That keeps app code from drifting into the reserved space.\nBuild ROM once and export the ELF\nCompile only the ROM sources (proj=ROM) and produce rom/rom.elf. This ELF is the authoritative list of addresses.\nLet the APP import those symbols\nDuring APP linking, pass -Wl,--just-symbols=rom/rom.elf. The linker borrows the symbol table but doesn’t pull in the ROM code.\nCall ROM functions directly\nAt runtime the APP jumps straight to the ROM addresses it imported, keeping ownership lines clean.\n%%{init: {\"theme\": \"neutral\", \"layout\": \"dagre\", \"look\": \"neo\"}}%% flowchart TD subgraph Compile_ROM R1[ROM sourcesrom/common, rom/os] --\u003e R2[Compile: functions -\u003e .rom/.xram] R2 --\u003e R3[Link with rom.ld] R3 --\u003e R4[Output rom.elf/rom.bin] end subgraph Compile_APP A1[APP sourcesdevice, driver, project] --\u003e A2[Compile] A2 --\u003e A3[Link with sc9610.ld--just-symbols=rom.elf] A3 --\u003e A4[Output app.elf/app.bin] end R4 -.symbol table.-\u003e A3 subgraph Memory MTP[MTP 0x0000~0x7FFFAPP code] ROM[ROM 0x8000~0xFFFFROM code] SRAM[SRAM 0x2000_0000~Data/Stack] XRAM[XRAM 0x2000_1E00~Shared area] end A4 --\u003e MTP R4 --\u003e ROM II. __weak Rewrite Mechanism ROM ships weak defaults\nFor example, parity() lives in ROM as __weak. If nobody else speaks up, that implementation runs.\nAPP supplies strong overrides\nDefine the same function without the weak attribute and the linker instantly prefers it.\nPrinciple: strong beats weak every single time.\n%%{init: {\"theme\": \"neutral\", \"layout\": \"dagre\", \"look\": \"neo\"}}%% graph TD subgraph No_Override R1[\"ROM weak function__weak parity()\"] --\u003e L1((Linker)) L1 --\u003e F1[Final uses ROM implementation] end subgraph With_Override R2[\"ROM weak function__weak parity()\"] --\u003e L2((Linker)) A2[\"APP strong functionparity()\"] --\u003e L2 L2 --\u003e F2[Final uses APP implementation] end III. Shared Variables and External Functions Shared structure and mapping\nROM defines rom_share_data_t in .xram and exposes a pointer g_share that will later reference APP memory.\nAPP registers shared memory and callbacks\nThe APP allocates g_share_mem, calls rom_share_attach, and fills in callback pointers.\nROM invokes APP functionality through:\nWeak/strong overrides when the APP replaces default behavior. Callback pointers stored inside g_share for explicit calls. %%{init: {\"theme\": \"neutral\", \"layout\": \"dagre\", \"look\": \"neo\"}}%% flowchart TD subgraph APP_Memory G[\"g_share_mem: rom_share_data_t{counter, callback}\"] CB[\"app_callback()\"] end subgraph XRAM P[g_share pointer] end subgraph ROM_Code INC[\"rom_share_inc()\"] GET[\"rom_share_get()\"] CALL[\"rom_share_call()\"] end G --\u003e|\"rom_share_attach(\u0026g_share_mem)\"| P INC --\u003e|counter++| G GET --\u003e|read counter| G CALL --\u003e|\"callback()\"| CB IV. Advantages Modular reuse: ROM can be built and tested once, then reused by multiple APPs. Flexible patching: __weak allows APP to extend or fix features without touching the ROM image. Smaller footprint: APP links against ROM symbols, avoiding duplicate code. Two-way communication: Shared structures and callbacks enable ROM and APP to exchange state and call each other. V. Keil Equivalent with --symdefs Build ROM with Keil → output rom.axf Export symbols: fromelf --symdefs rom.axf --output rom.symdefs Build APP with --symdefs=rom.symdefs so it can reuse ROM symbols without linking ROM code.\nThis is equivalent to GCC --just-symbols. ","permalink":"https://luoyao.info/posts/rom-just-symbols/","summary":"Packaging firmware as a frozen ROM image plus symbol export, and linking applications against it with GNU ld \u0026ndash;just-symbols or Keil \u0026ndash;symdefs.","title":"ROM Packaging with --just-symbols"},{"content":"Sometimes the best tour guide is a pair of running shoes. Before 6 a.m. on a September Saturday, I stepped out for a 21.34 km half-marathon loop through central Shanghai and let the city unfold in silence.\nI started near Zhongshan Park and slipped onto the Suzhou Creek path. The city was still yawning awake: museum facades mirrored in the water, Jing’an’s skyline stretching its shadows. Around the 6 km mark the Oriental Pearl Tower appeared, its pink spheres glowing softly through the haze like lanterns waiting for dusk.\nThe route carried me along the Bund, where art deco facades square off against Pudong’s glass towers across the Huangpu. Running there at sunrise feels like reading a history book while someone sketches in a new chapter beside you.\nBy 15 km I rolled into Xuhui Riverside, a redeveloped promenade that always feels a step removed from the city’s noise. The boardwalk widened, a river breeze swept in, and that was enough to launch the final push north.\nDistance: 21.34 km Time: 1:50:05 Average Pace: 5'10\u0026quot;/km Calories Burned: 1,272 kcal Fastest Split: 4'33\u0026quot;/km When I returned to the start, Shanghai had shifted gears: buses groaning, office workers clutching coffee, the weekend already in motion. I had my kilometers banked, my endorphins stacked, and a quiet tour of the city finished before breakfast.\nNot a bad way to spend a Saturday morning.\n","permalink":"https://luoyao.info/posts/shanghai-sep20-run/","summary":"A 21.34 km sunrise half-marathon loop through central Shanghai — Suzhou Creek, the Bund, and Xuhui Riverside, all before breakfast.","title":"A 21K Morning Run Around Shanghai"},{"content":"I set off near the Amber Museum in Palanga, letting the path pull me south. The forest smelled like pine needles rinsed in salt water, and the run began as a quiet conversation between footsteps and the Baltic breeze.\nThen a sign appeared. A wolf silhouette. Maybe it was just a warning for hikers, maybe it was serious. Either way, I wasn’t planning to test it. The pace quickened on instinct. If the splits looked sharp, I could thank the imaginary wolf pacing me from the trees.\nThe route eventually spilled out toward the dunes near Nemirseta, where the forest gives way to open sky. The Baltic stretched flat and silver, indifferent to my effort. For a moment running felt effortless, right up until the humidity reminded me otherwise.\nDistance: 10.20 km Time: 51m 29s Average Pace: 5'03\u0026quot;/km Calories: 625 kcal Somewhere in the pines I nearly missed the Treaty of Melno Monument—three stone slabs and a sword etched into granite, commemorating the 1422 agreement that settled the border between the Teutonic Order and the Grand Duchy of Lithuania for almost five centuries. It sits quietly in the woods like a relic from a fantasy story, as if knights might still be nearby polishing armor.\nThe run looped me back into Palanga, legs heavy but head clear. Running here isn’t just exercise—it’s geography, history, and folklore rolled into a single sweaty adventure.\nSo if you ever find yourself in Palanga, lace up. Just keep an eye out for wolves.\n","permalink":"https://luoyao.info/posts/palanga-run/","summary":"A 10.2 km run through pine forest and dunes along the Baltic coast in Palanga, Lithuania — with a wolf warning sign and a 1422 border monument.","title":"Running Along the Baltic in Palanga"},{"content":"Budapest wakes up slowly, but the Danube is already busy with light. At 5:02 a.m. on July 12, 2025, I slipped out of the hotel, shoes squeaking faintly on the cobblestones, and followed the cool air toward Margaret Island (Margit-sziget). The island sits like a park set afloat between Buda and Pest, and at dawn it belongs almost entirely to runners.\nThe lap is familiar on paper—two loops of the red track that wraps the island—but it never feels repetitive. The surface has just enough give, and the trees form a tunnel that lets the sunrise drip through in thin ribbons of gold.\nI began at the southern tip, ducked through the underground metro passage, and climbed the ramp toward Margaret Bridge. From there the Danube fans out, carrying early ferries downstream while Parliament glows softly on the Pest bank. By the time the path curved north toward Árpád Bridge, the island had woken up: cyclists ringing bells, elderly couples stretching, a dog darting between puddles from last night’s rain.\nWhat makes this circuit addictive?\nNo traffic—only shoes, wheels, and the occasional stroller. The forgiving track surface that keeps knees happy. A panorama that swings from the Buda Hills to the Pest skyline every few minutes. Built-in pacing—a full loop is 5.3 km, so two laps close the morning neatly. Distance: 10.55 km Time: 55m 20s Average pace: 5'15\u0026quot;/km Calories burned: 654 kcal I finished by easing back onto Margaret Bridge and letting the watch tick down. The river shimmered like polished steel, and the city on both sides felt half-awake, stretching before the day. Boats were already lining up at the quays, guides shouting to one another in Hungarian, German, English.\nIt’s tempting to tour a new city from a bus or tram, but nothing makes Budapest feel personal quite like earning the view with a steady rhythm of footsteps.\n","permalink":"https://luoyao.info/posts/budapest-margit-run/","summary":"A 10.55 km sunrise double loop around Budapest\u0026rsquo;s Margaret Island — a car-free track, Danube views, and built-in pacing.","title":"Morning Run on Margaret Island, Budapest"},{"content":"What is make? Picture a pile of Lego bricks scattered across the floor. You want a spaceship, not a cleanup headache. make is the friend who reads the instructions, remembers what you built yesterday, and only asks you to rebuild the wing your dog stepped on.\nUnder the hood, make reads a Makefile and decides:\nWhich targets are out of date and need rebuilding. Which commands to run—and in what order. Whether it should also tidy up, set permissions, or run your tests. Many IDEs quietly call make for you. Using it directly feels like hiring the butler instead of hoping he overhears you.\nCommand-line Flags Some popular make flags:\n-f file: Use a specific Makefile (or - for stdin). -i: Ignore errors. -s: Silent mode (no command echo). -r: Disable implicit rules. -n: Dry run (show commands, don’t execute). -t: Touch targets. -q: Just check if things are up-to-date. -p: Print all macros and rules. -d: Debug output (your console will cry). Example:\nmake -f custom.mk make \u0026#34;CUST=PROJECT\u0026#34; library What is a Makefile? A Makefile is like the recipe book for your software. It describes:\nTargets: What to cook (output files). Dependencies: Ingredients required. Commands: How to cook (shell commands). Format:\ntarget : dependencies \u0026lt;Tab\u0026gt; commands Yes, that Tab is mandatory. Forget it, and make will scold you.\nHow does make work? Read all Makefiles. Expand variables and rules. Build a dependency graph. Rebuild only what’s outdated. Execute the necessary commands. Think of it as a picky chef who refuses to recook soup unless the carrots are fresher than the broth.\nRules Example:\nhello.o : hello.c gcc -c hello.c If hello.c is newer than hello.o, make runs the command. Rules are recursive, dependencies are checked until everything is fresh.\nDependencies Multiple files can depend on the same ingredient:\na.obj b.obj : inc.h Update inc.h and both a.obj and b.obj get rebuilt. Efficient and unapologetic.\nMacros (a.k.a Variables) Macros save typing and mistakes. Use $(NAME) to expand:\nGNU_BASE = /usr/local/arm/csky-elfabiv2-tools GNU_BIN = $(GNU_BASE)/bin/csky-elfabiv2- CC = $(GNU_BIN)gcc # ARM compiler Now you can change paths once instead of hunting them like Easter eggs.\nInference Rules Wildcards save you from writing rules for every file:\n%.obj : %.c $(CC) $(FLAGS) -c $\u0026lt; All .c files compile into .obj without further instructions. Not magic, just pattern rules.\nComments Only # works. Example:\n# This is a comment project.exe : main.obj io.obj # another comment Automatic Variables $@: Target filename $^: All dependencies $\u0026lt;: First dependency $?: Dependencies newer than target $+: Like $^ but keeps duplicates $*: Target name without extension Example:\nhello.o : hello.c gcc -c $\u0026lt; Useful Functions subst: String substitution foreach: Loop through a list wildcard: Expand matching filenames notdir: Remove paths basename, suffix, addsuffix, addprefix join, strip, findstring, filter, sort if-then-else, call, origin In short: yes, you can write something that looks suspiciously like Lisp inside your Makefile.\n“Makefile: because life’s too short to compile everything manually.”\n","permalink":"https://luoyao.info/posts/makefile-tutorial/","summary":"A practical Makefile tutorial: targets, variables, pattern rules, and the command-line flags you actually use day to day.","title":"Makefile Tutorial: Taming the Beast"},{"content":"Plug in a new USB gadget and sometimes it politely announces, “I’m a lab instrument, not a thumb drive.” That’s USBTMC—the USB Test and Measurement Class—quietly enabling oscilloscopes, power supplies, and homebrew boards to speak SCPI without drama.\nAnatomy of a USBTMC Device USBTMC Class Codes: Class Codes\nA proper USBTMC device struts onto the bus with a few essentials:\nControl Endpoint (EP0): Mandatory. Think of it as the bouncer at the club—everybody has to check in here. Bulk-IN (EP1): The data firehose coming back from the device. Bulk-OUT (EP2): Where the host pours SCPI commands like *IDN? into your gadget. Interrupt-IN (EP3): Optional, but handy for poking the host when something interesting happens. The Communication Model The conversation goes something like this:\nHost says: “Identify yourself!” → sends *IDN? over Bulk-OUT. Device answers via Bulk-IN: “I am a respectable instrument, thank you very much.” Rinse and repeat with more SCPI commands until someone pulls the cable. Under the hood the usual descriptor crew shows up: device, configuration, interface, endpoints. They aren’t glamorous, but without them nothing enumerates.\nDevice Descriptor /* USB descriptor definition */ typedef struct _usb_desc_header { uint8_t bLength; /*!\u0026lt; size of the descriptor */ uint8_t bDescriptorType; /*!\u0026lt; type of the descriptor */ } usb_desc_header; typedef struct _usb_desc_dev { usb_desc_header header; /*!\u0026lt; descriptor header, including type and size */ uint16_t bcdUSB; /*!\u0026lt; BCD of the supported USB specification */ uint8_t bDeviceClass; /*!\u0026lt; USB device class */ uint8_t bDeviceSubClass; /*!\u0026lt; USB device subclass */ uint8_t bDeviceProtocol; /*!\u0026lt; USB device protocol */ uint8_t bMaxPacketSize0; /*!\u0026lt; size of the control (address 0) endpoint\u0026#39;s bank in bytes */ uint16_t idVendor; /*!\u0026lt; vendor ID for the USB product */ uint16_t idProduct; /*!\u0026lt; unique product ID for the USB product */ uint16_t bcdDevice; /*!\u0026lt; product release (version) number */ uint8_t iManufacturer; /*!\u0026lt; string index for the manufacturer\u0026#39;s name */ uint8_t iProduct; /*!\u0026lt; string index for the product name/details */ uint8_t iSerialNumber; /*!\u0026lt; string index for the product\u0026#39;s globally unique hexadecimal serial number */ uint8_t bNumberConfigurations; /*!\u0026lt; total number of configurations supported by the device */ } usb_desc_dev; /// USB standard device descriptor usb_desc_dev tmc_dev_desc = { /* Table 40 -- Device Descriptor */ .header = { .bLength = USB_DEV_DESC_LEN, .bDescriptorType = USB_DESCTYPE_DEV, }, .bcdUSB = 0x0200U, .bDeviceClass = USB_CLASS_DEVICE, .bDeviceSubClass = 0x00U, .bDeviceProtocol = 0x00U, .bMaxPacketSize0 = USBD_EP0_MAX_SIZE, .idVendor = USBD_VID, .idProduct = USBD_PID, .bcdDevice = 0x0001U, .iManufacturer = STR_IDX_MFC, .iProduct = STR_IDX_PRODUCT, .iSerialNumber = STR_IDX_SERIAL, .bNumberConfigurations = USBD_CFG_MAX_NUM, }; Configuration Descriptor typedef struct _usb_desc_config { usb_desc_header header; /*!\u0026lt; descriptor header, including type and size */ uint16_t wTotalLength; /*!\u0026lt; size of the configuration descriptor header, and all sub descriptors inside the configuration */ uint8_t bNumInterfaces; /*!\u0026lt; total number of interfaces in the configuration */ uint8_t bConfigurationValue; /*!\u0026lt; configuration index of the current configuration */ uint8_t iConfiguration; /*!\u0026lt; index of a string descriptor describing the configuration */ uint8_t bmAttributes; /*!\u0026lt; configuration attributes */ uint8_t bMaxPower; /*!\u0026lt; maximum power consumption of the device while in the current configuration */ } usb_desc_config; /// USB device configuration descriptor usb_tmc_desc_config_set tmc_config_desc = { .config = { .header = { .bLength = sizeof(usb_desc_config), .bDescriptorType = USB_DESCTYPE_CONFIG, }, .wTotalLength = sizeof(usb_tmc_desc_config_set), .bNumInterfaces = 0x01U, .bConfigurationValue = 0x01U, .iConfiguration = 0x00U, .bmAttributes = 0xe0U, .bMaxPower = 0x32U }, Interface Descriptor typedef struct _usb_desc_itf { usb_desc_header header; /*!\u0026lt; descriptor header, including type and size */ uint8_t bInterfaceNumber; /*!\u0026lt; index of the interface in the current configuration */ uint8_t bAlternateSetting; /*!\u0026lt; alternate setting for the interface number */ uint8_t bNumEndpoints; /*!\u0026lt; total number of endpoints in the interface */ uint8_t bInterfaceClass; /*!\u0026lt; interface class ID */ uint8_t bInterfaceSubClass; /*!\u0026lt; interface subclass ID */ uint8_t bInterfaceProtocol; /*!\u0026lt; interface protocol ID */ uint8_t iInterface; /*!\u0026lt; index of the string descriptor describing the interface */ } usb_desc_itf; /* Table 21 -- USB488 interface descriptor */ .interface = { .header = { .bLength = sizeof(usb_desc_itf), .bDescriptorType = USB_DESCTYPE_ITF, }, .bInterfaceNumber = 0x00U, .bAlternateSetting = 0x00U, .bNumEndpoints = 0x02U, .bInterfaceClass = USB_CLASS_APPLICATION, .bInterfaceSubClass = USB_APPLICATION_SUBCLASS_TMC, .bInterfaceProtocol = USBTMC_PROTOCOL_NONE, .iInterface = 0x00U }, Endpoint Descriptors typedef struct _usb_desc_ep { usb_desc_header header; /*!\u0026lt; descriptor header, including type and size */ uint8_t bEndpointAddress; /*!\u0026lt; logical address of the endpoint */ uint8_t bmAttributes; /*!\u0026lt; endpoint attribute */ uint16_t wMaxPacketSize; /*!\u0026lt; size of the endpoint bank, in bytes */ uint8_t bInterval; /*!\u0026lt; polling interval in milliseconds for the endpoint if it is an INTERRUPT or ISOCHRONOUS type */ } usb_desc_ep; /* 5.6.1 Bulk-IN Endpoint Descriptor */ .in_endpoint = { .header = { .bLength = sizeof(usb_desc_ep), .bDescriptorType = USB_DESCTYPE_EP }, .bEndpointAddress = TMC_IN_EP, .bmAttributes = USB_EP_ATTR_BULK, .wMaxPacketSize = TMC_EP_SIZE, // .bInterval = 0x00U .bInterval = 0x01U }, USBTMC Get Capabilities static const struct usb_tmc_get_capabilities_response capabilities = { .status = USBTMC_STATUS_SUCCESS, .reserved0 = 0x00, .bcdUSBTMC = 0x0100, .interface_capabilities = 0x00, .device_capabilities = 0x00, .reserved1 = {0x00}, .reserved2 = {0x00}, }; /// Table 37 -- GET_CAPABILITIES response format struct usb_tmc_get_capabilities_response { /* Status indication for this request. See Table 16. */ uint8_t status; /* Reserved. Must be 0x00. */ uint8_t reserved0; /* BCD version number of the relevant USBTMC specification for this USBTMC interface. Format is as specified for bcdUSB in the USB 2.0 specification, section 9.6.1. */ uint16_t bcdUSBTMC; /* bit2: 1 – The USBTMC interface accepts the INDICATOR_PULSE request. 0 – The USBTMC interface does not accept the INDICATOR_PULSE request. The device, when an INDICATOR_PULSE request is received, must treat this command as a non-defined command and return a STALL handshake packet. bit1: 1 – The USBTMC interface is talk-only. 0 – The USBTMC interface is not talk-only. bit0: 1 – The USBTMC interface is listen-only. 0 – The USBTMC interface is not listen-only.*/ uint8_t interface_capabilities; /* bit0: 1 – The device supports ending a Bulk-IN transfer from this USBTMC interface when a byte matches a specified TermChar. 0 – The device does not support ending a Bulk-IN transfer from this USBTMC interface when a byte matches a specified TermChar. */ uint8_t device_capabilities; /* Reserved for USBTMC use. All bytes must be 0x00. */ uint8_t reserved1[6]; /* Reserved for USBTMC subclass use. If no subclass specification applies, all bytes must be 0x00. */ uint8_t reserved2[12]; } __attribute__((packed)); USBTMC Bulk-IN Header /// Table 1 -- USBTMC message Bulk-OUT Header struct usb_tmc_bulk_header { /* Specifies the USBTMC message and the type of the USBTMC message. See Table 2. */ uint8_t MsgID; /* A transfer identifier. The Host must set bTag different than the bTag used in the previous Bulk- OUT Header. The Host should increment the bTag by 1 each time it sends a new Bulk-OUT Header. The Host must set bTag such that 1\u0026lt;=bTag\u0026lt;=255. */ uint8_t bTag; /* The inverse (one’s complement) of the bTag. For example, the bTagInverse of 0x5B is 0xA4. */ uint8_t bTagInverse; /* Reserved. Must be 0x00. */ uint8_t reserved; union { /* USBTMC command message specific. See section 3.2.1. */ uint8_t message[8]; /// Table 3 -- DEV_DEP_MSG_OUT Bulk-OUT Header with command specific content struct _dev_dep_msg_out { /* Total number of USBTMC message data bytes to be sent in this USB transfer. This does not include the number of bytes in this Bulk-OUT Header or alignment bytes. Sent least significant byte first, most significant byte last. TransferSize must be \u0026gt; 0x00000000. */ uint32_t transferSize; /* bit0 EOM. 1 - The last USBTMC message data byte in the transfer is the last byte of the USBTMC message. 0 – The last USBTMC message data byte in the transfer is not the last byte of the USBTMC message. */ uint8_t bmTransferAttributes; /* Reserved. Must be 0x000000. */ uint8_t reserved[3]; } dev_dep_msg_out; /// Table 4 -- REQUEST_DEV_DEP_MSG_IN Bulk-OUT Header with command specific content struct _dev_dep_msg_in { /* Maximum number of USBTMC message data bytes to be sent in response to the command. This does not include the number of bytes in this Bulk-IN Header or alignment bytes. Sent least significant byte first, most significant byte last. TransferSize must be \u0026gt; 0x00000000. */ uint32_t transferSize; /* bit1 TermCharEnabled. 1 – The Bulk-IN transfer must terminate on the specified TermChar. The Host may only set this bit if the USBTMC interface indicates it supports TermChar in the GET_CAPABILITIES response packet. 0 – The device must ignore TermChar. */ uint8_t bmTransferAttributes; /* If bmTransferAttributes.D1 = 1, TermChar is an 8-bit value representing a termination character. If supported, the device must terminate the Bulk-IN transfer after this character is sent. If bmTransferAttributes.D1 = 0, the device must ignore this field. */ uint8_t TermChar; /* Reserved. Must be 0x0000. */ uint8_t reserved[2]; } dev_dep_msg_in; /// Table 5 -- VENDOR_SPECIFIC_OUT Bulk-OUT Header with command specific content struct _vendor_specific_out { /* Total number of USBTMC message data bytes to be sent in this USB transfer. This does not include the number of bytes in this Bulk-OUT Header or alignment bytes. Sent least significant byte first, most significant byte last. TransferSize must be \u0026gt; 0x00000000. */ uint32_t transferSize; /* Reserved. Must be 0x0000000. */ uint8_t reserved[4]; } vendor_specific_out; /// Table 6 -- REQUEST_VENDOR_SPECIFIC_IN Bulk-OUT Header with command specific content struct _vendor_specific_in { /* Maximum number of USBTMC message data bytes to be sent in response to the command. This does not include the number of bytes in this Bulk-IN Header or alignment bytes. Sent least significant byte first, most significant byte last. TransferSize must be \u0026gt; 0x00000000. */ uint32_t transferSize; /* Reserved. Must be 0x00000000. */ uint8_t reserved[4]; } vendor_specific_in; } command_specific; } __attribute__((packed)); Example: A 488.2 USBTMC Message Send: *IDN? Response: Enumeration Process Demo Time When implemented correctly, sending SCPI commands through USB feels like magic:\nUseful Resources USBTMC spec: usb.org SCPI-99: IVI Foundation IEEE 488.2: IEEE Xplore Handy SCPI parser: jaybee.cz/scpi-parser Open-source STM32 example: Jana-Marie’s repo Closing Thoughts USBTMC turns your microcontroller into a lab-friendly chatterbox. With just a few endpoints and some SCPI sprinkled in, you can make your DIY gizmo play nice with professional tools—or at least pretend to.\nDescriptors might feel dull, yet the conversations they unlock—automated measurements, scripted calibrations, blinking front-panel LEDs—are worth the paperwork.\n","permalink":"https://luoyao.info/posts/usbtmc-scpi-fun/","summary":"How USBTMC works under the hood — descriptors, endpoints, and message framing — plus getting a device to answer SCPI commands like *IDN?.","title":"Talking USBTMC: From Boring Descriptors to Glorious SCPI Replies"},{"content":"If you’ve ever stared at a microcontroller and wondered why you were still wrestling with header files instead of just writing Python, MicroPython is the escape hatch. It squeezes Python down to MCU size and still leaves room to blink LEDs with flair.\nWhat is MicroPython? MicroPython is the language you already know, trimmed and caffeinated to run directly on microcontrollers. You can drop .py files onto the board, poke at it interactively, and still drive GPIO, ADC, or PWM with readable, concise code.\nTest Rig: My STM32F411CE Board MCU: STM32F411CE Frequency: 96 MHz ROM: 512 KB RAM: 128 KB Interface: USB (Mass storage + UART) Not exactly a workstation, but more than enough horsepower to make LEDs dance and sensors behave.\nBuilding the Firmware Windows (a.k.a. “Why do I keep doing this?”) Windows setup is… let’s call it “spicy.” I spin up Windows Sandbox with Ubuntu or Debian tucked inside, then install the cross compiler:\nsudo apt-get install gcc-arm-none-eabi Clone and prep source:\ngit clone https://github.com/micropython/micropython ~/micropython 7z x sc_micropython.7z ~/ Then copy files, run make -C mpy-cross, initialize submodules, and finally:\nmake BOARD=SOUTHCHIP Warning: Sandbox builds sneak in /mnt/c prefixes that confuse JLink. It feels like Windows’ way of reminding you who’s in charge.\nLinux (a.k.a. “This feels right”) Linux is calmer. Either fire up a Debian VM or stay native and run:\nsudo apt-get install gcc-arm-none-eabi sudo dpkg -i JLink_Linux_V782c_x86_64.deb sudo dpkg -i Ozone_Linux_V328a_x86_64.deb After that the build process mirrors Windows, just with fewer sighs.\nFlashing the Firmware On Windows, STM32CubeProg is your friend:\nConnect USB. Hold BOOT0 + NRST, then release NRST. Welcome to DFU mode. Select the firmware.hex you built. Click flash and enjoy the magic. Your MCU is now bilingual: binary + Python.\nDebugging (for those who love pain) Hook up JLink + Ozone on Linux. Install, configure, then marvel at memory dumps. Half the time you’ll question your life choices, which is simply part of the debugging ritual.\nFirst Steps with MicroPython Once flashed, you’ve got multiple ways to play:\nUART REPL — Type Python live and see instant results. Edit main.py on USB drive — Runs automatically on boot. Use pyboard tool — Slightly fancier control. Pick your flavor. Either way, your STM32 now speaks fluent Python.\nUseful Resources MicroPython Source Code Ozone Debugger JLink STM32CubeProg MSYS2 Closing Thoughts MicroPython makes embedded work feel playful again. Fewer bit masks, fewer linker incantations, more immediate feedback. Flash the board, open the REPL, and have fun.\nSo the next time someone insists, “That chip can’t run Python,” blink an LED in reply and let the board do the talking.\n","permalink":"https://luoyao.info/posts/micropython-stm32-blink/","summary":"Getting MicroPython running on an STM32F411CE board — flashing the firmware, reaching the REPL, and the obligatory LED blink.","title":"MicroPython on STM32: Because Blinking LEDs is a Lifestyle"},{"content":"When I decided to rebuild my personal site luoyao.info, I wanted a setup that’s fast, stable, and easy to maintain.\nAfter trying several stacks, I settled on Hugo + Nginx + Cloudflare, running on a Debian 12 Lightsail instance.\nThis post documents the full deployment process—from server setup to SSL configuration—so you can reproduce it easily.\n1. Environment Overview Component Description VPS Amazon Lightsail (Debian 12) Domain luoyao.info (managed via Cloudflare) SSL Mode Full (Strict) Web Server Nginx Site Generator Hugo Extended v0.150 Access SSH key (passwordless login) 2. Prepare the Server ssh admin@\u0026lt;YOUR_LIGHTSAIL_IP\u0026gt; sudo timedatectl set-timezone Asia/Shanghai sudo apt update sudo apt install -y nginx git unzip curl 3. Install Hugo (Extended) The Debian package is outdated, so install the latest release manually:\ncd /tmp curl -LO https://github.com/gohugoio/hugo/releases/download/v0.150.0/hugo_extended_0.150.0_Linux-64bit.deb sudo dpkg -i hugo_extended_0.150.0_Linux-64bit.deb hugo version Make sure it shows extended.\n4. Create the Hugo Site sudo mkdir -p /var/www/luoyao.info \u0026amp;\u0026amp; sudo chown -R $USER:$USER /var/www/luoyao.info cd /var/www/luoyao.info hugo new site blog cd blog git init Install PaperMod theme:\ngit submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod echo \u0026#39;theme = \u0026#34;PaperMod\u0026#34;\u0026#39; \u0026gt;\u0026gt; hugo.toml Create a quick test post:\nhugo new posts/hello-world.md sed -i \u0026#39;s/draft: true/draft: false/\u0026#39; content/posts/hello-world.md 5. Build Static Files hugo --minify -d /var/www/luoyao.info/public sudo chown -R www-data:www-data /var/www/luoyao.info/public All generated HTML now lives under /var/www/luoyao.info/public.\n6. Configure Cloudflare Origin SSL In Cloudflare → SSL/TLS → Origin Server,\ncreate a new Origin Certificate and private key, then copy them to your server:\nsudo mkdir -p /etc/ssl/cloudflare sudo nano /etc/ssl/cloudflare/luoyao.info.pem sudo nano /etc/ssl/cloudflare/luoyao.info.key sudo chmod 600 /etc/ssl/cloudflare/luoyao.info.* 7. Configure Nginx sudo tee /etc/nginx/sites-available/luoyao.info \u0026gt;/dev/null \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; server { listen 80; listen [::]:80; server_name luoyao.info www.luoyao.info; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name luoyao.info www.luoyao.info; root /var/www/luoyao.info/public; index index.html; ssl_certificate /etc/ssl/cloudflare/luoyao.info.pem; ssl_certificate_key /etc/ssl/cloudflare/luoyao.info.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { try_files $uri $uri/ =404; } location ~* \\.(css|js|jpg|jpeg|png|gif|svg|ico|webp|woff2?)$ { add_header Cache-Control \u0026#34;public, max-age=31536000, immutable\u0026#34;; try_files $uri =404; } } EOF sudo ln -s /etc/nginx/sites-available/luoyao.info /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl enable --now nginx Your site should now load correctly via https://luoyao.info.\n8. Cloudflare DNS and SSL Settings Setting Value A Record @ and www → your Lightsail IP (orange cloud = proxied) SSL/TLS Mode Full (Strict) Always Use HTTPS ON Automatic HTTPS Rewrites ON If you get a 526/525 error, check that your Nginx certificate matches the Cloudflare Origin cert.\n9. Optional: Git Push Deployment To auto-deploy updates from Git:\nsudo -u www-data mkdir -p /var/www/luoyao.info/repo cd /var/www/luoyao.info/repo git init --bare Add this hooks/post-receive script:\n#!/bin/bash set -e WORKTREE=/var/www/luoyao.info/blog PUBLIC=/var/www/luoyao.info/public git --work-tree=\u0026#34;$WORKTREE\u0026#34; --git-dir=\u0026#34;$(pwd)\u0026#34; checkout -f cd \u0026#34;$WORKTREE\u0026#34; git submodule update --init --recursive hugo --minify -d \u0026#34;$PUBLIC\u0026#34; chown -R www-data:www-data \u0026#34;$PUBLIC\u0026#34; Then activate it:\nchmod +x hooks/post-receive git remote add prod admin@\u0026lt;YOUR_LIGHTSAIL_IP\u0026gt;:/var/www/luoyao.info/repo git push prod main 10. Final Notes After fine-tuning SSL chains and CDN caching, luoyao.info went live smoothly.\nThis stack feels refreshingly minimal—no databases, no frameworks, just Markdown and static pages served fast worldwide.\nWhenever I publish a new post:\nhugo sudo systemctl reload nginx Within seconds, Cloudflare propagates it globally.\nIf you want a secure and efficient personal site, Hugo + Nginx + Cloudflare on a small Debian VPS is a perfect balance of control and simplicity.\n","permalink":"https://luoyao.info/posts/deploy-hugo-luoyao-info/","summary":"Full walkthrough of deploying a Hugo site on a Debian 12 Lightsail instance with Nginx and Cloudflare Full (Strict) SSL.","title":"Deploying luoyao.info with Hugo, Nginx, and Cloudflare on Debian 12 Lightsail"},{"content":"Back in 2010 I dove into WordPress, spent weekends tweaking themes, and posted whenever inspiration struck. I also tinkered with the server far more than I wrote. Inevitably the machine crashed, backups were missing, and the archive vanished in one swoop. (Lesson learned: backups are dull right up until they become priceless.) I attempted a few comebacks, but work ramped up and two small kids arrived, so the blog went into hibernation.\nNow I’m back. Writing is still the best way I know to untangle ideas. As a firmware engineer in the semiconductor world, I juggle technical details that deserve a permanent home. With tools like ChatGPT, documenting those notes feels less like homework and more like having a tireless editor nearby.\nOutside the office I run—slowly but steadily, at least 100 km a month. Once the shoes are on it’s hard to quit. The bathroom scale says there’s room for improvement, yet the miles help keep the numbers honest and justify the extra snacks.\nCooking is the other obsession. I experiment often, and the plates usually return empty—either from success or because the family is polite. I know my dishes still rely more on instinct than science, so I’ve been studying On Food and Cooking and The Food Lab to give them a sturdier foundation.\nSo this blog will be my digital notebook: part tech log, part running journal, part cooking diary. If nothing else, it will remind me why backups are important.\nStay tuned for posts about firmware bugs that kept me up at night, running milestones that kept me moving, and kitchen experiments that sometimes worked, and sometimes didn’t.\n","permalink":"https://luoyao.info/posts/why-start-blog/","summary":"Why I returned to blogging after a decade away — a digital notebook for firmware notes, running logs, and kitchen experiments.","title":"Why I’m Blogging Again"},{"content":"Hello, my name is Luo Yao. I live in Shanghai and enjoy a balance of technology and everyday life.\nI like programming, which keeps me curious and always learning. I also enjoy running, which gives me energy and focus, and cooking, where I experiment with flavors and ideas.\nThis blog is my personal space to share thoughts, notes, and experiences from both life and hobbies. Thanks for stopping by, and I hope you find something meaningful here.\nIf you’ve got burning questions, wild ideas, or just feel like sending digital smoke signals, drop me an email:\nContact: nrzo.cn@gmail.com\n","permalink":"https://luoyao.info/about/","summary":"\u003cp\u003eHello, my name is \u003cstrong\u003eLuo Yao\u003c/strong\u003e. I live in Shanghai and enjoy a balance of technology and everyday life.\u003c/p\u003e\n\u003cp\u003eI like \u003cstrong\u003eprogramming\u003c/strong\u003e, which keeps me curious and always learning. I also enjoy \u003cstrong\u003erunning\u003c/strong\u003e, which gives me energy and focus, and \u003cstrong\u003ecooking\u003c/strong\u003e, where I experiment with flavors and ideas.\u003c/p\u003e\n\u003cp\u003eThis blog is my personal space to share thoughts, notes, and experiences from both life and hobbies. Thanks for stopping by, and I hope you find something meaningful here.\u003c/p\u003e","title":"About"}]