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.
34 changes: 31 additions & 3 deletions watchdogs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2507,6 +2507,18 @@ def _poll_sdr(self):
if ac.icao not in self._sdr_aircraft_xp:
self._sdr_aircraft_xp.add(ac.icao)
self.gain_xp(10)
if self.loot:
lat = getattr(ac, 'lat', 0.0) or 0.0
lon = getattr(ac, 'lon', 0.0) or 0.0
self.loot.save_adsb_aircraft(
icao=ac.icao,
callsign=getattr(ac, 'callsign', '') or '',
lat=lat,
lon=lon,
alt_ft=int(getattr(ac, 'altitude', 0) or 0),
speed_kt=int(getattr(ac, 'speed', 0) or 0),
heading=int(getattr(ac, 'heading', 0) or 0),
)
elif etype == "sensor_new":
self.gain_xp(5)
except Exception:
Expand Down Expand Up @@ -3757,6 +3769,7 @@ def _load_loot_totals(self):
"et_captures": t.get("et_captures", 0),
"mc_nodes": t.get("mc_nodes", 0),
"mc_msgs": t.get("mc_messages", 0),
"adsb": t.get("adsb", 0),
}
except Exception:
pass
Expand Down Expand Up @@ -5541,7 +5554,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 +5592,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 +5623,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 Expand Up @@ -7492,6 +7505,17 @@ def _draw_loot_screen(self):
y += ROW_H
pyxel.text(col1, y, f"GPS points", C_DIM)
pyxel.text(col1 + 80, y, f"{len(self.loot_points)}", C_TEXT)
y += ROW_H
# Aircraft — prefer server-side deduped count from wardrive plugin
_ac_server = 0
for _p in self._plugins:
if hasattr(_p, '_user_stats'):
_ac_server = _p._user_stats.get('aircraft', 0)
break
_ac_local = t.get('adsb', 0)
_ac_total = _ac_server if _ac_server > 0 else _ac_local
pyxel.text(col1, y, f"Aircraft", C_DIM)
pyxel.text(col1 + 80, y, f"{_ac_total}", 9) # orange

# ─── COLUMN 2: THIS SESSION ───
y = 22
Expand All @@ -7517,6 +7541,10 @@ def _draw_loot_screen(self):
y += ROW_H
pyxel.text(col2, y, f"Hacked", C_DIM)
pyxel.text(col2 + 70, y, f"{n_pwn}", C_SUCCESS)
y += ROW_H
n_ac_ses = len(self._sdr.aircraft) if self._sdr and self._sdr.running else 0
pyxel.text(col2, y, f"Aircraft", C_DIM)
pyxel.text(col2 + 70, y, f"{n_ac_ses}", 9) # orange
y += 14
# XP bar
lv = self.level_title
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
Loading