Works Immediately, No API Required

Pexla sensors are designed so you can be measuring in seconds. Connect power and ground, open a serial terminal at 9600 bps, and distance readings appear instantly โ€” no commands, no initialization sequence, no drivers. For dataloggers, PLCs, and SCADA systems that simply need a distance measurement over UART, SDI-12, or Modbus, this is all you need.

The default output is a compact ASCII frame: distance in millimeters, updated at 1 Hz, on a single line. Add power, read numbers. That's it.

Plug-and-Play Operation

The sensor boots in under 200ms and begins streaming. Default settings cover the majority of level-sensing, distance, and inventory applications without any configuration.

Default output sample
R1234
R1235
R1233
R1234
โ†‘ Distance in mm ยท 1 Hz ยท 9600 bps ยท no config needed
Connect VCC (3โ€“6V) + GND
Open serial @ 9600 bps, 8-N-1
Read ASCII distance frames
Done โ€” no API needed

When the API Unlocks Real Value

The Pexla command API exposes over 100 configurable parameters. It uses a simple request-response packet format over the same UART connection โ€” no separate bus, no special mode, no reboots required. Changes written to RAM take effect immediately; write to Flash to survive power cycles.

Signal Filtering

Noise ยท Stability ยท Accuracy

In turbulent water, foam, or environments with vibration, the raw radar returns multiple candidate peaks. The API lets you set the number of averaging iterations, configure zoom-in mode to lock onto a target window, and define multi-target sorting so the sensor always reports the surface you care about โ€” not a reflection or a ripple.

SendIterationCnt EnZoomIn ZoomInThresh RadNumTargets RadSortMethod

Battery & Sleep Power

IoT ยท Solar ยท Remote Deployments

The sensor's STOP2 sleep mode draws as little as 3ยตA. Via the API you can configure the wakeup interval, trigger a measurement-and-sleep cycle from your MCU, and set the output cadence independently of the measurement rate โ€” letting you take 10 readings, average them, transmit once, then sleep for 15 minutes, all autonomously.

EnSleep SleepDelay WakePin BurstCount OutputRate

Multiple Target Tracking

Clarifiers ยท Multi-layer ยท Foam

FMCW radar naturally detects all reflective surfaces within range simultaneously. The API exposes up to 10 discrete target distances per measurement frame, sortable by proximity, signal strength, or a configured priority zone. Ideal for clarifier sludge blanket monitoring where you need both the water surface and the settled solids layer.

RadNumTargets RadSortMethod TargetZoneMin TargetZoneMax EnMultiTarget

Threshold Offloading

Alerts ยท Pump Control ยท SCADA

Rather than polling the sensor and running threshold logic in your MCU, you can configure the Pexla to assert a digital output pin or emit a special packet frame when the measured distance crosses a programmable high or low watermark. This offloads flood-alert and pump-start logic directly to the sensor, reducing host complexity and latency.

ThreshHigh ThreshLow ThreshPin ThreshMode ThreshHysteresis

Command Packet Format

All API commands share a single consistent frame format over UART. The sensor accepts both string key names and numeric key IDs. Commands take effect immediately and the sensor echoes a confirmation. There is no mode-switching โ€” commands and data coexist on the same wire.

RAM vs Flash: Writes without * go to RAM and are lost on power cycle โ€” useful for runtime tuning. Prefix with * to persist to Flash. Always prototype in RAM first, then commit to Flash once the values are confirmed.

Python Integration

Python's pyserial library makes Pexla integration straightforward on Linux, macOS, Windows, and Raspberry Pi. The examples below progress from a minimal read loop to a fully optimized configuration for a battery-powered water level monitoring station.

1 โ€” Minimal Read Loop (No API)

This is all you need for most deployments. Open the port, read lines, parse the distance prefix R.

pexla_basic.py
Python
"""
pexla_basic.py
Minimal Pexla read loop โ€” no API configuration needed.
Requires: pip install pyserial
"""
import serial

PORT     = '/dev/ttyUSB0'   # Windows: 'COM3', macOS: '/dev/cu.usbserial-...'
BAUDRATE = 9600             # Pexla default baud rate

