Skip to content

bresser.tc

Bresser Weather Station Receiver (CC1101 868 MHz)

Source on GitHub

// Bresser Weather Station Receiver (CC1101 868 MHz)
// Receives data from Bresser 5-in-1/6-in-1/7-in-1 weather sensors
// Supports weather station + soil moisture sensor (s_type 4)
// Demonstrates: SPI, CC1101 radio config, multi-protocol data decoding
// Hardware: CC1101 module on SPI bus, GDO0 on GPIO for packet detect

#define CS_PIN     8
#define GDO0_PIN   7
#define SPI_MHZ    4

// CC1101 Register addresses
#define REG_IOCFG2   0x00
#define REG_IOCFG0   0x02
#define REG_FIFOTHR  0x03
#define REG_SYNC1    0x04
#define REG_SYNC0    0x05
#define REG_PKTLEN   0x06
#define REG_PKTCTRL0 0x07
#define REG_PKTCTRL1 0x08
#define REG_ADDR     0x09
#define REG_CHANNR   0x0A
#define REG_FSCTRL1  0x0B
#define REG_FSCTRL0  0x0C
#define REG_FREQ2    0x0D
#define REG_FREQ1    0x0E
#define REG_FREQ0    0x0F
#define REG_MDMCFG4  0x10
#define REG_MDMCFG3  0x11
#define REG_MDMCFG2  0x12
#define REG_MDMCFG1  0x13
#define REG_MDMCFG0  0x14
#define REG_DEVIATN  0x15
#define REG_MCSM2    0x16
#define REG_MCSM1    0x17
#define REG_MCSM0    0x18
#define REG_FOCCFG   0x19
#define REG_BSCFG    0x1A
#define REG_AGCCTRL2 0x1B
#define REG_AGCCTRL1 0x1C
#define REG_AGCCTRL0 0x1D
#define REG_FREND1   0x21
#define REG_FREND0   0x22
#define REG_FSCAL3   0x23
#define REG_FSCAL2   0x24
#define REG_FSCAL1   0x25
#define REG_FSCAL0   0x26
#define REG_TEST2    0x2C
#define REG_TEST1    0x2D
#define REG_TEST0    0x2E
#define REG_PATABLE  0x3E
#define REG_FIFO     0x3F

// CC1101 status registers (read with burst bit 0xC0)
#define REG_RSSI     0x34
#define REG_LQI      0x33
#define REG_RXBYTES  0x3B

// CC1101 command strobes
#define CMD_SRES  0x30
#define CMD_SCAL  0x33
#define CMD_SRX   0x34
#define CMD_SIDLE 0x36
#define CMD_SPWD  0x39
#define CMD_SFRX  0x3A

// SPI access modes
#define WRITE_SINGLE 0x00
#define READ_SINGLE  0x80
#define READ_BURST   0xC0
#define WRITE_BURST  0x40

// Bresser protocol
#define MSG_BUF_SIZE 27  // FIFO read size: D4 prefix + 26 data bytes
#define DATA_SIZE    26  // decoded data size (after stripping D4)

// Sensor data - weather station
float br_temp = 0.0;
int   br_hum = 0;
float br_wind_gust = 0.0;
float br_wind_avg = 0.0;
float br_wind_dir = 0.0;
float br_rain = 0.0;
float br_lux = 0.0;
float br_uvi = 0.0;
float br_rssi = 0.0;
int   br_ok = 0;
int   br_id = 0;
int   br_batt = 0;
int   br_cnt = 0;

// Sensor data - soil moisture
float soil_temp = 0.0;
int   soil_moisture = 0;
float soil_rssi = 0.0;
int   soil_ok = 0;
int   soil_id = 0;
int   soil_batt = 0;
int   soil_cnt = 0;
char  br_lbl[32];

// Raw packet buffer (global for decoder access, 26 bytes after D4)
char msg[DATA_SIZE];

// SPI transfer buffer (reused for all SPI ops)
char spi[28];

// --- CC1101 Low-Level SPI ---

void cc_write_reg(int addr, int val) {
    spi[0] = addr;
    spi[1] = val;
    spiTransfer(1, spi, 2, 1);
}

int cc_read_reg(int addr) {
    spi[0] = addr | 0x80;
    spi[1] = 0;
    spiTransfer(1, spi, 2, 1);
    return spi[1];
}

