storage.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. from __future__ import annotations
  2. import os
  3. import sqlite3
  4. import threading
  5. import time
  6. from dataclasses import dataclass
  7. from pathlib import Path
  8. from typing import Dict, List, Optional, Tuple
  9. @dataclass(frozen=True)
  10. class RouteInfo:
  11. id: int
  12. name: str
  13. created_at: int
  14. finished_at: Optional[int]
  15. points: int
  16. hazards: int
  17. active: bool
  18. @dataclass(frozen=True)
  19. class RoutePoint:
  20. seq: int
  21. t: int
  22. lat: float
  23. lon: float
  24. speed_mps: float
  25. cum_dist_m: float
  26. @dataclass(frozen=True)
  27. class Hazard:
  28. id: int
  29. route_id: int
  30. seq: int
  31. t: int
  32. lat: float
  33. lon: float
  34. cum_dist_m: float
  35. type: str
  36. note: str
  37. class NavDB:
  38. def __init__(self, db_path: str):
  39. self.db_path = db_path
  40. os.makedirs(os.path.dirname(db_path), exist_ok=True)
  41. self._lock = threading.Lock()
  42. self._conn = sqlite3.connect(db_path, check_same_thread=False)
  43. self._conn.row_factory = sqlite3.Row
  44. self._init_schema()
  45. @classmethod
  46. def default(cls) -> "NavDB":
  47. # .../main/xiaozhi-server/plugins_func/nav_copilot/storage.py -> parents[2] == .../main/xiaozhi-server
  48. base = Path(__file__).resolve().parents[2]
  49. return cls(str(base / "data" / "nav_copilot.sqlite3"))
  50. def _init_schema(self) -> None:
  51. with self._lock:
  52. cur = self._conn.cursor()
  53. cur.execute(
  54. """
  55. CREATE TABLE IF NOT EXISTS routes (
  56. id INTEGER PRIMARY KEY AUTOINCREMENT,
  57. name TEXT NOT NULL,
  58. created_at INTEGER NOT NULL,
  59. finished_at INTEGER,
  60. active INTEGER NOT NULL DEFAULT 0
  61. )
  62. """
  63. )
  64. cur.execute(
  65. """
  66. CREATE TABLE IF NOT EXISTS route_points (
  67. route_id INTEGER NOT NULL,
  68. seq INTEGER NOT NULL,
  69. t INTEGER NOT NULL,
  70. lat REAL NOT NULL,
  71. lon REAL NOT NULL,
  72. speed_mps REAL NOT NULL,
  73. cum_dist_m REAL NOT NULL,
  74. PRIMARY KEY(route_id, seq)
  75. )
  76. """
  77. )
  78. cur.execute(
  79. """
  80. CREATE TABLE IF NOT EXISTS hazards (
  81. id INTEGER PRIMARY KEY AUTOINCREMENT,
  82. route_id INTEGER NOT NULL,
  83. seq INTEGER NOT NULL,
  84. t INTEGER NOT NULL,
  85. lat REAL NOT NULL,
  86. lon REAL NOT NULL,
  87. cum_dist_m REAL NOT NULL,
  88. type TEXT NOT NULL,
  89. note TEXT NOT NULL DEFAULT ''
  90. )
  91. """
  92. )
  93. self._conn.commit()
  94. def create_route(self, name: str) -> int:
  95. now = int(time.time())
  96. with self._lock:
  97. self._conn.execute("UPDATE routes SET active=0")
  98. cur = self._conn.execute(
  99. "INSERT INTO routes(name, created_at, active) VALUES(?, ?, 1)",
  100. (name, now),
  101. )
  102. self._conn.commit()
  103. return int(cur.lastrowid)
  104. def finish_route(self, route_id: int) -> None:
  105. now = int(time.time())
  106. with self._lock:
  107. self._conn.execute(
  108. "UPDATE routes SET finished_at=? WHERE id=?",
  109. (now, route_id),
  110. )
  111. self._conn.commit()
  112. def set_active_route(self, route_id: int) -> None:
  113. with self._lock:
  114. self._conn.execute("UPDATE routes SET active=0")
  115. self._conn.execute("UPDATE routes SET active=1 WHERE id=?", (route_id,))
  116. self._conn.commit()
  117. def get_active_route_id(self) -> Optional[int]:
  118. with self._lock:
  119. row = self._conn.execute(
  120. "SELECT id FROM routes WHERE active=1 ORDER BY id DESC LIMIT 1"
  121. ).fetchone()
  122. return int(row["id"]) if row else None
  123. def add_point(
  124. self,
  125. *,
  126. route_id: int,
  127. seq: int,
  128. t: int,
  129. lat: float,
  130. lon: float,
  131. speed_mps: float,
  132. cum_dist_m: float,
  133. ) -> None:
  134. with self._lock:
  135. self._conn.execute(
  136. """
  137. INSERT OR REPLACE INTO route_points(route_id, seq, t, lat, lon, speed_mps, cum_dist_m)
  138. VALUES(?, ?, ?, ?, ?, ?, ?)
  139. """,
  140. (route_id, seq, t, lat, lon, speed_mps, cum_dist_m),
  141. )
  142. self._conn.commit()
  143. def add_hazard(
  144. self,
  145. *,
  146. route_id: int,
  147. seq: int,
  148. t: int,
  149. lat: float,
  150. lon: float,
  151. cum_dist_m: float,
  152. hazard_type: str,
  153. note: str = "",
  154. ) -> int:
  155. with self._lock:
  156. cur = self._conn.execute(
  157. """
  158. INSERT INTO hazards(route_id, seq, t, lat, lon, cum_dist_m, type, note)
  159. VALUES(?, ?, ?, ?, ?, ?, ?, ?)
  160. """,
  161. (route_id, seq, t, lat, lon, cum_dist_m, hazard_type, note or ""),
  162. )
  163. self._conn.commit()
  164. return int(cur.lastrowid)
  165. def list_routes(self, limit: int = 20) -> List[RouteInfo]:
  166. with self._lock:
  167. rows = self._conn.execute(
  168. """
  169. SELECT
  170. r.id, r.name, r.created_at, r.finished_at, r.active,
  171. (SELECT COUNT(1) FROM route_points p WHERE p.route_id=r.id) AS points,
  172. (SELECT COUNT(1) FROM hazards h WHERE h.route_id=r.id) AS hazards
  173. FROM routes r
  174. ORDER BY r.id DESC
  175. LIMIT ?
  176. """,
  177. (int(limit),),
  178. ).fetchall()
  179. return [
  180. RouteInfo(
  181. id=int(row["id"]),
  182. name=str(row["name"]),
  183. created_at=int(row["created_at"]),
  184. finished_at=int(row["finished_at"]) if row["finished_at"] else None,
  185. points=int(row["points"]),
  186. hazards=int(row["hazards"]),
  187. active=bool(int(row["active"])),
  188. )
  189. for row in rows
  190. ]
  191. def load_route_points(self, route_id: int) -> List[RoutePoint]:
  192. with self._lock:
  193. rows = self._conn.execute(
  194. """
  195. SELECT seq, t, lat, lon, speed_mps, cum_dist_m
  196. FROM route_points
  197. WHERE route_id=?
  198. ORDER BY seq ASC
  199. """,
  200. (route_id,),
  201. ).fetchall()
  202. return [
  203. RoutePoint(
  204. seq=int(row["seq"]),
  205. t=int(row["t"]),
  206. lat=float(row["lat"]),
  207. lon=float(row["lon"]),
  208. speed_mps=float(row["speed_mps"]),
  209. cum_dist_m=float(row["cum_dist_m"]),
  210. )
  211. for row in rows
  212. ]
  213. def load_hazards(self, route_id: int) -> List[Hazard]:
  214. with self._lock:
  215. rows = self._conn.execute(
  216. """
  217. SELECT id, route_id, seq, t, lat, lon, cum_dist_m, type, note
  218. FROM hazards
  219. WHERE route_id=?
  220. ORDER BY cum_dist_m ASC
  221. """,
  222. (route_id,),
  223. ).fetchall()
  224. return [
  225. Hazard(
  226. id=int(row["id"]),
  227. route_id=int(row["route_id"]),
  228. seq=int(row["seq"]),
  229. t=int(row["t"]),
  230. lat=float(row["lat"]),
  231. lon=float(row["lon"]),
  232. cum_dist_m=float(row["cum_dist_m"]),
  233. type=str(row["type"]),
  234. note=str(row["note"] or ""),
  235. )
  236. for row in rows
  237. ]