def main():
    with serial.Serial(PORT, BAUDRATE, timeout=2) as sensor:
        print(f"Connected to {PORT} @ {BAUDRATE} bps")
        print("Reading distance (mm). Press Ctrl-C to stop.\n")

        while True:
            line = sensor.readline().decode('ascii', errors='ignore').strip()

            # Default output frame: "R1234" where 1234 is mm distance
            if line.startswith('R') and line[1:].isdigit():
                distance_mm = int(line[1:])
                print(f"Distance: {distance_mm:5d} mm  ({distance_mm / 1000:.3f} m)")

if __name__ == '__main__':
    main()
Output: Reads the default 1 Hz stream. No commands sent. Works immediately after connecting power.

2 โ€” Filtering & Zoom-In for Turbulent Water

For stormwater culverts, wastewater channels, or any surface with ripples and foam, configure the sensor's internal averaging and zoom-in window to lock onto the primary surface and suppress false returns.

pexla_filtering.py
Python
"""
pexla_filtering.py
Configure signal filtering and zoom-in mode for noisy water surfaces.
"""
import serial
import time

PORT     = '/dev/ttyUSB0'
BAUDRATE = 9600

def send(ser, command: str) -> str:
    """Send a Pexla API command and return the response line."""
    ser.write((command + '\n').encode('ascii'))
    time.sleep(0.05)                        # Allow sensor to process
    response = ser.readline().decode('ascii', errors='ignore').strip()
    return response

def configure_for_turbulent_water(ser):
    """
    Apply filtering settings for noisy water surfaces.
    Written to RAM only โ€” will reset on power cycle.
    Use <<<*Key=Value>>> prefix to persist to Flash.
    """
    config = [
        # Average 16 radar bursts per output reading (more stable, slower)
        ('<<>>', 'Averaging iterations โ†’ 16'),

        # Enable zoom-in mode: lock within ยฑ500mm of detected peak
        ('<<>>',          'Zoom-in mode โ†’ ON'),
        ('<<>>',    'Zoom-in window โ†’ ยฑ500 mm'),

        # Report only the closest target (suppress foam / back-reflections)
        ('<<>>',     'Target count โ†’ 1 (closest)'),
        ('<<>>',     'Sort method โ†’ nearest first'),
    ]

    print("Configuring sensor for turbulent water...\n")
    for cmd, description in config:
        resp = send(ser, cmd)
        status = "OK" if resp else "no echo"
        print(f"  {description:<35} [{status}]")

    print("\nConfiguration complete. Reading filtered distance:\n")

def main():
    with serial.Serial(PORT, BAUDRATE, timeout=2) as ser:
        configure_for_turbulent_water(ser)

        while True:
            line = ser.readline().decode('ascii', errors='ignore').strip()
            if line.startswith('R') and line[1:].isdigit():
                mm = int(line[1:])
                MOUNT_HEIGHT_MM = 2000       # Sensor is 2000mm above channel floor
                depth_mm = MOUNT_HEIGHT_MM - mm
                print(f"Surface:  {mm:5d} mm from sensor   |   "
                      f"Depth: {depth_mm:5d} mm   ({depth_mm/1000:.3f} m)")

if __name__ == '__main__':
    main()
Tip: Increase SendIterationCnt (8โ€“32) for smoother readings on agitated surfaces. Higher values reduce update rate proportionally.

3 โ€” Battery-Optimized Sleep / Wake Cycle

For solar or battery-powered deployments โ€” remote flood gauges, wetland monitors, or rural stormwater nodes โ€” use the sleep API to minimize active power. The pattern below wakes the sensor, takes a burst of readings, averages them, transmits, then returns to STOP2 sleep.

pexla_low_power.py
Python
"""
pexla_low_power.py
Battery-optimized read cycle: burst โ†’ average โ†’ sleep.
Suitable for solar-powered flood gauges and remote IoT nodes.

Power profile (approximate):
  Active measurement:  ~85 mA  for ~0.5 s
  STOP2 sleep:         ~6 ยตA   for the remaining interval
  Net @ 15-min cycles: ~0.05 mA average current
"""
import serial
import time
import statistics

PORT            = '/dev/ttyUSB0'
BAUDRATE        = 9600
SAMPLE_INTERVAL = 15 * 60      # seconds between measurement cycles
BURST_COUNT     = 8            # readings per wake cycle to average