int cc_read_status(int addr) {
    spi[0] = addr | 0xC0;
    spi[1] = 0;
    spiTransfer(1, spi, 2, 1);
    return spi[1];
}

void cc_strobe(int cmd) {
    spi[0] = cmd;
    spiTransfer(1, spi, 1, 1);
}

void cc_read_burst(int addr, int len) {
    int i;
    spi[0] = addr | 0xC0;
    for (i = 1; i <= len; i++) {
        spi[i] = 0;
    }
    spiTransfer(1, spi, len + 1, 1);
    // results in spi[1..len]
}

void cc_reset() {
    // manual CS toggle for reset sequence
    digitalWrite(CS_PIN, 1);
    delayMicroseconds(5);
    digitalWrite(CS_PIN, 0);
    delayMicroseconds(10);
    digitalWrite(CS_PIN, 1);
    delayMicroseconds(41);
    cc_strobe(CMD_SRES);
    delay(2);
}

// --- CC1101 Init for Bresser 868.3 MHz ---

int cc_init_bresser() {
    // Frequency: 868.3 MHz (26 MHz crystal)
    cc_write_reg(REG_FREQ2, 0x21);
    cc_write_reg(REG_FREQ1, 0x65);
    cc_write_reg(REG_FREQ0, 0x6A);

    // Data rate: 8.21 kbps, BW=270 kHz
    cc_write_reg(REG_MDMCFG4, 0x68);
    cc_write_reg(REG_MDMCFG3, 0x4B);

    // 2-FSK, 16/16 sync word match
    cc_write_reg(REG_MDMCFG2, 0x02);

    // 4 preamble bytes
    cc_write_reg(REG_MDMCFG1, 0x22);
    cc_write_reg(REG_MDMCFG0, 0xF8);

    // Freq deviation 57.129 kHz
    cc_write_reg(REG_DEVIATN, 0x51);

    // IF frequency
    cc_write_reg(REG_FSCTRL1, 0x06);
    cc_write_reg(REG_FSCTRL0, 0x00);

    // Sync word: 0xAA 0x2D (preamble end + first sync byte)
    // First FIFO byte will be 0xD4 (sacrificial, avoids first-byte corruption)
    cc_write_reg(REG_SYNC1, 0xAA);
    cc_write_reg(REG_SYNC0, 0x2D);

    // Fixed packet length = 27 bytes (D4 + 26 data), no CRC
    cc_write_reg(REG_PKTLEN, MSG_BUF_SIZE);
    cc_write_reg(REG_PKTCTRL0, 0x00);
    cc_write_reg(REG_PKTCTRL1, 0x00);

    // GDO0: assert on sync, deassert at end of packet
    cc_write_reg(REG_IOCFG0, 0x06);
    cc_write_reg(REG_IOCFG2, 0x2E);

    // AGC - maximum sensitivity
    cc_write_reg(REG_AGCCTRL2, 0x03);
    cc_write_reg(REG_AGCCTRL1, 0x40);
    cc_write_reg(REG_AGCCTRL0, 0x91);

    // Freq offset compensation
    cc_write_reg(REG_FOCCFG, 0x16);
    cc_write_reg(REG_BSCFG, 0x6C);

    // Front end
    cc_write_reg(REG_FREND1, 0x56);
    cc_write_reg(REG_FREND0, 0x10);

    // Freq synth calibration
    cc_write_reg(REG_FSCAL3, 0xE9);
    cc_write_reg(REG_FSCAL2, 0x2A);
    cc_write_reg(REG_FSCAL1, 0x00);
    cc_write_reg(REG_FSCAL0, 0x1F);

    // Test registers
    cc_write_reg(REG_TEST2, 0x81);
    cc_write_reg(REG_TEST1, 0x35);
    cc_write_reg(REG_TEST0, 0x09);

    // Main Radio State Machine
    cc_write_reg(REG_MCSM2, 0x07);
    // MCSM1: RXOFF_MODE=11 (stay in RX), CCA_MODE=11
    cc_write_reg(REG_MCSM1, 0x3C);
    // MCSM0: FS_AUTOCAL=01 (cal when IDLE->RX/TX)
    cc_write_reg(REG_MCSM0, 0x18);

    // PA Table
    cc_write_reg(REG_PATABLE, 0xC0);

    // Enter IDLE, calibrate, flush RX FIFO, then enter RX
    cc_strobe(CMD_SIDLE);
    delay(1);
    cc_strobe(CMD_SCAL);
    delay(2);
    cc_strobe(CMD_SIDLE);
    delay(1);
    cc_strobe(CMD_SFRX);
    cc_strobe(CMD_SRX);
    delay(2);

    // Retry SRX to ensure we settle into RX mode
    cc_strobe(CMD_SRX);
    delay(2);

    return 1;
}

