Skip to content

Commit bd11863

Browse files
committed
refactor and add functionality
1 parent eb792cf commit bd11863

8 files changed

Lines changed: 716 additions & 190 deletions

File tree

README.md

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,63 @@
1-
2-
31
# meshdb
42

53
A lightweight Python library for storing Meshtastic node, telemetry, and message data in per-node SQLite databases.
64

7-
## Quick Start
5+
## Installation
6+
7+
Install from source within a virtual environment:
8+
9+
```bash
10+
git clone https://github.com/pdxlocations/meshdb.git
11+
cd meshdb
12+
pip install -e .
13+
```
14+
15+
Or install via Poetry:
16+
17+
```bash
18+
poetry install
19+
```
20+
21+
## Quick Usage Example
22+
23+
Set a default database path and handle incoming packets:
824

925
```python
1026
from meshdb import handle_packet, set_default_db_path
1127

1228
set_default_db_path("./data")
1329

14-
# example packet (decoded from Meshtastic interface)
1530
packet = {
1631
"from": 12345678,
1732
"rxTime": 1700000000,
18-
"decoded": {"portnum": "NODEINFO_APP", "user": {"longName": "TestNode"}}
33+
"decoded": {
34+
"portnum": "NODEINFO_APP",
35+
"user": {"longName": "TestNode"}
36+
}
1937
}
2038

21-
handle_packet(packet, owner_node_num=12345678)
39+
handle_packet(packet, node_database_number=12345678)
2240
```
2341

24-
Run `python -m meshdb` to view all known nodes and their latest telemetry in JSON format.
42+
## Viewing Stored Data
43+
44+
You can run:
45+
46+
```bash
47+
python -m meshdb --db ./data
48+
```
49+
50+
This prints a JSON summary of all known nodes and their latest telemetry, if available.
51+
52+
## Lookups in Code
53+
54+
```python
55+
import meshdb
56+
57+
node = meshdb.get_node(12345678, owner_node_num=12345678)
58+
battery = meshdb.get_node_metric("TestNode", "battery_level", owner_node_num=12345678)
59+
```
60+
61+
## Project Status
62+
63+
Early development. Schema and API changes may occur.

examples/basic-integration.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