def send(ser, cmd: str, wait: float = 0.08) -> str:
    ser.write((cmd + '\n').encode('ascii'))
    time.sleep(wait)
    return ser.readline().decode('ascii', errors='ignore').strip()

def wake_sensor(ser):
    ser.write(b'\n')
    time.sleep(0.2)
    ser.reset_input_buffer()

def read_distance(ser, timeout: float = 2.0) -> int | None:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        line = ser.readline().decode('ascii', errors='ignore').strip()
        if line.startswith('R') and line[1:].isdigit():
            return int(line[1:])
    return None

def sleep_sensor(ser):
    send(ser, '<<>>', wait=0.1)

def measure_cycle(ser) -> float | None:
    wake_sensor(ser)
    readings = []
    for _ in range(BURST_COUNT):
        dist = read_distance(ser)
        if dist is not None:
            readings.append(dist)
    sleep_sensor(ser)
    if len(readings) < 3:
        return None
    return statistics.median(readings)

def publish(distance_mm: float):
    """Replace with your telemetry / MQTT / HTTP publish logic."""
    print(f"  โ†’ Published: {distance_mm:.0f} mm  ({distance_mm/1000:.3f} m)")

def main():
    print(f"Low-power mode: {BURST_COUNT} readings every {SAMPLE_INTERVAL//60} min\n")
    with serial.Serial(PORT, BAUDRATE, timeout=2) as ser:
        send(ser, '<<<*EnSleep=0>>>')     # Disable sleep during config
        send(ser, '<<<*BaudRate=9600>>>')
        cycle = 0
        while True:
            cycle += 1
            ts = time.strftime('%H:%M:%S')
            print(f"[{ts}] Cycle {cycle}: waking sensor...")
            median_mm = measure_cycle(ser)
            if median_mm is not None:
                print(f"[{ts}] Median of {BURST_COUNT} readings: {median_mm:.0f} mm")
                publish(median_mm)
            else:
                print(f"[{ts}] ERROR: insufficient readings this cycle")
            print(f"[{ts}] Sleeping {SAMPLE_INTERVAL//60} min...\n")
            time.sleep(SAMPLE_INTERVAL)

if __name__ == '__main__':
    main()
Net power: With 8 readings at ~85 mA active for ~0.4 s each, plus STOP2 sleep at 6 ยตA for 15 minutes, average current is under 0.06 mA โ€” compatible with small LiPo and solar cell combinations.

4 โ€” Multi-Target Tracking & Threshold Alerts

For clarifier monitoring, the sensor can simultaneously report the water surface and sludge blanket. Threshold alerts are configured on-sensor so your host only needs to act when a limit is crossed โ€” no polling logic required.

pexla_multi_target.py
Python
"""
pexla_multi_target.py
Multi-target detection and hardware threshold alert configuration.
Use case: wastewater clarifier โ€” track water surface + sludge blanket.
"""
import serial, time, re

PORT             = '/dev/ttyUSB0'
BAUDRATE         = 9600
SENSOR_HEIGHT_MM = 3500     # Sensor mounted 3.5m above clarifier floor
HIGH_ALERT_MM    = 800
LOW_ALERT_MM     = 3200

def send(ser, cmd): ser.write((cmd+'\n').encode()); time.sleep(0.06); return ser.readline().decode('ascii','ignore').strip()

def configure_multi_target(ser):
    cmds = [
        ('<<<*RadNumTargets=2>>>',               'Targets per frame โ†’ 2'),
        ('<<<*RadSortMethod=0>>>',               'Sort โ†’ nearest first'),
        (f'<<<*ThreshHigh={HIGH_ALERT_MM}>>>',  f'High alert โ†’ {HIGH_ALERT_MM} mm'),
        (f'<<<*ThreshLow={LOW_ALERT_MM}>>>',    f'Low alert  โ†’ {LOW_ALERT_MM} mm'),
        ('<<<*ThreshMode=1>>>',                  'Threshold mode โ†’ active-high'),
        ('<<<*ThreshHysteresis=50>>>',           'Hysteresis โ†’ 50 mm'),
    ]
    for cmd, label in cmds:
        send(ser, cmd)
        print(f"  {label}")