// --- RSSI Calculation ---

float cc_get_rssi() {
    int raw = cc_read_status(REG_RSSI);
    int rssi;
    if (raw >= 128) {
        rssi = (raw - 256) / 2 - 74;
    } else {
        rssi = raw / 2 - 74;
    }
    return (float)rssi;
}

// --- Bresser 5-in-1 Decoder ---

int decode_5in1() {
    int i;

    // first 13 bytes XOR with next 13 must be 0xFF
    for (i = 0; i < 13; i++) {
        if (((msg[i] ^ msg[i + 13]) & 0xFF) != 0xFF) {
            return 0;
        }
    }

    // checksum: count bits set in bytes 14-25
    int bits_set = 0;
    int expected = msg[13] & 0xFF;
    for (i = 14; i < DATA_SIZE; i++) {
        int b = msg[i] & 0xFF;
        while (b) {
            bits_set = bits_set + (b & 1);
            b = b >> 1;
        }
    }
    if (bits_set != expected) {
        return 0;
    }

    br_id = msg[14] & 0xFF;
    if (msg[25] & 0x80) {
        br_batt = 0;
    } else {
        br_batt = 1;
    }

    // temperature: BCD digits
    int temp_raw = (msg[20] & 0x0F) + ((msg[20] >> 4) & 0x0F) * 10 + (msg[21] & 0x0F) * 100;
    if (msg[25] & 0x0F) {
        temp_raw = 0 - temp_raw;
    }
    br_temp = (float)temp_raw * 0.1;

    // humidity
    br_hum = (msg[22] & 0x0F) + ((msg[22] >> 4) & 0x0F) * 10;

    // wind
    int wdir_raw = ((msg[17] >> 4) & 0x0F) * 225;
    int gust_raw = ((msg[17] & 0x0F) << 8) + (msg[16] & 0xFF);
    int wind_raw = (msg[18] & 0x0F) + ((msg[18] >> 4) & 0x0F) * 10 + (msg[19] & 0x0F) * 100;

    br_wind_dir = (float)wdir_raw * 0.1;
    br_wind_gust = (float)gust_raw * 0.1;
    br_wind_avg = (float)wind_raw * 0.1;

    // rain
    int rain_raw = (msg[23] & 0x0F) + ((msg[23] >> 4) & 0x0F) * 10 + (msg[24] & 0x0F) * 100 + ((msg[24] >> 4) & 0x0F) * 1000;
    br_rain = (float)rain_raw * 0.1;

    return 1;
}

// --- Bresser 6-in-1 Decoder ---

int lfsr_digest16(int start, int bytes, int gen, int key) {
    int sum = 0;
    int k;
    int i;
    for (k = 0; k < bytes; k++) {
        int data = msg[start + k] & 0xFF;
        for (i = 7; i >= 0; i--) {
            if ((data >> i) & 1) {
                sum = sum ^ key;
            }
            if (key & 1) {
                key = (key >> 1) ^ gen;
            } else {
                key = key >> 1;
            }
        }
    }
    return sum;
}

int add_bytes(int start, int num) {
    int result = 0;
    int i;
    for (i = 0; i < num; i++) {
        result = result + (msg[start + i] & 0xFF);
    }
    return result;
}

