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:
296
receiver_diy.py
Normal file
296
receiver_diy.py
Normal 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()
|
||||
Reference in New Issue
Block a user