def parse_multi_frame(line):
    return [int(m) for m in re.findall(r'(?:R|T|,)(\d+)', line)]

def main():
    with serial.Serial(PORT, BAUDRATE, timeout=2) as ser:
        configure_multi_target(ser)
        print(f"\n{'Surface':<20} {'Sludge':<20} {'Water Depth':<18} {'Blanket':<18} Alert")
        print("โ”€" * 85)
        while True:
            line = ser.readline().decode('ascii','ignore').strip()
            targets = parse_multi_frame(line)
            if len(targets) >= 1:
                surface = targets[0]
                sludge  = targets[1] if len(targets) >= 2 else None
                depth   = SENSOR_HEIGHT_MM - surface
                blanket = (SENSOR_HEIGHT_MM - sludge) if sludge else None
                alert   = "โš  HIGH" if surface < HIGH_ALERT_MM else ("โš  LOW" if surface > LOW_ALERT_MM else "")
                print(f"{surface:5d} mm from sensor   "
                      f"{str(sludge)+' mm' if sludge else 'N/A':>12}   "
                      f"{depth:5d} mm           "
                      f"{str(blanket)+' mm' if blanket else 'N/A':>12}   {alert}")

if __name__ == '__main__':
    main()
Threshold offloading: Once ThreshHigh / ThreshLow are set in Flash, the sensor asserts its threshold pin independently โ€” your MCU can sleep and only wake on interrupt, with no polling loop needed.

C Integration

The C examples target embedded MCUs (STM32, ESP32, Arduino, AVR) using standard UART peripherals. All Pexla commands are plain ASCII โ€” no binary framing, no CRC, no special mode entry. Suitable for bare-metal and RTOS environments.

UART configuration: Pexla defaults to 9600 bps, 8 data bits, no parity, 1 stop bit (8-N-1). I/O logic is 3.3V. Do not connect directly to 5V UART without a level shifter.

1 โ€” Minimal Read (Arduino / AVR)

Works on any Arduino-compatible board. Parses the R-prefixed distance frame with no API configuration needed.

pexla_basic.ino
C / Arduino
/*
 * pexla_basic.ino
 * Minimal Pexla distance read โ€” no API needed.
 * Tested: Arduino Uno, Nano, Mega
 *
 * Wiring:
 *   Pexla TX  โ†’  Arduino RX (pin 0, or SoftwareSerial)
 *   Pexla RX  โ†’  Arduino TX (pin 1)
 *   Pexla VCC โ†’  3.3V or 5V (check sensor spec)
 *   Pexla GND โ†’  GND
 */

#define PEXLA_BAUD  9600
#define BUF_SIZE    16

char    buf[BUF_SIZE];
uint8_t buf_idx = 0;

void setup() {
    Serial.begin(115200);       /* Debug output to PC */
    Serial1.begin(PEXLA_BAUD); /* Pexla on hardware UART1 */
    Serial.println("Pexla Basic Reader โ€” waiting for data...");
}

void loop() {
    while (Serial1.available()) {
        char c = Serial1.read();
        if (c == '\n' || c == '\r') {
            if (buf_idx > 0) {
                buf[buf_idx] = '\0';
                parse_frame(buf);
                buf_idx = 0;
            }
        } else if (buf_idx < BUF_SIZE - 1) {
            buf[buf_idx++] = c;
        }
    }
}

void parse_frame(const char *line) {
    /* Default output: "R1234" โ†’ distance 1234 mm */
    if (line[0] == 'R') {
        long dist_mm = atol(&line[1]);
        Serial.print("Distance: ");
        Serial.print(dist_mm);
        Serial.println(" mm");
    }
}
SoftwareSerial: If your board lacks a hardware UART1, replace Serial1 with a SoftwareSerial instance on any two digital pins.

2 โ€” STM32 HAL โ€” Full API Configuration

Production-grade STM32 implementation using HAL UART with interrupt receive. Configures filtering, sleep mode, and threshold alerting on startup, then reads from an interrupt-driven buffer.

pexla_stm32.c
C / STM32 HAL
/*
 * pexla_stm32.c โ€” STM32 HAL driver for Pexla sensor.
 * Configures: averaging, zoom-in, sleep mode, threshold alert.
 * Uses UART with IT (interrupt) receive.
 * Tested: STM32L4 (Nucleo-L476RG), STM32F4
 */