int decode_6in1() {
    int chkdgst = ((msg[0] & 0xFF) << 8) | (msg[1] & 0xFF);
    int digest = lfsr_digest16(2, 15, 0x8810, 0x5412);
    if (chkdgst != digest) {
        return 0;
    }

    int sum = add_bytes(2, 16);
    if ((sum & 0xFF) != 0xFF) {
        return 0;
    }

    int s_type = (msg[6] >> 4) & 0x0F;
    int id_tmp = ((msg[2] & 0xFF) << 8) | (msg[3] & 0xFF);
    int batt_tmp = (msg[13] >> 1) & 1;
    int flags = msg[16] & 0x0F;

    // Soil moisture sensor (type 4)
    if (s_type == 4) {
        soil_id = id_tmp;
        soil_batt = batt_tmp;
        soil_rssi = br_rssi;

        // temperature (same BCD format as weather)
        int sign = (msg[13] >> 3) & 1;
        int temp_raw = ((msg[12] >> 4) & 0x0F) * 100 + (msg[12] & 0x0F) * 10 + ((msg[13] >> 4) & 0x0F);
        if (sign) {
            temp_raw = temp_raw - 1000;
        }
        soil_temp = (float)temp_raw * 0.1;

        // humidity field (1-16) -> moisture percentage
        // Maps to: 0,7,13,20,27,33,40,47,53,60,67,73,80,87,93,99
        int hum_idx = ((msg[14] >> 4) & 0x0F) * 10 + (msg[14] & 0x0F);
        if (hum_idx >= 1 && hum_idx <= 16) {
            if (hum_idx <= 1) {
                soil_moisture = 0;
            } else if (hum_idx >= 16) {
                soil_moisture = 99;
            } else {
                soil_moisture = ((hum_idx - 1) * 100 + 8) / 15;
            }
        } else {
            soil_moisture = 0;
        }
        return 2;  // soil sensor
    }

    // Weather station (type 1) or other types
    br_id = id_tmp;
    br_batt = batt_tmp;

    // temperature + humidity (flags == 0)
    if (flags == 0) {
        int sign = (msg[13] >> 3) & 1;
        int temp_raw = ((msg[12] >> 4) & 0x0F) * 100 + (msg[12] & 0x0F) * 10 + ((msg[13] >> 4) & 0x0F);
        if (sign) {
            temp_raw = temp_raw - 1000;
        }
        br_temp = (float)temp_raw * 0.1;
        br_hum = ((msg[14] >> 4) & 0x0F) * 10 + (msg[14] & 0x0F);
    }

    // wind (inverted bytes, mask with 0xFF to avoid sign extension)
    int im7 = (msg[7] ^ 0xFF) & 0xFF;
    int im8 = (msg[8] ^ 0xFF) & 0xFF;
    int im9 = (msg[9] ^ 0xFF) & 0xFF;
    if (im7 <= 0x99 && im8 <= 0x99 && im9 <= 0x99) {
        int gust_raw = ((im7 >> 4) & 0x0F) * 100 + (im7 & 0x0F) * 10 + ((im8 >> 4) & 0x0F);
        int wavg_raw = ((im9 >> 4) & 0x0F) * 100 + (im9 & 0x0F) * 10 + (im8 & 0x0F);
        int wdir_raw = ((msg[10] >> 4) & 0x0F) * 100 + (msg[10] & 0x0F) * 10 + ((msg[11] >> 4) & 0x0F);
        br_wind_gust = (float)gust_raw * 0.1;
        br_wind_avg = (float)wavg_raw * 0.1;
        br_wind_dir = (float)wdir_raw;
    }

    // rain (inverted, flags == 1)
    if (flags == 1) {
        int im12 = (msg[12] ^ 0xFF) & 0xFF;
        int im13 = (msg[13] ^ 0xFF) & 0xFF;
        int im14 = (msg[14] ^ 0xFF) & 0xFF;
        int rain_raw = ((im12 >> 4) & 0x0F) * 100000 + (im12 & 0x0F) * 10000 + ((im13 >> 4) & 0x0F) * 1000 + (im13 & 0x0F) * 100 + ((im14 >> 4) & 0x0F) * 10 + (im14 & 0x0F);
        br_rain = (float)rain_raw * 0.1;
    }

    // UV Index (6-in-1): inverted bytes 15-16, BCD, ×0.1
    int im15 = (msg[15] ^ 0xFF) & 0xFF;
    int im16 = (msg[16] ^ 0xFF) & 0xFF;
    if ((msg[16] & 0x0F) == 0 && im15 <= 0x99 && (im16 & 0xF0) <= 0x90) {
        int uv_raw = ((im15 >> 4) & 0x0F) * 100 + (im15 & 0x0F) * 10 + ((im16 >> 4) & 0x0F);
        br_uvi = (float)uv_raw * 0.1;
    }

    return 1;
}

