Android Server for the Lighting Web Interface

build

Part of Home Lighting System

Once I had the lighting web interface working, I wanted it available whenever I was at home, from any device around the house.

The interface itself is static HTML, CSS, and JavaScript. Once loaded, the browser communicates directly with the ESP8266 controllers through the WLED API. So keeping the interface available does not require an application server or much computing power. It only needs something on the local network that can stay on and serve the files.

I had a few old Android phones lying around and started wondering whether one of them could do that job. I found Termux, installed it on an unused Redmi Note 12, and turned the phone into the local server for the interface.

Redmi note 12 phone showing Termux Android app terminal window on the screen.
The Android phone running Termux as the lighting web interface server.

How it works

ESP8266 controllers
      ▲
      │ WLED API
      │
Browser running the web interface
      ▲
      │ HTTP :4321
      │
Redmi Note 12
Android + Termux
├── serve
├── sshd
└── ~/lights-dashboard/
      ▲
      │ rsync over SSH :8022
      │
Linux development machine
Astro source
└── pnpm deploy

Development stays on my Linux machine. Running pnpm deploy builds the Astro project there and synchronizes the contents of dist/ to the phone over SSH.

The phone stores those production files in ~/lights-dashboard/ and serves them over HTTP on port 4321. A browser loads the interface from the phone and then talks directly to the ESP8266 controllers through the WLED API.

The phone is therefore not a central lighting controller. If the server becomes unavailable, the shared web interface disappears, but the ESP8266 controllers remain available through their individual WLED interfaces.

Server setup

The server on the phone is a small Termux environment with two long-running services.

sshd provides remote access and the SSH connection used during deployment. serve is the HTTP server that exposes the built interface to the local network. Both run under termux-services, which uses runit to supervise them.

rsync is also installed on the phone, but it is not another server process. It is used when a new build is synchronized from the development machine.

The required packages are installed inside Termux with:

pkg update
pkg upgrade

pkg install openssh nodejs-lts npm rsync termux-services
npm install -g serve

openssh provides sshd. Node.js and npm are needed because serve is distributed as an npm package. termux-services provides the service supervision used for both sshd and serve.

After installing termux-services, I restart Termux so its service environment is initialized.

The server does not contain the Astro project. It only needs a directory for the finished production files:

mkdir -p ~/lights-dashboard

That directory starts empty and is populated by deployment from the development machine.

Network address and SSH

Because the browser URL, SSH connection, and deployment script all refer to the phone by its local address, I reserve that address in the router rather than letting it change with normal DHCP assignment.

I use 192.168.0.12 for the reserved address.

Termux runs its SSH server on port 8022. I set a password for the Termux user and enable sshd under termux-services:

passwd

sv-enable sshd
sv status sshd

Once it is running, I can connect from the development machine with:

ssh -p 8022 192.168.0.12

That gives me normal terminal access to the phone and also provides the transport used later by rsync.

HTTP service

The web interface is served by a second supervised service.

I created a service directory called dashboard:

mkdir -p $PREFIX/var/service/dashboard

Its run script, $PREFIX/var/service/dashboard/run, starts serve with the deployed directory as its document root:

#!/data/data/com.termux/files/usr/bin/sh

exec serve "$HOME/lights-dashboard" -l 4321

serve listens on port 4321 and returns the files stored in ~/lights-dashboard/.

The exec is significant here. It replaces the shell with the serve process, so runit supervises the HTTP server itself rather than a shell that happens to have launched it.

I then make the script executable and enable the service:

chmod +x $PREFIX/var/service/dashboard/run

sv-enable dashboard
sv status dashboard

Once a build has been deployed, the interface is available to devices on the local network at:

http://192.168.0.12:4321

Deployment

Astro is built on the development machine, while the phone only receives and serves the finished files.

I wrapped the build and transfer into scripts/deploy.sh:

#!/usr/bin/env bash

set -e

pnpm build

rsync \
  -az \
  --delete \
  -e "ssh -p 8022" \
  dist/ \
  192.168.0.12:~/lights-dashboard/

set -e stops the script immediately if pnpm build fails, so a failed build cannot continue into deployment.

The rsync command connects through Termux’s SSH server on port 8022 and synchronizes the contents of dist/ into ~/lights-dashboard/.

--delete removes files on the phone that disappeared from the new build. That is useful with Astro because generated assets use hashed filenames; without it, assets from older builds would accumulate.

The trailing slash on dist/ is also intentional. It copies the contents of that directory into ~/lights-dashboard/ rather than creating ~/lights-dashboard/dist/.

The project exposes the deployment script through package.json:

{
  "scripts": {
    "deploy": "./scripts/deploy.sh"
  }
}

A normal deployment is therefore:

pnpm deploy

The running HTTP service does not need to restart afterward. serve continues using the same directory and serves the newly synchronized files.

Keeping the source and build environment off the phone also leaves very little state tied to the device. Replacing it would mainly mean recreating the Termux services and deploying the current build again.

Keeping the server running

The actual serving workload has not been a problem. But Android’s process management has.

I configured Termux with Battery usage: No restrictions and enabled Background autostart. I also acquire a Termux wake lock:

termux-wake-lock

The wake lock allows Termux to continue doing work while the screen is off. It does not keep the display itself awake.

On this Xiaomi phone, those settings still did not make the server reliable when Termux was left as a background application.

What has worked reliably is leaving Termux as the foreground app before locking the phone. The screen can then turn off normally while the services continue running.

This also defines the limit of the service supervision. runit can restart sshd or serve if either process exits, but it cannot restart anything if Android kills the entire Termux environment that contains runit.

That is the main compromise in using the phone this way.

Reboot behavior

The current setup does not automatically recover from a full Android reboot.

After restarting the phone, I unlock it and open Termux again. Once the Termux environment is running, its enabled services can be supervised normally.

Termux has a separate Termux:Boot add-on that can launch scripts during Android startup, but I am not using it in this setup.

The distinction matters because placing a script under:

~/.termux/boot/

does not make it run automatically. Those scripts are executed by the Termux:Boot application, so the directory alone does nothing without that add-on installed and configured.

Operation and maintenance

Under normal use, I rarely need to interact with the phone directly. Changes to the interface are built and deployed from the development machine with:

pnpm deploy

If I need to inspect the server itself, I connect over SSH:

ssh -p 8022 192.168.0.12

From there, the two long-running services can be checked with:

sv status dashboard
sv status sshd

If the HTTP server itself needs restarting:

sv restart dashboard

Deploying new static files does not normally require that restart.