From 4c401bc59b5c5cab772a1ae092e91a4801cd62da Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Wed, 15 Jul 2026 15:05:18 +0300 Subject: [PATCH] chore: initialize K1 connector pre-production scaffold --- .gitattributes | 12 + .gitignore | 44 + .python-version | 2 + AGENTS.md | 39 + README.md | 81 + artifacts/README.md | 9 + artifacts/manifests/.gitkeep | 1 + docs/00_TECHNICAL_AUDIT.md | 158 + docs/01_IMPLEMENTATION_PLAN.md | 188 ++ docs/02_FIRST_LAB_RUNBOOK.md | 134 + docs/03_ARTIFACT_POLICY.md | 60 + docs/adr/0001-actual-stand-and-no-lixelgo.md | 32 + .../CODEX_K1_MAC_LINK_PREPROD_PROMPT.md | 220 ++ docs/reference/README.md | 15 + .../XGRIDS_Lixel_K1_PreProduction_Bible.md | 2544 +++++++++++++++++ examples/session_manifest.redacted.json | 41 + pyproject.toml | 46 + schemas/session-manifest.schema.json | 113 + src/k1link/__init__.py | 3 + src/k1link/cli.py | 172 ++ tests/test_cli.py | 23 + tests/test_repository_inputs.py | 40 + uv.lock | 255 ++ 23 files changed, 4232 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 artifacts/README.md create mode 100644 artifacts/manifests/.gitkeep create mode 100644 docs/00_TECHNICAL_AUDIT.md create mode 100644 docs/01_IMPLEMENTATION_PLAN.md create mode 100644 docs/02_FIRST_LAB_RUNBOOK.md create mode 100644 docs/03_ARTIFACT_POLICY.md create mode 100644 docs/adr/0001-actual-stand-and-no-lixelgo.md create mode 100644 docs/reference/CODEX_K1_MAC_LINK_PREPROD_PROMPT.md create mode 100644 docs/reference/README.md create mode 100644 docs/reference/XGRIDS_Lixel_K1_PreProduction_Bible.md create mode 100644 examples/session_manifest.redacted.json create mode 100644 pyproject.toml create mode 100644 schemas/session-manifest.schema.json create mode 100644 src/k1link/__init__.py create mode 100644 src/k1link/cli.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_repository_inputs.py create mode 100644 uv.lock diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..193bcec --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +* text=auto eol=lf +*.md text diff=markdown +*.json text +*.jsonl text +*.toml text +*.pcap binary +*.pcapng binary +*.snoop binary +*.xbin binary +*.las binary +*.lcc binary + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..227e274 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +.DS_Store +.idea/ +.vscode/ + +# Project-local Python environment and caches +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ + +# Local configuration and secrets +.env +.env.* +!.env.example +config/local.toml +private/ + +# Real laboratory captures and decoded artifacts are sensitive and large. +captures/ +sessions/ +artifacts/raw/ +artifacts/decoded/ +*.pcap +*.pcapng +*.snoop +*.xbin +*.las +*.lcc +*.ply +*.pcd +*.npz +*.raw + +# Small synthetic/redacted fixtures under tests/fixtures are allowed. +!tests/fixtures/**/*.pcap +!tests/fixtures/**/*.pcapng + diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e0aad9e --- /dev/null +++ b/.python-version @@ -0,0 +1,2 @@ +3.12 + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..15af053 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# Repository operating rules + +This repository investigates an owner-controlled XGRIDS/LixelKity K1 as a +black-box sensor. Preserve the device, the Mac, the working LAN, and raw +evidence. + +## Non-negotiable safety boundaries + +- Default to passive or read-only operations. +- Do not send BLE writes until the exact service, characteristic, framing, + payload semantics, rollback, and expected state transition are documented. +- BLE notification subscription may perform the standard temporary CCCD write; + disclose it explicitly and do not conflate it with provisioning writes. +- Do not fuzz, brute-force, upload firmware, delete device files, guess SSH/ADB + credentials, or probe unrelated LAN devices. +- Network probes must target the confirmed K1 IP. A full home-subnet scan is + not a default operation. +- Never place a Wi-Fi password in CLI arguments, logs, manifests, source, test + fixtures, or Git history. +- Do not install Python packages globally. Use the repository-local `.venv` + managed by `uv`. +- Do not install or alter Homebrew/system components unless the user explicitly + authorizes that concrete change. + +## Evidence rules + +- Reference inputs under `docs/reference/` are immutable; write corrections in + audits or ADRs. +- Every real experiment gets a session ID, UTC and monotonic timestamps, a + redacted manifest, operator notes, and SHA-256 hashes for raw artifacts. +- Real PCAP, K1 projects, logs, images, point clouds, credentials, serials, and + router client lists stay out of normal Git. Git LFS solves size, not secrecy. +- Synthetic or explicitly redacted fixtures may be committed under + `tests/fixtures/`. + +## Implementation order + +Follow the gates in `docs/01_IMPLEMENTATION_PLAN.md`. Do not build heavy +decoders before BLE/Wi-Fi/data-session evidence exists. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f5868e --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# NDC XGRIDS K1 Connector + +Pre-production research project for connecting an owner-controlled +XGRIDS/LixelKity K1 to a Mac without LixelGO, firmware changes, device opening, +or speculative writes. + +Current status: repository scaffold and implementation plan. No command in the +repository currently writes to the K1, changes the router, or installs system +packages. + +## Available stand + +- one XGRIDS/LixelKity K1; +- one Apple Silicon MacBook running macOS; +- one ordinary TP-Link Deco/mesh network used by other devices; +- no LixelGO, phone, Linux host, dedicated AP, OpenWrt, or vendor SDK. + +The ordinary router is sufficient for the first gates. We first observe the +existing LAN without changing it. A Guest/IoT SSID is optional and may be +counterproductive if Deco isolates clients; Mac and K1 must ultimately be able +to reach each other. + +## What is actually being proved + +The project has three independent gates: + +1. K1 is operational and can record a project autonomously. +2. Mac can discover and inspect the K1 BLE/GATT surface safely. +3. Without LixelGO, K1 can be associated with Wi-Fi and a proprietary data + session can be opened. + +Only after gate 3 do point-cloud, pose, status, and camera stream decoders become +meaningful implementation work. Local `map.las` and `poses.csv` prove internal +capabilities, not an externally accessible network format. + +## Local environment + +The project uses Python 3.12 in a repository-local `.venv` managed by `uv`. +This does not install Python packages globally and does not modify neighboring +repositories. + +```bash +cd /Users/dcconstructions/Downloads/mnt/NODEDC/NDC_xgrids-k1-connector +uv sync --group dev +uv run k1link doctor +uv run pytest +``` + +`doctor` is intentionally non-invasive. It checks the local Python environment +and reports external tools; it does not request Bluetooth permission, scan the +LAN, touch the K1, alter Homebrew, or change capture permissions. + +## Documentation + +- [Technical audit](docs/00_TECHNICAL_AUDIT.md) +- [Implementation gates](docs/01_IMPLEMENTATION_PLAN.md) +- [First lab runbook](docs/02_FIRST_LAB_RUNBOOK.md) +- [Artifact and secret policy](docs/03_ARTIFACT_POLICY.md) +- [Session manifest schema](schemas/session-manifest.schema.json) +- [Reference input provenance](docs/reference/README.md) + +The two supplied source documents are retained unchanged under +`docs/reference/`. Corrections and decisions are recorded separately so their +provenance remains clear. + +## Safety boundary + +Allowed initial work is passive discovery, standard device-information reads, +controlled notification listening, autonomous button operation, targeted +capture of traffic to or from the confirmed K1 address, and offline analysis of +owned artifacts. + +BLE writes, provisioning, application-session packets, active service probes, +and router configuration changes require evidence and an explicit reviewed +step. Random writes, fuzzing, brute force, firmware operations, destructive file +access, and credential guessing are out of scope. + +Real captures, projects, router metadata, serials, credentials, maps, images, +and logs are ignored by normal Git. Redacted manifests and SHA-256 inventories +are committed; encrypted artifact storage will be selected only when real data +exists. diff --git a/artifacts/README.md b/artifacts/README.md new file mode 100644 index 0000000..1ddc60e --- /dev/null +++ b/artifacts/README.md @@ -0,0 +1,9 @@ +# Laboratory artifacts + +Only redacted manifests and SHA-256 inventories belong in ordinary Git. + +Real captures, K1 project copies, logs, point clouds, images, router client +lists, and decoded output must stay in ignored paths such as `artifacts/raw/` +or `sessions/`. If long-term versioned storage becomes necessary, choose and +document an encrypted artifact store. Git LFS alone is not encryption. + diff --git a/artifacts/manifests/.gitkeep b/artifacts/manifests/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/artifacts/manifests/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/00_TECHNICAL_AUDIT.md b/docs/00_TECHNICAL_AUDIT.md new file mode 100644 index 0000000..0caac03 --- /dev/null +++ b/docs/00_TECHNICAL_AUDIT.md @@ -0,0 +1,158 @@ +# Technical audit + +Status: pre-production planning, 2026-07-15. + +## Executive finding + +The project is feasible as a disciplined black-box investigation, but the +existence of a complete K1-to-Mac realtime link is not yet established. The +first hard problem is not decoding points or images. It is bootstrapping the K1 +from BLE availability into Wi-Fi association and then opening the proprietary +application data session without LixelGO. + +The supplied Bible is useful as an OSINT dossier. It is not an executable plan +for the actual stand because many experiments assume a phone and LixelGO. The +implementation must proceed through explicit gates and stop before speculative +writes. + +## Evidence classes + +### Version-scoped facts to verify on the physical unit + +- K1 has LiDAR, panoramic cameras, onboard compute, BLE and Wi-Fi. +- Official workflows use BLE before Wi-Fi network/data connection. +- Autonomous scan start/stop by the physical button is documented. +- A scan project may contain local `map.las`, `poses.csv`, `project.json`, logs, + preview/model and raw sensor artifacts depending on firmware and settings. +- macOS exposes a CoreBluetooth device UUID rather than a portable hardware MAC. +- A normal switched/mesh LAN does not mirror arbitrary client-to-client unicast + to the Mac, but the Mac can capture its own future K1 session completely. + +Each of these remains scoped to the actual firmware/hardware state observed in +the lab. The physical unit is authoritative. + +### Strong hypotheses + +- BLE is the bootstrap/control plane and Wi-Fi is the likely high-rate data + plane. +- The user-provided SSID and PSK are transported inside a vendor-defined GATT + protocol. +- The application may need a second token, certificate, handshake, or stream + subscription after ordinary Wi-Fi association. +- The externally exposed point cloud is more likely a processed/downsampled + preview than raw LiDAR packets. + +XGRIDS provides stronger product evidence for an external point-cloud path than +the local LAS alone: current LixelStudio materials describe K1 remote control +and realtime point-cloud streaming over USB/Wi-Fi. LixelStudio is Windows-only +and this does not disclose or guarantee access to the protocol from macOS. It +does, however, justify keeping realtime point cloud as the primary stream target. +No comparable official evidence proves an exportable raw panorama/camera stream. + +### Unsupported until measured + +- K1 remembers an existing Wi-Fi profile. +- K1 exposes its own access point. +- Provisioning fields are plain UTF-8, JSON, TLV, CBOR, or protobuf. +- A writable GATT characteristic can be used without bonding or an app token. +- Local `map.las` layout resembles the network stream. +- Live pose or camera frames leave the device. +- A point stream contains simple float32 XYZ tuples. +- Network payloads are unencrypted. + +## Major corrections to the source plan + +1. Experiments requiring LixelGO are removed from the active critical path. + There will be no app-session capture, provisioning diff, app-start comparison, + or temporary broker mode. +2. The first go/no-go is device activation and autonomous operation. An + unactivated unit may be a hard blocker before BLE provisioning is relevant. +3. Provisioning is not implemented merely because a characteristic is writable. + We need target UUID, framing, field encoding, ordering, integrity/auth fields, + connect/commit semantics, status response, and rollback. +4. Wi-Fi association and application data session are modeled as separate + states. Obtaining an IP is not success for the realtime connector. +5. `tcpdump` does not create a stream. A useful capture requires either K1 to + initiate traffic or Mac to become the authenticated/subscribed endpoint. +6. Full port/version/UDP scans are active operations, not passive safe defaults. + Discovery begins with power on/off diffs, router client list, ARP and + broadcast/multicast observations, then targets only the confirmed K1 IP. +7. Heavy packages are deferred. OpenCV, Open3D, pandas, Scapy, PyShark and NumPy + are not prerequisites for proving bootstrap connectivity. +8. Wi-Fi passwords must never be CLI arguments. Future provisioning will use a + hidden interactive prompt or macOS Keychain and must redact payloads. + +## Router audit + +The existing TP-Link Deco/mesh router is adequate for the initial work. No +router setting needs to change before the first observations. + +We need only DHCP, a shared reachable network, and ideally a way to view the +client list. Band steering and a single combined SSID are acceptable. A separate +Guest/IoT SSID is optional; on some Deco modes it isolates clients, which would +prevent Mac-to-K1 traffic. If a separate SSID is later used, peer-to-peer reachability +must be tested before provisioning K1 onto it. + +The main-LAN fallback is acceptable for a narrow, target-filtered experiment. +It must not include a broad scan or unfiltered capture of household traffic. + +## Environment audit + +Observed host baseline: + +- Apple Silicon (`arm64`); +- macOS 26.5.1; +- `uv` and Homebrew present; +- system Python is 3.13.5, while the project requires 3.12; +- `tcpdump` and `ffmpeg` present; +- `tshark` and `nmap` not currently found. +- the current default route is a VPN tunnel (`utun`), while Wi-Fi is `en0`. + +The project therefore pins Python 3.12 via `.python-version`; `uv` creates a +local `.venv`. Nothing in setup installs global Python packages. Missing external +network-analysis tools are reported by `k1link doctor` and are not installed +until their gate requires them and the change is explicitly accepted. +Future network commands must determine the route for the confirmed K1 IP; +neither the default route nor `en0` may be hardcoded. + +## Primary blockers + +| Gate | Blocking condition | Meaning | +|---|---|---| +| Device | fault, activation lock, no autonomous project | Stop before networking | +| BLE | no advertisement or only inaccessible/authenticated surface | Investigate state/permissions before writes | +| Wi-Fi | no remembered network/AP and opaque custom GATT | Provisioning research is the hard block | +| Data session | IP exists but service requires unknown token/handshake | Association succeeded; connector has not | +| Streams | session opens but no useful external stream | Realtime goal may be unsupported | +| Camera | point cloud works but frames never appear | Camera branch may be unsupported; not an MVP failure | + +## Implementation consequence + +The correct order is: + +```text +device baseline + -> autonomous project + USB metadata + -> passive BLE surface + -> remembered Wi-Fi / device AP check + -> evidence-led provisioning gate + -> application-session gate + -> targeted capture + -> flow classification + -> point cloud / pose / status / optional camera decoders +``` + +## Verified primary references + +- [XGRIDS K1 firmware and release notes](https://www.xgrids.com/intl/support/download?page=K1) +- [XGRIDS LixelStudio](https://www.xgrids.com/intl/lixelstudio) +- [XGRIDS LixelStudio releases](https://www.xgrids.com/intl/support/download?page=LixelStudio) +- [XGRIDS device activation and connection](https://docs.xgrids.com/en-us/02-lingguang-k/01-lingguang-k1/v2.4.0/03-device-activation-and-connection.html) +- [Apple packet-trace guidance](https://developer.apple.com/documentation/network/recording-a-packet-trace) +- [Bleak macOS backend](https://bleak.readthedocs.io/en/latest/backends/macos.html) +- [TP-Link Deco Guest Network behavior](https://www.tp-link.com/ca/support/faq/1460/) +- [TP-Link Deco IoT Network](https://www.tp-link.com/us/support/faq/4420/) +- [uv project environments](https://docs.astral.sh/uv/guides/projects/) + +Reference-document statements not independently repeated here remain inputs or +hypotheses, not verified conclusions of this audit. diff --git a/docs/01_IMPLEMENTATION_PLAN.md b/docs/01_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..4a4dcbf --- /dev/null +++ b/docs/01_IMPLEMENTATION_PLAN.md @@ -0,0 +1,188 @@ +# Implementation plan + +This plan supersedes the app-dependent experiment order in the reference Bible. +Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result. + +## Stage 0 — repository and host baseline + +Deliverables: + +- local Python 3.12 `.venv` and lock-file; +- `k1link doctor` with no side effects; +- immutable reference documents and checksums; +- safety policy, artifact policy, session manifest schema; +- no Homebrew changes and no K1 interaction. + +Acceptance: + +- `uv sync --group dev` succeeds; +- `uv run k1link doctor` identifies a local `.venv`; +- tests, Ruff and mypy pass; +- repository contains no secrets or real captures. + +## Gate 1 — physical and USB baseline + +Code needed: none initially. + +Actions: + +1. Record masked device identity, LED boot sequence, battery and visible state. +2. Determine whether the unit is activated; solid-yellow or an activation prompt + is treated as a distinct blocker. +3. Run one short autonomous button scan: start, keep still for at least 20 seconds, + move through a simple scene, stop. +4. Enter USB mode using the documented physical shortcut if available. +5. Copy a project tree and selected metadata/logs into an ignored session path. +6. Hash every copied file and create a redacted manifest. + +GO: the unit boots normally and records a usable autonomous project. + +PAUSE: USB access is unavailable but the scan succeeds; continue BLE while +documenting the missing ground truth. + +STOP: serious fault, overheating, activation lock, or destructive/ambiguous +device state. + +## Stage 1 — BLE read-only toolkit + +Minimal dependencies: `bleak` plus the existing CLI stack. Add them only in the +repository-local environment. + +Commands: + +```text +k1link ble scan +k1link ble gatt-dump +k1link ble listen +``` + +Requirements: + +- preserve advertisement snapshots, manufacturer/service data and RSSI; +- identify by observed name + macOS UUID + advertisement fingerprint; +- inspect standby, scanning and USB states separately; +- start with standard Device Information/Battery services; +- custom characteristic reads are opt-in because proprietary reads may have + side effects; +- notifications are subscribed one characteristic at a time and logged to + timestamped JSONL; +- no arbitrary characteristic or provisioning writes in this stage. Enabling a + notification normally updates the standard CCCD descriptor; this temporary, + reversible protocol write is disclosed and handled separately from device + configuration. + +GO: reproducible K1 advertisement and useful GATT map. + +PAUSE: permission or pairing/authentication is required. + +## Gate 2 — existing network behavior + +Do not change Deco settings first. + +Actions: + +1. Save the Mac interface and route table. +2. Observe router client list and Mac ARP/neighbor state with K1 off. +3. Repeat after K1 boot, during standby, scanning and after one power cycle. +4. Check for new Wi-Fi SSIDs exposed by K1 without attempting to join unknown + networks automatically. +5. Confirm any candidate by disappearance/reappearance with K1 power state. + +GO-A: K1 joins a remembered LAN. Provisioning leaves the immediate critical +path and work moves to the application-session gate. + +GO-B: K1 exposes its own AP. Document it as a separate topology and determine +whether Mac can join without modifying router configuration. + +BLOCKED: neither association nor AP exists; provisioning becomes Stage 2. + +## Stage 2 — no-app provisioning research + +This stage answers whether SSID/PSK can be sent safely from Mac. A generic BLE +transport does not define the vendor payload. + +Evidence sources allowed before a write: + +- GATT UUIDs, properties and descriptors; +- standard service identification; +- read/notify state transitions correlated with network state; +- owned K1 project logs and metadata; +- public official firmware/documentation static evidence if separately reviewed; +- deterministic structure inferred without sending device mutations. + +Before enabling any write profile, document: + +- exact service/characteristic and required security mode; +- packet framing, encoding, ordering and length rules; +- SSID/PSK redaction and secure input path; +- checksum, nonce, token, certificate or signature fields; +- commit/connect command and status response; +- timeout, failure state, power-cycle behavior and rollback; +- one explicit confirmation flag in addition to a named reviewed profile. + +GO: a deterministic provisioning profile exists with an explained expected +state transition and safe rollback. + +HARD BLOCK: K1 knows no network, exposes only opaque vendor GATT, and no evidence +source reveals the protocol. Random writes are not an alternative. + +## Stage 3 — application session and targeted capture + +Prerequisite: confirmed K1 IP or direct AP topology. + +Implementation order: + +1. Passive ARP/mDNS/SSDP and K1-filtered `tcpdump` baseline. +2. Small targeted TCP connect check against the confirmed IP. +3. Service detection only where the prior step supplies evidence. +4. Parse K1 logs for daemon names, ports and protocol strings. +5. Add protocol-specific connection attempts only for demonstrated endpoints. +6. Model `WIFI_ASSOCIATED_BUT_NO_DATA_SESSION` explicitly. + +`k1link net capture` must show the exact interface/filter, request privilege in +the foreground, handle Ctrl+C, and never configure persistent `sudo` or ChmodBPF. + +GO: Mac becomes a K1 endpoint or receives a reproducible scan-correlated flow. + +BLOCKED: application session requires an unavailable mutual-auth token, +certificate or undocumented handshake. + +## Stage 4 — session artifacts and flow analysis + +Implement only after real traffic exists: + +- versioned session manifest; +- UTC and monotonic timeline events; +- SHA-256 inventory; +- TCP reassembly and UDP flow grouping; +- packet/byte rates, length histograms, entropy and first-byte fingerprints; +- idle versus scanning correlation; +- negative-control flows to reduce false positives. + +GO: repeatable flows can be separated and ranked without losing raw evidence. + +## Stage 5 — decoders by evidence priority + +Priority: + +1. device/scan status; +2. point-cloud preview; +3. pose/trajectory; +4. camera/panorama/video. + +Add dependencies per decoder extra, not to the base environment. Point-cloud +and video signature probes operate on reassembled flows, not isolated packet +payloads. A candidate becomes a decoder only after a controlled physical test +confirms it. + +MVP GO: K1-to-Mac scan-correlated data can be captured reliably and at least one +useful stream is decoded or structurally identified. + +## Deferred work + +- ROS2/MCAP bridge; +- onboard computer and drone integration; +- automated scan-button electronics; +- OpenWrt/monitor-mode infrastructure; +- firmware or internal-Linux analysis; +- camera branch if no external frame stream is evidenced. diff --git a/docs/02_FIRST_LAB_RUNBOOK.md b/docs/02_FIRST_LAB_RUNBOOK.md new file mode 100644 index 0000000..ee4681c --- /dev/null +++ b/docs/02_FIRST_LAB_RUNBOOK.md @@ -0,0 +1,134 @@ +# First lab runbook + +Purpose: gather decisive evidence using only K1, the current MacBook and the +existing TP-Link Deco/mesh network. This runbook contains no BLE writes, router +changes, broad LAN scans or system package installation. + +## Before powering K1 + +- Place K1 on a stable, ventilated surface. +- Prepare its normal battery/charger and a known data-capable USB cable. +- Ensure enough Mac disk space for one copied project and short captures. +- Do not expose the Wi-Fi password in notes, terminal arguments or screenshots. +- If the router model becomes visible in the Deco UI, record only the model and + operating mode; do not change settings yet. + +Create an ignored session directory outside ordinary Git, for example: + +```text +sessions/20260715T120000Z_gate1_baseline/ +``` + +The operator notes should use masked serials and relative timestamps. + +## Experiment A — boot and activation state + +1. Start a phone/Mac video focused on the LED if convenient. +2. Power K1 on normally. +3. Record LED colors and durations until stable. +4. Record whether the final state is normal standby, activation warning or fault. +5. Stop immediately on serious fault, unusual heat, smell, battery problem or + undocumented state. + +Expected output: `notes.md`, optional local video, and a redacted manifest. + +## Experiment B — autonomous project + +1. From normal standby, start scanning with the documented double-click. +2. Keep K1 completely still for at least 20 seconds. +3. Move slowly through a simple scene for about one minute. +4. Stop scanning with the documented double-click and wait for stable standby. +5. Enter USB mode using the physical shortcut only if its state is understood. +6. Copy the project tree, `project.json`, `poses.csv`, `map.las` metadata and logs + when present. Raw project data remains ignored and local. +7. Generate a SHA-256 inventory; commit only a redacted report later. + +Decision: + +- GO if a valid autonomous project exists. +- PAUSE USB analysis if recording works but USB is unavailable. +- STOP on activation lock or device fault. + +## Experiment C — passive BLE surface + +Prerequisite: Stage 1 BLE commands implemented and reviewed. + +1. Run a 30-second scan in standby. +2. Save advertisement and manufacturer/service data. +3. Connect for service discovery without writes. +4. Read standard device-information/battery characteristics first. +5. Repeat advertisement/GATT observation during scanning and USB mode. +6. Subscribe to an individually selected notify/indicate characteristic only + after its properties are recorded. + +Decision: + +- GO if K1 has a reproducible BLE identity and observable surface. +- PAUSE if macOS asks for pairing/authentication that cannot be completed. + +## Experiment D — current network, no router changes + +1. Capture the Mac route/interface and its current neighbor table with K1 off. +2. Note the Deco client list if accessible. +3. Boot K1 and repeat in standby. +4. Repeat during a button scan. +5. Power-cycle K1 and confirm whether any candidate disappears/reappears. +6. Check visible SSIDs for a K1-associated AP. + +Do not infer identity from one unknown LAN client. Require power-state +correlation or an independent device fingerprint. + +Decision: + +- remembered-LAN GO: confirmed K1 address appears; +- device-AP GO: a repeatable K1 SSID appears; +- provisioning BLOCKED: neither appears and no known GATT provisioning profile + exists. + +## About Guest/IoT networks on Deco + +Do not create one during this first run. Depending on Deco model and Router/AP +mode, Guest/IoT networks may isolate clients from the main LAN or from each +other. A later dedicated SSID is useful only if both K1 and Mac can communicate +and its password can be entered through a known provisioning profile. + +## Minimum lab report + +```markdown +# K1 Lab Report 001 + +## Host +macOS: +Mac model/architecture: +Router model/mode (if known): + +## Device +Model: +Serial suffix only: +Activation/LED state: +Firmware evidence: + +## Autonomous scan +Start/stop behavior: +Project created: +USB accessible: +Files observed: + +## BLE +Advertised name: +macOS UUID: [redacted] +Service fingerprint: +Pairing/auth prompts: + +## Network +Remembered LAN candidate: +Device AP candidate: +Confirmed K1 IP: [redacted] + +## Decision +Current gate: +GO / PAUSE / BLOCKED: +Evidence: +Next smallest experiment: +``` + diff --git a/docs/03_ARTIFACT_POLICY.md b/docs/03_ARTIFACT_POLICY.md new file mode 100644 index 0000000..01dba9f --- /dev/null +++ b/docs/03_ARTIFACT_POLICY.md @@ -0,0 +1,60 @@ +# Artifact and secret policy + +The experiment should be reproducible from Git without placing household +credentials, spatial maps or device secrets into normal Git history. + +## Sensitive by default + +- Wi-Fi SSID and PSK; +- full K1 serial, BLE UUID, MAC/IP addresses and router client lists; +- provisioning payloads, tokens, nonces, certificates and session keys; +- PCAP/PCAPNG, Bluetooth logs and extracted flows; +- K1 logs, `project.json`, `.xbin`, LAS/point clouds and trajectories; +- camera frames, previews, panoramas and mapped interiors; +- command lines or shell history containing a credential. + +Private Git is still durable replicated storage. Git LFS changes storage and +size behavior; it does not provide confidentiality. + +## Commit to normal Git + +- source, tests and lock-file; +- immutable reference documents and their hashes; +- schemas, runbooks, ADRs and redacted reports; +- synthetic or explicitly redacted fixtures; +- redacted manifests and SHA-256 inventories; +- commands needed to reproduce analysis from a locally supplied artifact. + +## Keep outside normal Git + +- `.venv`, local config and Keychain references; +- actual credentials; +- real captures and decoded output; +- complete K1 project copies; +- unredacted device and router metadata. + +## Session integrity + +Every real session should record: + +- schema version and unique session ID; +- tool Git commit; +- UTC start/end and Mac monotonic start/end; +- masked device/host/network identities; +- physical state timeline; +- exact capture filter and interface; +- artifact path, size and SHA-256; +- redaction status and storage location classification; +- operator notes and final GO/PAUSE/BLOCKED decision. + +Raw artifacts are never silently deleted by code. That does not imply infinite +retention: capacity, encryption, backup and deliberate deletion policy are an +operator decision outside the capture command. + +## Future provisioning secret input + +The Wi-Fi password must not be accepted as a normal CLI argument. A reviewed +provisioner should use a hidden prompt, stdin/file descriptor with strict +permissions, or macOS Keychain. Logs and manifests record only that a credential +was supplied, never its value or unredacted payload bytes. + diff --git a/docs/adr/0001-actual-stand-and-no-lixelgo.md b/docs/adr/0001-actual-stand-and-no-lixelgo.md new file mode 100644 index 0000000..25bf5ba --- /dev/null +++ b/docs/adr/0001-actual-stand-and-no-lixelgo.md @@ -0,0 +1,32 @@ +# ADR 0001: Treat LixelGO as unavailable + +Status: accepted. + +## Context + +The actual stand consists of one K1, one MacBook and one ordinary TP-Link +Deco/mesh router. There is no phone, LixelGO reference session, Linux host, +dedicated capture router or public SDK. + +The supplied design documents include several app-dependent experiments and +assume that provisioning payloads can be recovered by comparing LixelGO BLE +writes. That evidence source does not exist. + +## Decision + +- LixelGO is not a temporary dependency, broker or test oracle. +- App-dependent experiments remain only as historical reference. +- The active critical path is physical baseline, passive BLE, remembered Wi-Fi + or device AP, evidence-led provisioning, application-session discovery, then + stream capture and decoding. +- A fully opaque provisioning protocol with no evidence source is reported as a + hard black-box blocker rather than approached with random writes. +- The existing router is used without changes for the first observation gate. + +## Consequences + +The project may reach a legitimate BLOCKED result before receiving live data. +This is still a successful pre-production conclusion because it identifies the +exact missing interface and prevents premature decoder work or risky device +mutation. + diff --git a/docs/reference/CODEX_K1_MAC_LINK_PREPROD_PROMPT.md b/docs/reference/CODEX_K1_MAC_LINK_PREPROD_PROMPT.md new file mode 100644 index 0000000..fd26f38 --- /dev/null +++ b/docs/reference/CODEX_K1_MAC_LINK_PREPROD_PROMPT.md @@ -0,0 +1,220 @@ +# Codex prompt: k1-mac-link + +Ты работаешь над проектом `k1-mac-link`. + +## Контекст + +Есть официальный XGRIDS / LixelKity K1, MacBook, Wi-Fi router. Linux как отдельного стенда нет. Цель - использовать K1 как black-box realtime computer vision sensor без модификации firmware и без вскрытия на первом этапе. + +Нужны realtime/cached данные: + +- point cloud / preview cloud; +- pose / trajectory / odometry-like stream, если есть; +- camera / panoramic preview / RGB stream, если есть; +- device status; +- raw packets для reverse engineering. + +Не нужно в runtime: + +- считать LAS; +- считать LCC; +- считать Gaussian splats; +- менять firmware; +- делать brute force; +- слать random BLE writes. + +## Главная архитектура + +Официальные мануалы K1 показывают такую модель: + +- BLE используется для discovery/provisioning; +- SSID/password - это реквизиты нашей Wi-Fi сети; +- после подключения к Wi-Fi есть отдельный `data connection status`; +- K1 умеет автономно стартовать scanning double-click кнопкой; +- K1 пишет `map.las` как realtime point cloud output, но downsampled; +- K1 пишет `poses.csv` как trajectory recorded during scanning; +- K1 содержит 2 panoramic cameras и 360 deg LiDAR; +- live camera stream наружу не доказан, его надо искать экспериментально. + +## Создай проект + +Python 3.12, macOS-first, Typer CLI. + +Структура: + +```text +k1-mac-link/ + README.md + pyproject.toml + docs/ + src/k1link/ + cli.py + config.py + logging.py + artifacts.py + ble/ + scanner.py + gatt_dump.py + listener.py + provisioner.py + net/ + discover.py + ports.py + capture.py + mdns.py + tshark.py + analysis/ + pcap_summary.py + flow_classifier.py + payload_extract.py + payload_fingerprint.py + protocol_diff.py + pointcloud_probe.py + video_probe.py + timeline.py + decode/ + pointcloud.py + pose.py + video.py + status.py + cache/ + writer.py + manifest.py + raw_stream.py + export/ + ply.py + pcd.py + npz.py + frames.py + tests/ +``` + +## Dependencies + +Use: + +```text +bleak +typer +rich +pydantic +pydantic-settings +loguru +pyshark +scapy +construct +numpy +pandas +opencv-python +open3d +pillow +zeroconf +ifaddr +psutil +pytest +pytest-asyncio +ruff +mypy +``` + +## CLI commands + +Implement: + +```bash +k1link doctor +k1link ble scan --duration 30 --out captures/ble_scan.json +k1link ble gatt-dump --device --out captures/gatt_dump.json +k1link ble listen --device --duration 120 --out captures/ble_notifications.jsonl +k1link net discover --subnet 192.168.1.0/24 --out captures/net_discover.json +k1link net ports --ip --out captures/ports.json +k1link net capture --iface en0 --host --out captures/session.pcap +k1link analyze summarize --pcap captures/session.pcap --out captures/session_summary.json +k1link analyze extract-flows --pcap captures/session.pcap --out-dir captures/flows +k1link analyze fingerprint --flows captures/flows --out captures/fingerprint.json +k1link analyze pointcloud-probe --flows captures/flows --out-dir captures/pointcloud_candidates +k1link analyze video-probe --flows captures/flows --out-dir captures/video_candidates +``` + +## Safety rules + +Allowed by default: + +- BLE scan; +- GATT discovery; +- read characteristics; +- notifications; +- network discovery; +- port scanning of owned device; +- tcpdump capture; +- pcap analysis. + +Forbidden by default: + +- BLE write; +- random fuzzing; +- brute force; +- firmware upload; +- deleting/modifying K1 files; +- SSH/ADB login attempts. + +Any BLE write must require: + +```bash +--i-understand-this-writes-to-device +``` + +Provisioner stays NotImplemented until a known payload profile is manually added. + +## Outputs + +Every session creates: + +```text +session_manifest.json +notes.md +captures/*.pcap +captures/*.json +flows/*.bin +flows/*.jsonl +analysis/*.json +decoded/pointcloud/* +decoded/video/* +decoded/pose/* +``` + +Raw captures are never deleted. + +## Implement pointcloud probe + +Try: + +- float32 little-endian XYZ; +- offsets 0..128; +- strides 12, 16, 20, 24, 28, 32, 40, 48; +- scaled int16/int32 fallback; +- score finite ratio, coordinate range, variance, temporal continuity; +- export PLY/NPZ for top candidates. + +## Implement video probe + +Detect/extract: + +- JPEG FF D8 FF ... FF D9; +- PNG signature; +- H264/H265 Annex B; +- MP4 ftyp/moof/mdat; +- RTSP/RTP; +- MJPEG; +- WebSocket binary large frames. + +Try ffmpeg/OpenCV extraction where possible. + +## README must include + +- macOS setup; +- Wireshark ChmodBPF note; +- Bluetooth permission note; +- Bleak macOS UUID caveat; +- K1 lab network setup; +- experiment flow: idle capture, button scan capture, app scan capture, camera perturbation, point cloud perturbation. diff --git a/docs/reference/README.md b/docs/reference/README.md new file mode 100644 index 0000000..89e7696 --- /dev/null +++ b/docs/reference/README.md @@ -0,0 +1,15 @@ +# Reference inputs + +These files are immutable copies of the user-supplied pre-production material. +They are retained as inputs, not treated as verified specifications. + +| File | SHA-256 | Role | +|---|---|---| +| `CODEX_K1_MAC_LINK_PREPROD_PROMPT.md` | `f24537119bd76f76d1914ca7f34a6736d8c5d3b6d19ead6d0ae8a0e5966c9770` | Requested project shape and CLI scope | +| `XGRIDS_Lixel_K1_PreProduction_Bible.md` | `22ea9d0dc6f5a623ba26d6f08108a905170db08112e0531a1d80bddd6101689c` | OSINT dossier, hypotheses, experiments and long-form design | + +Important contextual correction: the actual stand has no LixelGO and no mobile +device. All experiments in the source documents that require an app or a +reference app session are unavailable. The active plan is therefore defined by +`docs/01_IMPLEMENTATION_PLAN.md`. + diff --git a/docs/reference/XGRIDS_Lixel_K1_PreProduction_Bible.md b/docs/reference/XGRIDS_Lixel_K1_PreProduction_Bible.md new file mode 100644 index 0000000..020de22 --- /dev/null +++ b/docs/reference/XGRIDS_Lixel_K1_PreProduction_Bible.md @@ -0,0 +1,2544 @@ + +# XGRIDS / LixelKity K1 как realtime CV-сенсор: pre-production Bible + +Документ: инженерный аудит и ТЗ для пилота на macOS +Версия: 1.0 +Дата: 2026-07-15 +Цель: использовать имеющийся официальный XGRIDS / LixelKity K1 как black-box сенсор для computer vision, realtime-кэша LiDAR/pose/camera данных и дальнейшей интеграции с робототехническим стеком. + +## 0. Зачем этот документ существует + +Предыдущий короткий документ был слишком верхнеуровневым. Этот документ фиксирует не итоговую выжимку, а полную pre-production карту: факты из мануалов, OSINT по железу, сетевую модель, гипотезы, эксперименты, план reverse engineering штатных интерфейсов, macOS-инструментарий и ТЗ на разработку `k1-mac-link`. + +Фокус проекта: + +- получить realtime point cloud / preview cloud; +- получить route / poses / trajectory / odometry-like stream, если он существует в live-сессии; +- получить realtime camera / panoramic preview / RGB stream, если он реально передается наружу; +- писать raw traffic и decoded данные на SSD; +- оставить штатную запись на K1 как fallback; +- не считать LAS/LCC/3DGS в runtime; +- не менять firmware на первом этапе; +- не вскрывать устройство на первом этапе. + +Гражданские сценарии применения: инспекция, картография, навигационная осведомленность, локальное построение карты препятствий, запись исследовательских датасетов, офлайн-анализ собственных данных. + +## 1. Главная концепция решения + +### 1.1. Что хотим получить + +Идеальный runtime: + +```text +XGRIDS LixelKity K1 + -> Wi-Fi / local network + -> собственный macOS-клиент + -> realtime parser + -> SSD raw cache + -> decoded cache + -> OpenCV / ML / ROS2-compatible bridge +``` + +Важный принцип: мы не пытаемся сразу заменить внутренний SLAM K1. Наоборот, мы хотим использовать то, что K1 уже умеет внутри: real-time mapping, realtime point cloud output, собственную локальную запись проекта и работу камер. Наша задача - стать внешним клиентом, который получает те же или близкие live-данные, которые получает LixelGO. + +### 1.2. Минимально полезный результат + +Минимальный полезный результат пилота: + +```text +MacBook видит K1 по BLE +MacBook видит K1 в Wi-Fi сети +MacBook умеет снимать PCAP с K1-сессии +MacBook выделяет candidate live streams +MacBook пишет raw packets на SSD +``` + +Это уже полезно, потому что превращает неизвестную систему в измеряемую. + +### 1.3. Практический результат уровня MVP + +MVP: + +```text +MacBook без LixelGO открывает data/control session +получает realtime point cloud или stream-кандидат +пишет raw stream на SSD +экспортирует decoded-кандидаты в PLY/NPZ/PCD +``` + +### 1.4. Максимальный результат + +Максимальный результат первого производственного этапа: + +```text +BLE provisioning без LixelGO +Wi-Fi data connection без LixelGO +realtime point cloud decoder +realtime pose/status decoder +camera/video/panoramic preview decoder, если поток доступен +session cache на SSD +OpenCV input / ROS2-compatible bridge +``` + +### 1.5. Что не входит в первый этап + +Не входит: + +- прошивка K1; +- root/jailbreak; +- стирание internal storage; +- изменение factory calibration; +- вскрытие LiDAR/camera узла; +- пересчет LAS, LCC, Gaussian splats; +- попытки обойти лицензии или облачные функции XGRIDS. + +## 2. Опорные факты из официальной документации K1 + +### 2.1. Что K1 представляет собой физически + +Официальный Product Overview описывает Lixel K1 как компактное handheld real-time 3D reconstruction устройство весом менее 1 кг. В нем указаны два ключевых сенсорных блока: `48 MP x2 panoramic vision` и `360 deg LiDAR`, которые используются для генерации цветных моделей в реальном времени. Источник: [S01]. + +Для нашего проекта это означает: + +- камеры внутри действительно есть; +- LiDAR внутри действительно 360-градусный; +- устройство уже выполняет real-time reconstruction / mapping; +- телефон, скорее всего, не делает тяжелую реконструкцию, а отображает поток/результаты устройства. + +### 2.2. Физическая кнопка: autonomous mode без телефона + +Официальный Basic Operation фиксирует аппаратную кнопку и состояния: + +| Действие | Операция | Состояние | +|---|---|---| +| Power On | удерживать 4 секунды | индикатор из slow-flashing blue переходит в solid green, standby | +| Power Off | удерживать 4 секунды в standby | система сохраняет данные и выключается | +| Start Scanning | double-click в standby | solid green -> fast-flashing green -> slow-flashing green, LiDAR начинает вращаться | +| Stop Scanning | double-click в scanning mode | slow-flashing green -> fast-flashing green -> solid green, LiDAR останавливается | +| Control Point | single-click во время скана | контрольная точка записана | +| USB Mode | single-click -> white LED -> single-click | переключение standby/USB mode | + +Источник: [S02]. + +Ключевой вывод: K1 способен запускать и останавливать скан кнопкой. Для старта записи LixelGO не обязателен. Для проекта это критично: можно оставить физический запуск, а data-клиент делать отдельно. + +### 2.3. LED state machine + +| LED | Значение | +|---|---| +| Off | устройство выключено | +| Slow-flashing green | scanning mode | +| Solid green | standby | +| Solid blue | USB mode | +| Solid yellow | device not activated | +| Solid red | serious fault | +| Slow-flashing blue около 30 секунд | power on | +| Solid white | switching standby/USB | +| Fast-flashing green | starting/stopping scan | +| Alternating red/green | firmware upgrade | + +Источник: [S02]. + +Для тестового стенда это внешний state machine. Даже без доступа к протоколу можно синхронизировать эксперименты по LED. + +### 2.4. USB mode и локальная запись + +В Basic Operation указано, что для передачи данных используется USB 3.1 cable; устройство нужно перевести в USB mode через LixelGO или shortcut key. После перезапуска USB mode отключается; если нужно снова сканировать, USB mode надо выключить вручную или перезапустить устройство. Источник: [S02]. + +Практический вывод: + +- USB mode - канал для выгрузки проектов; +- во время USB mode сканировать нельзя или нужно выйти из режима; +- для realtime CV USB mode не является главным каналом; +- USB нужен как offline fallback для проверки того, что K1 записал внутри. + +### 2.5. Thermal / mechanical ограничения + +Basic Operation также говорит: + +- устройство - precision surveying/mapping device; +- удары/падения могут повредить устройство или ухудшить точность; +- IP54; +- LiDAR и lens надо держать чистыми; +- нельзя закрывать heat dissipation area; +- при перегреве устройство может выключиться. + +Источник: [S02]. + +Для БПЛА/робототехники: до любых полетов нужна стендовая проверка вибрации, вентиляции и тепла. На первом этапе не крепить так, чтобы перекрывать тепловые зоны. + +## 3. Официальная модель подключения LixelGO + +### 3.1. Роль LixelGO + +Device Activation and Connection говорит, что K1 подключается, активируется и управляется через LixelGO. LixelGO поддерживает Wi-Fi и 5G/4G real-time transmission, true-color/elevation switching, viewing/managing projects. Источник: [S03]. + +Для нас это не просто описание приложения. Это указатель на архитектуру: приложение является внешним control/data client. + +### 3.2. Direct Connect Mode + +В Direct Connect Mode официальная последовательность такая: + +1. включить Bluetooth телефона; +2. найти Bluetooth устройства; +3. подключиться к Bluetooth устройства; +4. включить hotspot телефона; +5. ввести hotspot name и password; +6. нажать Configure; +7. устройство само подключается к hotspot; +8. дождаться `network connection status` и `data connection status` = success; +9. нажать Done и смотреть basic device information. + +Источник: [S03]. + +Выводы: + +- Bluetooth используется как provisioning channel; +- SSID/password - это реквизиты Wi-Fi сети пользователя, а не магический заводской секрет; +- после Wi-Fi подключения есть отдельная `data connection`, то есть второй handshake / data session; +- заменить LixelGO значит воспроизвести минимум два слоя: BLE provisioning и Wi-Fi data session. + +### 3.3. Bridge Mode + +Bridge Mode в официальной документации описан как режим, где сначала выполняется поиск Bluetooth устройства, потом network configuration/verification, после правильного подключения приложение показывает basic device information. Источник: [S03]. + +Для нашей задачи Bridge Mode - основной режим: + +```text +K1 -> подключается к нашей Wi-Fi сети +MacBook -> в той же Wi-Fi сети +MacBook -> data/control client +``` + +Это лучше, чем phone hotspot, потому что телефон не должен быть частью runtime. + +### 3.4. Quick Connect Mode + +Документация говорит: + +- Android LixelGO с firmware V3.2.1 использует Quick Connect Mode по умолчанию; +- New Quick Connect Mode требует firmware >= 3.0; +- iOS Quick Connect Mode используется по умолчанию с firmware V3.2.1 и LixelGO V1.3.0; +- для iPhone имя устройства используется как hotspot SSID и должно быть без пробелов/спецсимволов. + +Источник: [S03]. + +Вывод: Quick Connect может иметь device-specific логику, но для исследовательского pilot лучше начинать с Bridge Mode/Direct Connect, где SSID/password явно задаются пользователем. + +### 3.5. Что такое SSID/password в этом проекте + +SSID - имя нашей Wi-Fi сети. Password - пароль нашей Wi-Fi сети. + +Пример лабораторной сети: + +```text +SSID: K1_LAB +Password: K1lab123456 +Band: 5 GHz сначала, 2.4 GHz как fallback +Client isolation: off +DHCP: on +``` + +Вероятность, что в Bridge/Direct Mode нужен огромный заводской список `serial -> password`, низкая, потому что официальная схема прямо говорит: пользователь вводит hotspot name/password, а приложение передает их устройству. + +При этом нельзя исключать app-layer token/session token для `data connection`. Это другая сущность, не Wi-Fi пароль. + +## 4. Scanning Workflow и важные детали для CV + +### 4.1. Time sync + +Scanning Workflow говорит, что device time откалиброван на заводе, но после долгого хранения может дрейфовать; LixelGO по умолчанию синхронизирует время телефона с устройством, а также позволяет задавать время вручную. Источник: [S04]. + +Для realtime CV это важно: + +- timestamps в project files могут зависеть от device time; +- custom клиент должен либо уметь sync time, либо записывать offset между Mac monotonic time и K1 device time; +- все captures должны иметь `session_start_wall_time`, `mac_monotonic_start`, `k1_reported_time`, если его удастся прочитать. + +### 4.2. Mount Type включает Drone + +Официальный Scanning Workflow перечисляет Mount Type: Handheld, Vehicle-Mounted, Drone, Backpack. LixelStudio читает mount type автоматически. Источник: [S04]. + +Для нашего проекта это две подсказки: + +1. XGRIDS уже предусматривает drone-like сценарий. +2. Если стартовать скан кнопкой, надо проверить, какой mount type реально попадает в `project.json`. + +Вопрос для эксперимента: автономный кнопочный старт использует last-used settings или default settings? + +### 4.3. Static initialization 15 секунд + +Официально: после старта скана LiDAR начинает работать, LED становится fast-flashing green, начинается static initialization countdown на 15 секунд; в это время устройство нельзя двигать. Источник: [S04]. + +Для стенда: + +- все эксперименты делаются так: нажали Start, ждем 20 секунд неподвижно, только потом двигаем; +- для БПЛА: старт/инициализация на земле, потом движение; +- любые пакеты первых 15-20 секунд помечать как initialization phase. + +### 4.4. Project file structure и значение каждого файла + +Официальная структура проекта K1: + +| Файл/папка | Официальное описание | Значение для нас | +|---|---|---| +| `xxx.xbin` | raw sensor data recorded by the device | offline raw fallback; не runtime target | +| `map.las` | point cloud output by the device in real time | главное доказательство, что realtime cloud pipeline есть внутри K1 | +| `control_points.csv` | control point info | проверка single-click events | +| `gnss.csv` | GNSS info when using RTK | потенциальный route/geo fallback | +| `poses.csv` | trajectory file recorded during scanning | offline pose ground truth для поиска live pose stream | +| `project.json` | device attributes and related information | настройки scan mode, firmware, mount, metadata | +| `model` | in-app model preview file | возможно связано с live preview/data chunks | +| `photo` | camera image at moment a control point is added | доказательство доступа к камере, но не continuous video | +| `preview_photo.jpg` | project preview image | preview fallback | +| `log` | scanning logs | источник протоколов, ошибок, IP/ports, daemon names | +| `external_data` | files for LixelStudio post-processing | не runtime target | + +Источник: [S04]. + +Критическое замечание из официального мануала: direct point cloud data is downsampled; полный point cloud делается через LixelStudio. Источник: [S04]. + +Для CV на борту downsampled cloud может быть даже полезнее полного: меньше трафика, меньше latency, меньше CPU. + +## 5. Что подтверждают LixelGO App Store / Google Play + +### 5.1. Real-time point cloud data существует + +App Store описание говорит, что LixelGO позволяет подключаться к XGRIDS LiDAR scanners, управлять устройствами, менять настройки и просматривать real-time point cloud data. Источник: [S05]. + +Google Play описание говорит про real-time scanner control, live data preview и device monitoring. Источник: [S06]. + +Это подтверждает, что live data path существует как продуктовая функция. + +### 5.2. RGB point cloud rendering и camera hints + +В App Store version history есть пункты: + +- RGB point cloud rendering; +- scanning guidance & range indicator; +- real-time quality feedback; +- fixed issue where real-time point clouds were not displayed on some Samsung Exynos phones; +- fixed occasional camera loading failures; +- support for mount mode AIR Drone; +- external power status display for vehicle-mounted or airborne power supply. + +Источник: [S05]. + +Интерпретация: + +- приложение не просто получает final LAS; +- есть live render layer; +- есть RGB point cloud, возможно цвет уже интегрирован в cloud preview; +- `camera loading failures` может означать отдельный camera/preview component, но это не доказательство raw 360 video stream; +- AIR Drone и airborne power status полезны для нашей постановки, но не доказывают open API. + +### 5.3. Риск шифрования + +Google Play data safety указывает `Data is encrypted in transit`. Источник: [S06]. + +Это может означать: + +- HTTPS/TLS для облака/аккаунта; +- TLS/QUIC для локального device data; +- app-layer encryption; +- общую декларацию магазина. + +Вывод: в плане обязательно считать шифрование реальным риском. PCAP может показать endpoints, bitrate и session structure, но не payload. + +## 6. Сторонний полевой гайд: связь теряется, запись продолжается + +Гайд Alpine Reality Capture по LixelGO сообщает важную практическую деталь: если связь телефона со сканером потеряна, сканер продолжит recording, но real-time preview и app-контроль будут потеряны. Там же Bridge Mode описан как режим, где телефон и сканер находятся в одной Wi-Fi сети, а приложение общается со сканером по этой shared network. Источник: [S07]. + +Это не официальный K1 manual, но очень хорошо согласуется с официальной архитектурой: + +```text +запись проекта идет на K1 +телефон - control/preview client +при потере клиента recording остается +preview/control теряются +``` + +Для runtime без LixelGO это значит: + +- нельзя рассчитывать на reconnect LixelGO в качестве надежной архитектуры; +- нужен собственный постоянно работающий client на борту; +- кнопочный старт + локальная запись остаются fallback даже если live stream упал. + +## 7. Железо K1 по FCC / OSINT + +### 7.1. FCC ID и радиодиапазоны + +FCC ID: `2A9PI-K1`. FCC page идентифицирует устройство как SHENZHEN XGRIDS-INNOVATION CO., LTD - K1 и показывает одобренные частотные диапазоны: 5 GHz Wi-Fi, 2.4 GHz Wi-Fi, Bluetooth/BLE диапазоны. Источник: [S08]. + +### 7.2. Wi-Fi возможности + +FCC 5 GHz Wi-Fi report для LixelKity K1 указывает: + +- U-NII-1: 5180-5240 MHz; +- U-NII-3: 5745-5825 MHz; +- 802.11a/n/ac; +- 256QAM для 802.11ac; +- 20/40/80 MHz каналы. + +Источник: [S09]. + +2.4 GHz Wi-Fi report указывает 802.11b/g/n, HT20/HT40 и bit rate up to 150 Mbps для 802.11n. Источник: [S10]. + +Вывод: Wi-Fi - единственный реалистичный внешний канал для live cloud/video. Bluetooth для этого узкий. + +### 7.3. Bluetooth/BLE возможности + +BT report указывает Bluetooth 5.0 BR+EDR, 79 channels, 1/2/3 Mbps modulation rates. Источник: [S11]. + +BLE report указывает Bluetooth 5.0 BLE, 40 channels, 1/2 Mbps transmitter bit rate. Источник: [S12]. + +Вывод: Bluetooth/BLE подходит для discovery/provisioning/status, но не для тяжелого point cloud/camera stream. + +### 7.4. Батарея и питание + +FCC Wi-Fi/BT/BLE reports указывают батарею `DC 14.4V 1900mAh 27.36Wh` и charger output `16.8V 1.2A`. Источники: [S09], [S11], [S12]. + +Для пилота: + +- не проектировать внешнее питание без отдельного power-аудита; +- в стенде использовать штатную батарею; +- записывать battery status, если он доступен через data/status connection. + +### 7.5. Внутренние фото: compute и radio + +FCC internal photos показывают: + +- несколько отдельных PCB; +- модуль/плату с маркировкой Rockchip RK3588S; +- Wi-Fi/BT модуль Realtek RTL8822CE; +- отдельные антенны WLAN/RLAN и WLAN/RLAN/BT; +- коннекторы/шлейфы, что говорит о модульной внутренней архитектуре. + +Источник: [S13]. + +Инженерный вывод: + +- внутри есть Linux-class compute, а не только MCU; +- realtime mapping, вероятно, считается внутри устройства; +- телефон, скорее всего, получает уже готовый preview/data, а не сам считает SLAM; +- при провале black-box подхода потенциальный fallback - доступ к внутреннему Linux/RK3588S, но это вне первого этапа. + +## 8. Главное архитектурное предположение + +На основе мануалов, приложений и FCC складывается такая модель: + +```text +BLE layer: + - advertise / discovery + - connect + - network provisioning + - possibly status notifications + - possibly activation/pair token + +Wi-Fi layer: + - network connection + - data connection handshake + - status/control + - realtime preview / point cloud / maybe pose + - maybe camera/preview data + +Local storage: + - .xbin raw sensor data + - map.las realtime output, downsampled + - poses.csv trajectory + - project.json metadata + - model preview + - photo/control-point images + - logs +``` + +Это не доказанная структура протокола. Это рабочая модель, которую надо подтвердить экспериментами. + +## 9. Где именно скрывается ценность для нашего решения + +### 9.1. Не в LAS и не в Gaussian splats + +LAS/LCC/3DGS - это важный offline/post-processing мир, но для realtime CV он вторичен. Нас интересуют не файлы после обработки, а live data path. + +### 9.2. Внутренний realtime point cloud pipeline + +`map.las` описан как point cloud output by the device in real time. Это означает, что внутри K1 уже формируется realtime cloud. Даже если он downsampled, он может быть идеален для onboard CV. + +### 9.3. Trajectory / poses + +`poses.csv` после скана - trajectory file recorded during scanning. Это почти точно производная внутреннего SLAM/odometry. Нам нужно проверить, идет ли pose live в data session. Если live pose не идет, `poses.csv` использовать как offline ground truth при reverse engineering. + +### 9.4. Камеры + +Камеры подтверждены официально как `48 MP x2 panoramic vision`. Но доступность live raw/preview frames наружу не подтверждена. В документации есть `photo` как camera image at the moment a control point is added, а не continuous stream. LixelGO version history упоминает RGB point cloud rendering и camera loading fixes, что дает надежду на camera/preview component, но это гипотеза. + +Поэтому камера - главный риск проекта. + +## 10. Вероятностная матрица доступности данных + +| Данные | Вероятность live-доступа снаружи | Уверенность | Почему | +|---|---:|---:|---| +| Device status: battery/storage/firmware/scan state | высокая | высокая | LixelGO показывает device info/status; есть data connection status | +| Realtime point cloud preview | высокая | высокая | App Store прямо говорит real-time point cloud; `map.las` - realtime output | +| Realtime RGB/colorized point cloud | средняя/высокая | средняя | App Store говорит RGB point cloud rendering; но формат неизвестен | +| Live pose/trajectory | средняя | средняя | `poses.csv` существует; app может иметь route/status, но live export не доказан | +| Live raw 360 camera frames | низкая/средняя | низкая | камеры есть; camera loading fixes есть; но docs не обещают video stream наружу | +| Live rendered preview/model chunks | средняя | средняя | `model` in-app preview file; LixelGO live preview; может быть не raw frames | +| Full raw LiDAR stream | низкая/средняя | низкая | наружу вероятнее идет processed/downsampled cloud, raw в `.xbin` | +| Full raw sensor `.xbin` live stream | низкая | низкая | docs говорят `.xbin` recorded by device, не live external stream | + +## 11. Почему Bluetooth - не канал облака + +Bluetooth Classic BR/EDR у K1 по FCC - максимум 1/2/3 Mbps; BLE - 1/2 Mbps. Даже сильно downsampled облако и тем более камеры требуют больше. Wi-Fi у K1 поддерживает 802.11ac 5 GHz и 802.11n 2.4 GHz. + +Следовательно: + +```text +Bluetooth = pairing/provisioning/control/status +Wi-Fi = data/control/live preview +``` + +Именно поэтому первым слоем разработки должен быть BLE discovery/GATT/provisioning, а главным слоем reverse engineering - Wi-Fi data session. + +## 12. Лабораторный стенд на MacBook без Linux + +### 12.1. Доступное железо + +Есть: + +- K1; +- MacBook; +- Wi-Fi роутер; +- любая Wi-Fi сеть; +- Bluetooth на Mac. + +Нет: + +- Linux как отдельный стенд; +- OpenWrt/tcpdump на роутере, если текущий роутер обычный. + +### 12.2. Рекомендуемая лабораторная сеть + +```text +SSID: K1_LAB +Password: K1lab123456 +Wi-Fi band: 5 GHz first, 2.4 GHz fallback +Security: WPA2-Personal or WPA2/WPA3 mixed +Client isolation: OFF +DHCP: ON +Multicast/mDNS: allowed +Internet: optional for local tests +``` + +Пароль и SSID специально простые: официальная документация рекомендует keep hotspot name/password as simple as possible. + +### 12.3. Важное ограничение обычного роутера + +Если K1 и телефон с LixelGO общаются через обычный роутер, Mac как третий Wi-Fi клиент обычно не увидит unicast-трафик между телефоном и K1. Он увидит broadcast/multicast, ARP, DNS/mDNS, но не полный TCP/UDP payload чужой сессии. + +Для baseline-захвата LixelGO сессии нужны варианты: + +| Вариант | Сложность | Надежность | Комментарий | +|---|---:|---:|---| +| Mac как AP через Internet Sharing | средняя | средняя | можно пробовать tcpdump на bridge/interface | +| Роутер с packet capture | низкая/средняя | высокая | если роутер умеет capture/port mirror | +| OpenWrt/travel router | средняя | высокая | лучший вариант, но это Linux/router | +| Wi-Fi monitor mode | высокая | низкая/средняя | WPA расшифровка и modern Wi-Fi нюансы | +| Android HCI log для BLE + Mac direct session для Wi-Fi | средняя | высокая для BLE, средняя для Wi-Fi | pragmatic route | + +Для первого этапа можно начинать без полного phone<->K1 sniffing: сначала снять BLE/GATT с Mac, затем пробовать Mac как собственный data client. + +### 12.4. macOS ограничения BLE + +Bleak на macOS работает через CoreBluetooth/pyobjc. Важно: + +- macOS не использует обычный BLE MAC address как Linux; CoreBluetooth дает UUID, уникальный для пары Mac<->device; +- explicit pairing в Bleak на macOS не реализован; macOS сама покажет prompt при доступе к защищенной characteristic; +- нужны Bluetooth permissions; +- passive scanning не поддерживается. + +Источник: [S14]. + +Это означает: в документации и логах нашего инструмента нельзя полагаться на привычный MAC как стабильный ID. Нужно сохранять both: `macos_uuid`, advertised name, manufacturer data, service UUIDs. + +## 13. macOS инструменты + +### 13.1. Установка базового инструментария + +```bash +xcode-select --install + +brew install python@3.12 +brew install wireshark +brew install nmap +brew install arp-scan +brew install jq +brew install ffmpeg +brew install socat +brew install cmake pkg-config +``` + +Wireshark на macOS требует ChmodBPF launch daemon для packet capture. Источник: [S15]. + +Apple указывает PacketLogger в Additional Tools for Xcode как инструмент для Bluetooth debugging. Источник: [S16]. + +Nmap официально поддерживает macOS. Источник: [S17]. + +### 13.2. Python зависимости + +```bash +python3.12 -m venv .venv +source .venv/bin/activate +pip install --upgrade pip + +pip install bleak typer rich pydantic pydantic-settings loguru +pip install pyshark scapy construct kaitaistruct bitstruct +pip install numpy pandas opencv-python pillow open3d +pip install zeroconf ifaddr psutil +pip install pytest pytest-asyncio ruff mypy +``` + +Зачем: + +| Пакет | Роль | +|---|---| +| `bleak` | BLE scan/connect/GATT/read/notify/write | +| `typer`, `rich` | CLI и удобный вывод | +| `pydantic` | схемы конфигураций/артефактов | +| `pyshark` | анализ PCAP через tshark/Wireshark dissectors | +| `scapy` | низкоуровневый PCAP/payload анализ | +| `construct` | бинарные структуры | +| `numpy` | numeric/probe point cloud | +| `opencv-python` | video/frame probe | +| `open3d` | point cloud visualization/export | +| `zeroconf` | mDNS/Bonjour discovery | + +## 14. Устройство как state machine + +### 14.1. Физический state machine + +```text +POWER_OFF + hold 4s + -> BOOTING / slow blue ~30s + -> STANDBY / solid green + +STANDBY + double-click + -> STARTING_SCAN / fast green + -> STATIC_INIT / 15 sec + -> SCANNING / slow green + +SCANNING + single-click + -> CONTROL_POINT_MARK + +SCANNING + double-click + -> STOPPING / fast green + -> STANDBY / solid green + +STANDBY + single-click + white + single-click + -> USB_MODE / solid blue +``` + +### 14.2. Communication state machine + +```text +BLE_ADVERTISING + -> BLE_CONNECTED + -> WIFI_CONFIG_SENT + -> WIFI_ASSOCIATED + -> IP_ASSIGNED + -> NETWORK_CONNECTION_SUCCESS + -> DATA_CONNECTION_HANDSHAKE + -> DATA_CONNECTION_SUCCESS + -> STREAM_SUBSCRIBED +``` + +Официальный manual разделяет `network connection` и `data connection`, поэтому их надо считать разными состояниями. + +## 15. BLE provisioning: что именно надо восстановить + +### 15.1. Термины + +| Термин | Значение | +|---|---| +| SSID | имя Wi-Fi сети, например `K1_LAB` | +| Wi-Fi password / PSK | пароль Wi-Fi сети | +| BLE address / macOS UUID | идентификатор BLE device на Mac | +| Service UUID | идентификатор GATT-сервиса | +| Characteristic UUID | идентификатор поля read/write/notify | +| CCCD / 0x2902 | descriptor для включения notifications/indications | +| ATT handle | низкоуровневый номер атрибута | + +### 15.2. Что ищем + +Нужно найти: + +```text +K1 BLE advertised name +manufacturer data +service UUIDs +GATT services +writable characteristic для Wi-Fi config +notify characteristic для status/progress +format provisioning payload +``` + +### 15.3. Нельзя делать + +Нельзя слать случайные write payloads в неизвестные характеристики. Любой write только после того, как: + +- мы сняли эталонную LixelGO сессию; +- знаем UUID/handle; +- знаем payload format; +- команда явно включена флагом `--i-understand-this-writes-to-device`. + +### 15.4. Как восстановить BLE provisioning без гаданий + +Сценарий: + +```text +1. Настроить сеть A: + SSID = K1_LAB_A + PASS = AAAAAAAA11 + пройти Bridge/Direct Connect через LixelGO + сохранить BLE log + +2. Настроить сеть B: + SSID = K1_LAB_B + PASS = BBBBBBBB22 + пройти Bridge/Direct Connect через LixelGO + сохранить BLE log + +3. Сравнить ATT Write payloads. + +4. Найти байты, где меняются SSID/password. + +5. Определить формат: + - ASCII/JSON + - TLV + - protobuf-like + - encrypted blob +``` + +Если SSID/password видны в plain text, provisioning можно сделать быстро. Если это encrypted blob, provisioning может требовать app-layer key/token. + +### 15.5. Сценарий, где BLE provisioning вообще не нужен в runtime + +Если K1 запоминает Wi-Fi profile, то можно один раз настроить его через LixelGO на `K1_LAB`, а потом в runtime: + +```text +Mac/router поднимает K1_LAB +K1 включается +K1 сам подключается к знакомой сети +Mac находит IP +Mac открывает data session +``` + +Это важный shortcut: BLE provisioning нужен для полного replacement, но не обязательно для первого runtime-пилота. + +## 16. Wi-Fi data session: где живут нужные данные + +### 16.1. Что должен сделать Wi-Fi клиент + +После появления K1 в сети: + +```text +1. Discover IP. +2. Enumerate services/ports. +3. Observe idle traffic. +4. Establish or imitate data connection. +5. Subscribe to streams. +6. Cache raw traffic. +7. Decode candidate streams. +``` + +### 16.2. Что искать по портам + +```text +80 / 8080 / 8000 / 8888 HTTP/WebSocket/API +443 / 8443 HTTPS/TLS +50051 gRPC +554 / 8554 RTSP +1883 / 8883 MQTT/MQTTS +5353 mDNS +1900 SSDP +22 SSH, только если открыт, без bruteforce +5555 ADB/Android debug, только если открыт, без abuse +custom UDP high ports likely realtime stream +``` + +### 16.3. Фингерпринт протоколов + +| Наблюдение | Возможное значение | +|---|---| +| Большой UDP поток K1 -> client | realtime point cloud/video chunks | +| TCP/WebSocket binary frames | app-level stream, часто удобно парсить | +| HTTP/2/gRPC | protobuf/streaming API | +| TLS | payload скрыт, но видны endpoints/bitrates/certs | +| QUIC/UDP encrypted | сложнее, но fingerprint возможен | +| RTSP/RTP | video/camera/preview stream | +| Малый heartbeat | status/control только | + +### 16.4. Что искать для point cloud + +Признаки point cloud: + +- поток появляется/растет только во время scanning mode; +- packet rate стабильный; +- payload binary; +- stride похож на 12/16/20/24/32 bytes per point; +- есть координаты float32 little-endian или scaled integers; +- диапазоны координат похожи на метры; +- при движении сцена меняется в decoded candidate; +- при закрытии LiDAR/стене перед сканером меняется распределение. + +### 16.5. Что искать для camera/video + +Признаки видео/камер: + +```text +JPEG: FF D8 FF +PNG: 89 50 4E 47 +H264 Annex B: 00 00 00 01 +H265/HEVC NAL +MP4 boxes: ftyp / moof / mdat +MJPEG over HTTP +RTSP DESCRIBE / SETUP / PLAY +RTP packet streams +WebSocket binary frames +protobuf chunks with large compressed images +``` + +Специальный эксперимент для камер: + +```text +1. Запустить capture. +2. K1 стоит неподвижно. +3. Закрыть одну камеру черной тканью на 10 секунд. +4. Открыть. +5. Закрыть вторую камеру. +6. Направить яркий источник/цветной паттерн. +7. Поставить control point single-click. +8. Сравнить payload bitrate/signature/frame candidates. +``` + +Если никакой stream не реагирует на визуальную сцену, возможно наружу идет только already-colorized point cloud или rendered preview, а не camera frames. + +## 17. Полная программа экспериментов + +### 17.1. E0 - инвентаризация устройства + +Цель: зафиксировать baseline до любых действий. + +Действия: + +```text +1. Модель: LixelKity K1. +2. Серийник / last 4 digits. +3. Firmware version в LixelGO. +4. LixelGO version. +5. Активирован / не активирован. +6. Состояние батареи. +7. Storage free/used. +8. Последний способ подключения: Quick/Direct/Bridge. +``` + +Через USB mode после короткого тестового скана: + +```bash +find /Volumes -maxdepth 3 -type d | sort +find /Volumes// -maxdepth 4 -type f | sort > project_tree.txt +find /Volumes// -maxdepth 4 -type f -exec ls -lh {} \; > project_files.txt +``` + +Скопировать: + +```text +project.json +poses.csv +map.las +log/* +preview_photo.jpg +photo/* +model/* +``` + +Критерий успеха: есть baseline-папка проекта и файл `project_tree.txt`. + +### 17.2. E1 - кнопочный автономный скан без телефона + +Цель: доказать автономный runtime без LixelGO. + +Шаги: + +```text +1. Выключить LixelGO. +2. Включить K1. +3. Дождаться solid green. +4. Double-click. +5. Ждать 20 секунд. +6. 60 секунд двигать по простой сцене. +7. Double-click stop. +8. Дождаться solid green. +9. Перейти в USB mode. +10. Проверить проект. +``` + +Фиксировать: + +- LED timings; +- появление проекта; +- наличие `map.las`, `poses.csv`, `project.json`, `log`; +- settings в `project.json`. + +### 17.3. E2 - Bridge Mode через LixelGO на нашу сеть + +Цель: настроить K1 на `K1_LAB` и понять, запоминает ли он сеть. + +Шаги: + +```text +1. Поднять SSID K1_LAB. +2. Телефон/Mac подключить к K1_LAB. +3. В LixelGO выбрать Bridge Mode. +4. Через Bluetooth найти K1. +5. Ввести SSID/password. +6. Дождаться network connection success. +7. Дождаться data connection success. +8. Перезапустить K1. +9. Проверить, подключается ли K1 к K1_LAB автоматически. +``` + +Критерий успеха: K1 появляется в DHCP/ARP без повторного LixelGO provisioning. + +### 17.4. E3 - BLE scan/GATT dump на Mac + +Цель: получить карту BLE. + +Команды будущего инструмента: + +```bash +k1link ble scan --duration 30 --out captures/e3_ble_scan.json +k1link ble gatt-dump --device --out captures/e3_gatt.json +k1link ble listen --device --out captures/e3_notifications.jsonl +``` + +Сохранять: + +- advertised name; +- UUIDs; +- manufacturer data; +- services/chars/properties/descriptors; +- read results для read-only chars; +- notifications. + +### 17.5. E4 - BLE provisioning diff + +Цель: определить characteristic/payload для Wi-Fi config. + +Нужно две или больше LixelGO-сессии с разными SSID/PASS. Captures сравниваются. + +Артефакты: + +```text +ble_session_A.snoop +ble_session_B.snoop +att_writes_A.jsonl +att_writes_B.jsonl +provisioning_diff.md +``` + +Критерий успеха: найден UUID/handle и payload pattern. + +### 17.6. E5 - Wi-Fi discovery + +Команды: + +```bash +arp -a +sudo arp-scan --localnet +nmap -Pn -sV -p- +sudo nmap -Pn -sU --top-ports 200 +dns-sd -B _services._dns-sd._udp local +``` + +Артефакты: + +```text +net_discover.json +ports_tcp.txt +ports_udp.txt +mdns.txt +``` + +### 17.7. E6 - idle capture + +```bash +sudo tcpdump -i en0 -s 0 -B 524288 -w captures/e6_idle.pcap 'host ' +``` + +Сценарий: K1 включен, standby, не сканирует. + +Цель: baseline heartbeat/status. + +### 17.8. E7 - scan by button capture + +```bash +sudo tcpdump -i en0 -s 0 -B 524288 -w captures/e7_scan_button.pcap 'host ' +``` + +Шаги: + +```text +1. K1 standby. +2. Start capture. +3. Double-click start. +4. 20 sec static init. +5. 60 sec slow movement. +6. 20 sec fast movement. +7. 20 sec stationary. +8. Double-click stop. +9. Stop capture. +``` + +Критерий: потоки, появляющиеся/увеличивающиеся в scanning mode. + +### 17.9. E8 - scan by app capture + +Цель: сравнить app-start и button-start. + +Шаги: + +```text +1. Start scan via LixelGO. +2. Capture network. +3. Stop via LixelGO. +4. Compare with E7. +``` + +Если app-start включает дополнительные streams, а button-start нет, значит data subscription зависит от LixelGO. + +### 17.10. E9 - reconnect behavior + +Цель: проверить, можно ли подключиться к уже идущему скану. + +Шаги: + +```text +1. Start scan button/app. +2. Убить LixelGO / выключить phone Wi-Fi. +3. K1 продолжает scanning. +4. Попробовать reconnect. +5. Смотреть: возвращается ли preview/data connection. +``` + +Артефакт: `reconnect_notes.md`. + +### 17.11. E10 - camera perturbation test + +Цель: доказать или опровергнуть наличие live camera stream. + +Шаги: + +```text +1. Capture во время scan. +2. Неподвижная сцена 10 сек. +3. Закрыть camera A 10 сек. +4. Открыть. +5. Закрыть camera B 10 сек. +6. Цветной паттерн перед камерой. +7. Single-click control point. +8. Сравнить traffic и decoded video candidates. +``` + +Критерий: video/image stream должен реагировать на визуальные изменения. + +### 17.12. E11 - point cloud perturbation test + +Цель: найти point cloud stream. + +Шаги: + +```text +1. Сцена: плоская стена на 2-5 м. +2. Захват 30 сек. +3. Быстро повернуть K1 на другой объект. +4. Пройти мимо угла/коридора. +5. Поставить крупный объект перед LiDAR. +6. Сравнить candidate decoded points. +``` + +Критерий: candidate point decoder показывает изменяющуюся геометрию. + +### 17.13. E12 - после скана сопоставить live capture с файлами + +Цель: понять, что было в live stream. + +После E7/E8 скачать проект и сравнить: + +```text +map.las size / point count +poses.csv count / timestamps +project.json settings +log messages +model preview timestamps +photo/control point timestamps +``` + +Идея: timestamps/packet counts в PCAP сопоставить с `poses.csv` и `map.las`. + +## 18. Анализ PCAP: методика + +### 18.1. Первичная сводка + +```bash +tshark -r captures/session.pcap -q -z conv,tcp -z conv,udp +``` + +Что смотреть: + +- biggest flows; +- flow start/stop relative to scan start; +- packets per second; +- average payload size; +- direction: K1 -> client или client -> K1. + +### 18.2. Протокольные фильтры + +```bash +tshark -r captures/session.pcap -Y "dns || mdns || ssdp || http || websocket || http2 || grpc || rtsp || rtp || tls || quic" + +tshark -r captures/session.pcap -Y "udp && frame.len > 600" + +tshark -r captures/session.pcap -Y "tcp.len > 1000" +``` + +### 18.3. Payload extraction + +Для каждого крупного flow: + +```text +flow_id = proto + src_ip + src_port + dst_ip + dst_port +raw_payloads/flow_.bin +raw_payloads/flow_.jsonl # packet metadata +``` + +Metadata per packet: + +```json +{ + "ts_epoch": 0.0, + "relative_ts": 0.0, + "proto": "udp", + "src": "192.168.1.10:50000", + "dst": "192.168.1.20:60000", + "payload_len": 1024, + "sha1_16": "...", + "first16_hex": "..." +} +``` + +## 19. Point cloud reverse-probe + +### 19.1. Float32 little-endian probe + +Алгоритм: + +```text +for each flow: + concatenate payloads or process packet-wise + for offset in 0..128: + for stride in [12, 16, 20, 24, 28, 32, 40, 48]: + parse x,y,z as float32 little-endian + score: + valid finite ratio + coordinate range ratio [-200,200] + spatial variance + non-zero points + temporal continuity + save top candidates +``` + +### 19.2. Scaled integer probe + +Если float не сработал: + +```text +try int16/int32 little-endian +scale candidates: 0.001, 0.005, 0.01, 0.1 +look for coordinate bounds and structure +``` + +### 19.3. Packet header detection + +Искать: + +- monotonic sequence numbers; +- timestamps; +- packet type; +- chunk length; +- CRC/checksum; +- frame ID. + +Метод: + +```text +compare first 64 bytes across packets +bytes with monotonic increments -> seq/time +bytes constant -> magic/version/type +bytes equal payload len -> length field +``` + +### 19.4. Validation через физический эксперимент + +Кандидат считается реальным point cloud, если: + +- decoded точки визуализируются как облако; +- движение/стена/угол влияют на геометрию; +- packet rate совпадает с scanning phase; +- количество точек/сек логически похоже на downsampled preview; +- после скана `map.las` похож по масштабу/координатам. + +## 20. Camera/video reverse-probe + +### 20.1. Сигнатуры + +```text +JPEG start: FF D8 FF +JPEG end: FF D9 +PNG: 89 50 4E 47 0D 0A 1A 0A +H264 Annex B: 00 00 00 01 or 00 00 01 +H265: similar NAL patterns +MP4/fragmented: ftyp, moof, mdat +RTSP: OPTIONS/DESCRIBE/SETUP/PLAY +RTP: sequence/timestamp/SSRC fields +MJPEG HTTP: multipart/x-mixed-replace +``` + +### 20.2. Сжатый custom frame + +Если нет сигнатур: + +- payload может быть protobuf with compressed image bytes; +- может быть raw YUV/RGB chunks; +- может быть rendered preview tiles; +- может быть RGB point cloud, а не image frames. + +Нужны perturbation tests: закрытие камер, яркий цветной паттерн, single-click control point. + +### 20.3. Условия признания camera stream найденным + +Camera stream найден, если: + +- есть декодируемые frames; +- или есть устойчивые frame boundaries и payload меняется при визуальной сцене; +- или есть protocol names/endpoints в app/traffic: `camera`, `preview`, `image`, `panorama`, `rgb`, `isp`; +- можно извлечь хотя бы 1 кадр в PNG/JPEG/NPY. + +## 21. Проект `k1-mac-link`: полное ТЗ + +### 21.1. Назначение + +`k1-mac-link` - macOS CLI tool для исследования и постепенной замены LixelGO в части локального подключения к K1. + +Он должен: + +- сканировать BLE; +- дампить GATT; +- безопасно слушать BLE notifications; +- воспроизводить BLE provisioning только после определения payload; +- обнаруживать K1 в Wi-Fi сети; +- запускать tcpdump/tshark captures; +- суммировать flows; +- извлекать raw payloads; +- искать point cloud candidates; +- искать camera/video candidates; +- писать все raw данные на SSD; +- не терять raw captures при failure decode. + +### 21.2. Структура репозитория + +```text +k1-mac-link/ + README.md + pyproject.toml + docs/ + 00_scope.md + 01_k1_manual_facts.md + 02_lab_setup_macos.md + 03_experiments.md + 04_protocol_notes.md + 05_risks.md + src/k1link/ + __init__.py + cli.py + config.py + logging.py + artifacts.py + ble/ + scanner.py + gatt_dump.py + listener.py + provisioner.py + protocol_guess.py + macos_notes.py + net/ + discover.py + ports.py + capture.py + tshark.py + session.py + mdns.py + analysis/ + pcap_summary.py + flow_classifier.py + payload_extract.py + payload_fingerprint.py + protocol_diff.py + pointcloud_probe.py + video_probe.py + timeline.py + decode/ + pointcloud.py + pose.py + video.py + status.py + unknown.py + cache/ + writer.py + manifest.py + raw_stream.py + metadata.py + export/ + ply.py + pcd.py + npz.py + frames.py + tests/ + test_payload_fingerprint.py + test_pointcloud_probe.py + test_video_signatures.py + scripts/ + setup_macos.sh + collect_session.sh +``` + +### 21.3. CLI команды + +```bash +k1link doctor + +k1link ble scan \ + --duration 30 \ + --out captures/ble_scan.json + +k1link ble gatt-dump \ + --device \ + --out captures/gatt_dump.json + +k1link ble listen \ + --device \ + --duration 120 \ + --out captures/ble_notifications.jsonl + +k1link ble write-known \ + --device \ + --profile wifi-provision-v1 \ + --ssid K1_LAB \ + --password K1lab123456 \ + --i-understand-this-writes-to-device + +k1link net discover \ + --subnet 192.168.1.0/24 \ + --out captures/net_discover.json + +k1link net ports \ + --ip \ + --out captures/ports.json + +k1link net capture \ + --iface en0 \ + --host \ + --out captures/session.pcap + +k1link analyze summarize \ + --pcap captures/session.pcap \ + --out captures/session_summary.json + +k1link analyze extract-flows \ + --pcap captures/session.pcap \ + --out-dir captures/flows + +k1link analyze fingerprint \ + --flows captures/flows \ + --out captures/fingerprint.json + +k1link analyze pointcloud-probe \ + --flows captures/flows \ + --out-dir captures/pointcloud_candidates + +k1link analyze video-probe \ + --flows captures/flows \ + --out-dir captures/video_candidates + +k1link run-pilot \ + --ssid K1_LAB \ + --password K1lab123456 \ + --cache /Volumes/K1SSD/k1-cache +``` + +### 21.4. `doctor` + +Проверяет: + +```text +macOS version +Python version +Bluetooth permission / bleak scan works +Wireshark/tshark installed +tcpdump available +ChmodBPF installed or capture permission available +nmap installed +ffmpeg installed +write permission to cache path +``` + +### 21.5. BLE scanner output schema + +```json +{ + "session_id": "20260715-120000", + "adapter": "CoreBluetooth", + "devices": [ + { + "macos_uuid": "...", + "name": "Lixel-1234", + "rssi": -55, + "service_uuids": [], + "manufacturer_data_hex": {}, + "service_data_hex": {}, + "timestamp": "..." + } + ] +} +``` + +### 21.6. GATT dump output schema + +```json +{ + "device": { + "macos_uuid": "...", + "name": "..." + }, + "services": [ + { + "uuid": "...", + "description": "...", + "characteristics": [ + { + "uuid": "...", + "handle": 1, + "properties": ["read", "write", "notify"], + "descriptors": [ + {"uuid": "00002902-0000-1000-8000-00805f9b34fb", "handle": 2} + ], + "read_result_hex": "...", + "read_error": null + } + ] + } + ] +} +``` + +### 21.7. Flow summary schema + +```json +{ + "flow_id": "udp_192.168.1.10_50000_192.168.1.20_60000", + "proto": "udp", + "src": "192.168.1.10:50000", + "dst": "192.168.1.20:60000", + "packets": 12345, + "bytes": 987654321, + "first_ts": 0.0, + "last_ts": 120.0, + "duration": 120.0, + "pps": 102.8, + "bps": 65800000, + "avg_payload_len": 980, + "direction_guess": "k1_to_client", + "protocol_guess": ["custom_binary", "pointcloud_candidate"] +} +``` + +### 21.8. Session manifest + +Каждый эксперимент должен создавать `session_manifest.json`: + +```json +{ + "session_id": "E7_20260715_120000", + "experiment": "scan_button_capture", + "k1_model": "LixelKity K1", + "k1_firmware": "unknown", + "lixelgo_version": "not_used", + "network": { + "ssid": "K1_LAB", + "mac_iface": "en0", + "k1_ip": "192.168.1.50" + }, + "timeline": [ + {"t": 0, "event": "capture_start"}, + {"t": 5, "event": "button_double_click_start"}, + {"t": 25, "event": "static_init_expected_done"}, + {"t": 90, "event": "button_double_click_stop"}, + {"t": 110, "event": "capture_stop"} + ], + "artifacts": [ + "session.pcap", + "flows.json", + "pointcloud_candidates/candidate_001.ply" + ] +} +``` + +## 22. Safety policy для софта + +### 22.1. Безопасные операции по умолчанию + +Разрешены по умолчанию: + +- BLE scan; +- GATT service discovery; +- read readable characteristics; +- subscribe notifications; +- network scan собственному IP/subnet; +- tcpdump capture; +- packet analysis; +- raw cache. + +### 22.2. Ограниченные операции + +BLE writes, TCP/UDP packet sending, provisioning commands - только с явным флагом: + +```bash +--i-understand-this-writes-to-device +``` + +### 22.3. Запрещено в v1 + +- random write fuzzing; +- brute force passwords/tokens; +- firmware upload; +- deletion/modification of K1 files; +- SSH/ADB login guessing; +- exploit attempts. + +## 23. Реализация BLE модуля + +### 23.1. `ble scan` + +Требования: + +- async scan через Bleak; +- показывать все devices; +- выделять candidates by name: `Lixel`, `K1`, `XGRIDS`, `Lixel-XXXX`; +- сохранять advertisements; +- учитывать macOS UUID вместо MAC. + +### 23.2. `ble gatt-dump` + +Требования: + +- connect; +- enumerate services; +- enumerate characteristics/properties/descriptors; +- читать only read characteristics; +- не писать; +- timeout минимум 30-60 секунд, потому что macOS может показывать permission/auth prompts; +- JSON export. + +### 23.3. `ble listen` + +Требования: + +- включить notifications на всех notify/indicate characteristics по очереди или выбранных; +- писать JSONL: + +```json +{"ts": 0.0, "char_uuid": "...", "data_hex": "...", "len": 20} +``` + +### 23.4. `ble write-known` + +Требования: + +- не реализовывать до появления documented encoder; +- поддержать profiles: + - `wifi-provision-v1-json`; + - `wifi-provision-v1-tlv`; + - `wifi-provision-v1-protobuf`; +- profile активируется только после ручного внесения UUID/format в config. + +## 24. Реализация Wi-Fi discovery/capture + +### 24.1. `net discover` + +Методы: + +- ARP table; +- arp-scan; +- mDNS/zeroconf; +- optional ping sweep; +- DHCP lease file если доступен. + +Кандидаты K1: + +- vendor OUI if visible; +- hostname containing `lixel`, `xgrids`, `k1`; +- device that appears when K1 is powered and disappears when off; +- IP involved in LixelGO traffic. + +### 24.2. `net ports` + +Использовать nmap wrapper: + +```bash +nmap -Pn -sV -p- +sudo nmap -Pn -sU --top-ports 200 +``` + +Результат парсить в JSON. + +### 24.3. `net capture` + +Wrapper над tcpdump: + +```bash +sudo tcpdump -i -s 0 -B 524288 -w 'host ' +``` + +Требования: + +- правильная обработка Ctrl+C; +- писать manifest; +- опционально писать timeline events вручную через клавиатуру: + - `s` start scan; + - `i` init done; + - `m` moving; + - `c` camera cover; + - `p` control point; + - `x` stop scan. + +## 25. Реализация анализатора потоков + +### 25.1. Flow classifier + +Признаки: + +- protocol; +- src/dst; +- port; +- packet count; +- byte count; +- pps/bps; +- payload length histogram; +- entropy; +- first bytes histogram; +- scanning phase correlation. + +### 25.2. Protocol fingerprint + +Ищет: + +- ASCII strings; +- JSON; +- HTTP headers; +- protobuf-like fields; +- TLS handshake; +- QUIC long header; +- RTSP; +- RTP; +- image/video signatures; +- float/int point patterns. + +### 25.3. Timeline correlation + +Для каждого flow: + +```text +bytes/sec over time +packets/sec over time +payload_len over time +correlation with events: + capture_start + scan_start + static_init_done + movement + camera_cover + control_point + scan_stop +``` + +Flow, который живет только во время scan, получает высокий priority. + +## 26. Реализация decoders/probes + +### 26.1. PointCloudProbe + +Выход: + +```text +pointcloud_candidates/ + candidate_001.json + candidate_001.ply + candidate_001.npz + candidate_001_preview.png +``` + +Скоринг: + +```text +finite_ratio +range_score +variance_score +temporal_score +packet_alignment_score +visual_score_manual +``` + +### 26.2. VideoProbe + +Выход: + +```text +video_candidates/ + candidate_001.json + candidate_001.raw + frames/ + frame_0001.jpg +``` + +Пробовать: + +- ffmpeg decode; +- OpenCV VideoCapture; +- JPEG extraction; +- H264 Annex B extraction; +- RTP depayload if ports/protocol match. + +### 26.3. PoseProbe + +Искать: + +- 3 floats position + 4 floats quaternion; +- 4x4 transform matrices; +- euler angles; +- timestamps; +- repeated packets with smooth temporal evolution; +- values comparable to `poses.csv` after scan. + +## 27. Файловая структура кеша + +На SSD каждая сессия: + +```text +/Volumes/K1SSD/k1-cache/ + 2026-07-15_E7_scan_button/ + session_manifest.json + notes.md + captures/ + ble_scan.json + gatt_dump.json + session.pcap + flows/ + flows.json + flow_001.bin + flow_001.jsonl + analysis/ + pcap_summary.json + fingerprints.json + timeline.csv + decoded/ + pointcloud/ + video/ + pose/ + k1_project_copy/ + project_tree.txt + project.json + poses.csv + map.las + log/ +``` + +Правило: raw всегда сохраняется; decoded failure не должен терять capture. + +## 28. Интеграция с OpenCV / ML / ROS-like pipeline + +### 28.1. На Mac pilot + +Первый pilot на Mac может не поднимать ROS2. Достаточно: + +- raw capture; +- decoded frames to directory; +- decoded point clouds to PLY/NPZ; +- optional ZeroMQ/WebSocket local publish. + +### 28.2. Далее на бортовом компьютере + +Потом можно сделать: + +```text +/k1/points_preview PointCloud2-like +/k1/image_left Image-like +/k1/image_right Image-like +/k1/panorama Image-like +/k1/pose Odometry-like +/k1/status diagnostics +``` + +Если ROS2 будет не на Mac, exporter должен быть независимым: + +- MCAP; +- NPZ; +- PCD/PLY; +- PNG/JPEG frames; +- JSONL metadata. + +## 29. Аппаратный запуск scan без LixelGO + +### 29.1. Первый стенд + +На первом стенде кнопка нажимается вручную. + +### 29.2. Бортовая версия + +Не ставить сервопривод, если можно не ставить. Лучше: + +```text +MCU GPIO -> optocoupler / PhotoMOS -> parallel to K1 button +``` + +Логика: + +```text +click 80 ms +pause 180 ms +click 80 ms +wait 20 sec static init +``` + +### 29.3. Чтение состояния + +Лучше: + +- через Wi-Fi status stream. + +Fallback: + +- фотодиод/цветовой датчик на LED; +- камера на LED в стенде; +- ручная запись таймлайна. + +## 30. Decision tree: что делать в зависимости от результата + +### 30.1. K1 сам подключается к сохраненной Wi-Fi сети + +Тогда BLE provisioning в runtime не нужен. Фокус на Wi-Fi data session. + +### 30.2. K1 требует provisioning каждый раз + +Надо реализовать BLE provisioning или оставить LixelGO/phone только как temporary provisioning tool. + +### 30.3. Wi-Fi data session открытая/простая + +Пишем client/parser. + +### 30.4. Wi-Fi data session TLS/encrypted + +Варианты: + +- понимать endpoints/flow timings; +- смотреть app logs/APK/IPA metadata; +- использовать LixelGO как session broker на пилоте; +- искать официальный/local SDK; +- переходить к internal Linux fallback только после исчерпания black-box пути. + +### 30.5. Point cloud найден, camera нет + +Для первого CV использовать point cloud/pose. Камеры искать дальше через perturbation/app analysis или внутренний доступ. + +### 30.6. Camera найден, point cloud нет + +Использовать K1 как panoramic camera source + локальная запись K1 как LiDAR fallback. Но это менее вероятный сценарий. + +### 30.7. Ничего live не отдается без LixelGO + +Имитировать LixelGO handshake. Если невозможно - temporary broker: + +```text +LixelGO starts session +Mac captures/receives via same network if possible +``` + +Если и это невозможно - рассматривать аппаратный fallback. + +## 31. Главные риски и блокеры + +| Риск | Вероятность | Урон | Митигирование | +|---|---:|---:|---| +| BLE provisioning payload encrypted | средняя | средний | LixelGO baseline, diff sessions, configure once | +| Wi-Fi data session requires token | высокая/средняя | высокий | app traffic analysis, logs, APK/IPA research | +| Local data encrypted/TLS | средняя | высокий | traffic metadata, official APIs, broker mode | +| Camera stream not exported | средняя/высокая | высокий для CV | perturbation tests, app analysis, internal fallback | +| Live point cloud only downsampled | высокая | низкий/средний | acceptable for obstacle mapping; offline .xbin fallback | +| Ordinary router cannot sniff unicast | высокая | средний | Mac as AP, router capture, OpenWrt later | +| macOS BLE limitations | высокая | средний | Bleak/CoreBluetooth workflow, PacketLogger, Android HCI if needed | +| Firmware versions change behavior | высокая | средний | record firmware/app version, avoid updates during experiment | +| K1 overheats/vibration issues | средняя | высокий | bench thermal/vibration tests before flight | + +## 32. Что сделать первым: 48-часовой план + +### 32.1. День 1: инвентаризация и автономная запись + +```text +1. Записать firmware version и LixelGO version. +2. Сделать 1 кнопочный скан без app. +3. Скопировать проект через USB mode. +4. Сохранить tree/project.json/poses.csv/map.las/log. +5. Сделать K1_LAB Wi-Fi. +6. Настроить Bridge Mode через LixelGO на K1_LAB. +7. Перезагрузить K1 и проверить, подключается ли сам. +``` + +### 32.2. День 2: Mac capture framework + +```text +1. Установить toolchain. +2. BLE scan через Bleak. +3. GATT dump. +4. Network discover K1 IP. +5. Idle capture. +6. Button scan capture. +7. App scan capture, если можно. +8. Flow summary. +9. Выделить top candidate streams. +``` + +Результат второго дня: не decoder, а карта потоков. + +## 33. ТЗ для Codex: stage-by-stage + +### 33.1. Общая задача для Codex + +Создать Python 3.12 CLI проект `k1-mac-link` для macOS. Проект должен быть исследовательским framework, а не финальным декодером. Все операции, которые пишут в K1, по умолчанию запрещены. + +### 33.2. Stage 0 - project skeleton + +Deliverables: + +- `pyproject.toml`; +- Typer CLI; +- Rich logging; +- config loader; +- artifact/session manifest helpers; +- tests. + +### 33.3. Stage 1 - BLE tools + +Deliverables: + +- `k1link ble scan`; +- `k1link ble gatt-dump`; +- `k1link ble listen`; +- JSON/JSONL outputs; +- no writes. + +### 33.4. Stage 2 - Network tools + +Deliverables: + +- `k1link net discover`; +- `k1link net ports` wrapper; +- `k1link net capture` wrapper; +- PCAP session manifests. + +### 33.5. Stage 3 - PCAP analysis + +Deliverables: + +- flow summary; +- payload extraction; +- protocol fingerprint; +- timeline correlation. + +### 33.6. Stage 4 - Point cloud probe + +Deliverables: + +- float32/strided probe; +- int scaled probe; +- candidate scoring; +- PLY/NPZ export. + +### 33.7. Stage 5 - Video probe + +Deliverables: + +- JPEG/PNG/H264/H265/RTSP/RTP/MJPEG detectors; +- ffmpeg/OpenCV attempts; +- frames extraction. + +### 33.8. Stage 6 - Known provisioning profile + +Only after manual reverse: + +- add safe provisioner; +- profile-based encoder; +- explicit flag required. + +## 34. Кодовые наброски для Codex + +### 34.1. BLE scan skeleton + +```python +import asyncio +import json +from bleak import BleakScanner + +async def scan(duration: float = 30.0): + devices = await BleakScanner.discover(timeout=duration, return_adv=True) + rows = [] + for dev, adv in devices.values(): + rows.append({ + "address_or_uuid": dev.address, + "name": dev.name or adv.local_name, + "rssi": adv.rssi, + "service_uuids": adv.service_uuids, + "manufacturer_data_hex": { + str(k): v.hex() for k, v in adv.manufacturer_data.items() + }, + "service_data_hex": { + str(k): v.hex() for k, v in adv.service_data.items() + }, + }) + return rows + +if __name__ == "__main__": + print(json.dumps(asyncio.run(scan()), ensure_ascii=False, indent=2)) +``` + +### 34.2. Payload signature probe skeleton + +```python +SIGNATURES = { + "jpeg_start": bytes.fromhex("ff d8 ff"), + "png": bytes.fromhex("89 50 4e 47 0d 0a 1a 0a"), + "h264_annexb_4": bytes.fromhex("00 00 00 01"), + "h264_annexb_3": bytes.fromhex("00 00 01"), + "mp4_ftyp": b"ftyp", + "mp4_moof": b"moof", + "mp4_mdat": b"mdat", +} + +def find_signatures(blob: bytes): + hits = [] + for name, sig in SIGNATURES.items(): + pos = blob.find(sig) + while pos != -1: + hits.append({"signature": name, "offset": pos}) + pos = blob.find(sig, pos + 1) + return hits +``` + +### 34.3. Float cloud probe skeleton + +```python +import numpy as np + +def score_xyz(blob: bytes, offset: int, stride: int): + usable = len(blob) - offset + n = usable // stride + if n < 100: + return None + arr = np.frombuffer(blob[offset:offset + n * stride], dtype=np.uint8).reshape(n, stride) + xyz = arr[:, :12].copy().view("/log /project.json +``` + +## 37. Что уже не нашли / что не является решением + +### 37.1. Не найдены публичные custom firmware + +По ранее пройденным OSINT-маршрутам публичных кастомных прошивок, root-thread, jailbreak-инструкций или firmware dumps для K1 не найдено. + +### 37.2. Публичный GitHub XGRIDS - не про live K1 + +XGRIDS публично выкладывает LCC/SDK/whitepaper/viewer-инструменты, но это про LCC/3DGS/rendering ecosystem, а не про live connection protocol K1. Источник: [S18]. + +### 37.3. Open-source аналоги полезны как fallback, не как K1 decoder + +FAST-LIVO2, LIV_handhold, livo-handheld, mandeye_controller, HDMapping полезны, если придется строить собственный open sensor stack на Livox/camera/IMU. Но они не расшифровывают K1 live protocol. Источники: [S19]-[S23]. + +## 38. Аппаратный fallback, если black-box route провалится + +Не делать на первом этапе, но держать в голове: + +1. FCC photos показывают RK3588S compute и RTL8822CE radio. +2. Теоретически устройство может иметь Linux userspace. +3. Если внешние протоколы закрыты, можно исследовать внутренний storage/logs/debug ports. +4. Это рискованный route: можно потерять калибровки, гарантию, работоспособность. + +Fallback routes: + +| Route | Что сохраняет | Риск | +|---|---|---:| +| Black-box Wi-Fi client | все штатное железо/калибровки | низкий | +| LixelGO broker + Mac passive/capture | штатный workflow | низкий/средний | +| Internal Linux access | сенсоры/калибровки, если read-only | высокий | +| Brain transplant | корпус/часть сенсоров | очень высокий | + +## 39. Проверка гипотезы о “заводских SSID/password” + +Гипотеза: китайцы могли зашить SSID/password по серийнику. + +Оценка: + +- Для Bridge/Direct Mode: маловероятно. Мануал говорит, что пользователь вводит hotspot name/password. Это обычный Wi-Fi profile. +- Для Quick Connect: возможно есть device-specific workflow, где приложение читает BLE advertisement/serial/config и подключается автоматически. +- Для data connection: возможно есть токен/сертификат/сессионный ключ, но это не Wi-Fi SSID/password. + +Практический тест: + +```text +1. Настроить K1 на K1_LAB через LixelGO. +2. Изменить пароль K1_LAB на роутере. +3. Проверить, перестал ли K1 подключаться. +4. Вернуть пароль. +5. Проверить, подключился ли снова. +``` + +Если behavior зависит от нашего пароля - это обычный Wi-Fi profile. + +## 40. Почему `data connection status` важнее `network connection status` + +Официальная документация говорит, что после настройки должны быть успешны оба статуса: network connection status и data connection status. + +Значит: + +```text +network connection = K1 получил Wi-Fi/IP + +data connection = LixelGO открыл application-layer session +``` + +Проект может застрять на втором слое. Поэтому `k1-mac-link` должен разделять: + +- Wi-Fi reachability; +- service discovery; +- session handshake; +- stream subscription. + +## 41. Режим “сначала без BLE” + +Самый быстрый route: + +```text +1. Через LixelGO один раз настроить K1 на K1_LAB. +2. Закрыть LixelGO. +3. Перезагрузить K1. +4. Проверить, появляется ли K1 в K1_LAB. +5. Если появляется, BLE provisioning временно отложить. +6. Делать Wi-Fi discovery/capture/client. +``` + +Это может сэкономить дни. + +## 42. Режим “LixelGO как временный session broker” + +Если K1 не отдает live streams без app handshake: + +```text +LixelGO starts/opens session +K1 begins streaming to app +Mac captures traffic if AP can mirror/sniff +or Mac probes endpoints discovered from app session +``` + +Это не финальное решение, но полезный bridge для reverse engineering. + +## 43. Отдельная проблема камер + +### 43.1. Что знаем + +Знаем: + +- K1 имеет 2 panoramic cameras по official overview; +- LixelGO имеет true-color/elevation modes; +- App Store говорит про RGB point cloud rendering; +- version history упоминает camera loading failures; +- project structure содержит `photo` at control point. + +### 43.2. Чего не знаем + +Не знаем: + +- идут ли raw frames наружу; +- есть ли stitched panorama live; +- есть ли compressed camera stream; +- получает ли LixelGO картинку, или только цветной point cloud/rendered preview; +- можно ли подписаться на камеры отдельно. + +### 43.3. Практическая стратегия + +Параллельно искать: + +1. RTSP/RTP/video signatures. +2. JPEG/H264 payloads. +3. Endpoints with names `camera`, `photo`, `preview`, `rgb`. +4. Traffic reaction to covering cameras. +5. App binary strings, если понадобится. + +## 44. Отдельная проблема pose/route + +`poses.csv` после скана - сильная зацепка. Для live pose искать: + +- small high-frequency status packets; +- packets with 7 floats: x,y,z,qx,qy,qz,qw; +- packets with 16 floats: 4x4 matrix; +- timestamp + pose fields; +- smooth trajectory over time; +- correlation with movement. + +Эксперимент: + +```text +1. Стоять 20 сек. +2. Переместить K1 на 1 метр вперед. +3. Повернуть на 90 градусов. +4. Стоять. +5. Сравнить candidate pose values. +``` + +## 45. Что делать с firmware versions + +Не обновлять firmware во время исследования без необходимости. + +Причины: + +- Quick Connect/default behavior зависит от версии; +- `.xbin` format менялся в новых firmware; +- app compatibility меняется; +- firmware update может форматировать internal storage или менять project structure. + +Перед любым update: + +```text +1. Скопировать все проекты. +2. Сохранить firmware package checksum. +3. Сохранить текущую версию firmware/app. +4. Сделать полный capture старого поведения. +``` + +## 46. Формат pre-production отчета после первой лаборатории + +После 2-3 дней работы должен быть отчет: + +```markdown +# K1 Lab Report 001 + +## Device +Model: +Serial: +Firmware: +LixelGO: + +## Network +SSID: +Router: +K1 IP: + +## BLE +Advertised name: +Services: +Writable chars: +Notify chars: +Provisioning status: + +## Wi-Fi +Open TCP ports: +Open UDP candidates: +Data connection behavior: + +## Streams +Point cloud candidate: +Camera candidate: +Pose candidate: +Status candidate: + +## Evidence +PCAP: +Screenshots: +Project files: +Logs: + +## Blockers +... + +## Next steps +... +``` + +## 47. Готовый prompt для Codex + +```text +Ты работаешь над проектом k1-mac-link. + +Цель: создать macOS Python 3.12 CLI framework для исследования XGRIDS/LixelKity K1 как black-box realtime sensor. Нельзя прошивать устройство, нельзя делать random BLE writes, нельзя brute force. Все write-действия должны быть выключены по умолчанию и требовать явного флага. + +Создай проект с Typer CLI и модулями: +- ble scan/gatt-dump/listen +- net discover/ports/capture +- pcap flow summary +- payload extraction +- protocol fingerprint +- pointcloud probe +- video probe +- raw cache/session manifest + +Сделай команды: +- k1link doctor +- k1link ble scan +- k1link ble gatt-dump +- k1link ble listen +- k1link net discover +- k1link net ports +- k1link net capture +- k1link analyze summarize +- k1link analyze extract-flows +- k1link analyze fingerprint +- k1link analyze pointcloud-probe +- k1link analyze video-probe + +Сначала реализуй только safe read/capture tools. BLE write/provisioner оставь skeleton, который завершает работу с ошибкой NotImplemented, пока не будет добавлен known protocol profile. + +Все результаты сохраняй в JSON/JSONL/PCAP/RAW. Raw captures никогда не удалять. Добавь README с macOS setup, Bluetooth permissions, Wireshark ChmodBPF, tcpdump notes. +``` + +## 48. Source map + +### 48.1. Основные официальные источники + +| ID | Источник | Что подтверждает | +|---|---|---| +| S01 | XGRIDS K1 Product Overview, `https://docs.xgrids.cn/en-us/02-lingguang-k/01-lingguang-k1/v2.4.0/01-product-overview.html` | K1: <1 кг, 48MP x2 panoramic vision, 360 deg LiDAR, real-time colorized models | +| S02 | XGRIDS K1 Basic Operation, `https://docs.xgrids.cn/en-us/02-lingguang-k/01-lingguang-k1/v2.4.0/02-basic-operation.html` | кнопки, LED, autonomous start/stop, USB mode, thermal/IP54 notes | +| S03 | XGRIDS K1 Device Activation and Connection, `https://docs.xgrids.cn/en-us/02-lingguang-k/01-lingguang-k1/v2.4.0/03-device-activation-and-connection.html` | LixelGO, Bluetooth provisioning, Direct/Bridge/Quick Connect, network/data statuses | +| S04 | XGRIDS K1 Scanning Workflow, `https://docs.xgrids.cn/en-us/02-lingguang-k/01-lingguang-k1/v2.4.0/04-scanning-workflow.html` | time sync, Drone mount type, 15 sec static init, project structure, map.las realtime, poses.csv, downsampled note | +| S05 | Apple App Store LixelGO, `https://apps.apple.com/gr/app/lixelgo/id6476125173` | real-time point cloud data, RGB point cloud rendering, camera loading fixes, airborne/mount hints | +| S06 | Google Play LixelGO, `https://play.google.com/store/apps/details?hl=en&id=com.XGrids.LixelGo.os` | real-time scanner control, live data preview, monitoring, encrypted in transit | + +### 48.2. OSINT / FCC + +| ID | Источник | Что подтверждает | +|---|---|---| +| S08 | FCC ID 2A9PI-K1, `https://fcc.report/FCC-ID/2A9PI-K1` | FCC ID, радиодиапазоны, документы K1 | +| S09 | FCC 5 GHz Wi-Fi report, `https://fcc.report/FCC-ID/2A9PI-K1/7248480.pdf` | 802.11a/n/ac, 5 GHz channels, battery/version | +| S10 | FCC 2.4 GHz Wi-Fi report, `https://fcc.report/FCC-ID/2A9PI-K1/7248312.pdf` | 802.11b/g/n, 2.4 GHz, up to 150 Mbps | +| S11 | FCC BT report, `https://fcc.report/FCC-ID/2A9PI-K1/7248224.pdf` | Bluetooth 5.0 BR+EDR 1/2/3 Mbps, battery/version | +| S12 | FCC BLE report, `https://fcc.report/FCC-ID/2A9PI-K1/7248310.pdf` | BLE 1/2 Mbps, 40 channels, battery/version | +| S13 | FCC internal photos, `https://fcc.report/FCC-ID/2A9PI-K1/7248458.pdf` | RK3588S, RTL8822CE, antennas, modular PCBs | + +### 48.3. Инструменты + +| ID | Источник | Что подтверждает | +|---|---|---| +| S14 | Bleak macOS backend, `https://bleak.readthedocs.io/en/latest/backends/macos.html` | CoreBluetooth UUID behavior, pairing limitation, permissions | +| S15 | Wireshark macOS install, `https://www.wireshark.org/docs/wsug_html_chunked/ChBuildInstallOSXInstall.html` | `.dmg`, ChmodBPF required for capture | +| S16 | Apple Developer Bluetooth, `https://developer.apple.com/bluetooth/` | Core Bluetooth and PacketLogger in Additional Tools for Xcode | +| S17 | Nmap macOS install, `https://nmap.org/book/inst-macosx.html` | Nmap support for macOS | + +### 48.4. Secondary / context + +| ID | Источник | Что использовать осторожно | +|---|---|---| +| S07 | Alpine Reality Capture LixelGO setup guide, `https://www.alpinerealitycapture.com/guide/lixelgo-setup` | phone loss -> recording continues but preview/control lost; Bridge Mode shared network | +| S18 | XGRIDS GitHub, `https://github.com/xgrids` | LCC/SDK ecosystem, not live K1 protocol | +| S19 | FAST-LIVO2, `https://github.com/hku-mars/FAST-LIVO2` | open LiDAR-inertial-visual architecture fallback | +| S20 | LIV_handhold, `https://github.com/xuankuzcr/LIV_handhold` | hardware sync/CAD/UAV-mounted reference | +| S21 | livo-handheld, `https://github.com/RomanStadlhuber/livo-handheld` | Livox + Raspberry Pi + camera reference | +| S22 | mandeye_controller, `https://github.com/JanuszBedkowski/mandeye_controller` | open handheld scanner controller reference | +| S23 | HDMapping, `https://github.com/MapsHD/HDMapping` | mobile mapping open stack reference | + +## 49. Итоговая карта решения + +```text +Phase 0: no-code inventory + -> firmware/app versions + -> USB project structure + -> autonomous button scan confirmed + +Phase 1: Mac lab tooling + -> BLE scan/GATT dump + -> Wi-Fi discover/ports/capture + -> PCAP flow summary + +Phase 2: LixelGO behavior capture + -> Direct/Bridge provisioning baseline + -> app scan vs button scan + -> reconnect behavior + +Phase 3: Stream identification + -> point cloud candidate + -> pose candidate + -> camera/video candidate + -> status candidate + +Phase 4: Decoding + -> point cloud probe/export + -> camera frame extraction, if available + -> pose/status decoder + +Phase 5: Runtime connector + -> K1 connects to lab/onboard Wi-Fi + -> Mac/client opens data session + -> raw cache + decoded streams + -> OpenCV/ML/ROS-compatible outputs + +Phase 6: fallback only if needed + -> LixelGO broker + -> Android HCI/APK analysis + -> internal Linux/RK3588S route +``` + +## 50. Ключевые takeaways + +1. SSID/password - это обычная сеть пользователя; проблема не в них, а в data connection. +2. Bluetooth почти точно нужен для provisioning, а не для live cloud/video. +3. Wi-Fi - главный канал live data. +4. K1 умеет автономно стартовать scan кнопкой. +5. K1 пишет `map.las` как realtime point cloud output, но downsampled. +6. `poses.csv` - offline trajectory ground truth. +7. Камеры подтверждены аппаратно, но live camera stream наружу не доказан. +8. Внутри есть RK3588S и RTL8822CE; это подтверждает мощный onboard compute. +9. Первый этап должен быть black-box, read-only, с полным raw cache. +10. `k1-mac-link` должен быть не “финальным драйвером”, а framework для экспериментов и декодеров. diff --git a/examples/session_manifest.redacted.json b/examples/session_manifest.redacted.json new file mode 100644 index 0000000..5fe4156 --- /dev/null +++ b/examples/session_manifest.redacted.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "session_id": "20260715T120000Z_gate1_baseline", + "experiment": "autonomous_button_scan", + "tool": { + "version": "0.1.0", + "git_commit": "UNCOMMITTED" + }, + "host": { + "macos_version": "26.5.1", + "machine": "arm64", + "python_version": "3.12.13", + "wifi_interface": "en0", + "vpn_active": true + }, + "device": { + "model": "XGRIDS LixelKity K1", + "serial_suffix": null, + "activation_state": "unknown", + "firmware": null + }, + "network": { + "topology": "unknown", + "k1_address_redacted": null + }, + "timeline": [ + { + "event": "operator_started_notes", + "monotonic_seconds": 0, + "utc": "2026-07-15T12:00:00Z", + "note": null + } + ], + "artifacts": [], + "decision": { + "status": "PAUSE", + "gate": "gate1_device_baseline", + "reason": "Example only; no physical device observation has been performed.", + "next_smallest_experiment": "Record the K1 boot LED sequence without changing the router." + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..393b920 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["hatchling>=1.27,<2"] +build-backend = "hatchling.build" + +[project] +name = "ndc-xgrids-k1-connector" +version = "0.1.0" +description = "macOS-first, evidence-led research tooling for an XGRIDS Lixel K1" +readme = "README.md" +requires-python = ">=3.12,<3.13" +license = { text = "Proprietary" } +authors = [{ name = "NODE.DC" }] +dependencies = [ + "rich>=13.9,<15", + "typer>=0.15,<1", +] + +[project.scripts] +k1link = "k1link.cli:app" + +[dependency-groups] +dev = [ + "mypy>=1.15,<2", + "pytest>=8.3,<9", + "ruff>=0.11,<1", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/k1link"] + +[tool.pytest.ini_options] +addopts = "-q" +testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] + +[tool.mypy] +python_version = "3.12" +strict = true +packages = ["k1link"] + diff --git a/schemas/session-manifest.schema.json b/schemas/session-manifest.schema.json new file mode 100644 index 0000000..72e0997 --- /dev/null +++ b/schemas/session-manifest.schema.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.dcserve.ru/SILVER/NDC_xgrids-k1-connector/schemas/session-manifest.schema.json", + "title": "K1 laboratory session manifest", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "session_id", + "experiment", + "tool", + "host", + "device", + "network", + "timeline", + "artifacts", + "decision" + ], + "properties": { + "schema_version": { "const": 1 }, + "session_id": { + "type": "string", + "pattern": "^[0-9]{8}T[0-9]{6}Z_[a-z0-9_-]+$" + }, + "experiment": { "type": "string", "minLength": 1 }, + "tool": { + "type": "object", + "additionalProperties": false, + "required": ["version", "git_commit"], + "properties": { + "version": { "type": "string" }, + "git_commit": { "type": "string" } + } + }, + "host": { + "type": "object", + "additionalProperties": false, + "required": ["macos_version", "machine", "python_version"], + "properties": { + "macos_version": { "type": "string" }, + "machine": { "type": "string" }, + "python_version": { "type": "string" }, + "wifi_interface": { "type": ["string", "null"] }, + "vpn_active": { "type": "boolean" } + } + }, + "device": { + "type": "object", + "additionalProperties": false, + "required": ["model", "activation_state", "firmware"], + "properties": { + "model": { "type": "string" }, + "serial_suffix": { "type": ["string", "null"], "maxLength": 8 }, + "activation_state": { + "enum": ["unknown", "active", "activation_lock", "fault"] + }, + "firmware": { "type": ["string", "null"] } + } + }, + "network": { + "type": "object", + "additionalProperties": false, + "required": ["topology", "k1_address_redacted"], + "properties": { + "topology": { + "enum": ["unknown", "current_lan", "device_ap", "direct"] + }, + "k1_address_redacted": { "type": ["string", "null"] } + } + }, + "timeline": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["event", "monotonic_seconds"], + "properties": { + "event": { "type": "string", "minLength": 1 }, + "monotonic_seconds": { "type": "number", "minimum": 0 }, + "utc": { "type": ["string", "null"], "format": "date-time" }, + "note": { "type": ["string", "null"] } + } + } + }, + "artifacts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["path", "size_bytes", "sha256", "classification"], + "properties": { + "path": { "type": "string", "minLength": 1 }, + "size_bytes": { "type": "integer", "minimum": 0 }, + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "classification": { + "enum": ["public", "redacted", "sensitive", "secret"] + } + } + } + }, + "decision": { + "type": "object", + "additionalProperties": false, + "required": ["status", "gate", "reason"], + "properties": { + "status": { "enum": ["GO", "PAUSE", "BLOCKED"] }, + "gate": { "type": "string", "minLength": 1 }, + "reason": { "type": "string", "minLength": 1 }, + "next_smallest_experiment": { "type": ["string", "null"] } + } + } + } +} diff --git a/src/k1link/__init__.py b/src/k1link/__init__.py new file mode 100644 index 0000000..ee4620f --- /dev/null +++ b/src/k1link/__init__.py @@ -0,0 +1,3 @@ +"""NDC XGRIDS K1 connector research tooling.""" + +__version__ = "0.1.0" diff --git a/src/k1link/cli.py b/src/k1link/cli.py new file mode 100644 index 0000000..3f1e0ed --- /dev/null +++ b/src/k1link/cli.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import json +import platform +import shutil +import subprocess +import sys +from pathlib import Path +from typing import TypedDict + +import typer +from rich.console import Console +from rich.table import Table + +from k1link import __version__ + +app = typer.Typer( + name="k1link", + help="Safe, evidence-led research tooling for an owner-controlled XGRIDS K1.", + no_args_is_help=True, +) +console = Console() + + +class ToolStatus(TypedDict): + name: str + available: bool + path: str | None + + +class NetworkStatus(TypedDict): + wifi_interface: str | None + default_route_interface: str | None + vpn_default_route: bool + + +class DoctorPayload(TypedDict): + k1link_version: str + python_version: str + python_executable: str + python_is_3_12: bool + local_venv: bool + platform: str + machine: str + network: NetworkStatus + tools: list[ToolStatus] + notes: list[str] + + +def _tool_status(name: str) -> ToolStatus: + path = shutil.which(name) + return {"name": name, "available": path is not None, "path": path} + + +def _command_output(args: list[str]) -> str | None: + try: + result = subprocess.run( + args, + check=False, + capture_output=True, + text=True, + timeout=5, + ) + except (OSError, subprocess.SubprocessError): + return None + if result.returncode != 0: + return None + return result.stdout.strip() + + +def _default_route_interface() -> str | None: + output = _command_output(["route", "-n", "get", "default"]) + if output is None: + return None + for line in output.splitlines(): + key, separator, value = line.strip().partition(":") + if separator and key == "interface": + return value.strip() + return None + + +def _wifi_interface() -> str | None: + output = _command_output(["networksetup", "-listallhardwareports"]) + if output is None: + return None + blocks = output.split("\n\n") + for block in blocks: + if "Hardware Port: Wi-Fi" not in block: + continue + for line in block.splitlines(): + key, separator, value = line.partition(":") + if separator and key.strip() == "Device": + return value.strip() + return None + + +def _doctor_payload() -> DoctorPayload: + executable = Path(sys.executable) + default_route = _default_route_interface() + wifi_interface = _wifi_interface() + notes = [ + "Bluetooth permission is intentionally not requested by doctor.", + "Missing tshark/nmap is acceptable before the network-analysis gate.", + "No Homebrew or system changes are performed by this command.", + ] + if default_route is not None and default_route.startswith("utun"): + notes.append( + "Default route uses a VPN/tunnel interface; future K1 commands must resolve " + "the route for the confirmed K1 IP instead of assuming the default route." + ) + return { + "k1link_version": __version__, + "python_version": platform.python_version(), + "python_executable": str(executable), + "python_is_3_12": sys.version_info[:2] == (3, 12), + "local_venv": Path(sys.prefix).name == ".venv", + "platform": platform.platform(), + "machine": platform.machine(), + "network": { + "wifi_interface": wifi_interface, + "default_route_interface": default_route, + "vpn_default_route": bool(default_route and default_route.startswith("utun")), + }, + "tools": [_tool_status(name) for name in ("uv", "tcpdump", "tshark", "nmap", "ffmpeg")], + "notes": notes, + } + + +@app.callback() +def main() -> None: + """Run safe research commands for an owner-controlled XGRIDS K1.""" + + +@app.command() +def doctor( + json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."), +) -> None: + """Inspect the local toolchain without touching the K1 or system configuration.""" + payload = _doctor_payload() + if json_output: + typer.echo(json.dumps(payload, ensure_ascii=False, indent=2)) + return + + console.print(f"k1link {payload['k1link_version']}") + console.print(f"Python {payload['python_version']} ({payload['python_executable']})") + console.print( + "Local .venv: " + ("[green]yes[/green]" if payload["local_venv"] else "[red]no[/red]") + ) + network = payload["network"] + console.print( + "Wi-Fi interface: " + f"{network['wifi_interface'] or '-'}; default route: " + f"{network['default_route_interface'] or '-'}" + ) + + table = Table(title="External tools") + table.add_column("Tool") + table.add_column("Available") + table.add_column("Path") + for item in payload["tools"]: + table.add_row( + str(item["name"]), + "yes" if item["available"] else "no", + str(item["path"] or "-"), + ) + console.print(table) + for note in payload["notes"]: + console.print(f"- {note}") + + +if __name__ == "__main__": + app() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..f24e208 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,23 @@ +import json + +from typer.testing import CliRunner + +from k1link.cli import app + +runner = CliRunner() + + +def test_help() -> None: + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "evidence-led" in result.stdout + + +def test_doctor_json() -> None: + result = runner.invoke(app, ["doctor", "--json"]) + assert result.exit_code == 0 + payload = json.loads(result.stdout) + assert payload["k1link_version"] == "0.1.0" + assert isinstance(payload["tools"], list) + assert isinstance(payload["network"], dict) + assert any(item["name"] == "tcpdump" for item in payload["tools"]) diff --git a/tests/test_repository_inputs.py b/tests/test_repository_inputs.py new file mode 100644 index 0000000..5fb537b --- /dev/null +++ b/tests/test_repository_inputs.py @@ -0,0 +1,40 @@ +import hashlib +import json +from pathlib import Path + +ROOT = Path(__file__).parents[1] + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def test_reference_inputs_are_unchanged() -> None: + expected = { + "CODEX_K1_MAC_LINK_PREPROD_PROMPT.md": ( + "f24537119bd76f76d1914ca7f34a6736d8c5d3b6d19ead6d0ae8a0e5966c9770" + ), + "XGRIDS_Lixel_K1_PreProduction_Bible.md": ( + "22ea9d0dc6f5a623ba26d6f08108a905170db08112e0531a1d80bddd6101689c" + ), + } + for filename, expected_hash in expected.items(): + assert _sha256(ROOT / "docs" / "reference" / filename) == expected_hash + + +def test_redacted_manifest_example_is_json() -> None: + path = ROOT / "examples" / "session_manifest.redacted.json" + payload = json.loads(path.read_text(encoding="utf-8")) + assert payload["schema_version"] == 1 + assert payload["decision"]["status"] in {"GO", "PAUSE", "BLOCKED"} + + +def test_manifest_schema_is_json() -> None: + path = ROOT / "schemas" / "session-manifest.schema.json" + payload = json.loads(path.read_text(encoding="utf-8")) + assert payload["$schema"] == "https://json-schema.org/draft/2020-12/schema" + assert payload["properties"]["schema_version"]["const"] == 1 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9e85b5e --- /dev/null +++ b/uv.lock @@ -0,0 +1,255 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "ndc-xgrids-k1-connector" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "rich" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "rich", specifier = ">=13.9,<15" }, + { name = "typer", specifier = ">=0.15,<1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.15,<2" }, + { name = "pytest", specifier = ">=8.3,<9" }, + { name = "ruff", specifier = ">=0.11,<1" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "rich" +version = "14.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "typer" +version = "0.26.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +]