Wifi capture base
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,3 +3,6 @@
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
captures/
|
||||
.DS_Store
|
||||
*.pyc
|
||||
|
||||
6
partitions_captainjack.csv
Normal file
6
partitions_captainjack.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x300000,
|
||||
spiffs, data, spiffs, 0x310000,0xE0000,
|
||||
coredump, data, coredump,0x3F0000,0x10000,
|
||||
|
@@ -12,5 +12,8 @@
|
||||
platform = platformio/espressif32@6.4.0
|
||||
board = ttgo-t-watch
|
||||
framework = arduino
|
||||
board_build.filesystem = littlefs
|
||||
board_build.partitions = partitions_captainjack.csv
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
0ldev/Politician@^1.0.0
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
#include "user_app.h"
|
||||
|
||||
#include "wifi_app.h"
|
||||
|
||||
void userAppSetup(TTGOClass *watch)
|
||||
{
|
||||
(void)watch;
|
||||
|
||||
// Add one-time setup for your application here.
|
||||
wifiAppSetup(watch);
|
||||
}
|
||||
|
||||
void userAppLoop()
|
||||
{
|
||||
// Add recurring application work here. Keep this quick so LVGL stays smooth.
|
||||
wifiAppLoop();
|
||||
}
|
||||
|
||||
void userAppBuildMenu(lv_obj_t *menuList)
|
||||
{
|
||||
(void)menuList;
|
||||
|
||||
/*
|
||||
Add custom LVGL menu entries here, for example:
|
||||
|
||||
lv_obj_t *item = lv_list_add_btn(menuList, NULL, "My Action");
|
||||
lv_obj_set_event_cb(item, [](lv_obj_t *obj, lv_event_t event) {
|
||||
(void)obj;
|
||||
if (event == LV_EVENT_CLICKED) {
|
||||
watchFrameworkRefreshActivity();
|
||||
// Run your action here.
|
||||
}
|
||||
});
|
||||
*/
|
||||
wifiAppBuildMenu(menuList);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ enum class WatchState {
|
||||
WATCH_FACE,
|
||||
MENU,
|
||||
CLOCK_SETTINGS,
|
||||
ALERT,
|
||||
SLEEPING
|
||||
};
|
||||
|
||||
@@ -58,6 +59,8 @@ uint32_t gSleepDeadline = 0;
|
||||
uint32_t gLastClockRefresh = 0;
|
||||
uint32_t gLastClickMs = 0;
|
||||
uint8_t gPendingClicks = 0;
|
||||
uint8_t gVibrationPulsesRemaining = 0;
|
||||
uint32_t gNextVibrationMs = 0;
|
||||
|
||||
char gTimeZoneName[32] = "UTC";
|
||||
|
||||
@@ -74,6 +77,8 @@ uint8_t gDraftMinute = 0;
|
||||
uint8_t gDraftTimeZone = 0;
|
||||
SettingsField gActiveField = SettingsField::YEAR;
|
||||
|
||||
void updateSleepDeadline();
|
||||
|
||||
void IRAM_ATTR onAxpInterrupt()
|
||||
{
|
||||
gAxpIrq = true;
|
||||
@@ -191,6 +196,45 @@ void wakeDisplay()
|
||||
gWatch->setBrightness(SCREEN_BRIGHTNESS);
|
||||
}
|
||||
|
||||
void drawAlert(const char *titleText, const char *line1Text, const char *line2Text, const char *line3Text)
|
||||
{
|
||||
wakeDisplay();
|
||||
setScreenBase(LV_COLOR_BLACK);
|
||||
|
||||
lv_obj_t *title = lv_label_create(lv_scr_act(), NULL);
|
||||
lv_label_set_text(title, titleText ? titleText : "Alert");
|
||||
lv_obj_set_style_local_text_color(title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_YELLOW);
|
||||
lv_obj_set_style_local_text_font(title, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &lv_font_montserrat_28);
|
||||
lv_obj_align(title, NULL, LV_ALIGN_IN_TOP_MID, 0, 22);
|
||||
|
||||
lv_obj_t *line1 = lv_label_create(lv_scr_act(), NULL);
|
||||
lv_label_set_long_mode(line1, LV_LABEL_LONG_BREAK);
|
||||
lv_obj_set_width(line1, 220);
|
||||
lv_label_set_align(line1, LV_LABEL_ALIGN_CENTER);
|
||||
lv_label_set_text(line1, line1Text ? line1Text : "");
|
||||
lv_obj_set_style_local_text_color(line1, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE);
|
||||
lv_obj_align(line1, title, LV_ALIGN_OUT_BOTTOM_MID, 0, 18);
|
||||
|
||||
lv_obj_t *line2 = lv_label_create(lv_scr_act(), NULL);
|
||||
lv_label_set_long_mode(line2, LV_LABEL_LONG_BREAK);
|
||||
lv_obj_set_width(line2, 220);
|
||||
lv_label_set_align(line2, LV_LABEL_ALIGN_CENTER);
|
||||
lv_label_set_text(line2, line2Text ? line2Text : "");
|
||||
lv_obj_set_style_local_text_color(line2, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_SILVER);
|
||||
lv_obj_align(line2, line1, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
|
||||
|
||||
lv_obj_t *line3 = lv_label_create(lv_scr_act(), NULL);
|
||||
lv_label_set_long_mode(line3, LV_LABEL_LONG_BREAK);
|
||||
lv_obj_set_width(line3, 220);
|
||||
lv_label_set_align(line3, LV_LABEL_ALIGN_CENTER);
|
||||
lv_label_set_text(line3, line3Text ? line3Text : "");
|
||||
lv_obj_set_style_local_text_color(line3, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_SILVER);
|
||||
lv_obj_align(line3, line2, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
|
||||
|
||||
updateSleepDeadline();
|
||||
gState = WatchState::ALERT;
|
||||
}
|
||||
|
||||
void updateSleepDeadline()
|
||||
{
|
||||
gSleepDeadline = millis() + SCREEN_TIMEOUT_MS;
|
||||
@@ -605,6 +649,24 @@ void configurePowerButton()
|
||||
gWatch->power->clearIRQ();
|
||||
}
|
||||
|
||||
void handleVibration()
|
||||
{
|
||||
if (gVibrationPulsesRemaining == 0 || !gWatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
if (static_cast<int32_t>(now - gNextVibrationMs) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef LILYGO_WATCH_HAS_MOTOR
|
||||
gWatch->shake();
|
||||
#endif
|
||||
--gVibrationPulsesRemaining;
|
||||
gNextVibrationMs = now + 180;
|
||||
}
|
||||
|
||||
void configureBatteryAdc()
|
||||
{
|
||||
gWatch->power->adc1Enable(
|
||||
@@ -666,6 +728,12 @@ void handleTimeouts()
|
||||
return;
|
||||
}
|
||||
|
||||
if (gState == WatchState::ALERT &&
|
||||
static_cast<int32_t>(now - gSleepDeadline) >= 0) {
|
||||
watchFrameworkSleep();
|
||||
return;
|
||||
}
|
||||
|
||||
if (gState == WatchState::WATCH_FACE && now - gLastClockRefresh >= 1000) {
|
||||
updateWatchFace();
|
||||
gLastClockRefresh = now;
|
||||
@@ -681,12 +749,16 @@ void watchFrameworkSetup(TTGOClass *watch)
|
||||
loadSettings();
|
||||
configureBatteryAdc();
|
||||
configurePowerButton();
|
||||
#ifdef LILYGO_WATCH_HAS_MOTOR
|
||||
gWatch->motor_begin();
|
||||
#endif
|
||||
drawBootConfirmation();
|
||||
}
|
||||
|
||||
void watchFrameworkLoop()
|
||||
{
|
||||
handleAxpInterrupt();
|
||||
handleVibration();
|
||||
handleTimeouts();
|
||||
}
|
||||
|
||||
@@ -722,6 +794,22 @@ void watchFrameworkRefreshActivity()
|
||||
updateSleepDeadline();
|
||||
}
|
||||
|
||||
bool watchFrameworkIsSleeping()
|
||||
{
|
||||
return gState == WatchState::SLEEPING;
|
||||
}
|
||||
|
||||
void watchFrameworkShowAlert(const char *title, const char *line1, const char *line2, const char *line3)
|
||||
{
|
||||
drawAlert(title, line1, line2, line3);
|
||||
}
|
||||
|
||||
void watchFrameworkVibrateDouble()
|
||||
{
|
||||
gVibrationPulsesRemaining = 2;
|
||||
gNextVibrationMs = 0;
|
||||
}
|
||||
|
||||
TTGOClass *watchFrameworkWatch()
|
||||
{
|
||||
return gWatch;
|
||||
|
||||
@@ -18,6 +18,8 @@ void watchFrameworkWakeFace();
|
||||
void watchFrameworkOpenMenu();
|
||||
void watchFrameworkSleep();
|
||||
void watchFrameworkRefreshActivity();
|
||||
bool watchFrameworkIsSleeping();
|
||||
void watchFrameworkShowAlert(const char *title, const char *line1, const char *line2, const char *line3);
|
||||
void watchFrameworkVibrateDouble();
|
||||
|
||||
TTGOClass *watchFrameworkWatch();
|
||||
|
||||
|
||||
1039
src/wifi_app.cpp
Normal file
1039
src/wifi_app.cpp
Normal file
File diff suppressed because it is too large
Load Diff
7
src/wifi_app.h
Normal file
7
src/wifi_app.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "watch_framework.h"
|
||||
|
||||
void wifiAppSetup(TTGOClass *watch);
|
||||
void wifiAppLoop();
|
||||
void wifiAppBuildMenu(lv_obj_t *menuList);
|
||||
265
tools/capture_export.py
Executable file
265
tools/capture_export.py
Executable file
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download CaptainJack capture files from the watch over USB serial."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import binascii
|
||||
import datetime as dt
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
except ImportError: # pragma: no cover - dependency error path
|
||||
serial = None
|
||||
list_ports = None
|
||||
|
||||
|
||||
BAUD = 115200
|
||||
MARKER = "@WS "
|
||||
CAPTURE_FILES = ("captures.pcapng", "captures.22000")
|
||||
|
||||
|
||||
class ProtocolError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def require_serial() -> None:
|
||||
if serial is None:
|
||||
raise SystemExit("pyserial is required: python3 -m pip install pyserial")
|
||||
|
||||
|
||||
def open_serial(port: str, timeout: float = 3.0):
|
||||
require_serial()
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = BAUD
|
||||
ser.timeout = timeout
|
||||
ser.write_timeout = timeout
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
ser.open()
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
time.sleep(0.15)
|
||||
ser.reset_input_buffer()
|
||||
return ser
|
||||
|
||||
|
||||
def protocol_line(ser, timeout: float = 8.0) -> str:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
raw = ser.readline()
|
||||
if not raw:
|
||||
continue
|
||||
text = raw.decode("utf-8", "replace").strip()
|
||||
pos = text.find(MARKER)
|
||||
if pos >= 0:
|
||||
return text[pos:]
|
||||
raise TimeoutError("timed out waiting for @WS response")
|
||||
|
||||
|
||||
def send(ser, command: str) -> None:
|
||||
ser.write((command.strip() + "\n").encode("ascii"))
|
||||
ser.flush()
|
||||
|
||||
|
||||
def ping(ser) -> bool:
|
||||
for _ in range(3):
|
||||
send(ser, "PING")
|
||||
try:
|
||||
line = protocol_line(ser, timeout=4.0)
|
||||
except TimeoutError:
|
||||
continue
|
||||
if line == "@WS READY":
|
||||
send(ser, "PING")
|
||||
continue
|
||||
if line == "@WS PONG":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def list_captures(ser) -> list[tuple[str, int]]:
|
||||
send(ser, "LIST")
|
||||
files: list[tuple[str, int]] = []
|
||||
saw_begin = False
|
||||
while True:
|
||||
line = protocol_line(ser)
|
||||
if line == "@WS LIST BEGIN":
|
||||
saw_begin = True
|
||||
continue
|
||||
if line == "@WS LIST END":
|
||||
return files
|
||||
if line.startswith("@WS ERR "):
|
||||
raise ProtocolError(line)
|
||||
if line.startswith("@WS FILE "):
|
||||
if not saw_begin:
|
||||
raise ProtocolError("FILE before LIST BEGIN")
|
||||
parts = line.split(" ", 3)
|
||||
if len(parts) != 4:
|
||||
raise ProtocolError(f"bad FILE line: {line}")
|
||||
files.append((parts[2], int(parts[3])))
|
||||
|
||||
|
||||
def download_one(ser, name: str, out_dir: Path) -> Path:
|
||||
send(ser, f"GET {name}")
|
||||
first = protocol_line(ser)
|
||||
if first.startswith("@WS ERR "):
|
||||
raise ProtocolError(first)
|
||||
parts = first.split(" ", 3)
|
||||
if len(parts) != 4 or parts[:2] != ["@WS", "BEGIN"]:
|
||||
raise ProtocolError(f"bad BEGIN line: {first}")
|
||||
|
||||
remote_name = parts[2]
|
||||
expected_size = int(parts[3])
|
||||
data = bytearray()
|
||||
|
||||
while True:
|
||||
line = protocol_line(ser, timeout=12.0)
|
||||
if line.startswith("@WS DATA "):
|
||||
try:
|
||||
data.extend(binascii.unhexlify(line[9:]))
|
||||
except binascii.Error as exc:
|
||||
raise ProtocolError(f"bad DATA hex for {remote_name}") from exc
|
||||
continue
|
||||
if line == f"@WS END {remote_name}":
|
||||
break
|
||||
if line.startswith("@WS ERR "):
|
||||
raise ProtocolError(line)
|
||||
raise ProtocolError(f"unexpected line while downloading {remote_name}: {line}")
|
||||
|
||||
if len(data) != expected_size:
|
||||
raise ProtocolError(f"{remote_name}: expected {expected_size} bytes, got {len(data)}")
|
||||
|
||||
out_path = out_dir / remote_name
|
||||
out_path.write_bytes(data)
|
||||
return out_path
|
||||
|
||||
|
||||
def clear_watch(ser) -> str:
|
||||
send(ser, "CLEAR")
|
||||
line = protocol_line(ser)
|
||||
if line.startswith("@WS OK "):
|
||||
return line[7:]
|
||||
raise ProtocolError(line)
|
||||
|
||||
|
||||
def yes_no(prompt: str) -> bool:
|
||||
answer = input(f"{prompt} [y/N] ").strip().lower()
|
||||
return answer in {"y", "yes"}
|
||||
|
||||
|
||||
def default_output_dir() -> Path:
|
||||
stamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
return Path("captures") / stamp
|
||||
|
||||
|
||||
def cmd_ports(_args) -> int:
|
||||
require_serial()
|
||||
ports = list(list_ports.comports())
|
||||
if not ports:
|
||||
print("No serial ports found.")
|
||||
return 1
|
||||
for port in ports:
|
||||
desc = f" - {port.description}" if port.description else ""
|
||||
print(f"{port.device}{desc}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_ping(args) -> int:
|
||||
with open_serial(args.port) as ser:
|
||||
if ping(ser):
|
||||
print("Watch responded: PONG")
|
||||
return 0
|
||||
print("No PONG from watch.")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_list(args) -> int:
|
||||
with open_serial(args.port) as ser:
|
||||
files = list_captures(ser)
|
||||
if not files:
|
||||
print("No captures stored.")
|
||||
return 0
|
||||
for name, size in files:
|
||||
print(f"{name}\t{size} bytes")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_download(args) -> int:
|
||||
out_dir = args.output
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open_serial(args.port) as ser:
|
||||
if not ping(ser):
|
||||
raise SystemExit("watch did not respond to PING")
|
||||
files = list_captures(ser)
|
||||
if not files:
|
||||
print("No captures stored.")
|
||||
return 0
|
||||
|
||||
downloaded: list[Path] = []
|
||||
for name, size in files:
|
||||
if size <= 0:
|
||||
continue
|
||||
path = download_one(ser, name, out_dir)
|
||||
downloaded.append(path)
|
||||
print(f"Downloaded {name} -> {path} ({path.stat().st_size} bytes)")
|
||||
|
||||
if downloaded and not args.no_clean:
|
||||
if yes_no("Clear downloaded captures from the watch now?"):
|
||||
result = clear_watch(ser)
|
||||
print(f"Watch cleanup: {result}")
|
||||
else:
|
||||
print("Left captures on watch.")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_clear(args) -> int:
|
||||
if not args.yes and not yes_no("Delete capture files and captured-BSSID cache from the watch?"):
|
||||
print("Cancelled.")
|
||||
return 0
|
||||
with open_serial(args.port) as ser:
|
||||
result = clear_watch(ser)
|
||||
print(f"Watch cleanup: {result}")
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
sub.add_parser("ports", help="list available serial ports").set_defaults(func=cmd_ports)
|
||||
|
||||
for name, help_text in (
|
||||
("ping", "check watch protocol response"),
|
||||
("list", "list capture files stored on the watch"),
|
||||
("download", "download capture files and optionally clear them"),
|
||||
("clear", "delete capture files from the watch"),
|
||||
):
|
||||
p = sub.add_parser(name, help=help_text)
|
||||
p.add_argument("--port", required=True, help="serial port, e.g. /dev/cu.usbserial-...")
|
||||
if name == "download":
|
||||
p.add_argument("-o", "--output", type=Path, default=default_output_dir())
|
||||
p.add_argument("--no-clean", action="store_true", help="do not ask to clear after download")
|
||||
if name == "clear":
|
||||
p.add_argument("-y", "--yes", action="store_true", help="clear without prompting")
|
||||
p.set_defaults(func=globals()[f"cmd_{name}"])
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
return args.func(args)
|
||||
except (TimeoutError, ProtocolError, serial.SerialException if serial else Exception) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user