A system in which an ESP32 MCU monitors temperature via a DHT22 sensor, activates a fan when a threshold is exceeded, communicates with a Linux host using a custom UART protocol, and allows the user to control the MCU through a custom Linux kernel driver.
ESP32-based smart fan node controlled from Linux through a custom kernel driver.
This project implements an end-to-end embedded system consisting of:
- An ESP32 firmware running multiple FreeRTOS tasks
- A custom binary UART protocol with CRC
- A Linux kernel module using a tty line discipline
- A character device driver exposing a synchronous ioctl interface
- A userspace CLI tool for monitoring and control
The goal of this project is to explore low-level Linux–MCU communication, kernel-space design, and real-time firmware architecture in a single coherent system.
A demo video is available here.
- Language: C
- Embedded / Firmware: ESP-IDF, FreeRTOS, UART, GPIO, PWM
- Linux Kernel: Linux Kernel Module, tty line discipline
- Userspace: POSIX
- Build: GNU Make, ESP-IDF build system
- ESP32 DevKitC V4
- DHT22 temperature & humidity sensor
- SG90 servo motor (fan actuator)
- CP2102 USB-to-UART bridge
- Built with ESP-IDF
- Uses FreeRTOS with multiple tasks:
uart_read_task: receives raw UART bytes and parses framescmd_handler_task: processes protocol commandssensor_task: reads DHT22 sensor periodicallyfan_control_task: controls servo based on system state- A central system state (
sys_state_t) stored in a shared structure - UART communication uses the same protocol definition as the Linux side
- Custom tty line discipline
- Parses incoming UART frames in kernel space
- Implements a character device
/dev/fanctl - Supports synchronous command/response using:
- A mutex for request serialization
- Completion for blocking wait
- Exposes control operations via
ioctl
- Communicates only through
/dev/fanctl - No direct UART access from userspace
The diagram illustrates the end-to-end control flow from userspace, through the Linux kernel, down to the ESP32 firmware.
When the CLI tool calls an ioctl() on /dev/fanctl, the following sequence occurs:
The calling process enters kernel mode via the ioctl system call and executes the driver’s unlocked_ioctl handler.
This happens in the context of the same userspace process.
The ioctl handler builds a protocol frame and sends it to the ESP32 via the TTY subsystem (tty->ops->write()).
After transmitting the request, the ioctl handler goes to sleep using a completion object, waiting for the corresponding response frame.
When response bytes arrive from the ESP32 over UART, they are handled by the TTY core and forwarded to the custom line discipline’s RX callback (receive_buf2).
The RX callback feeds incoming bytes into the protocol parser.
Once a complete response frame matching the outstanding request is received, it stores the frame and wakes up the sleeping ioctl context via complete().
The ioctl handler resumes execution, validates the response, copies the result back to userspace using copy_to_user(), and returns.
The ioctl() system call finishes and execution returns to userspace, where the CLI tool processes and displays the result.
A custom binary protocol is used instead of text-based commands to ensure:
- deterministic parsing
- compact messages
- CRC-based integrity checking
The line discipline allows raw byte-stream processing directly in kernel space, avoiding userspace buffering and framing issues.
The kernel module exposes a synchronous request/response interface using ioctl, making the userspace API simple and explicit.
- UART binary protocol format and framing (
proto.*) - Userspace & kernel ABI definitions (
fanctl_uapi.h)
- Protocol description (
protocol.md)
- ESP32 firmware for the smart fan node.
- Linux kernel driver for the ESP32 fan node.
- A raw serial protocol test script (
proto_test.py)
udevrule installation (install_udev.sh)
- Primary userspace control tool.
- Legacy userspace tool using raw serial acess.
It is strongly recommended to run this project inside a virtual machine for safety, as it involves building and loading a custom Linux kernel module.
- This project was developed and tested on
Ubuntu 5.15.0-164-genericusingUTM.
Build and flash the firmware using ESP-IDF:
cd firmware/fan_node
idf.py build
idf.py -p [PORT] flash monitorTo simplify device handling, it is recommended to create a persistent symlink for the CP2102 USB-to-UART adapter.
Without this, the device name may vary (/dev/ttyUSB0, /dev/ttyUSB1, ...) depending on connection order or number of USB peripherals.
Install the udev rule:
cd tools/scripts/udev
chmod +x install_udev.sh
sudo ./install_udev.shReconnect the USB device and verify:
ls -l /dev/ttyFANExample output:
lrwxrwxrwx 1 root root 7 Dec 18 13:13 /dev/ttyFAN -> ttyUSB1
The tty device must be configured for raw binary transmission:
stty -F /dev/ttyFAN 115200 raw -echo -ixon -ixoff -crtsctsThis disables: • line buffering • echo • software/hardware flow control
cd kernel/fanctl
make
sudo insmod fanctl.koAttach the line discipline:
sudo ldattach 27 /dev/ttyFAN- 27: line discpline ID (
N_FANCTL, defined inkernel/fanctl/fanctl.h)
cd userspace/fanctl_ioctl
make
./fanctl <cmd>cmd:
ping: connection check - expectsPONGstatus: request current temperature, humidity, fan mode, fan state, and errorsauto: set fan mode automatic (fan will be on when the temperature reaches the threshold)manual: set fan mode manualon: set fan state on (when the mode is manual)off: set fan state off (when the mode is manual)threshold <tempC>: set threshold
This project is licensed under the GNU General Public License, version 2.