// --- Bresser 7-in-1 Decoder ---

// de-whitened message buffer
char msgw[DATA_SIZE];

int decode_7in1() {
    int i;

    // data de-whitening (26 bytes)
    for (i = 0; i < DATA_SIZE; i++) {
        msgw[i] = msg[i] ^ 0xAA;
    }

    // LFSR-16 digest check (mask with 0xFF to avoid sign extension)
    int chkdgst = ((msgw[0] & 0xFF) << 8) | (msgw[1] & 0xFF);
    int digest = 0;
    int key = 0xBA95;
    int gen = 0x8810;
    int k;
    for (k = 0; k < 23; k++) {
        int data = msgw[k + 2] & 0xFF;
        for (i = 7; i >= 0; i--) {
            if ((data >> i) & 1) {
                digest = digest ^ key;
            }
            if (key & 1) {
                key = (key >> 1) ^ gen;
            } else {
                key = key >> 1;
            }
        }
    }
    if ((chkdgst ^ digest) != 0x6DF1) {
        return 0;
    }

    br_id = ((msgw[2] & 0xFF) << 8) | (msgw[3] & 0xFF);
    int s_type = (msg[6] >> 4) & 0x0F;
    int flags = msgw[15] & 0x0F;
    if ((flags & 0x06) == 0x06) {
        br_batt = 0;
    } else {
        br_batt = 1;
    }

    // Weather station (type 1)
    if (s_type == 1) {
        int wdir = ((msgw[4] >> 4) & 0x0F) * 100 + (msgw[4] & 0x0F) * 10 + ((msgw[5] >> 4) & 0x0F);
        int wgst = ((msgw[7] >> 4) & 0x0F) * 100 + (msgw[7] & 0x0F) * 10 + ((msgw[8] >> 4) & 0x0F);
        int wavg = (msgw[8] & 0x0F) * 100 + ((msgw[9] >> 4) & 0x0F) * 10 + (msgw[9] & 0x0F);
        int rain_raw = ((msgw[10] >> 4) & 0x0F) * 100000 + (msgw[10] & 0x0F) * 10000 + ((msgw[11] >> 4) & 0x0F) * 1000 + (msgw[11] & 0x0F) * 100 + ((msgw[12] >> 4) & 0x0F) * 10 + (msgw[12] & 0x0F);
        int temp_raw = ((msgw[14] >> 4) & 0x0F) * 100 + (msgw[14] & 0x0F) * 10 + ((msgw[15] >> 4) & 0x0F);
        int humidity = ((msgw[16] >> 4) & 0x0F) * 10 + (msgw[16] & 0x0F);

        float temp_c = (float)temp_raw * 0.1;
        if (temp_raw > 600) {
            temp_c = (float)(temp_raw - 1000) * 0.1;
        }

        br_temp = temp_c;
        br_hum = humidity;
        br_wind_dir = (float)wdir;
        br_wind_gust = (float)wgst * 0.1;
        br_wind_avg = (float)wavg * 0.1;
        br_rain = (float)rain_raw * 0.1;

        // Light (lux) — 6 BCD digits in de-whitened bytes 17-19
        int lght_raw = ((msgw[17] >> 4) & 0x0F) * 100000 + (msgw[17] & 0x0F) * 10000 + ((msgw[18] >> 4) & 0x0F) * 1000 + (msgw[18] & 0x0F) * 100 + ((msgw[19] >> 4) & 0x0F) * 10 + (msgw[19] & 0x0F);
        br_lux = (float)lght_raw;

        // UV Index — 3 BCD digits in de-whitened bytes 20-21, ×0.1
        int uv_raw = ((msgw[20] >> 4) & 0x0F) * 100 + (msgw[20] & 0x0F) * 10 + ((msgw[21] >> 4) & 0x0F);
        br_uvi = (float)uv_raw * 0.1;

        return 1;
    }

    // Soil moisture sensor (type 4) - uses raw msg[], NOT de-whitened
    if (s_type == 4) {
        int temp_raw = (msg[20] & 0x0F) + ((msg[20] >> 4) & 0x0F) * 10 + (msg[21] & 0x0F) * 100;
        if (msg[25] & 0x0F) {
            temp_raw = 0 - temp_raw;
        }
        soil_temp = (float)temp_raw * 0.1;

        // humidity field (1-16) -> moisture percentage
        int hum_idx = (msg[22] & 0x0F) + ((msg[22] >> 4) & 0x0F) * 10;
        if (hum_idx >= 1 && hum_idx <= 16) {
            if (hum_idx <= 1) {
                soil_moisture = 0;
            } else if (hum_idx >= 16) {
                soil_moisture = 99;
            } else {
                soil_moisture = ((hum_idx - 1) * 100 + 8) / 15;
            }
        } else {
            soil_moisture = 0;
        }

        soil_id = br_id;
        soil_batt = br_batt;
        soil_rssi = br_rssi;
        return 2;  // soil sensor
    }

    // Unknown sensor type - valid LFSR but we don't decode it
    return 0;
}

