Fix NL43 DRD field mapping to match official specification
Corrected the parsing of NL43 DRD (Dynamic Range Data) and DOD (Data On Demand) responses according to the NL43 Communications Guide. The previous implementation incorrectly mapped d0 (counter field) as a measurement. Changes: - Updated DRD/DOD parsing to skip d0 (counter: 1-600) - Correctly map d1-d5 to lp/leq/lmax/lmin/lpeak measurements - Added inline documentation referencing DRD format specification - Included database migration script to revert incorrect field names DRD format per NL43 spec: - d0 = counter (1-600) - NOT a measurement - d1 = Lp (instantaneous sound pressure level) - d2 = Leq (equivalent continuous sound level) - d3 = Lmax (maximum level) - d4 = Lmin (minimum level) - d5 = Lpeak (peak level) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,11 +29,11 @@ class NL43Status(Base):
|
||||
unit_id = Column(String, primary_key=True, index=True)
|
||||
last_seen = Column(DateTime, default=func.now())
|
||||
measurement_state = Column(String, default="unknown") # Measure/Stop
|
||||
lp = Column(String, nullable=True)
|
||||
leq = Column(String, nullable=True)
|
||||
lmax = Column(String, nullable=True)
|
||||
lmin = Column(String, nullable=True)
|
||||
lpeak = Column(String, nullable=True)
|
||||
lp = Column(String, nullable=True) # Instantaneous sound pressure level
|
||||
leq = Column(String, nullable=True) # Equivalent continuous sound level
|
||||
lmax = Column(String, nullable=True) # Maximum level
|
||||
lmin = Column(String, nullable=True) # Minimum level
|
||||
lpeak = Column(String, nullable=True) # Peak level
|
||||
battery_level = Column(String, nullable=True)
|
||||
power_source = Column(String, nullable=True)
|
||||
sd_remaining_mb = Column(String, nullable=True)
|
||||
|
||||
@@ -652,11 +652,11 @@ async def stream_live(websocket: WebSocket, unit_id: str):
|
||||
"unit_id": unit_id,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"measurement_state": snap.measurement_state,
|
||||
"lp": snap.lp,
|
||||
"leq": snap.leq,
|
||||
"lmax": snap.lmax,
|
||||
"lmin": snap.lmin,
|
||||
"lpeak": snap.lpeak,
|
||||
"lp": snap.lp, # Instantaneous sound pressure level
|
||||
"leq": snap.leq, # Equivalent continuous sound level
|
||||
"lmax": snap.lmax, # Maximum level
|
||||
"lmin": snap.lmin, # Minimum level
|
||||
"lpeak": snap.lpeak, # Peak level
|
||||
"raw_payload": snap.raw_payload,
|
||||
})
|
||||
except Exception as e:
|
||||
|
||||
@@ -25,11 +25,11 @@ logger = logging.getLogger(__name__)
|
||||
class NL43Snapshot:
|
||||
unit_id: str
|
||||
measurement_state: str = "unknown"
|
||||
lp: Optional[str] = None
|
||||
leq: Optional[str] = None
|
||||
lmax: Optional[str] = None
|
||||
lmin: Optional[str] = None
|
||||
lpeak: Optional[str] = None
|
||||
lp: Optional[str] = None # Instantaneous sound pressure level
|
||||
leq: Optional[str] = None # Equivalent continuous sound level
|
||||
lmax: Optional[str] = None # Maximum level
|
||||
lmin: Optional[str] = None # Minimum level
|
||||
lpeak: Optional[str] = None # Peak level
|
||||
battery_level: Optional[str] = None
|
||||
power_source: Optional[str] = None
|
||||
sd_remaining_mb: Optional[str] = None
|
||||
@@ -188,19 +188,20 @@ class NL43Client:
|
||||
|
||||
snap = NL43Snapshot(unit_id="", raw_payload=resp, measurement_state="Measure")
|
||||
|
||||
# Parse known positions (based on NL43 communication guide)
|
||||
# DOD format: Main Lp, Main Leq, Main LE, Main Lmax, Main Lmin, LN1-5, Lpeak, LIeq, Leq,mov, Ltm5, flags...
|
||||
# Parse known positions (based on NL43 communication guide - DRD format)
|
||||
# DRD format: d0=counter, d1=Lp, d2=Leq, d3=Lmax, d4=Lmin, d5=Lpeak, d6=LIeq, ...
|
||||
try:
|
||||
if len(parts) >= 1:
|
||||
snap.lp = parts[0]
|
||||
# Skip d0 (counter) - start from d1
|
||||
if len(parts) >= 2:
|
||||
snap.leq = parts[1]
|
||||
snap.lp = parts[1] # d1: Instantaneous sound pressure level
|
||||
if len(parts) >= 3:
|
||||
snap.leq = parts[2] # d2: Equivalent continuous sound level
|
||||
if len(parts) >= 4:
|
||||
snap.lmax = parts[3]
|
||||
snap.lmax = parts[3] # d3: Maximum level
|
||||
if len(parts) >= 5:
|
||||
snap.lmin = parts[4]
|
||||
if len(parts) >= 11:
|
||||
snap.lpeak = parts[10]
|
||||
snap.lmin = parts[4] # d4: Minimum level
|
||||
if len(parts) >= 6:
|
||||
snap.lpeak = parts[5] # d5: Peak level
|
||||
except (IndexError, ValueError) as e:
|
||||
logger.warning(f"Error parsing DOD data points: {e}")
|
||||
|
||||
@@ -439,18 +440,20 @@ class NL43Client:
|
||||
|
||||
snap = NL43Snapshot(unit_id="", raw_payload=line, measurement_state="Measure")
|
||||
|
||||
# Parse known positions
|
||||
# Parse known positions (DRD format - same as DOD)
|
||||
# DRD format: d0=counter, d1=Lp, d2=Leq, d3=Lmax, d4=Lmin, d5=Lpeak, d6=LIeq, ...
|
||||
try:
|
||||
if len(parts) >= 1:
|
||||
snap.lp = parts[0]
|
||||
# Skip d0 (counter) - start from d1
|
||||
if len(parts) >= 2:
|
||||
snap.leq = parts[1]
|
||||
snap.lp = parts[1] # d1: Instantaneous sound pressure level
|
||||
if len(parts) >= 3:
|
||||
snap.leq = parts[2] # d2: Equivalent continuous sound level
|
||||
if len(parts) >= 4:
|
||||
snap.lmax = parts[3]
|
||||
snap.lmax = parts[3] # d3: Maximum level
|
||||
if len(parts) >= 5:
|
||||
snap.lmin = parts[4]
|
||||
if len(parts) >= 11:
|
||||
snap.lpeak = parts[10]
|
||||
snap.lmin = parts[4] # d4: Minimum level
|
||||
if len(parts) >= 6:
|
||||
snap.lpeak = parts[5] # d5: Peak level
|
||||
except (IndexError, ValueError) as e:
|
||||
logger.warning(f"Error parsing DRD data points: {e}")
|
||||
|
||||
|
||||
116
migrate_revert_field_names.py
Normal file
116
migrate_revert_field_names.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to revert NL43 measurement field names back to correct DRD format.
|
||||
|
||||
The previous migration was incorrect. According to NL43 DRD documentation:
|
||||
- d0 = counter (1-600) - NOT a measurement!
|
||||
- d1 = Lp (instantaneous sound pressure level)
|
||||
- d2 = Leq (equivalent continuous sound level)
|
||||
- d3 = Lmax (maximum level)
|
||||
- d4 = Lmin (minimum level)
|
||||
- d5 = Lpeak (peak level)
|
||||
|
||||
Changes:
|
||||
- laeq -> lp (was incorrectly mapped to counter field!)
|
||||
- lae -> leq
|
||||
- lasmax -> lmax
|
||||
- lasmin -> lmin
|
||||
- lapeak -> lpeak
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def migrate_database(db_path: str):
|
||||
"""Revert database schema to correct DRD field names."""
|
||||
|
||||
print(f"Reverting database migration: {db_path}")
|
||||
|
||||
# Connect to database
|
||||
conn = sqlite3.connect(db_path)
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if migration is needed
|
||||
cur.execute("PRAGMA table_info(nl43_status)")
|
||||
columns = [row[1] for row in cur.fetchall()]
|
||||
|
||||
if 'lp' in columns:
|
||||
print("✓ Database already has correct field names")
|
||||
return
|
||||
|
||||
if 'laeq' not in columns:
|
||||
print("✗ Database schema does not match expected format")
|
||||
sys.exit(1)
|
||||
|
||||
print("Starting revert migration...")
|
||||
|
||||
# Create new table with correct column names
|
||||
cur.execute("""
|
||||
CREATE TABLE nl43_status_new (
|
||||
unit_id VARCHAR PRIMARY KEY,
|
||||
last_seen DATETIME,
|
||||
measurement_state VARCHAR,
|
||||
lp VARCHAR,
|
||||
leq VARCHAR,
|
||||
lmax VARCHAR,
|
||||
lmin VARCHAR,
|
||||
lpeak VARCHAR,
|
||||
battery_level VARCHAR,
|
||||
power_source VARCHAR,
|
||||
sd_remaining_mb VARCHAR,
|
||||
sd_free_ratio VARCHAR,
|
||||
raw_payload TEXT
|
||||
)
|
||||
""")
|
||||
print("✓ Created new table with correct DRD field names")
|
||||
|
||||
# Copy data from old table to new table
|
||||
# Note: laeq was incorrectly mapped to d0 (counter), so we discard it
|
||||
# The actual measurements start from d1
|
||||
cur.execute("""
|
||||
INSERT INTO nl43_status_new
|
||||
(unit_id, last_seen, measurement_state, lp, leq, lmax, lmin, lpeak,
|
||||
battery_level, power_source, sd_remaining_mb, sd_free_ratio, raw_payload)
|
||||
SELECT
|
||||
unit_id, last_seen, measurement_state, lae, lasmax, lasmin, lapeak, NULL,
|
||||
battery_level, power_source, sd_remaining_mb, sd_free_ratio, raw_payload
|
||||
FROM nl43_status
|
||||
""")
|
||||
rows_copied = cur.rowcount
|
||||
print(f"✓ Copied {rows_copied} rows (note: discarded incorrect 'laeq' counter field)")
|
||||
|
||||
# Drop old table
|
||||
cur.execute("DROP TABLE nl43_status")
|
||||
print("✓ Dropped old table")
|
||||
|
||||
# Rename new table
|
||||
cur.execute("ALTER TABLE nl43_status_new RENAME TO nl43_status")
|
||||
print("✓ Renamed new table to nl43_status")
|
||||
|
||||
# Commit changes
|
||||
conn.commit()
|
||||
print("✓ Revert migration completed successfully")
|
||||
print("\nNote: The 'lp' field will be populated correctly on next device measurement")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"✗ Migration failed: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Default database path
|
||||
db_path = Path(__file__).parent / "data" / "slmm.db"
|
||||
|
||||
# Allow custom path as command line argument
|
||||
if len(sys.argv) > 1:
|
||||
db_path = Path(sys.argv[1])
|
||||
|
||||
if not db_path.exists():
|
||||
print(f"✗ Database not found: {db_path}")
|
||||
sys.exit(1)
|
||||
|
||||
migrate_database(str(db_path))
|
||||
Reference in New Issue
Block a user