Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions launch_xiao.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
XIAO=$(ls /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_* 2>/dev/null | head -1)
if [ -z "$XIAO" ]; then
echo "XIAO ESP32-C5 not found — plug it in and try again"
read -p "Press Enter to close..."
exit 1
fi
cd /home/fusedstamen/python/WatchDogsGo
exec sudo -E .venv/bin/python3 -m watchdogs "$XIAO"
15 changes: 12 additions & 3 deletions plugins/wardrive_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,8 @@ def _pending_sessions(self) -> list[Path]:
# Session has data if any of these exist
has_data = any((d / f).is_file() for f in
["wardriving.csv", "adsb_aircraft.csv", "meshcore_nodes.csv"])
if has_data and d.name not in self._uploaded_sessions:
active = Path(self.app.loot.session_path).name if self.app.loot else ""
if has_data and d.name not in self._uploaded_sessions and d.name != active:
pending.append(d)
return pending

Expand Down Expand Up @@ -628,7 +629,7 @@ def _upload_worker(self, sessions: list[Path]):
req = Request(self._api_url, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("X-API-Key", self._api_key)
with _open(req, timeout=30) as resp:
with _open(req, timeout=90) as resp:
result = json.loads(resp.read().decode())
imp = result.get("imported", 0)
dup = result.get("duplicates", 0)
Expand Down Expand Up @@ -672,7 +673,7 @@ def _upload_worker(self, sessions: list[Path]):
req = Request(self._api_url, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("X-API-Key", self._api_key)
with _open(req, timeout=30) as resp:
with _open(req, timeout=90) as resp:
result = json.loads(resp.read().decode())
imp = result.get("imported", 0)
self._log_add(f" Retry OK: +{imp}", 11)
Expand All @@ -689,6 +690,14 @@ def _upload_worker(self, sessions: list[Path]):
self._log_add(f" HTTP {e.code}: {body}", 8)
except URLError as e:
self._log_add(f" Connection error: {e.reason}", 8)
except (TimeoutError, OSError) as e:
# Upload likely succeeded but response timed out — mark as
# uploaded to prevent re-sending duplicate data on retry.
self._log_add(
f" Timeout — server may have received data. "
f"Marking uploaded to avoid duplicates.", 10)
self._mark_uploaded(session_dir)
uploaded += 1
except Exception as e:
self._log_add(f" Error: {e}", 8)

Expand Down
8 changes: 7 additions & 1 deletion run.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ if [ "$(id -u)" -ne 0 ]; then
fi
fi

exec sudo -E "$VENV_PYTHON" -m watchdogs "$@"
# Wait for bridge PTY to be ready
for i in $(seq 1 10); do
[ -e /tmp/esp32-pty ] && break
sleep 0.5
done

exec sudo -E "$VENV_PYTHON" -m watchdogs "${@:-/tmp/esp32-pty}"
Empty file modified setup.sh
100644 → 100755
Empty file.
16 changes: 9 additions & 7 deletions watchdogs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
("SYSTEM", [
("x", "STOP ALL", "stop", "_stop_all", None),
("r", "Reboot ESP32", "_reboot_esp32", "_reboot", None),
("m", "Download Map (WIP)", "_download_map", "_dl_map", None),
("m", "Download Map", "_download_map", "_dl_map", None),
("g", "GPS", "_toggle_gps", "_gps_toggle", None),
("l", "LoRa", "_toggle_lora", "_lora_toggle", None),
("d", "SDR", "_toggle_sdr", "_sdr_toggle", None),
Expand Down Expand Up @@ -1655,9 +1655,11 @@ def _execute_item(self, cmd: str, state_key: str, name: str,
self.msg("[SYS] Reboot command sent", C_DIM)
return
if state_key == "_dl_map":
# Temporarily disabled — needs rework (tile source, quota, resume)
self.msg("[MAP] Download disabled — work in progress", C_WARNING)
self.msg("[MAP] Feature will return in a future update", C_DIM)
if self._map_downloading:
self._map_download_cancel = True
self.msg("[MAP] Cancelling download...", C_WARNING)
else:
self._start_map_download()
return
if state_key == "_bt_hid_wip":
self.msg("[HID] BLE HID disabled — work in progress", C_WARNING)
Expand Down Expand Up @@ -5541,7 +5543,7 @@ def _nfc_read():
lines = self._flipper.storage_list("/ext/nfc")
files = []
for l in lines:
if l.startswith("[F]") and ".nfc" in l:
if "[F]" in l and ".nfc" in l:
fname = l.split("]")[-1].strip().split(" ")[0]
path = f"/ext/nfc/{fname}"
display = fname.replace(".nfc", "")
Expand Down Expand Up @@ -5579,7 +5581,7 @@ def _load_flipper_sd(self):
name = l.split("/ext/subghz/")[-1]
if "/" not in name and name not in ("assets", "playlist", "remote"):
folders.add(name)
elif l.startswith("[F] /ext/subghz/") and ".sub" in l:
elif "[F]" in l and "/ext/subghz/" in l and ".sub" in l:
path = l.split(" ")[1] if " " in l else l[4:]
path = path.split(" ")[0] # remove size
fname = path.split("/")[-1]
Expand Down Expand Up @@ -5610,7 +5612,7 @@ def _load_flipper_folder(self, folder: str):
lines = self._flipper.storage_list(f"/ext/subghz/{folder}")
files = []
for l in lines:
if l.startswith("[F]") and ".sub" in l:
if "[F]" in l and ".sub" in l:
fname = l.split("]")[-1].strip().split(" ")[0]
path = f"/ext/subghz/{folder}/{fname}"
files.append((path, fname))
Expand Down
4 changes: 2 additions & 2 deletions watchdogs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def _env(*keys: str, default: str = "") -> str:

_secrets = _load_secrets()

BAUD_RATE = 115200
BAUD_RATE = 460800
SCAN_TIMEOUT = 15
READ_TIMEOUT = 2
SNIFFER_UPDATE_INTERVAL = 1
Expand Down Expand Up @@ -151,7 +151,7 @@ def _env(*keys: str, default: str = "") -> str:

# GPS module — default: AIO UART on uConsole
# Override with env vars: WDG_GPS_DEVICE, WDG_GPS_BAUD (legacy: JANOS_GPS_*)
GPS_DEVICE = _env("WDG_GPS_DEVICE", "JANOS_GPS_DEVICE", default="/dev/ttyAMA0")
GPS_DEVICE = _env("WDG_GPS_DEVICE", "JANOS_GPS_DEVICE", default="/tmp/gps-pty")
GPS_BAUD_RATE = int(_env("WDG_GPS_BAUD", "JANOS_GPS_BAUD", default="9600"))
GPS_PRIVACY_NOISE_DEG = 0.01 # ±0.01° ≈ ±1.1km randomization in private mode

Expand Down
8 changes: 4 additions & 4 deletions watchdogs/flipper_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def ensure_connected(self) -> bool:
pass
return self.reconnect()

def send(self, cmd: str) -> list[str]:
def send(self, cmd: str, timeout: float = 2.0) -> list[str]:
"""Send command and return response lines (blocking, short timeout)."""
if not self.connected:
return []
Expand All @@ -138,7 +138,7 @@ def send(self, cmd: str) -> list[str]:
self._ser.flush()
time.sleep(0.3)
lines = []
deadline = time.time() + 2
deadline = time.time() + timeout
while time.time() < deadline:
if self._ser.in_waiting:
raw = self._ser.readline().decode(errors="replace").strip()
Expand Down Expand Up @@ -247,7 +247,7 @@ def subghz_stop(self) -> None:

def storage_list(self, path: str = "/ext/subghz") -> list[str]:
"""List files on Flipper SD card."""
return self.send(f"storage list {path}")
return self.send(f"storage list {path}", timeout=5.0)

def storage_read(self, path: str) -> str:
"""Read file content from Flipper SD card."""
Expand Down Expand Up @@ -304,7 +304,7 @@ def nfc_emulate(self, filepath: str) -> list[str]:
"""
self.send_async("nfc")
time.sleep(0.5)
self.send_async(f"emulate {filepath}")
self.send_async(f"emulate -f {filepath}")

def nfc_field(self, on: bool = True) -> None:
"""Toggle NFC field on/off."""
Expand Down
35 changes: 33 additions & 2 deletions watchdogs/loot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def _scan_session_dir(self, session_path: Path) -> dict:
"""Count loot items in a single session directory."""
counts = {"pcap": 0, "hccapx": 0, "hc22000": 0, "passwords": 0, "et_captures": 0,
"mc_nodes": 0, "mc_messages": 0, "bt_devices": 0, "bt_airtags": 0,
"bt_smarttags": 0, "bt_devices_gps": 0, "wardriving": 0,
"bt_smarttags": 0, "bt_devices_gps": 0, "wardriving": 0, "adsb": 0,
"mitm_pcaps": 0}
hs_dir = session_path / "handshakes"
if hs_dir.is_dir():
Expand Down Expand Up @@ -339,6 +339,12 @@ def _scan_session_dir(self, session_path: Path) -> dict:
counts["wardriving"] = max(0, lines - 2) # minus pre-header + header
except OSError:
pass
adsb_file = session_path / "adsb_aircraft.csv"
if adsb_file.is_file():
try:
counts["adsb"] = max(0, sum(1 for _ in open(adsb_file, encoding="utf-8")) - 1) # minus header
except OSError:
pass
mitm_dir = session_path / "mitm"
if mitm_dir.is_dir():
try:
Expand All @@ -353,7 +359,7 @@ def _recalc_totals(self, db: dict) -> None:
"""Recalculate totals from all session entries."""
keys = ("pcap", "hccapx", "hc22000", "passwords", "et_captures",
"mc_nodes", "mc_messages", "bt_devices", "bt_airtags", "bt_smarttags",
"bt_devices_gps", "wardriving")
"bt_devices_gps", "wardriving", "adsb")
totals: dict = {k: 0 for k in keys}
totals["sessions"] = len(db["sessions"])
for session_counts in db["sessions"].values():
Expand Down Expand Up @@ -1192,6 +1198,31 @@ def save_bt_airtag_event(self, airtags: int, smarttags: int) -> None:
_sync_append(path, f"[{ts}] AirTags:{airtags} | SmartTags:{smarttags}\n")
self.update_session_loot()

def save_adsb_aircraft(self, icao: str, callsign: str = "",
lat: float = 0.0, lon: float = 0.0,
alt_ft: int = 0, speed_kt: int = 0,
heading: int = 0) -> None:
"""Append an ADS-B aircraft to adsb_aircraft.csv (dedup by ICAO). fsync'd."""
if not self._session_active or not icao:
return
path = self._session / "adsb_aircraft.csv"
if path.is_file():
try:
existing = path.read_text(encoding="utf-8")
except OSError:
existing = ""
if f",{icao}," in existing or f"\n{icao}," in existing:
return # already recorded this session
else:
_sync_write(path, "timestamp,icao,callsign,lat,lon,alt_ft,speed_kt,heading\n")
ts = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
_sync_append(
path,
f"{ts},{icao},{callsign or ''},"
f"{round(lat, 7)},{round(lon, 7)},"
f"{alt_ft},{speed_kt},{heading}\n")
self.update_session_loot()

# ------------------------------------------------------------------
# GPS point collection (for Map tab)
# ------------------------------------------------------------------
Expand Down
39 changes: 36 additions & 3 deletions watchdogs/tile_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,41 @@
]

OSM_TILE_SIZE = 256
USER_AGENT = "ESP32WatchDogs/1.0 (security-research-device)"
TILE_URL = "https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png"
USER_AGENT = "WatchDogsGo/1.0 (security-research-game)"

# Stadia Maps — Alidade Smooth Dark style
# Free tier, raster PNG tiles, dark theme matching game aesthetic
# API key loaded from secrets.conf (STADIA_API_KEY=...)
# Sign up free at https://stadiamaps.com
_STADIA_BASE = "https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}@2x.png"


def _load_stadia_key() -> str:
"""Load Stadia Maps API key from secrets.conf."""
try:
from pathlib import Path as _Path
conf = _Path(__file__).resolve().parent.parent / "secrets.conf"
if conf.is_file():
for line in conf.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("STADIA_API_KEY="):
return line.split("=", 1)[1].strip()
except Exception:
pass
return ""


def _tile_url(z: int, x: int, y: int) -> str:
"""Build Stadia Maps tile URL with API key if configured."""
url = _STADIA_BASE.format(z=z, x=x, y=y)
key = _load_stadia_key()
if key:
url += f"?api_key={key}"
return url


# Legacy constant kept for compatibility
TILE_URL = _STADIA_BASE


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -233,7 +266,7 @@ def download_tiles(lat: float, lon: float, maps_dir: Path,
if callback:
callback(done / total * 100, f"{done}/{total} tiles")

url = TILE_URL.format(z=z, x=tx, y=ty)
url = _tile_url(z, tx, ty)
try:
req = Request(url, headers={"User-Agent": USER_AGENT})
with urlopen(req, timeout=10) as resp:
Expand Down
Loading