// --- Message Dispatcher ---

int decode_message() {
    int res;
    res = decode_7in1();
    if (res) return res;  // 1=weather, 2=soil
    res = decode_6in1();
    if (res) return res;
    res = decode_5in1();
    if (res) return 1;
    return 0;
}

// --- Receive Message ---

int receive_message() {
    int rxBytes = cc_read_status(REG_RXBYTES);

    if (rxBytes >= MSG_BUF_SIZE) {
        // burst read FIFO (27 bytes: D4 + 26 data bytes)
        cc_read_burst(REG_FIFO, MSG_BUF_SIZE);

        // get RSSI before flushing
        br_rssi = cc_get_rssi();

        // flush and restart RX
        cc_strobe(CMD_SIDLE);
        cc_strobe(CMD_SFRX);
        cc_strobe(CMD_SRX);
        delay(1);

        // With sync AA 2D, spi[1]=D4 (sacrificial), spi[2..27]=data
        // Copy data bytes: spi[2..27] -> msg[0..25]
        int i;
        for (i = 0; i < DATA_SIZE; i++) {
            msg[i] = spi[i + 2];
        }

        return decode_message();
    }
    return 0;
}

// --- Callbacks ---

void Every50ms() {
    // Called 20x/sec - non-blocking check for CC1101 packet
    int res = receive_message();
    if (res == 1) {
        br_ok = 1;
        br_cnt = br_cnt + 1;
    }
    if (res == 2) {
        soil_ok = 1;
        soil_cnt = soil_cnt + 1;
    }
}

void WebCall() {
    char out[80];
    if (br_ok) {
        sprintf(out, "{s}Bresser ID{m}0x%04X{e}", br_id);
        webSend(out);
        LGetString(0, br_lbl);
        strcpy(out, "{s}");
        strcat(out, br_lbl);
        strcat(out, "{m}");
        webSend(out);
        sprintf(out, "%.1f C{e}", br_temp);
        webSend(out);
        LGetString(1, br_lbl);
        strcpy(out, "{s}");
        strcat(out, br_lbl);
        strcat(out, "{m}");
        webSend(out);
        sprintf(out, "%d %{e}", br_hum);
        webSend(out);
        sprintf(out, "{s}Wind Gust{m}%.1f m/s{e}", br_wind_gust);
        webSend(out);
        sprintf(out, "{s}Wind Avg{m}%.1f m/s{e}", br_wind_avg);
        webSend(out);
        sprintf(out, "{s}Wind Dir{m}%.0f deg{e}", br_wind_dir);
        webSend(out);
        sprintf(out, "{s}Rain{m}%.1f mm{e}", br_rain);
        webSend(out);
        LGetString(15, br_lbl);
        strcpy(out, "{s}");
        strcat(out, br_lbl);
        strcat(out, "{m}");
        webSend(out);
        sprintf(out, "%.0f lux{e}", br_lux);
        webSend(out);
        sprintf(out, "{s}UV Index{m}%.1f{e}", br_uvi);
        webSend(out);
        sprintf(out, "{s}RSSI{m}%.0f dBm{e}", br_rssi);
        webSend(out);
        if (br_batt) {
            webSend("{s}Battery{m}OK{e}");
        } else {
            webSend("{s}Battery{m}Low{e}");
        }
        sprintf(out, "{s}Packets{m}%d{e}", br_cnt);
        webSend(out);
    } else {
        webSend("{s}Bresser{m}waiting for data{e}");
    }
    if (soil_ok) {
        sprintf(out, "{s}Soil ID{m}0x%04X{e}", soil_id);
        webSend(out);
        LGetString(0, br_lbl);
        strcpy(out, "{s}Soil ");
        strcat(out, br_lbl);
        strcat(out, "{m}");
        webSend(out);
        sprintf(out, "%.1f C{e}", soil_temp);
        webSend(out);
        LGetString(17, br_lbl);
        strcpy(out, "{s}Soil ");
        strcat(out, br_lbl);
        strcat(out, "{m}");
        webSend(out);
        sprintf(out, "%d %{e}", soil_moisture);
        webSend(out);
        sprintf(out, "{s}Soil RSSI{m}%.0f dBm{e}", soil_rssi);
        webSend(out);
        if (soil_batt) {
            webSend("{s}Soil Battery{m}OK{e}");
        } else {
            webSend("{s}Soil Battery{m}Low{e}");
        }
        sprintf(out, "{s}Soil Packets{m}%d{e}", soil_cnt);
        webSend(out);
    }
}