examples/meshdb_demo.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import os
2+
import time
3+
import json
4+
from typing import List
5+
from pubsub import pub
6+
7+
import meshtastic.serial_interface
8+
import meshdb
9+
10+
"""
11+
Basic MeshDB integration demo
12+
=============================
13+
14+
How to customize:
15+
1) Set DB_BASE to where you want your per-owner DB files written.
16+
2) Edit TARGETS to the nodes you want to query (id/hex suffix/short/long/numeric).
17+
3) Run the script while a Meshtastic device is attached over USB/serial.
18+
19+
What this demo shows:
20+
- Resolving node numbers from many identifier forms (hex suffix, names, numeric)
21+
- Pretty-printing a full node snapshot (nodeinfo + latest telemetry + latest position)
22+
- Fetching a single metric (e.g., battery_level) via a convenience helper
23+
- Live packet handling that persists NODEINFO/POSITION/TELEMETRY/TEXT_MESSAGE
24+
- Auto-syncing the device's NodeDB into your local SQL DB on connect
25+
"""
26+
27+
# -----------------------------
28+
# 1) Easy knobs
29+
# -----------------------------
30+
# Where to store DB files. Examples:
31+
# "~/meshdb_data" → creates <owner>.db files in that directory
32+
# "./mesh.sqlite3" → creates mesh.<owner>.sqlite3 alongside this script
33+
# None or "" → current working directory
34+
DB_BASE = os.environ.get("MESHDB_BASE", "~/Meshtastic/github/pdxlocations/meshdb/")
35+
36+
# Nodes to query for examples. You can use any of:
37+
# - "!deadbeef" (full hex id)
38+
# - "deadbeef" or "1adc" (hex suffix)
39+
# - "FONE" (short name)
40+
# - "New Phone Who Dis?" (long name)
41+
# - 4062650989 (node number)
42+
TARGETS: List[object] = [
43+
"Meshtastic 1adc", # free text with hex suffix
44+
"1adc", # hex suffix
45+
"SenseRAT", # long name
46+
"FONE", # short name
47+
]
48+
49+
# A single metric to fetch for demonstration
50+
DEMO_METRIC = os.environ.get("MESHDB_METRIC", "hw_model") # e.g., "battery_level"
51+
52+
# -----------------------------
53+
# 2) Configure MeshDB base path
54+
# -----------------------------
55+
meshdb.set_default_db_path(DB_BASE)
56+
print(f"[meshdb] DB base set to: {DB_BASE}")
57+
58+
# -----------------------------
59+
# 3) Connect to device (Serial)
60+
# -----------------------------
61+
# You can also use meshtastic.tcp_interface.TCPInterface(hostname="127.0.0.1:4403")
62+
interface = meshtastic.serial_interface.SerialInterface()
63+
64+
# Resolve connected device node number AND sync its NodeDB into our local DB
65+
CONNECTED_NODE_NUM = meshdb.get_connected_device_node_num(interface)
66+
if CONNECTED_NODE_NUM is None:
67+
print("[meshdb] Warning: Could not resolve connected device node number; falling back to 0.")
68+
CONNECTED_NODE_NUM = 0
69+
else:
70+
print(f"[meshdb] Connected device node number: {CONNECTED_NODE_NUM}")
71+
72+
# -----------------------------
73+
# 4) Helper to pretty print JSON
74+
# -----------------------------
75+
76+
77+
def jprint(obj):
78+
print(json.dumps(obj, indent=2, sort_keys=True, ensure_ascii=False))
79+
80+
81+
# -----------------------------
82+
# 5) Show resolution examples
83+
# -----------------------------
84+
print("\n=== Node number resolution examples ===")
85+
for ident in TARGETS:
86+
resolved = meshdb.get_node_num(ident, owner_node_num=CONNECTED_NODE_NUM)
87+
print(f"identifier= {ident!r} → node_num= {resolved}")
88+
89+
# -----------------------------
90+
# 6) Show full snapshots for targets
91+
# -----------------------------
92+
print("\n=== Full node snapshots (nodeinfo + latest telemetry + latest position) ===")
93+
for ident in TARGETS:
94+
snap = meshdb.get_node(ident, owner_node_num=CONNECTED_NODE_NUM)
95+
if snap is None:
96+
print(f"snapshot for {ident!r}: NOT FOUND")
97+
else:
98+
print(f"\n# snapshot for {ident!r}")
99+
jprint(snap)
100+
101+
# -----------------------------
102+
# 7) Show a single metric value (first-found subtype priority)
103+
# -----------------------------
104+
print("\n=== Single metric lookup ===")
105+
for ident in TARGETS:
106+
val = meshdb.get_node_metric(ident, DEMO_METRIC, owner_node_num=CONNECTED_NODE_NUM)
107+
print(f"{DEMO_METRIC} for {ident!r}{val}")
108+
109+
# Bonus: show a NodeInfo field via metric helper
110+
111+
val = meshdb.get_node_metric("SenseRAT", "hw_model", owner_node_num=CONNECTED_NODE_NUM)
112+
print(f"hw_model for 'SenseRAT' → {val}")
113+
114+
# -----------------------------
115+
# 8) Live receive: store packets and optionally observe
116+
# -----------------------------
117+
118+
119+
def on_receive(packet=None, interface=None):
120+
"""Store NODEINFO/POSITION/TELEMETRY/TEXT_MESSAGE into the DB automatically."""
121+
try:
122+
result = meshdb.handle_packet(packet, node_database_number=CONNECTED_NODE_NUM)
123+
124+
# Example: derive readable sender names
125+
sender = packet.get("from")
126+
ln = meshdb.get_long_name(sender, node_database_number=CONNECTED_NODE_NUM)
127+
sn = meshdb.get_short_name(sender, node_database_number=CONNECTED_NODE_NUM)
128+
print(f"saved={result} from={sender} long='{ln}' short='{sn}' port={packet.get('decoded',{}).get('portnum')}")
129+
130+
except Exception as e:
131+
print(f"on_receive error: {e}")
132+
133+
134+
# Hook PubSub topic and idle forever
135+
pub.subscribe(on_receive, "meshtastic.receive")
136+
print("\n[meshdb] Listening for packets… (Ctrl+C to exit)")
137+
while True:
138+
time.sleep(1)