#include "main.h"
#include "pexla_stm32.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#define PEXLA_UART          huart2
#define PEXLA_TIMEOUT_MS    500
#define PEXLA_BUF_SIZE      64
#define PEXLA_MOUNT_HEIGHT  2000   /* mm: sensor to floor/datum */
#define PEXLA_THRESH_HIGH   600    /* Assert alert if closer than this */
#define PEXLA_THRESH_LOW    1900   /* Assert alert if farther than this */

static char     rx_buf[PEXLA_BUF_SIZE];
static uint8_t  rx_char;
static uint8_t  rx_idx  = 0;
static uint8_t  rx_ready = 0;

extern UART_HandleTypeDef PEXLA_UART;

static HAL_StatusTypeDef pexla_send(const char *cmd, char *resp, size_t resp_len)
{
    char frame[80];
    int  len = snprintf(frame, sizeof(frame), "%s\n", cmd);

    rx_idx = 0; rx_ready = 0;
    HAL_UART_Transmit(&PEXLA_UART, (uint8_t *)frame, len, PEXLA_TIMEOUT_MS);

    uint32_t t0 = HAL_GetTick();
    while (!rx_ready) {
        if ((HAL_GetTick() - t0) > PEXLA_TIMEOUT_MS) return HAL_TIMEOUT;
    }
    if (resp && resp_len > 0) {
        strncpy(resp, rx_buf, resp_len - 1);
        resp[resp_len - 1] = '\0';
    }
    rx_idx = 0; rx_ready = 0;
    return HAL_OK;
}

void pexla_init(void)
{
    HAL_UART_Receive_IT(&PEXLA_UART, &rx_char, 1);
    HAL_Delay(300);

    /* Filtering: 16-iteration averaging + zoom-in */
    pexla_send("<<<*SendIterationCnt=16>>>", NULL, 0);
    pexla_send("<<<*EnZoomIn=1>>>",          NULL, 0);
    pexla_send("<<<*ZoomInThresh=400>>>",    NULL, 0);

    /* Single closest target */
    pexla_send("<<<*RadNumTargets=1>>>",     NULL, 0);
    pexla_send("<<<*RadSortMethod=0>>>",     NULL, 0);

    /* Threshold alert (persisted to Flash) */
    char cmd[48];
    snprintf(cmd, sizeof(cmd), "<<<*ThreshHigh=%d>>>", PEXLA_THRESH_HIGH);
    pexla_send(cmd, NULL, 0);
    snprintf(cmd, sizeof(cmd), "<<<*ThreshLow=%d>>>",  PEXLA_THRESH_LOW);
    pexla_send(cmd, NULL, 0);
    pexla_send("<<<*ThreshMode=1>>>",        NULL, 0);
    pexla_send("<<<*ThreshHysteresis=30>>>", NULL, 0);

    /* Low-power sleep between measurements */
    pexla_send("<<<*EnSleep=1>>>",           NULL, 0);
    pexla_send("<<<*SleepDelay=5000>>>",     NULL, 0);  /* 5 s interval */
}

pexla_status_t pexla_read(int32_t *dist_mm)
{
    if (!rx_ready) return PEXLA_NO_DATA;
    if (rx_buf[0] == 'R') {
        *dist_mm = atol(&rx_buf[1]);
        rx_idx = 0; rx_ready = 0;
        return PEXLA_OK;
    }
    rx_idx = 0; rx_ready = 0;
    return PEXLA_PARSE_ERR;
}

/* UART ISR callback โ€” accumulates bytes, sets rx_ready on newline */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance != PEXLA_UART.Instance) return;
    char c = (char)rx_char;
    if (c == '\n' || c == '\r') {
        if (rx_idx > 0) { rx_buf[rx_idx] = '\0'; rx_ready = 1; }
    } else if (rx_idx < PEXLA_BUF_SIZE - 1) {
        rx_buf[rx_idx++] = c;
    }
    HAL_UART_Receive_IT(&PEXLA_UART, &rx_char, 1);
}