void JsonCall() {
    char out[80];
    if (br_ok) {
        sprintf(out, ",\"Bresser\":{\"ID\":\"%04X\"", br_id);
        responseAppend(out);
        sprintf(out, ",\"Temp\":%.1f", br_temp);
        responseAppend(out);
        sprintf(out, ",\"Hum\":%d", br_hum);
        responseAppend(out);
        sprintf(out, ",\"WindGust\":%.1f", br_wind_gust);
        responseAppend(out);
        sprintf(out, ",\"WindAvg\":%.1f", br_wind_avg);
        responseAppend(out);
        sprintf(out, ",\"WindDir\":%.0f", br_wind_dir);
        responseAppend(out);
        sprintf(out, ",\"Rain\":%.1f", br_rain);
        responseAppend(out);
        sprintf(out, ",\"Lux\":%.0f", br_lux);
        responseAppend(out);
        sprintf(out, ",\"UVI\":%.1f", br_uvi);
        responseAppend(out);
        sprintf(out, ",\"RSSI\":%.0f", br_rssi);
        responseAppend(out);
        sprintf(out, ",\"Batt\":%d", br_batt);
        responseAppend(out);
        sprintf(out, ",\"Cnt\":%d}", br_cnt);
        responseAppend(out);
    }
    if (soil_ok) {
        sprintf(out, ",\"Soil\":{\"ID\":\"%04X\"", soil_id);
        responseAppend(out);
        sprintf(out, ",\"Temp\":%.1f", soil_temp);
        responseAppend(out);
        sprintf(out, ",\"Moisture\":%d", soil_moisture);
        responseAppend(out);
        sprintf(out, ",\"RSSI\":%.0f", soil_rssi);
        responseAppend(out);
        sprintf(out, ",\"Batt\":%d", soil_batt);
        responseAppend(out);
        sprintf(out, ",\"Cnt\":%d}", soil_cnt);
        responseAppend(out);
    }
}

int main() {
    // init SPI (CS managed manually during reset, then by driver)
    spiInit(-1, -1, -1, SPI_MHZ);

    // init GDO0 as input
    gpioInit(GDO0_PIN, 0);

    // CC1101 reset needs manual CS toggling - do before spiSetCS claims the pin
    gpioInit(CS_PIN, 1);
    cc_reset();

    // now let SPI driver manage CS
    spiSetCS(1, CS_PIN);

    // Check CC1101 version register
    int ver = cc_read_status(0x31);
    char buf[64];
    sprintf(buf, "CC1101 version: 0x%02X\n", ver);
    printString(buf);

    if (ver == 0 || ver == 0xFF) {
        printStr("ERROR: CC1101 not found\n");
        return 1;
    }

    if (!cc_init_bresser()) {
        printStr("ERROR: CC1101 init failed\n");
        return 1;
    }

    br_ok = 0;
    br_cnt = 0;
    soil_ok = 0;
    soil_cnt = 0;

    printStr("Bresser CC1101 receiver ready\n");
    return 0;
}