meshdb/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@
88
LocationDB,
99
TelemetryDB,
1010
)
11+
12+
from .db_lookup import (
13+
get_node_num,
14+
get_nodeinfo,
15+
get_node,
16+
get_node_metric,
17+
)

meshdb/__main__.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ def _fmt_ts(ts: Optional[int]) -> str:
3434
return str(ts)
3535

3636

37-
def _latest_location(ldb: LocationDB, node_id: int) -> Optional[Dict[str, Any]]:
37+
def _latest_location(ldb: LocationDB, node_num: int) -> Optional[Dict[str, Any]]:
3838
with ldb.connect() as con:
3939
cur = con.cursor()
4040
cur.execute(
4141
f"SELECT timestamp, latitude, longitude, altitude, location_source FROM {ldb.table} "
42-
"WHERE node_id = ? ORDER BY timestamp DESC LIMIT 1",
43-
(node_id,),
42+
"WHERE node_num = ? ORDER BY timestamp DESC LIMIT 1",
43+
(node_num,),
4444
)
4545
row = cur.fetchone()
4646
if not row:
@@ -54,13 +54,13 @@ def _latest_location(ldb: LocationDB, node_id: int) -> Optional[Dict[str, Any]]:
5454
}
5555

5656

57-
def _latest_device_telemetry(tdb: TelemetryDB, node_id: int) -> Optional[Dict[str, Any]]:
57+
def _latest_device_telemetry(tdb: TelemetryDB, node_num: int) -> Optional[Dict[str, Any]]:
5858
with tdb.connect() as con:
5959
cur = con.cursor()
6060
cur.execute(
6161
f"SELECT timestamp, battery_level, voltage, channel_utilization, air_util_tx, uptime_seconds "
62-
f"FROM {tdb.table_device} WHERE node_id = ? ORDER BY timestamp DESC LIMIT 1",
63-
(node_id,),
62+
f"FROM {tdb.table_device} WHERE node_num = ? ORDER BY timestamp DESC LIMIT 1",
63+
(node_num,),
6464
)
6565
row = cur.fetchone()
6666
if not row:
@@ -75,13 +75,13 @@ def _latest_device_telemetry(tdb: TelemetryDB, node_id: int) -> Optional[Dict[st
7575
}
7676

7777

78-
def _latest_power_telemetry(tdb: TelemetryDB, node_id: int) -> Optional[Dict[str, Any]]:
78+
def _latest_power_telemetry(tdb: TelemetryDB, node_num: int) -> Optional[Dict[str, Any]]:
7979
with tdb.connect() as con:
8080
cur = con.cursor()
8181
cur.execute(
8282
f"SELECT timestamp, ch1_voltage, ch1_current, ch2_voltage, ch2_current, ch3_voltage, ch3_current "
83-
f"FROM {tdb.table_power} WHERE node_id = ? ORDER BY timestamp DESC LIMIT 1",
84-
(node_id,),
83+
f"FROM {tdb.table_power} WHERE node_num = ? ORDER BY timestamp DESC LIMIT 1",
84+
(node_num,),
8585
)
8686
row = cur.fetchone()
8787
if not row:
@@ -122,7 +122,7 @@ def _infer_owner_candidates(db_hint: str | None) -> list[int]:
122122
return sorted(out)
123123

124124

125-
def main() -> None:
125+
def start() -> None:
126126
db_base = os.environ.get("MESHTASTIC_DB")
127127
set_default_db_path(db_base)
128128

@@ -157,7 +157,7 @@ def main() -> None:
157157
with ndb.connect() as con:
158158
cur = con.cursor()
159159
cur.execute(
160-
f"SELECT node_id, long_name, short_name, last_heard, hops_away, snr FROM {ndb.table} "
160+
f"SELECT node_num, long_name, short_name, last_heard, hops_away, snr FROM {ndb.table} "
161161
"ORDER BY (last_heard IS NULL), last_heard DESC"
162162
)
163163
rows = cur.fetchall()
@@ -167,15 +167,15 @@ def main() -> None:
167167
return
168168

169169
node_list = []
170-
for node_id, long_name, short_name, last_heard, hops_away, snr in rows:
171-
uid = int(node_id) if isinstance(node_id, (int, str)) else node_id
170+
for node_num, long_name, short_name, last_heard, hops_away, snr in rows:
171+
uid = int(node_num) if isinstance(node_num, (int, str)) else node_num
172172
name_long = long_name or ndb.get_name(uid, "long")
173173
name_short = short_name or ndb.get_name(uid, "short")
174174
loc = _latest_location(ldb, uid)
175175
dev = _latest_device_telemetry(tdb, uid)
176176
pwr = _latest_power_telemetry(tdb, uid)
177177
node_data = {
178-
"node_id": uid,
178+
"node_num": uid,
179179
"long_name": name_long,
180180
"short_name": name_short,
181181
"last_heard": last_heard,
@@ -193,7 +193,7 @@ def main() -> None:
193193
cur = con.cursor()
194194
# Environment telemetry
195195
cur.execute(
196-
f"SELECT * FROM {tdb.table_environment} WHERE node_id = ? ORDER BY timestamp DESC LIMIT 1", (uid,)
196+
f"SELECT * FROM {tdb.table_environment} WHERE node_num = ? ORDER BY timestamp DESC LIMIT 1", (uid,)
197197
)
198198
env_row = cur.fetchone()
199199
if env_row:
@@ -202,22 +202,22 @@ def main() -> None:
202202

203203
# Air quality telemetry
204204
cur.execute(
205-
f"SELECT * FROM {tdb.table_air_quality} WHERE node_id = ? ORDER BY timestamp DESC LIMIT 1", (uid,)
205+
f"SELECT * FROM {tdb.table_air_quality} WHERE node_num = ? ORDER BY timestamp DESC LIMIT 1", (uid,)
206206
)
207207
aq_row = cur.fetchone()
208208
if aq_row:
209209
columns = [d[0] for d in cur.description]
210210
node_data["telemetry_air_quality"] = dict(zip(columns, aq_row))
211211

212212
# Health telemetry
213-
cur.execute(f"SELECT * FROM {tdb.table_health} WHERE node_id = ? ORDER BY timestamp DESC LIMIT 1", (uid,))
213+
cur.execute(f"SELECT * FROM {tdb.table_health} WHERE node_num = ? ORDER BY timestamp DESC LIMIT 1", (uid,))
214214
health_row = cur.fetchone()
215215
if health_row:
216216
columns = [d[0] for d in cur.description]
217217
node_data["telemetry_health"] = dict(zip(columns, health_row))
218218

219219
# Host telemetry
220-
cur.execute(f"SELECT * FROM {tdb.table_host} WHERE node_id = ? ORDER BY timestamp DESC LIMIT 1", (uid,))
220+
cur.execute(f"SELECT * FROM {tdb.table_host} WHERE node_num = ? ORDER BY timestamp DESC LIMIT 1", (uid,))
221221
host_row = cur.fetchone()
222222
if host_row:
223223
columns = [d[0] for d in cur.description]
@@ -228,4 +228,4 @@ def main() -> None:
228228

229229

230230
if __name__ == "__main__":
231-
main()
231+
start()

0 commit comments

Comments
 (0)