bresser_chart.tc¶
Bresser Weather Station + 24h Google Charts (CC1101 868 MHz)
// Bresser Weather Station + 24h Google Charts (CC1101 868 MHz)
// Full Bresser 5/6/7-in-1 receiver with 6 ring-buffer charts
// 288 points per chart (every 5 min, 24 hours)
// Uses WebChart() for automatic Google Charts rendering
// Uses persist keyword for automatic chart data save/restore across reboots
// Globals: 6 × 288 = 1728 slots (of dynamic max on ESP32)
// Uncomment to simulate sensor data (no CC1101 hardware needed)
//#define SIMULATE 1
#ifdef SIMULATE
#define STORE_INTERVAL 10 // fast: 10 sec between samples for testing
#endif
// Comment out to build the lean receiver without the 24h charts + .bin store
// (equivalent to the old bresser.tc). Defined = the full chart dashboard.
#define WITH_CHARTS 1
#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
#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
#define DATA_SIZE 26
#ifdef WITH_CHARTS
// Chart ring buffer: 288 points = 24h at 5-min intervals
#define NPTS 288
#ifndef SIMULATE
#define STORE_INTERVAL 300 // seconds between samples (5 min)
#endif
#endif
// Sensor data - weather station (live values)
global float train = 0.0; // total rain (mm) - auto-sent via UDP
global float rtemp = 0.0; // current temperature (C) - auto-sent via UDP
#ifdef WITH_CHARTS
global float avgt = 0.0; // average temperature over the day 0-24h (C) - UDP
#endif
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_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];
// Additional sensor data (e.g. from I2C light/UV sensors)
float br_lux = 0.0; // brightness in lux
float br_uvi = 0.0; // UV index
// Rain today (0-24h, since local midnight)
float rain_today = 0.0; // today's rain (mm) = br_rain - rain_base
persist int rain_day = -1; // calendar day-number (timeToSecs/86400) of rain_base
persist float rain_base = 0.0; // cumulative br_rain at the start of rain_day
persist float rain_prev = -1.0; // cumulative br_rain at the previous chart sample (for the per-interval delta; -1 = none yet)
#ifdef WITH_CHARTS
// Ring buffers for 24h charts (stored as native float values)
// Charted variables: temp, hum, windavg, rain, brightness, uvi
// Stored in a dedicated binary file (CHARTFILE) via save_chart()/load_chart(),
// NOT the .pvs persist store: 6x288 floats are too big to round-trip through
// saveVars() every sample. The file owns header[2]={h_pos,h_count} + 6 arrays.
float h_temp[NPTS];
float h_hum[NPTS];
float h_wavg[NPTS];
float h_rain[NPTS];
float h_lux[NPTS];
float h_uvi[NPTS];
int h_pos = 0; // ring buffer write position
int h_count = 0; // total samples stored (saturates at NPTS)
int sec_cnt = 0; // seconds counter for store interval
int chdr[3]; // file header: [magic, h_pos, h_count]
#endif
// Raw packet buffer and SPI buffer
char msg[DATA_SIZE];
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);
}
void cc_reset() {
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() {
cc_write_reg(REG_FREQ2, 0x21);
cc_write_reg(REG_FREQ1, 0x65);
cc_write_reg(REG_FREQ0, 0x6A);
cc_write_reg(REG_MDMCFG4, 0x68);
cc_write_reg(REG_MDMCFG3, 0x4B);
cc_write_reg(REG_MDMCFG2, 0x02);
cc_write_reg(REG_MDMCFG1, 0x22);
cc_write_reg(REG_MDMCFG0, 0xF8);
cc_write_reg(REG_DEVIATN, 0x51);
cc_write_reg(REG_FSCTRL1, 0x06);
cc_write_reg(REG_FSCTRL0, 0x00);
cc_write_reg(REG_SYNC1, 0xAA);
cc_write_reg(REG_SYNC0, 0x2D);
cc_write_reg(REG_PKTLEN, MSG_BUF_SIZE);
cc_write_reg(REG_PKTCTRL0, 0x00);
cc_write_reg(REG_PKTCTRL1, 0x00);
cc_write_reg(REG_IOCFG0, 0x06);
cc_write_reg(REG_IOCFG2, 0x2E);
cc_write_reg(REG_AGCCTRL2, 0x03);
cc_write_reg(REG_AGCCTRL1, 0x40);
cc_write_reg(REG_AGCCTRL0, 0x91);
cc_write_reg(REG_FOCCFG, 0x16);
cc_write_reg(REG_BSCFG, 0x6C);
cc_write_reg(REG_FREND1, 0x56);
cc_write_reg(REG_FREND0, 0x10);
cc_write_reg(REG_FSCAL3, 0xE9);
cc_write_reg(REG_FSCAL2, 0x2A);
cc_write_reg(REG_FSCAL1, 0x00);
cc_write_reg(REG_FSCAL0, 0x1F);
cc_write_reg(REG_TEST2, 0x81);
cc_write_reg(REG_TEST1, 0x35);
cc_write_reg(REG_TEST0, 0x09);
cc_write_reg(REG_MCSM2, 0x07);
cc_write_reg(REG_MCSM1, 0x3C);
cc_write_reg(REG_MCSM0, 0x18);
cc_write_reg(REG_PATABLE, 0xC0);
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);
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;
for (i = 0; i < 13; i++) {
if (((msg[i] ^ msg[i + 13]) & 0xFF) != 0xFF) {
return 0;
}
}
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; }
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;
br_hum = (msg[22] & 0x0F) + ((msg[22] >> 4) & 0x0F) * 10;
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;
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;
if (s_type == 4) {
soil_id = id_tmp;
soil_batt = batt_tmp;
soil_rssi = br_rssi;
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;
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;
}
br_id = id_tmp;
br_batt = batt_tmp;
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);
}
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;
}
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 ---
char msgw[DATA_SIZE];
int decode_7in1() {
int i;
for (i = 0; i < DATA_SIZE; i++) {
msgw[i] = msg[i] ^ 0xAA;
}
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; }
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 — 6 BCD digits (lux) in de-whitened bytes 17-19, stored as kLux
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 * 0.001; // kLux
// 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;
}
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;
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;
}
return 0;
}
// --- Message Dispatcher ---
int decode_message() {
int res;
res = decode_7in1();
if (res) return res;
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) {
cc_read_burst(REG_FIFO, MSG_BUF_SIZE);
br_rssi = cc_get_rssi();
cc_strobe(CMD_SIDLE);
cc_strobe(CMD_SFRX);
cc_strobe(CMD_SRX);
delay(1);
int i;
for (i = 0; i < DATA_SIZE; i++) {
msg[i] = spi[i + 2];
}
return decode_message();
}
return 0;
}
// --- Callbacks ---
#ifndef SIMULATE
void Every50ms() {
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;
}
}
#endif
#ifdef WITH_CHARTS
// --- Chart persistence: own a dedicated binary file (not .pvs) ---
// Layout: int header[2] = {h_pos, h_count}, then the 6 float[NPTS] arrays
// back to back. Size = 2*4 + 6*NPTS*4 bytes.
#define CHARTFILE "/bresser_chart.bin"
#define CHART_MAGIC 0x42430102 // bump when stored layout/units change -> old file ignored (0102: rain = per-interval delta, not accumulated)
void save_chart() {
int h = fileOpen(CHARTFILE, 1); // 1 = write (truncate)
if (h < 0) return;
chdr[0] = CHART_MAGIC; chdr[1] = h_pos; chdr[2] = h_count;
fileWriteBin(h, chdr, 3);
fileWriteBin(h, h_temp, NPTS);
fileWriteBin(h, h_hum, NPTS);
fileWriteBin(h, h_wavg, NPTS);
fileWriteBin(h, h_rain, NPTS);
fileWriteBin(h, h_lux, NPTS);
fileWriteBin(h, h_uvi, NPTS);
fileClose(h);
}
void load_chart() {
int want = 12 + 6 * NPTS * 4; // magic + 2 ints + 6 float arrays
if (fileSize(CHARTFILE) != want) return; // missing/old-format -> fresh start
int h = fileOpen(CHARTFILE, 0); // 0 = read
if (h < 0) return;
fileReadBin(h, chdr, 3);
if (chdr[0] != CHART_MAGIC) { fileClose(h); return; } // version bump -> ignore old data
h_pos = chdr[1]; h_count = chdr[2];
fileReadBin(h, h_temp, NPTS);
fileReadBin(h, h_hum, NPTS);
fileReadBin(h, h_wavg, NPTS);
fileReadBin(h, h_rain, NPTS);
fileReadBin(h, h_lux, NPTS);
fileReadBin(h, h_uvi, NPTS);
fileClose(h);
}
void store_sample() {
h_temp[h_pos] = br_temp;
h_hum[h_pos] = (float)br_hum;
h_wavg[h_pos] = br_wind_avg;
// rain chart = delta since the last sample (rainfall per interval), not the
// running total. rain_prev < 0 = first-ever sample -> 0 (no startup spike);
// br_rain < rain_prev = sensor counter reset -> clamp to 0.
float rd = 0.0;
if (rain_prev >= 0.0 && br_rain >= rain_prev) { rd = br_rain - rain_prev; }
h_rain[h_pos] = rd;
rain_prev = br_rain;
h_lux[h_pos] = br_lux;
h_uvi[h_pos] = br_uvi;
h_pos++;
if (h_pos >= NPTS) h_pos = 0;
if (h_count < NPTS) h_count++;
// Flush the chart arrays to their own binary file after every sample
save_chart();
}
#endif
int sim_tick = 0; // simulation counter
void EverySecond() {
#ifdef SIMULATE
// Generate fake sensor data with realistic variations
if (!br_ok) {
br_ok = 1;
br_id = 0x1234;
br_batt = 1;
soil_ok = 1;
soil_id = 0x5678;
soil_batt = 1;
}
sim_tick++;
br_temp = 18.0 + (float)(sim_tick % 60) * 0.1 - 3.0;
br_hum = 55 + (sim_tick % 30);
br_wind_avg = 1.0 + (float)(sim_tick % 20) * 0.2;
br_rain = 0.0 + (float)(sim_tick % 100) * 0.1;
br_lux = 5000.0 + (float)(sim_tick % 200) * 50.0;
br_uvi = (float)(sim_tick % 10) * 0.5;
br_rssi = -60.0;
br_cnt = sim_tick;
soil_temp = 12.0 + (float)(sim_tick % 40) * 0.05;
soil_moisture = 30 + (sim_tick % 40);
soil_cnt = sim_tick;
#endif
if (!br_ok) return;
// Auto-sent via UDP on assignment (global float)
rtemp = br_temp; // current temperature
train = br_rain; // total rain (cumulative mm)
// rain today (0-24h, since local midnight) — only once NTP time is valid
char rts[32]; timeStamp(rts);
int rday = timeToSecs(rts) / 86400;
if (rday > 18000) {
if (br_rain < rain_base) { rain_base = br_rain; } // sensor counter reset
if (rain_day != rday) { rain_base = br_rain; rain_day = rday; }
rain_today = br_rain - rain_base;
}
#ifdef WITH_CHARTS
// average temperature over the day 0-24h = mean of stored chart samples
float st = 0.0; int jj;
for (jj = 0; jj < h_count; jj++) { st = st + h_temp[jj]; }
if (h_count > 0) { avgt = st / (float)h_count; }
// Store first sample immediately so charts appear on first page load
if (h_count == 0) {
store_sample();
sec_cnt = 0;
return;
}
sec_cnt++;
if (sec_cnt >= STORE_INTERVAL) {
sec_cnt = 0;
store_sample();
}
#endif
}
#ifdef WITH_CHARTS
// WebChart(type, title, unit, color, pos, count, array, decimals, interval, ymin, ymax)
// type: 0=line, 1=column
// Empty title "" = add series to previous chart
// decimals = number of decimal places to display
// interval = minutes between samples (for X-axis time)
// ymin, ymax = Y-axis range (if ymin >= ymax → auto-scale)
void WebPage() {
if (h_count < 1) {
webSend("<p>Collecting data... Charts appear after first sample.</p>");
return;
}
// All charts in one fieldset "card frame". Match energy_dashboard's proven
// centering: WebChartSize(640,280) (width + margin:0 auto) PLUS ottelo's
// -30px wrapper that nudges the plot back to true-centre (the y-axis gutter).
WebChartSize(640, 280);
webSend("<fieldset style='border:1px solid rgba(150,150,150,.4);border-radius:8px;margin:8px 2px;padding:4px 9px 8px'><legend style='font-weight:bold;padding:0 6px'>Wetterverlauf 24h</legend><div style='margin-left:-20px'>");
WebChart(0, "Temperature (24h)", "°C", 0xe74c3c, h_pos, NPTS, h_temp, 1, 5, -20, 50);
WebChart(0, "Humidity (24h)", "%", 0x3498db, h_pos, NPTS, h_hum, 1, 5, 0, 100);
WebChart(0, "Wind Average (24h)","m/s", 0x27ae60, h_pos, NPTS, h_wavg, 1, 5, 0, 0);
WebChartJS("o.vAxis=o.vAxis||{};var mx=0;for(var r=0;r<dt.getNumberOfRows();r++){var v=dt.getValue(r,1);if(v>mx)mx=v;}o.vAxis.viewWindow={min:0,max:(mx<2?2:mx*1.15)};"); // wind>=0: floor 0, top=1.15x peak (>=2) so the axis never collapses to a negative default
WebChart(1, "Rain (24h)", "mm", 0x2980b9, h_pos, NPTS, h_rain, 1, 5, 0, 0); // type 1 = column (per-interval rainfall)
WebChart(0, "Brightness (24h)", "kLux", 0xf39c12, h_pos, NPTS, h_lux, 1, 5, 0, 0);
WebChart(0, "UV Index (24h)", "UVI", 0x9b59b6, h_pos, NPTS, h_uvi, 1, 5, 0, 12);
webSend("</div></fieldset>");
}
#endif
void WebCall() {
char out[80];
if (br_ok) {
webSend("{s}{m}{e}<tr><td colspan=2 style='background:#2980b9;color:#fff;padding:6px 10px;font-weight:bold;text-align:center;border-radius:4px'>Wetterstation</td></tr>");
sprintf(out, "{s}Bresser ID{m}0x%04X{e}", br_id);
webSend(out);
LGetString(0, br_lbl);
sprintf(out, "{s}%s{m}", br_lbl);
webSend(out);
sprintf(out, "%.1f °C{e}", br_temp);
webSend(out);
LGetString(1, br_lbl);
sprintf(out, "{s}%s{m}", br_lbl);
webSend(out);
sprintf(out, "%d %%{e}", br_hum);
webSend(out);
sprintf(out, "{s}Wind Avg{m}%.1f m/s{e}", br_wind_avg);
webSend(out);
sprintf(out, "{s}Wind Max{m}%.1f m/s{e}", br_wind_gust);
webSend(out);
sprintf(out, "{s}Wind Dir{m}%.0f°{e}", br_wind_dir);
webSend(out);
sprintf(out, "{s}Rain{m}%.1f mm{e}", br_rain);
webSend(out);
sprintf(out, "{s}Rain today{m}%.1f mm{e}", rain_today);
webSend(out);
LGetString(15, br_lbl);
sprintf(out, "{s}%s{m}", br_lbl);
webSend(out);
sprintf(out, "%.1f kLux{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}<span style='color:#27ae60;font-weight:bold'>OK</span>{e}"); }
else { webSend("{s}Battery{m}<span style='color:#e74c3c;font-weight:bold'>Low</span>{e}"); }
sprintf(out, "{s}Packets{m}%d{e}", br_cnt);
webSend(out);
#ifdef WITH_CHARTS
sprintf(out, "{s}Avg Temp 0-24h{m}%.1f °C{e}", avgt);
webSend(out);
#endif
} else {
webSend("{s}Bresser{m}waiting for data{e}");
}
if (soil_ok) {
webSend("{s}{m}{e}<tr><td colspan=2 style='background:#8e6b3f;color:#fff;padding:6px 10px;font-weight:bold;text-align:center;border-radius:4px'>Bodensensor</td></tr>");
sprintf(out, "{s}Soil ID{m}0x%04X{e}", soil_id);
webSend(out);
LGetString(0, br_lbl);
sprintf(out, "{s}Soil %s{m}", br_lbl);
webSend(out);
sprintf(out, "%.1f °C{e}", soil_temp);
webSend(out);
LGetString(17, br_lbl);
sprintf(out, "{s}Soil %s{m}", br_lbl);
webSend(out);
sprintf(out, "%d %%{e}", soil_moisture);
webSend(out);
if (soil_batt) { webSend("{s}Soil Battery{m}<span style='color:#27ae60;font-weight:bold'>OK</span>{e}"); }
else { webSend("{s}Soil Battery{m}<span style='color:#e74c3c;font-weight:bold'>Low</span>{e}"); }
sprintf(out, "{s}Soil RSSI{m}%.0f dBm{e}", soil_rssi);
webSend(out);
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, ",\"WindAvg\":%.1f", br_wind_avg);
responseAppend(out);
sprintf(out, ",\"Rain\":%.1f", br_rain);
responseAppend(out);
sprintf(out, ",\"kLux\":%.1f", 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, ",\"Batt\":%d", soil_batt);
responseAppend(out);
sprintf(out, ",\"Cnt\":%d}", soil_cnt);
responseAppend(out);
}
}
int main() {
#ifndef SIMULATE
spiInit(-1, -1, -1, SPI_MHZ);
gpioInit(GDO0_PIN, 0);
gpioInit(CS_PIN, 1);
cc_reset();
spiSetCS(1, CS_PIN);
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;
}
#else
printStr("SIMULATE mode — no CC1101 hardware\n");
#endif
// Init runtime state (not persisted)
br_ok = 0;
br_cnt = 0;
soil_ok = 0;
soil_cnt = 0;
#ifdef WITH_CHARTS
// Chart data + counters live in a dedicated binary file (not .pvs)
load_chart();
if (h_count > 0) {
char ibuf[64];
sprintf(ibuf, "Restored %d chart points from file\n", h_count);
printString(ibuf);
}
#endif
printStr("Bresser CC1101 ready\n");
#ifdef WITH_CHARTS
printStr("Charts: 6 variables, 288 pts each (5-min interval)\n");
#endif
return 0;
}