From 1fb786c2625db0c16b70b73efe5e9dbea4af3c5f Mon Sep 17 00:00:00 2001 From: serversdwn Date: Wed, 7 Jan 2026 03:42:26 +0000 Subject: [PATCH] Fix NL43 DRD field mapping to match official specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/models.py | 10 +-- app/routers.py | 10 +-- app/services.py | 47 +++++++------- migrate_revert_field_names.py | 116 ++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 32 deletions(-) create mode 100644 migrate_revert_field_names.py diff --git a/app/models.py b/app/models.py index df79233..ce454f9 100644 --- a/app/models.py +++ b/app/models.py @@ -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) diff --git a/app/routers.py b/app/routers.py index 2e8220f..e9c5006 100644 --- a/app/routers.py +++ b/app/routers.py @@ -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: diff --git a/app/services.py b/app/services.py index 24f99c5..b582748 100644 --- a/app/services.py +++ b/app/services.py @@ -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}") diff --git a/migrate_revert_field_names.py b/migrate_revert_field_names.py new file mode 100644 index 0000000..17a0124 --- /dev/null +++ b/migrate_revert_field_names.py @@ -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))