Pool sensor v2: VCC monitoring, database resilience, receiver improvements

- Added voltage monitoring table and storage pipeline
- Extended pool payload to 17 bytes with VCC field (protocol v2)
- Improved database connection pool resilience (reduced pool size, aggressive recycling, pool disposal on failure)
- Added environment variable support for database configuration
- Fixed receiver MQTT deprecation warning (CallbackAPIVersion.VERSION2)
- Silenced excessive RSSI status logging in receiver
- Added reset flag tracking and reporting
- Updated Docker compose with DB config and log rotation limits
This commit is contained in:
2026-01-25 11:25:15 +00:00
parent d1c1f63cb9
commit f55c1fe6f1
9 changed files with 1512 additions and 101 deletions

296
receiver_diy.py Normal file
View File

@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""
Minimal CC1101 GFSK receiver @868.3 MHz
Optimized for sensitivity: 1.2 kBaud, 5 kHz deviation, 58 kHz RX BW
Prints timestamp, RSSI, payload (hex) on each packet.
"""
import time
import struct
import json
from datetime import datetime, timezone
import spidev
import RPi.GPIO as GPIO
import paho.mqtt.client as mqtt
from cc1101_config import (
IOCFG2, IOCFG0, FIFOTHR, PKTLEN, PKTCTRL0, PKTCTRL1,
FSCTRL1, FREQ2, FREQ1, FREQ0, MDMCFG4, MDMCFG3, MDMCFG2, MDMCFG1, MDMCFG0,
DEVIATN, MCSM1, MCSM0, FOCCFG, AGCCTRL2, AGCCTRL1, AGCCTRL0, FREND0,
FSCAL3, FSCAL2, FSCAL1, FSCAL0,
SRX, SIDLE, SFRX, SRES, MARCSTATE, RXBYTES, calculate_freq_registers
)
DIY_FREQ_HZ = 868_300_000
DIY_SYNC1 = 0xD3
DIY_SYNC0 = 0x91
DIY_PACKET_LEN = 17 # Payload with VCC: 17 bytes (magic1, magic2, version, nodeId, seq, temps, hum, pres, vcc, crc)
class CC1101DIY:
def __init__(self, bus=0, device=0, gdo0_pin=25): # default BCM25 (pin 22)
self.spi = spidev.SpiDev()
self.spi.open(bus, device)
self.spi.max_speed_hz = 50000
self.spi.mode = 0
self.gdo0_pin = gdo0_pin
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.gdo0_pin, GPIO.IN)
print("CC1101 DIY receiver initialized")
def close(self):
self.spi.close()
GPIO.cleanup()
def send_strobe(self, strobe):
self.spi.xfer([strobe])
def reset(self):
self.send_strobe(SRES)
time.sleep(0.1)
def write_reg(self, addr, val):
self.spi.xfer([addr, val])
def read_status(self, addr):
return self.spi.xfer([addr | 0xC0, 0x00])[1]
def read_burst(self, addr, length):
return self.spi.xfer([addr | 0xC0] + [0x00] * length)[1:]
def verify_chip(self):
"""Verify CC1101 chip is present and responding correctly"""
# Read PARTNUM (should be 0x00 for CC1101)
partnum = self.read_status(0x30)
# Read VERSION (typically 0x04 or 0x14 for CC1101)
version = self.read_status(0x31)
print(f"CC1101 Chip Detection:")
print(f" PARTNUM: 0x{partnum:02X} (expected: 0x00)")
print(f" VERSION: 0x{version:02X} (expected: 0x04 or 0x14)")
if partnum != 0x00:
print(f"ERROR: Invalid PARTNUM. Expected 0x00, got 0x{partnum:02X}")
return False
if version not in (0x04, 0x14):
if version in (0x00, 0xFF):
print(f"ERROR: No chip detected (VERSION=0x{version:02X}). Check SPI wiring.")
else:
print(f"WARNING: Unexpected VERSION 0x{version:02X}, but proceeding...")
return False
print("✓ CC1101 chip detected and verified")
return True
def get_rssi(self):
rssi_dec = self.read_status(0x34)
return ((rssi_dec - 256) / 2 - 74) if rssi_dec >= 128 else (rssi_dec / 2 - 74)
def get_marc_state(self):
return self.read_status(MARCSTATE) & 0x1F
def get_rx_bytes(self):
return self.read_status(RXBYTES) & 0x7F
def flush_rx(self):
self.send_strobe(SFRX)
def enter_rx(self):
self.send_strobe(SRX)
def configure(self):
print("Configuring CC1101 for DIY GFSK @868.3 MHz (1.2kBaud, 5kHz dev, 58kHz BW)...")
freq_regs = calculate_freq_registers(DIY_FREQ_HZ)
# GPIO mapping
self.write_reg(IOCFG2, 0x2E) # GDO2 hi-Z (not wired)
self.write_reg(IOCFG0, 0x06) # GDO0: assert on sync word detected
# FIFO threshold - low to read quickly and prevent overflow
self.write_reg(FIFOTHR, 0x07) # Trigger at 32 bytes
# Packet control - fixed length, capture sync'd packets (17-byte payload including VCC)
self.write_reg(PKTLEN, DIY_PACKET_LEN)
self.write_reg(PKTCTRL1, 0x00) # Do NOT append status bytes (matches sender)
self.write_reg(PKTCTRL0, 0x00) # Fixed length, CRC disabled
# Frequency synthesizer
self.write_reg(FSCTRL1, 0x06) # IF frequency control (matches sender)
self.write_reg(FREQ2, freq_regs[FREQ2])
self.write_reg(FREQ1, freq_regs[FREQ1])
self.write_reg(FREQ0, freq_regs[FREQ0])
# Modem configuration for GFSK - sensitivity optimized
# MDMCFG4: CHANBW_E=3, CHANBW_M=3 → 58 kHz BW, DRATE_E=8
self.write_reg(MDMCFG4, 0xF8) # BW=58kHz, DRATE_E=8 (matches sender)
self.write_reg(MDMCFG3, 0x83) # DRATE_M=131 for 1.2 kBaud
self.write_reg(MDMCFG2, 0x12) # GFSK, 16/16 sync word, DC filter ON
self.write_reg(MDMCFG1, 0x22) # 4 preamble bytes, CHANSPC_E=2
self.write_reg(MDMCFG0, 0xF8) # CHANSPC_M=248
# Sync word configuration (0xD391 - matches sender)
self.write_reg(0x04, DIY_SYNC1) # SYNC1 = 0xD3
self.write_reg(0x05, DIY_SYNC0) # SYNC0 = 0x91
# Deviation - 5 kHz for narrow channel
# DEVIATN calculation: (5000 * 2^17) / 26MHz = 25.2 ≈ 0x15
self.write_reg(DEVIATN, 0x15) # ~5 kHz deviation
# State machine - auto-calibrate, stay in RX
self.write_reg(MCSM1, 0x3F) # CCA always, stay in RX after RX/TX
self.write_reg(MCSM0, 0x18) # Auto-calibrate from IDLE to RX/TX
# Frequency offset compensation - enable AFC for better lock
self.write_reg(FOCCFG, 0x1D) # FOC_BS_CS_GATE, FOC_PRE_K=3K, FOC_POST_K=K/2, FOC_LIMIT=BW/4
self.write_reg(0x1A, 0x1C) # BSCFG: Bit sync config (matches sender)
# AGC for GFSK sensitivity - match sender settings
self.write_reg(AGCCTRL2, 0xC7) # Max DVGA gain, target 42 dB (matches sender)
self.write_reg(AGCCTRL1, 0x00) # LNA priority, AGC relative threshold (matches sender)
self.write_reg(AGCCTRL0, 0xB0) # Medium hysteresis, 16 samples
# Front end - match sender configuration exactly
self.write_reg(0x21, 0xB6) # FREND1: PA current = max (matches sender)
self.write_reg(FREND0, 0x17) # FREND0: PA_POWER index = 7 (matches sender)
# Calibration
self.write_reg(FSCAL3, 0xE9)
self.write_reg(FSCAL2, 0x2A)
self.write_reg(FSCAL1, 0x00)
self.write_reg(FSCAL0, 0x1F)
print("Configuration applied")
def read_packet(self):
# Fixed-length: expect 17-byte payload (with VCC)
num_bytes = self.get_rx_bytes()
if self.read_status(RXBYTES) & 0x80:
self.flush_rx()
return None, False
if num_bytes >= DIY_PACKET_LEN:
data = self.read_burst(0x3F, DIY_PACKET_LEN)
return data, True
return None, False
def reverse_bits_byte(b):
"""Reverse bit order in a byte (MSB<->LSB)"""
result = 0
for i in range(8):
result = (result << 1) | ((b >> i) & 1)
return result
def main():
# Setup MQTT client
mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv311)
mqtt_host = "192.168.43.102"
mqtt_topic = "rtl_433/DietPi/events"
try:
mqtt_client.connect(mqtt_host, 1883, 60)
mqtt_client.loop_start()
print(f"Connected to MQTT broker at {mqtt_host}")
except Exception as e:
print(f"Warning: Could not connect to MQTT broker: {e}")
print("Continuing without MQTT...")
radio = CC1101DIY()
try:
radio.reset()
# Verify chip is correctly recognized
if not radio.verify_chip():
print("\nTroubleshooting:")
print(" 1. Check SPI is enabled: sudo raspi-config")
print(" 2. Verify wiring: MOSI, MISO, SCLK, CSN, GND, VCC")
print(" 3. Check power supply (3.3V, sufficient current)")
print(" 4. Try running with sudo for GPIO/SPI permissions")
return
radio.configure()
radio.enter_rx()
print("Listening... Press Ctrl+C to stop\n")
packet_count = 0
last_status = time.time()
while True:
payload, crc_ok = radio.read_packet()
if payload and len(payload) == DIY_PACKET_LEN:
fmt = '<BBBBHhhHHHB'
magic1, magic2, version, node_id, seq, t_ds10, t_bme10, hum10, pres10, vcc_mv, crc_rx = \
struct.unpack(fmt, bytes(payload))
# Only process packets with correct magic bytes
if magic1 != 0x42 or magic2 != 0x99:
continue
crc_expected = 0
for b in payload[:-1]:
crc_expected ^= b
crc_valid = (crc_expected == crc_rx)
packet_count += 1
rssi = radio.get_rssi()
ts = datetime.now(timezone.utc).isoformat()
# Convert payload to hex string
hex_data = ''.join(f'{b:02x}' for b in payload)
# Create MQTT message in rtl_433 format
mqtt_message = {
"time": ts,
"model": "pool",
"count": packet_count,
"num_rows": 1,
"rows": [{"len": len(payload) * 8, "data": hex_data}],
"codes": [f"{{{len(payload) * 8}}}{hex_data}"]
}
# Send to MQTT
try:
mqtt_client.publish(mqtt_topic, json.dumps(mqtt_message))
except Exception as e:
print(f" MQTT publish error: {e}")
print(f"[{packet_count}] {ts} | RSSI {rssi:.1f} dBm")
print(f" Magic: 0x{magic1:02X}{magic2:02X} | Ver: {version} | Node: {node_id} | Seq: {seq}")
print(f" DS18: {t_ds10/10.0:.1f}°C | BME: {t_bme10/10.0:.1f}°C")
print(f" Humidity: {hum10/10.0:.1f}% | Pressure: {pres10/10.0:.1f} hPa")
print(f" VCC: {vcc_mv} mV")
print(f" CRC: rx=0x{crc_rx:02X} expected=0x{crc_expected:02X} valid={crc_valid} (len {len(payload)} bytes)")
print(f" Raw: {hex_data}")
print(f" → MQTT sent to {mqtt_topic}")
# Status every 5s - only log errors/warnings
if time.time() - last_status >= 5:
state = radio.get_marc_state()
# Check if stuck (not in RX state) and recover
if state != 13: # 13 = RX
ts_status = datetime.now(timezone.utc).isoformat()
print(f"[{ts_status}] WARNING: Not in RX state ({state}) - recovering...")
radio.flush_rx()
radio.send_strobe(SIDLE)
time.sleep(0.01)
radio.enter_rx()
time.sleep(0.01) # Let RX stabilize
# Normal operation: silent (no status spam)
last_status = time.time()
time.sleep(0.0001) # Poll at 10kHz to catch all packets
except KeyboardInterrupt:
print("\nStopping...")
finally:
mqtt_client.loop_stop()
mqtt_client.disconnect()
radio.close()
if __name__ == "__main__":
main()