The R60ABD1 is a 60 GHz sleep and breathing radar module. It reports presence, breathing rate, sleep state and sleep quality over a 3.3 V TTL UART using the SIP-S V1.0 protocol — and it has no Wi-Fi and no MQTT stack. To get that data into a broker or a dashboard, you add a small gateway MCU.

This guide wires the R60ABD1 to the ESP32-S3-WROOM-1 development board (the native-USB board we sell) and shows how to parse the frames and, optionally, publish them to any MQTT broker. It is the bridge half of the two-architecture story in MQTT vs UART: the module does not speak MQTT, the ESP32 does.

What you need

Power (from the R60ABD1 manual)

The R60ABD1 hardware-design section (user manual v3.6, §3) states:

The board’s 5V pin can power it; if the radar’s peak draw would exceed the board’s 5 V budget, use a separate 5 V supply but keep GND common with the ESP32.

ESP32-S3-WROOM-1 GPIO allocation

This board uses native USB (GPIO19/GPIO20) for Serial/JTAG and flashing — there is no external USB-to-serial bridge on GPIO43/44. Pinout is DevKitC-1 compatible, all 44 module pins broken out at 2.54 mm.

Radar ↔ ESP32 wiring (locked):

R60ABD1 ESP32-S3 Note
TX (3.3 V out) GPIO18 (UART1 RX) radar → ESP
RX (3.3 V in) GPIO17 (UART1 TX) ESP → radar
GND GND common ground
VCC (5 V) 5V pin per manual §3

Both UART lines are 3.3 V TTL, so no level shifter is needed.

Pins to avoid (so you do not lose the debugger or hit strapping): | GPIO | Why avoid | |—|—| | 19, 20 | native USB D+/D- (flash, log, JTAG) | | 0 | strapping — boot mode | | 3 | strapping — boot-log enable | | 26–32 | flash / PSRAM (internal) | | 45, 46 | internal flash-voltage strapping, not broken out | | 43, 44 | UART0 — free at hardware level, but ESP-IDF’s default console; leave it for logs |

We use UART1 on GPIO17/18: it is clear of every reserved function, and it leaves the native USB and the default console untouched.

Wiring diagram

R60ABD1                 ESP32-S3-WROOM-1
 5V   ───────────────▶  5V
 GND  ───────────────▶  GND
 TX   ───────────────▶  GPIO18   (UART1 RX)
 RX   ───────────────▶  GPIO17   (UART1 TX)

Firmware (ESP-IDF, Espressif IDE 3.1.0)

Develop in Espressif IDE with the ESP-IDF framework. Configure UART1 at 115200 / 8N1 and bind it to GPIO17/18 with uart_set_pin. Then read bytes and run the SIP-S parser.

SIP-S V1.0 frame (from the R60ABD1 manual §7 / EN §4.2):

Field Bytes Value
Header 2 0x53, 0x59
Control 1 function class
Command 1 content tag
Length 2 data byte count (big-endian)
Data n payload
Checksum 1 low 8 bits of sum of all preceding bytes
Footer 2 0x54, 0x43

Control classes: 0x01 heartbeat, 0x02 product info, 0x03 OTA, 0x05 work state, 0x07 range info, 0x80 presence, 0x81 breathing, 0x84 sleep, 0x85 heart rate.

Minimal ESP-IDF parse + dispatch:

#include "driver/uart.h"

#define RADAR_UART UART_NUM_1
#define TX_PIN     17
#define RX_PIN     18

void radar_init(void) {
    uart_config_t cfg = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity    = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
    };
    uart_param_config(RADAR_UART, &cfg);
    uart_set_pin(RADAR_UART, TX_PIN, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(RADAR_UART, 256, 256, 0, NULL, 0);
}

// Returns true on a valid frame; fills ctrl/cmd/data/dlen.
static bool parse_frame(const uint8_t *b, int n, uint8_t *ctrl, uint8_t *cmd,
                        const uint8_t **data, int *dlen) {
    for (int i = 0; i + 9 <= n; i++) {
        if (b[i] != 0x53 || b[i+1] != 0x59) continue;
        *ctrl = b[i+2]; *cmd = b[i+3];
        int len = (b[i+4] << 8) | b[i+5];
        int data_end = i + 6 + len;
        if (data_end + 3 > n) return false;
        int sum = 0; for (int k = i; k < data_end; k++) sum += b[k];
        if ((sum & 0xFF) != b[data_end]) continue;
        if (b[data_end+1] != 0x54 || b[data_end+2] != 0x43) continue;
        *data = &b[i+6]; *dlen = len;
        return true;
    }
    return false;
}

Decoding the breathing and sleep frames

The R60ABD1 manual defines these report frames (manual v3.6, §7 command tables). After a valid frame, dispatch on ctrl/cmd:

ctrl cmd Meaning Data
0x81 0x01 breathing info (on change) 01 normal, 02 high (>25/min), 03 low (<10/min), 04 none
0x81 0x02 breathing rate 1 byte, 0–35 breaths/min (every 3 s)
0x81 0x05 breathing waveform 5 bytes, sine, midline 128 (1 s)
0x84 0x02 sleep state (10 min) 0 deep, 1 light, 2 awake, 3 off-bed/none
0x84 0x06 sleep quality score 1 byte, 0–100
0x84 0x0C comprehensive sleep status 8 bytes: presence(1/0), state(3/2/1/0), avg breath, avg heart, turns, large-motion %, small-motion %, apnea

Example: a breathing-rate frame is 53 59 81 02 00 01 [rate] [chk] 54 43. A sleep-state frame is 53 59 84 02 00 01 [state] [chk] 54 43. The manual’s own test vector 53 59 80 81 00 01 0F BD 54 43 (checksum 0xBD) is a handy parser sanity check.

Optional: publish to MQTT

The ESP32 publishes — the module never does. Using the esp-mqtt component, post a broker-agnostic topic such as radar/R60ABD1/state with JSON like {"breath":22,"sleep":1}. Point it at your own Mosquitto/EMQX/broker; nothing here is tied to a vendor cloud. This is exactly the “add your own microcontroller” path the architecture article describes for modules that lack networking.

Notes and safety

Where to go next

Both the R60ABD1 module and the ESP32-S3 board are on Tindie.