STM32 Blue Pill & Nucleo + ASI Biont: UART-Based Microcontroller Integration with an AI Agent for Smart Home and Industrial Automation

{
"title": "STM32 Blue Pill & Nucleo + ASI Biont: UART-Based Microcontroller Integration with an AI Agent for Smart Home and Industrial Automation",
"content": "# STM32 Blue Pill & Nucleo + ASI Biont: UART-Based Microcontroller Integration with an AI Agent for Smart Home and Industrial Automation\n\nMicrocontrollers have no problem talking to sensors and relays, but they are terrible at deciding when to act. An AI agent, on the other hand, is great at understanding intent but is completely blind without real hardware. ASI Biont was designed to bridge that gap. It connects software intelligence to physical I/O using the simplest, most battle-tested link in embedded systems: UART.\n\nIn this article, we walk through a complete STM32 + ASI Biont integration. We use two popular boards, the STM32 Blue Pill and an STM32 Nucleo, and show how to exchange commands over a 3.3V UART at 115200 baud. You will get the exact pinout, production-ready firmware in C++ (Arduino framework), and the ASI Biont side configuration in both visual flow and Python. Finally, we apply the setup to two scenarios: a temperature-triggered ventilation system for a server closet, and a remote industrial alarm gate.\n\n## Why UART?\n\nBefore jumping into the schematic, it is worth explaining why UART remains the right choice. Unlike SPI or I2C, UART requires no clock line and no addressing. Two wires can carry bidirectional data over distances of up to 15 meters at moderate baud rates. For an AI agent running on a PC, a Raspberry Pi, or a BeagleBone, a USB-to-TTL adapter turns that serial stream into a /dev/ttyUSB0 or COMx port that ASI Biont can enumerate as a device node.\n\nThe STM32's USART peripherals also support direct memory access and interrupt-driven transfer. With ring buffers on both ends, the AI agent can send a request and receive a response in under 2 ms at 115200 baud. This deterministic latency is exactly what you want for closed-loop automation.\n\n## Hardware and Wiring\n\nFor this project, you need:\n\n- STM32 Blue Pill (STM32F103C8) or any Nucleo board (e.g., NUCLEO-F446RE)\n- USB-to-TTL adapter (CP2102 or CH340, 3.3V logic)\n- One LED and a 220-ohm resistor (for feedback)\n- A DHT22 temperature and humidity sensor (scenario 1)\n- A relay module and a push button (scenario 2)\n- Jumper wires, breadboard\n\nThe connection is straightforward. The Blue Pill's USART1 is on pins PA9 (TX) and PA10 (RX). On the Nucleo-F446RE, USART1 is also available on PA9/PA10, but the default USB ST-Link exposes a virtual COM port that we can use instead. We will use the physical UART for the Blue Pill and the ST-Link virtual COM for the Nucleo to show both methods.\n\n

| STM32 Blue Pill (PA9/PA10) | USB-TTL Adapter | Notes |\n|---|---|---|\n| TX (PA9) | RX | 3.3V logic, do not connect to 5V adapter without level shifter |\n| RX (PA10) | TX | same |\n| GND | GND | common ground is mandatory |\n| 3.3V | VCC (if adapter outputs 3.3V) | optional, if powering the adapter from the board |\n\nFor the Nucleo, the virtual COM port appears on the host as a normal serial device (e.g., COM7 on Windows). It connects to USART2 via the ST-Link firmware, so no external adapter is needed. If the ST-Link firmware is used, the UART signals are on PA2 (TX) and PA3 (RX).\n\n| Nucleo ST-Link VCP | STM32 (USART2) | Host sees |\n|---|---|---|\n| TX | PA2 | RX on host |\n| RX | PA3 | TX on host |\n\nImportant: if you use a USB-TTL adapter that has 5V logic levels, insert a logic level converter between the adapter and the STM32. The Blue Pill is not 5V-tolerant on PA9/PA10.\n\n## Setting Up the STM32 Firmware\n\nWe use the Arduino framework for speed of development, but the exact same protocol works with STM32 CubeIDE. The firmware initializes USART1 at 115200 baud, configures a command parser, and controls a relay and an LED.\n\nThe protocol is a simple newline-terminated ASCII string:\n\n- TEMP? -> response TEMP:23.50 (Celsius)\n- HUM? -> response HUM:61.20\n- RELAY:1:ON -> energizes relay 1 and returns OK\n- RELAY:1:OFF -> de-energizes relay 1 and returns OK\n- PING -> returns PONG\n\nHere is the complete firmware for the Blue Pill:\n\ncpp\n#include <Arduino.h>\n#include <DHT.h>\n\n#define LED_PIN PC13\n#define RELAY_PIN PB0\n#define DHT_PIN PA1\n\nDHT dht(DHT_PIN, DHT22);\nString inputBuffer = \"\";\n\nvoid setup() {\n pinMode(LED_PIN, OUTPUT);\n pinMode(RELAY_PIN, OUTPUT);\n Serial1.begin(115200); // USART1 on PA9/PA10\n dht.begin();\n inputBuffer.reserve(64);\n digitalWrite(LED_PIN, LOW); // Blue Pill LED is active-low\n}\n\nvoid loop() {\n while (Serial1.available()) {\n char c = Serial1.read();\n if (c == '\\n') {\n handleCommand(inputBuffer);\n inputBuffer = \"\";\n } else {\n inputBuffer += c;\n }\n }\n}\n\nvoid handleCommand(String cmd) {\n cmd.trim();\n if (cmd == \"PING\") {\n Serial1.println(\"PONG\");\n } else if (cmd == \"TEMP?\") {\n float t = dht.readTemperature();\n if (isnan(t)) { Serial1.println(\"ERROR:READ_TEMP\"); return; }\n Serial1.println(\"TEMP:\" + String(t, 2));\n } else if (cmd == \"HUM?\") {\n float h = dht.readHumidity();\n if (isnan(h)) { Serial1.println(\"ERROR:READ_HUM\"); return; }\n Serial1.println(\"HUM:\" + String(h, 2));\n } else if (cmd.startsWith(\"RELAY:\")) {\n // expected format: RELAY:1:ON or RELAY:1:OFF\n if (cmd.endsWith(\"ON\")) {\n digitalWrite(RELAY_PIN, HIGH);\n Serial1.println(\"OK\");\n } else if (cmd.endsWith(\"OFF\")) {\n digitalWrite(RELAY_PIN, LOW);\n Serial1.println(\"OK\");\n } else {\n Serial1.println(\"ERROR:SYNTAX\");\n }\n } else {\n Serial1.println(\"ERROR:UNKNOWN\");\n }\n}\n\n\nFor the Nucleo, change Serial1 to Serial if you use the ST-Link virtual COM port. The rest of the logic is identical.\n\nA short note on buffering: the 64-byte reserve is more than enough for these commands. In a production system, look at the STM32 HAL's HAL_UART_Receive_IT with a DMA ring buffer instead of polling. That leaves CPU time available for control loops.\n\n## Going Beyond Polling: Interrupt-Driven UART\n\nThe polling loop in the example is fine for a single board. In production, you want the USART to run in interrupt mode so the main loop can handle timing-critical tasks like PID controllers or DMX output.\n\nEnable the RXNE (RX Not Empty) interrupt and read the data register inside the handler. Store bytes in a circular buffer; the main loop consumes the buffer and calls handleCommand() when it sees a newline. Here's an abbreviated implementation:\n\ncpp\n#define RX_BUF_SIZE 128\nvolatile uint8_t rx_buffer[RX_BUF_SIZE];\nvolatile uint16_t rx_head = 0, rx_tail = 0;\n\nvoid setup() {\n // ...\n USART1->CR1 |= USART_CR1_RXNEIE;\n NVIC_EnableIRQ(USART1_IRQn);\n}\n\nextern \"C\" void USART1_IRQHandler() {\n if (USART1->SR & USART_SR_RXNE) {\n uint8_t byte = USART1->DR;\n rx_buffer[rx_head] = byte;\n rx_head = (rx_head + 1) % RX_BUF_SIZE;\n }\n}\n\n\nThe main loop polls rx_tail != rx_head, copies bytes into the command string, and responds. This offloads byte-wise processing from the CPU and allows the board to maintain exact timing on other peripherals.\n\nWith this pattern, a single STM32 can handle hundreds of AI-agent requests per second while simultaneously driving a stepper motor.\n\n## Protocol Design: Keep It Simple and Deterministic\n\nThe plain ASCII protocol above works well for a point-to-point link, but when you start to add more sensors or multiple STM32 nodes, you need a framing scheme. The easiest upgrade is a fixed field separator and an optional checksum.\n\nA robust format is:\n\n<CMD>:<ARG1>:<ARG2>;<CHECKSUM>\\n\n\nwhere CHECKSUM is an XOR of all bytes between the first character and the semicolon. The host and the STM32 each recompute the checksum and discard the frame if it fails. At 115200 baud, the overhead of a 2-character checksum is only about 20 microseconds. This turns the UART link into a soft real-time bus with error detection.\n\nOn the ASI Biont side, create a regular expression node to split the incoming frame. For example:\n\n\npattern: '^(?<cmd>[A-Z_]+)\\:(?<a1>[^:;]+)?(?:\\:(?<a2>[^;]+))?;\\w*\\n'\n\n\nThis keeps your automation rules independent of the physical wire format.\n\n## Configuring ASI Biont to Talk to the STM32\n\nASI Biont treats serial devices as first-class citizens. In the ASI Biont web dashboard or desktop app, go to Devices > Add UART Device. Set the port name (e.g., /dev/ttyUSB0 on Linux or COM5 on Windows), baud rate to 115200, data bits to 8, parity to none, stop bits to 1, and keep the line ending as \\n.\n\nThere are two ways to program the AI agent: a visual logic builder and a Python SDK. We cover both.\n\n### Option 1: Visual flow\n\nIn the ASI Biont Logic Editor, add a UART In node and a UART Out node. Create a message flow:\n\n1. Node On Start\n2. Node UART Query with payload TEMP?\n3. Node Parse Response (uses a regex or substring to extract the number after TEMP:)\n4. Node If Else checks the value. If temperature is above 28 °C, set RELAY:1:ON, otherwise RELAY:1:OFF.\n\nThe visual rule file can be exported as YAML:\n\nyaml\ntriggers:\n - cron: \"*/30 * * * * *\"\nflow:\n - uart.write: { data: \"TEMP?\\n\", port: \"uart0\" }\n - uart.read: { port: \"uart0\", until: \"\\n\", timeout: 1000 }\n - parse.with_regex: { source: \"${last_message}\", pattern: \"TEMP:([0-9.]+)\" }\n - if:\n condition: \"${parse_result} > 28\"\n then:\n - uart.write: { data: \"RELAY:1:ON\\n\", port: \"uart0\" }\n else:\n - uart.write: { data: \"RELAY:1:OFF\\n\", port: \"uart0\" }\n\n\nNotice that the YAML uses double-quoted strings and explicit \\n escapes. In the ASI Biont editor, these are written automatically when you press Enter in a text field.\n\n### Option 2: ASI Biont Python SDK\n\nFor a more flexible integration, use the biont Python package.\n\n\npip install asibiont\n\n\npython\nfrom biont import SerialDevice\n\ndev = SerialDevice(port='/dev/ttyUSB0', baudrate=115200, timeout=1.0)\n\n# PING\nprint(dev.command('PING')) # -> 'PONG'\n\n# Read temperature\nresp = dev.command('TEMP?')\nprint(resp) # -> 'TEMP:24.31'\ntemperature = float(resp.split(':')[1])\n\nif temperature > 28:\n print(dev.command('RELAY:1:ON'))\nelse:\n print(dev.command('RELAY:1:OFF'))\n\n\nWrap this logic in an ASI Biont scheduled task or an MQTT-triggered lambda. The SDK also exposes the parsed messages as structured events, so the AI agent can push them to SQLite, InfluxDB, or a webhook.\n\n## Scaling to a Multi-Node UART Network\n\nAll of the examples above are point-to-point. To connect several STM32s to one ASI Biont instance, use RS-485 transceivers such as the MAX485. Wire all nodes on a twisted pair, set the STM32 in half-duplex mode, and assign each node a unique address. Extend the protocol to include an address byte, for example 01:TEMP?;checksum. ASI Biont's serial connector can then address each device by the first field. RS-485 extends the bus to 1200 meters, which makes it suitable for industrial sites. The Biont SDK also supports an RS-485 mode that automatically toggles the DE pin when transmitting.\n\n## Real-World Automation Scenarios\n\n### Scenario 1: Temperature-Controlled Ventilation in a Server Closet\n

A typical setup uses a single STM32 board (e.g., an STM32F103 “Blue Pill”) with a DHT22 or DS18B20 sensor and a relay module controlling a 12 V exhaust fan. The STM32 runs a simple firmware loop: read the sensor every five seconds, send TEMP:26.40 over UART, and listen for RELAY:1:ON or RELAY:1:OFF commands to toggle the fan. On the ASI Biont side, create a scheduled task that fires every minute. Use the parse.with_regex action to extract the temperature from the latest serial message, then apply a hysteresis check to prevent rapid relay toggling. Instead of comparing directly against a fixed threshold, store the previous fan state in a Biont variable: yaml - variables.set: fan_state: false # initial state - parse.with_regex: source: "${last_message}" pattern: "TEMP:([0-9.]+)" as: temperature - if: condition: "${temperature} > 30.0" then: - variables.set: { fan_state: true } elif: condition: "${temperature} < 25.0" then: - variables.set: { fan_state: false } - if: condition: "${fan_state}" then: - uart.write: { data: "RELAY:1:ON\n", port: "uart0" } else: - uart.write: { data: "RELAY:1:OFF\n", port: "uart0" } The 5 °C deadband means the fan turns on at 30 °C and off at 25 °C, avoiding rapid cycling when the temperature hovers around one setpoint. You can also log every reading to an InfluxDB bucket using the built-in time-series connector, then build a Grafana dashboard that shows both the raw temperature curve and the fan state overlay. ### Scenario 2: Multi-Zone Environmental Monitoring for a Greenhouse Here, the RS-485 multi-node network from the previous section shines. Place four STM32 nodes around the greenhouse, each with a soil moisture sensor on one analog pin and an air temperature/humidity sensor on another. Every node has a unique address (01 through 04). The master STM32 (or a USB-to-RS485 adapter connected to the ASI Biont host) polls each node in turn: 01:TEMP?;checksum 01:MOIST?;checksum 02:TEMP?;checksum ... ASI Biont receives the responses like 01:TEMP:22.30;sum and 01:MOIST:41.2;sum. Configure the serial connector to split the first address field and forward the rest as a parsed event. Then, a Biont workflow can compute averages and trigger irrigation when any zone’s moisture drops below 30%: python # Biont Python SDK snippet responses = dev.poll_multiple(['01:MOIST?', '02:MOIST?', '03:MOIST?', '04:MOIST?']) moistures = {} for addr, resp in responses.items(): value = float(resp.split(':')[1].split(';')[0]) moistures[addr] = value avg = sum(moistures.values()) / len(moistures) if avg < 30.0: dev.command('VALVE:1:ON') else: dev.command('VALVE:1:OFF') This pattern scales to dozens of nodes because RS-485 supports up to 32 unit loads per segment, and you can add repeaters for longer runs. ### Scenario 3: Automated Safety Shutdown for a CNC Machine For industrial gear, latency and fail-safes matter. Suppose a CNC spindle driver outputs a temperature reading over a serial line. If the temperature exceeds 85 °C, you must cut power within 50 ms — a speed that a Python loop on a host PC cannot guarantee. Instead, run the safety check on the STM32 itself. The STM32 firmware immediately sets an emergency stop pin when it reads an over-temperature value, while still sending periodic TEMP: frames to ASI Biont for logging. The host only handles the “softer” actions: sending a notification, updating the dashboard, and generating a maintenance ticket. This hybrid approach is a common pattern: let the microcontroller handle hard real-time constraints, and let ASI Biont handle orchestration, analytics, and human notifications. The serial protocol stays the same — you just partition the logic appropriately. ## Debugging Serial Integrations When things don’t work, the fastest path is to check three things: 1. Baud rate mismatch — the STM32 firmware and the ASI Biont serial connector must agree on every parameter (speed, data bits, parity, stop bits). A mismatch usually produces garbled characters that show up as hex in the Biont logs. 2. Line endings — STM32 printf often sends \n only, while some parsers expect \r\n. Use a serial monitor first to see the exact bytes. Your regex should match what is physically on the wire. 3. Buffer overflow — if your STM32 sends long periodic messages, the host’s serial buffer may drop data. Increase the ASI Biont receive buffer size or add a simple start/end delimiter ([]) and parse only complete packets. The Biont SDK includes a serial.sniff() utility that prints raw bytes with timestamps, which is invaluable when bringing up a new board. ## Final Thoughts Connecting an STM32 to ASI Biont over a simple text-based UART protocol is one of the most robust ways to bridge bare-metal microcontrollers with an intelligent automation layer. You avoid complex Ethernet stacks on the MCU, keep the firmware flash footprint tiny, and still get the full power of Biont’s scheduling, parsing, and cloud integrations. Whether you’re running one sensor in a closet or a hundred nodes in a factory, the same pattern — structured text in, automated action out — will serve you well. Start with the PING test, get one sensor reading flowing, and let the rest follow naturally. The STM32 side is just five lines of C; the Biont side is a few YAML blocks. Once you see that first RELAY:1:ON fire automatically, you’ll wonder why you ever wired a relay directly to a GPIO pin without a brain in between.

← All posts

Comments