ESP32-based BACnet/IP device with ST7789 TFT display featuring 86 BACnet objects: 40 Analog Values, 40 Binary Values, 2 Analog Inputs, 2 Binary Inputs, and 2 Binary Outputs (expandable).
It can simultaneously connect the BACnet device through WiFi (BACnet/IP), WiFi to Ethernet bridge, and MS/TP (RS485 using a MAX485 module).
You can easily add extra BACnet objects and map them to ESP32 GPIO for analog and digital inputs/outputs.
- BACnet/IP Protocol: Full BACnet/IP stack implementation
- BACnet MS/TP: RS485 MS/TP support alongside BACnet/IP (dual stack)
- Live Display: Real-time monitoring of BACnet objects on 170x320 TFT display
- 86 BACnet Objects:
- 40 Analog Values (AV1-40) - read/write with COV and NVS persistence
- 40 Binary Values (BV1-40) - read/write with COV and NVS persistence
- 2 Analog Inputs (AI1-2) - sensor inputs with COV and NVS persistence
- 2 Binary Inputs (BI1-2) - binary states with COV and NVS persistence
- 2 Binary Outputs (BO1-2) - writable control outputs with COV and NVS persistence
- Writable Metadata: Object
NameandDescriptionare writable for AV/BV/AI/BI/BO - WiFi Connectivity: ESP32 with built-in WiFi for BACnet/IP communication
- Arduino Framework: Leverages Arduino ecosystem for easy hardware control
- Change of Value (COV): Implements BACnet COV notifications for efficient real-time updates
- Persistent Storage: Attribute values modifiable from BACnet supervisor are automatically saved to ESP32 non-volatile memory (NVS) for retention across power cycles
- NVS Override: When
USER_OVERRIDE_NVS_ON_FLASH=1, NVS is erased on boot and all values reset to defaults - Centralized Configuration: User settings are centralized in main/User_Settings.c
- Microcontroller: ESP32-WROOM-32
- CPU: Xtensa dual-core LX6, up to 240 MHz
- RAM: 520 KB SRAM (internal)
- ROM: 448 KB (bootloader & libraries)
- Flash: 4 MB integrated SPI flash
- App partition: 1 MB (see partitions.csv)
- NVS partition: 64 KB (BACnet object persistence)
- WiFi: 802.11 b/g/n, 2.4 GHz
- GPIO: 34 programmable pins
- ADC: 12-bit SAR, up to 18 channels
- UART: 3× (UART0 = USB debug, UART2 = MS/TP RS485)
- SPI: 4× (VSPI used for TFT display)
- Operating voltage: 3.3 V (USB 5 V via onboard regulator)
- Display: ST7789 SPI TFT (170x320 pixels)
- Display Connections:
- MOSI: GPIO 23
- SCLK: GPIO 18
- CS: GPIO 15
- DC: GPIO 2
- RST: GPIO 4
- BL (Backlight): GPIO 32
- Resolution: 170x320 pixels
- Interface: SPI (4-wire)
- Driver: Custom TFT_eSPI component with offset calibration for clone displays
- Built-in ESP32 WiFi for BACnet/IP communication
- Configured via main/User_Settings.c
- Static IP option in main/User_Settings.c. Set
USER_WIFI_USE_STATIC_IPto 1 or 0
- Transceiver: MAX485 or equivalent RS485 converter
- UART: UART2
- Connections:
- DI (TX) → ESP32 GPIO17
- RO (RX) → ESP32 GPIO16
- DE/RE → ESP32 GPIO5
- Baud Rate: 38400 (default)
- MS/TP Settings: MAC 7, Max Master 127, Max Info Frames 80
- Discovery: Some controllers (e.g., NAE) require manual add on the MS/TP field bus
| Pin | Component | Signal | Definition |
|---|---|---|---|
| GPIO 2 | TFT Display | DC (Data/Command) | components/TFT_eSPI/User_Setup.h |
| GPIO 4 | TFT Display | RST (Reset) | components/TFT_eSPI/User_Setup.h |
| GPIO 15 | TFT Display | CS (Chip Select) | components/TFT_eSPI/User_Setup.h |
| GPIO 18 | TFT Display | SCLK (SPI Clock) | components/TFT_eSPI/User_Setup.h |
| GPIO 23 | TFT Display | MOSI (SPI Data) | components/TFT_eSPI/User_Setup.h |
| GPIO 32 | TFT Display | BACKLIGHT | components/TFT_eSPI/User_Setup.h |
| GPIO 16 | MAX485 | RO (RX) | main/mstp_rs485.c |
| GPIO 17 | MAX485 | DI (TX) | main/mstp_rs485.c |
| GPIO 5 | MAX485 | DE/RE | main/mstp_rs485.c |
- ESP-IDF v5.5.1
- Python 3.11+
- xtensa-esp-elf toolchain
cd BACnet-ESP32-86objects
idf.py buildidf.py flash -p COM3Or use the provided build/flash tasks in VS Code.
idf.py monitor -p COM3The ST7789 display has a framebuffer offset that's compensated in components/TFT_eSPI/User_Setup.h:
#define TFT_OFFSET_X 0 // Horizontal offset
#define TFT_OFFSET_Y 0 // Vertical offsetLegacy TFT_COLSTART/TFT_ROWSTART examples are also present in comments in the same file; use one offset method consistently.
Arduino framework requires FreeRTOS tick rate of 1000Hz. This is set in sdkconfig:
CONFIG_FREERTOS_HZ=1000
Most user-configurable settings are centralized in main/User_Settings.c and declared in main/User_Settings.h, including:
- WiFi SSID/password and static IP settings
- BACnet Device Instance and BBMD registration
- BACnet/IP and MS/TP enable flags (
USER_ENABLE_BACNET_IP,USER_ENABLE_BACNET_MSTP) - MS/TP parameters (MAC, baud rate, max master, max info frames)
- Default object names, descriptions, units, and initial values
-
Analog Values (AV1-40): Configure names, descriptions, units, and initial values in main/User_Settings.c
-
Binary Values (BV1-40): Configure names, descriptions, active/inactive text, and initial states in main/User_Settings.c
-
Analog Inputs (AI1-2): Configure names, descriptions, units, and COV increments in main/User_Settings.c. Read-only inputs suitable for sensor integration.
-
Binary Inputs (BI1-2): Configure names, descriptions, active/inactive text in main/User_Settings.c. Read-only binary states.
-
Binary Outputs (BO1-2): Configure names, descriptions, active/inactive text, and initial states in main/User_Settings.c. Writable control outputs with priority support.
- components/bacnet-stack - BACnet/IP stack (modified from bacnet-stack/bacnet-stack)
- components/TFT_eSPI - TFT graphics library
- main - Application code
main.c- BACnet initialization and main loopanalog_value.c/h- Analog Value object creation and NVS persistencebinary_value.c/h- Binary Value object creation and NVS persistenceanalog_input.c/h- Analog Input object creation and NVS persistencebinary_input.c/h- Binary Input object creation and NVS persistencebinary_output.c/h- Binary Output object creation and NVS persistencedisplay.cpp- TFT display driverwifi_helper.c- WiFi configuration helpers
| Item | Type | Display |
|---|---|---|
| AV1 | Analog Value | Numeric (1 decimal) |
| AV2 | Analog Value | Numeric (1 decimal) |
| AV3 | Analog Value | Numeric (1 decimal) |
| AV4 | Analog Value | Numeric (1 decimal) |
| BV1 | Binary Value | ON/OFF + Status Dot (Blue=OFF, Green=ON) |
| BV2 | Binary Value | ON/OFF + Status Dot (Blue=OFF, Green=ON) |
| BV3 | Binary Value | ON/OFF + Status Dot (Blue=OFF, Green=ON) |
| BV4 | Binary Value | ON/OFF + Status Dot (Blue=OFF, Green=ON) |
The device broadcasts its Device ID and manages BACnet objects that can be read/written by any BACnet/IP or BACnet MS/TP client (e.g., YABE, Tridium Niagara, Metasys).
- Device: Configurable in main/User_Settings.c
- Analog Values: Instance 1-40
- Binary Values: Instance 1-40
- Analog Inputs: Instance 1-2
- Binary Inputs: Instance 1-2
- Binary Outputs: Instance 1-2
This project uses the official bacnet-stack with the following modifications:
- components/bacnet-stack/ - Configured as ESP-IDF component
- Simplified for embedded systems (reduced features, optimized for ESP32)
- WiFi-based BACnet/IP instead of Ethernet
For a list of specific changes, see BACNET_STACK_CHANGES.md (if available).
The display code uses boundary constants for easy layout modification:
#define DISP_X0 17 // Left edge
#define DISP_Y0 40 // Top edge
#define DISP_X1 151 // Right edge
#define DISP_Y1 278 // Bottom edge
#define DISP_WIDTH 135
#define DISP_HEIGHT 239Position all elements relative to these constants to avoid hardcoding coordinates.
If text appears misaligned, adjust TFT_OFFSET_X and TFT_OFFSET_Y in components/TFT_eSPI/User_Setup.h and recompile.
Check SSID/password in main/User_Settings.c, then verify WiFi init/connection flow in main/wifi_helper.c.
Ensure CONFIG_FREERTOS_HZ=1000 is set in sdkconfig and rebuild with idf.py fullclean && idf.py build.