/* Call from main loop or RTOS task */
void pexla_task(void)
{
    int32_t dist_mm;
    if (pexla_read(&dist_mm) == PEXLA_OK) {
        int32_t level_mm = PEXLA_MOUNT_HEIGHT - dist_mm;
        printf("Level: %ld mm  (%.3f m)\r\n", level_mm, level_mm / 1000.0f);

        /* Hardware threshold pin already asserted โ€” mirror in software */
        GPIO_PinState alert = (dist_mm < PEXLA_THRESH_HIGH) ? GPIO_PIN_SET : GPIO_PIN_RESET;
        HAL_GPIO_WritePin(ALERT_LED_GPIO_Port, ALERT_LED_Pin, alert);
    }
}
Header file: Add pexla_status_t enum with PEXLA_OK, PEXLA_NO_DATA, PEXLA_PARSE_ERR in pexla_stm32.h. All settings use * prefix to persist across power cycles.

3 โ€” ESP32 โ€” IoT Upload with Threshold Wake

ESP32 implementation using hardware UART2. The sensor's threshold pin wakes the ESP32 from deep sleep via GPIO interrupt โ€” eliminating the need to poll continuously.

pexla_esp32.c
C / ESP-IDF
/*
 * pexla_esp32.c โ€” ESP32 (ESP-IDF) with deep-sleep + threshold GPIO wake.
 *
 * Cycle: wake โ†’ burst read โ†’ MQTT publish โ†’ deep sleep
 *
 * GPIO wiring:
 *   Pexla TX  โ†’ GPIO16 (UART2 RX)
 *   Pexla RX  โ†’ GPIO17 (UART2 TX)
 *   Pexla THR โ†’ GPIO34 (ext0 wakeup from deep sleep)
 */

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_sleep.h"
#include "esp_log.h"
#include <string.h>
#include <stdlib.h>

#define TAG             "PEXLA"
#define PEXLA_UART_NUM  UART_NUM_2
#define PEXLA_TX_PIN    17
#define PEXLA_RX_PIN    16
#define PEXLA_BAUD      9600
#define PEXLA_THRESH_PIN GPIO_NUM_34
#define BURST_COUNT     8
#define SLEEP_US        (15 * 60 * 1000000ULL)   /* 15 min */
#define MOUNT_HEIGHT_MM 2000

static void pexla_uart_init(void) {
    const uart_config_t cfg = {
        .baud_rate = PEXLA_BAUD, .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(PEXLA_UART_NUM, &cfg);
    uart_set_pin(PEXLA_UART_NUM, PEXLA_TX_PIN, PEXLA_RX_PIN,
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(PEXLA_UART_NUM, 256, 0, 0, NULL, 0);
}

static void pexla_cmd(const char *cmd) {
    char frame[80];
    int n = snprintf(frame, sizeof(frame), "%s\n", cmd);
    uart_write_bytes(PEXLA_UART_NUM, frame, n);
    vTaskDelay(pdMS_TO_TICKS(80));
}

static void pexla_configure_thresholds(void) {
    pexla_cmd("<<<*ThreshHigh=500>>>");     /* High water: < 500mm  */
    pexla_cmd("<<<*ThreshLow=1900>>>");     /* Low water:  > 1900mm */
    pexla_cmd("<<<*ThreshMode=1>>>");       /* Active-high           */
    pexla_cmd("<<<*ThreshHysteresis=40>>>"); /* 40mm dead-band        */
    pexla_cmd("<<<*EnSleep=1>>>");
    ESP_LOGI(TAG, "Threshold config written to Flash");
}

static int32_t pexla_burst_read(void) {
    int32_t samples[BURST_COUNT];
    int count = 0;
    uint8_t buf[32];

    /* Wake sensor */
    uart_write_bytes(PEXLA_UART_NUM, "\n", 1);
    vTaskDelay(pdMS_TO_TICKS(200));
    uart_flush(PEXLA_UART_NUM);

    TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(5000);
    while (count < BURST_COUNT && xTaskGetTickCount() < deadline) {
        size_t len = uart_read_bytes(PEXLA_UART_NUM, buf, sizeof(buf)-1,
                                     pdMS_TO_TICKS(500));
        if (!len) continue;
        buf[len] = '\0';
        if (buf[0] == 'R') samples[count++] = atol((char*)&buf[1]);
    }

    if (count < 3) return -1;

    /* Insertion sort for median */
    for (int i = 1; i < count; i++) {
        int32_t k = samples[i]; int j = i-1;
        while (j >= 0 && samples[j] > k) { samples[j+1] = samples[j]; j--; }
        samples[j+1] = k;
    }
    return samples[count / 2];
}

void app_main(void) {
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    pexla_uart_init();

    if (cause == ESP_SLEEP_WAKEUP_UNDEFINED) {
        pexla_configure_thresholds();  /* First boot only */
    }

    int32_t dist_mm = pexla_burst_read();
    if (dist_mm > 0) {
        int32_t level_mm = MOUNT_HEIGHT_MM - dist_mm;
        ESP_LOGI(TAG, "Level: %ld mm (%.3f m)", level_mm, level_mm / 1000.0f);
        if (cause == ESP_SLEEP_WAKEUP_EXT0)
            ESP_LOGW(TAG, "THRESHOLD ALERT โ€” emergency reading");
        /* TODO: WiFi + MQTT publish */
    }

    /* Configure wakeup sources */
    esp_sleep_enable_ext0_wakeup(PEXLA_THRESH_PIN, 1); /* Threshold pin */
    esp_sleep_enable_timer_wakeup(SLEEP_US);            /* 15-min timer  */

    uart_driver_delete(PEXLA_UART_NUM);
    esp_deep_sleep_start();
}
Power profile: ESP32 deep sleep ~10ยตA + Pexla STOP2 ~6ยตA. Active only during measurement bursts (~0.5 s) and WiFi transmission (~1.5 s). Suitable for 18650 LiPo + small solar panel for multi-year remote deployments.

Key API Parameters

Selected parameters for the four optimization areas. Prefix with * to persist to Flash. Full parameter list available in the Pexla API documentation.

Parameter Type / Range Default Description
Filtering & Accuracy
SendIterationCnt int / 1โ€“64 1 Radar bursts averaged per output frame. Higher = smoother but slower update rate.
EnZoomIn bool / 0,1 0 Enable zoom-in window around detected peak. Reduces false returns from foam and walls.
ZoomInThresh int / mm 1800 Half-width of zoom-in window in mm. Lock-on range around primary target.
RadSortMethod int / 0โ€“3 0 0=nearest, 1=strongest, 2=zone priority, 3=custom weight.
Multiple Target Tracking
RadNumTargets int / 1โ€“10 1 Number of simultaneous targets reported per frame.
EnMultiTarget bool / 0,1 0 Enable multi-target output format. Required for RadNumTargets > 1.
TargetZoneMin int / mm 0 Minimum range for zone-priority target selection.
TargetZoneMax int / mm 23000 Maximum range for zone-priority target selection.
Power Management
EnSleep bool / 0,1 0 Enable STOP2 sleep mode between measurements. 3โ€“8.5ยตA sleep current.
SleepDelay int / ms 1000 Duration of sleep period between measurement cycles in milliseconds.
BurstCount int / 1โ€“32 1 Number of measurements taken per wake cycle before returning to sleep.
WakePin bool / 0,1 0 Enable external GPIO wake from STOP2 sleep on rising edge.
Threshold Offloading
ThreshHigh int / mm 0 High watermark. Assert threshold pin if distance < this value.
ThreshLow int / mm 23000 Low watermark. Assert threshold pin if distance > this value.
ThreshMode int / 0โ€“2 0 0=disabled, 1=active-high, 2=active-low output on threshold pin.
ThreshHysteresis int / mm 0 Dead-band around threshold to prevent chatter on noisy surfaces.
Output & Format
BaudRate int 9600 UART baud rate. Supported: 9600, 19200, 38400, 115200, 230400, 500000.
OutputRate int / Hz 1 Output frame rate in Hz, independent of measurement rate.
EnTemp bool / 0,1 0 Append internal temperature (ยฐC ร— 10) to each output frame.
configurations read-only โ€” Burst-read: returns all current RAM settings in one response.
ResetConfigs write / 1 โ€” Factory reset all parameters to firmware defaults.
Burst read all configs: Send <<<configurations?>>> to receive every current RAM setting in one response โ€” useful for logging the sensor state or verifying configuration after deployment.