add altitude to table

This commit is contained in:
pablorevilla-meshtastic committed 2026-10-28 10:39:25 -07:00
1 parent 3e1d95ed2c
commit 93fc31efd4
6 files changed
+94

No files matched your search

@@ -0,0 +1,29 @@
"""add last altitude to node
Revision ID: 44612e74a798
Revises: 4d2a7c9e1b30
Create Date: 2026-10-28 10:37:11.340792
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '44612e74a798'
down_revision: str | None = '4d2a7c9e1b30'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
with op.batch_alter_table("node") as batch_op:
batch_op.add_column(sa.Column("last_altitude", sa.BigInteger(), nullable=True))
def downgrade() -> None:
with op.batch_alter_table("node") as batch_op:
batch_op.drop_column("last_altitude")
+7
View File
@@ -73,6 +73,7 @@ Response Example
"role": "client",
"last_lat": 377749000,
"last_long": -1224194000,
"last_altitude": 125,
"channel": "main",
"is_mqtt_gateway": true,
"first_seen_us": 1736283723456789,
@@ -82,6 +83,12 @@ Response Example
}
```
`last_altitude` is the latest reported altitude in meters above sea level, or `null`
when unknown. It updates with valid node locations from position packets and map reports.
Position packets without altitude preserve the previous value. Map reports lack altitude
field presence, so zero values in map reports also preserve the previous value.
Existing nodes receive altitude as new reports arrive.
---
## 2. Packets API
+1
View File
@@ -21,6 +21,7 @@ class Node(Base):
role: Mapped[str] = mapped_column(nullable=True)
last_lat: Mapped[int] = mapped_column(BigInteger, nullable=True)
last_long: Mapped[int] = mapped_column(BigInteger, nullable=True)
last_altitude: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
channel: Mapped[str] = mapped_column(nullable=True)
is_mqtt_gateway: Mapped[bool] = mapped_column(nullable=True)
first_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
+8
View File
@@ -224,6 +224,9 @@ async def process_envelope(topic, env):
if has_location:
node.last_lat = map_report.latitude_i
node.last_long = map_report.longitude_i
# MapReport has no presence flag for altitude.
if map_report.altitude != 0:
node.last_altitude = map_report.altitude
node.firmware = map_report.firmware_version
node.last_seen_us = now_us
if node.first_seen_us is None:
@@ -244,6 +247,9 @@ async def process_envelope(topic, env):
if has_location:
node.last_lat = map_report.latitude_i
node.last_long = map_report.longitude_i
# MapReport has no presence flag for altitude.
if map_report.altitude != 0:
node.last_altitude = map_report.altitude
session.add(node)
except Exception as e:
print(f"Error processing MAP_REPORT_APP: {e}")
@@ -427,6 +433,8 @@ async def process_envelope(topic, env):
now_us = int(time.time() * 1_000_000)
node.last_lat = position.latitude_i
node.last_long = position.longitude_i
if position.HasField("altitude"):
node.last_altitude = position.altitude
node.last_seen_us = now_us
if node.first_seen_us is None:
node.first_seen_us = now_us
+1
View File
@@ -276,6 +276,7 @@ async def api_nodes(request):
"role": n.role,
"last_lat": getattr(n, "last_lat", None),
"last_long": getattr(n, "last_long", None),
"last_altitude": n.last_altitude,
"channel": n.channel,
"is_mqtt_gateway": getattr(n, "is_mqtt_gateway", None),
# "last_update": n.last_update.isoformat(),
+48
View File
@@ -0,0 +1,48 @@
"""Persist reported altitudes without losing them when a report omits altitude."""
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from meshtastic.protobuf.mesh_pb2 import Position
from meshtastic.protobuf.mqtt_pb2 import MapReport, ServiceEnvelope
from meshtastic.protobuf.portnums_pb2 import PortNum
from meshview import mqtt_database, mqtt_store
from meshview.models import Base, Node
@pytest.mark.asyncio
@pytest.mark.parametrize("report_type", [Position, MapReport])
async def test_altitude_updates(monkeypatch, report_type):
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
sessions = async_sessionmaker(engine, expire_on_commit=False)
monkeypatch.setattr(mqtt_database, "async_session", sessions, raising=False)
monkeypatch.setattr(mqtt_store, "MQTT_GATEWAY_CACHE", set())
port = PortNum.POSITION_APP if report_type is Position else PortNum.MAP_REPORT_APP
try:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
cases = [
(None, 200000000, None),
(125, 200000000, 125),
(None, 200000000, 125),
(-30, 200000000, -30),
(900, 0, -30), # Invalid location must not update altitude.
(0, 200000000, 0 if report_type is Position else -30),
]
for packet_id, (altitude, coordinate, expected) in enumerate(cases, start=1):
report = report_type(latitude_i=coordinate, longitude_i=coordinate)
if altitude is not None:
report.altitude = altitude
env = ServiceEnvelope(gateway_id="!00000002", channel_id="test")
env.packet.id = packet_id
setattr(env.packet, "from", 123)
env.packet.decoded.portnum = port
env.packet.decoded.payload = report.SerializeToString()
await mqtt_store.process_envelope("test", env)
async with sessions() as session:
node = (await session.execute(select(Node).where(Node.node_id == 123))).scalar_one()
assert node.last_altitude == expected
assert node.to_dict()["last_altitude"] == expected
finally:
await engine.dispose()