Skip to content

Push Version 0.1.0 to main - #1

Merged
AntiWireless merged 99 commits into
mainfrom
test
Nov 1, 2026
Merged

AntiWireless merged 99 commits into
mainfrom
test

Conversation

@AntiWireless

Copy link
Copy Markdown
Owner

No description provided.

claude added 30 commits October 14, 2026 10:05
Analyse von OWL, OpenDrop und dem iOS-26-tauglichen airdrop-mt7921-Projekt,
ESP-IDF-Fähigkeiten für Raw-TX/Promiscuous-RX, C3 vs. C5, Architektur-
optionen für Linux/Windows und ein Fahrplan beginnend mit Hardware-Spikes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Captures and serial logs contain MAC addresses and device names of nearby
people; keep them out of the public repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Portiert aus OWL (GPL-3.0): Aufbau von PSF/MIF-Action-Frames und
AWDL-Datenframes sowie Parsing empfangener Action-Frames (Kanalsequenz,
ARPA-Name, Version, Election-Master). Offset-basiert über awdl_wire.h, damit
endian- und alignment-sicher und ohne ESP-IDF nativ testbar.

Der Host-Test baut PSF/MIF/Daten, parst sie zurück und prueft, dass die
TLVs den Puffer exakt kacheln. Der erzeugte MIF stimmt im Layout mit OWL
ueberein (u.a. Sync-Params-TLV-Laenge 73, DPS-Flags 0x8f24).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
ESP-IDF projekt in firmware/spikes/ mit USB-Serial/JTAG-Konsole:
- radio.c: esp_wifi-Glue, Raw-TX (esp_wifi_80211_tx), Promiscuous-RX
  gefiltert auf die AWDL-BSSID, RX/TX-Zaehler, Peer-Ereignis-Queue,
  tx-done-Status fuer den ACK-Test.
- console.c: Befehle info/mac/chan/rx/tx/txdata/peers/stats/chanlat/usbbench
  fuer die fuenf Spikes aus docs/SPIKES.md.
- Baut sauber fuer esp32c3 (ESP-IDF 6.0.3); Host-Test weiterhin gruen.

Dazu docs/SPIKES.md (Testplan + Wireshark-/Monitor-Setup), firmware/README.md
und tools/usb_throughput.py. Der awdl-Kern bleibt ESP-IDF-frei und testbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Die oeffentliche esp_wifi_register_80211_tx_cb / esp_80211_tx_info_t API
kam erst in ESP-IDF 5.5; auf aelteren Installationen schlug der Build fehl
(unknown type esp_80211_tx_info_t). Die interne esp_wifi_set_tx_done_cb aus
esp_private/wifi.h gibt es mit identischer Signatur in 5.1..6.x und liefert
den txStatus, den der ACK-Test braucht. Keine Versionsabhaengigkeit mehr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Ohne Default-Event-Loop scheitert das Posten der WLAN-Events
(E wifi:failed to post WiFi event ... ret=259 = ESP_ERR_INVALID_STATE).
esp_netif_init() + esp_event_loop_create_default() in radio_init ergaenzt;
esp_event/esp_netif zu REQUIRES. Behebt die Fehlermeldungen beim Start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Sendet von der Monitor-Karte des Laptops Unicast-AWDL-Datenframes an eine
Ziel-MAC (den C3), byte-genau so aufgebaut wie awdl_parse_data der Firmware
sie erwartet. So klaert Spike 3: ACKt der C3 Frames an seine eigene MAC
(parallele Aufzeichnung) und liefert er sie an den Promiscuous-Callback
(C3-Zaehler rx_data). Variante A/B und die Pruefschritte stehen im
Modul-Docstring. --dry-run baut/hexdumpt ohne scapy/root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Ein 802.11-ACK traegt nur eine Adresse (RA), und das ist der Absender des
bestaetigten Frames, also unsere --src, nicht die MAC des C3. Die Doku und
der --listen-Filter pruefen jetzt RA == --src. Spike 3 damit eindeutig:
Zustellung ja (rx_data) und HW-ACK ja (ACK mit RA == --src).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
usb_throughput.py kann den usbbench-Befehl jetzt selbst an den C3 senden
(--trigger KIB) und misst gap-basiert (Uhr startet beim ersten Byte, stoppt
nach einer Pause), statt ein festes Fenster zu mitteln. Macht Spike 5 zu
einem Host-seitigen Einzelbefehl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Plattformunabhaengige Portierung aus OWL (GPL-3.0), host-getestet:
- awdl_sync.{h,c}: AWDL-Zeitmathematik (AW/EAW, Phasenfehler, Nachfuehren
  auf den Master) aus src/sync.c.
- awdl_election.{h,c}: Election-Zustand und Vergleiche aus src/election.c.
- awdl_peers.{h,c}: feste Peer-Tabelle ohne dynamischen Speicher plus der
  Election-Lauf ueber das Array.
- Parser zieht jetzt auch Sync-Params (tx_down_counter, aw_counter) und die
  vollen Election-Felder (v1 und v2) heraus.

Host-Test erweitert (Sync-Params/Election durchs Parsen, Sync-Mathematik,
Election-Lauf inkl. Alterung) - alle gruen; C3-Build sauber (ESP-IDF 6.0.3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Aus dem adversarialen Review der OWL-Portierung:
- tlv_end schrieb die Laengen-Rueckpatchung ungeprueft und bedingungslos;
  bei zu kleinem cap ein Out-of-Bounds-Write + size_t-Unterlauf. tlv_end
  nimmt jetzt cap und ist ein No-op, wenn der TL-Header nicht passte. Damit
  haelt awdl_build_action den 'schreibt nie ueber cap'-Vertrag ein.
- Der Parser schrieb Election-Felder direkt in *out; ein laengen-verkuerztes
  v2-TLV konnte bereits geparste v1-Daten teilweise ueberschreiben und galt
  trotzdem als gueltig. Sync-/v1-/v2-Faelle lesen jetzt in lokale Variablen
  und uebernehmen erst bei vollstaendigem Erfolg (Frames kommen aus der Luft).

Regressionstests ergaenzt (cap-Respekt mit Canary, verkuerztes v2 ohne
Clobber); Host-Tests inkl. ASan/UBSan gruen, C3-Build sauber. Sync- und
Election-Module wurden als korrekt bestaetigt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- awdl_node.{h,c}: plattformunabhaengiges Gehirn, das Self/Sync/Election/Peers
  verbindet. on_rx_action aktualisiert den Peer, faehrt die Election und fuehrt
  bei unserem Master die AW-Uhr nach; tick altert Peers und waehlt neu; build
  erzeugt PSF/MIF mit der synchronisierten Zeit. Host-Test prueft: iPhone wird
  Master, Sync-Fehler ~0 nach Adoption, Peer wird gehalten und altert korrekt.
- awdl_build_action nimmt jetzt die Sync-Zeitwerte als Parameter (aus unserer
  Uhr) statt einer freilaufenden Epoche.
- radio.c: statt eigener Peer-Queue ein registrierter Handler, der geparste
  Action-Frames an die App gibt.
- main/awdl_app.{h,c}: ein FreeRTOS-Task besitzt den Node, verarbeitet RX aus
  einer Queue und sendet PSF/MIF + Cleanup zeitgesteuert; alles unter einem
  Mutex. Frame-Puffer statisch (Stack-Schutz), groessere Task-Stacks.
- console: tx/peers ueber die App, neuer sync-Befehl (Master/Hoehe/Sync-Fehler/
  Peers); mac/chan bauen den Node neu.

Host-Tests inkl. ASan/UBSan gruen; C3-Build sauber (ESP-IDF 6.0.3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Der erste Lock auf einen Master verglich zwei fremde Uhren (Boot-Epoche vs.
iPhone-Epoche) und meldete einen riesigen Scheinfehler (z.B. -235896 TU).
Das ist kein Drift, sondern nur der einmalige Versatz. Der Node merkt sich
jetzt den getrackten Master; der erste Lock meldet sync_err 0, erst Folge-
Frames desselben Masters zeigen den echten (kleinen) Drift. sync_locked wird
geloescht, sobald kein Master mehr da ist. Host-Test prueft beides.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
iOS lehnt eine Sequenz ab, die stur einen Kanal in allen 16 Slots behauptet
(mt7921-Findings). Der Node spiegelt jetzt die Sequenz des Masters: Kanal 6
genau in dessen Ch-6-Slots, sonst 0 (abwesend) - ehrlich und strukturell
Apple-aehnlich. Ohne Master faellt er auf die statische Einzelkanal-Sequenz
zurueck. awdl_self_set_chanseq_intersect/_static ergaenzt; Node ruft das nach
jeder Election. Host-Test prueft Spiegelung (6 in Slots 0/8, sonst 0) und
Rueckfall; inkl. ASan/UBSan gruen, C3-Build sauber.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Aus dem Design-Review der Kanalsequenz-Spiegelung:
- Leere Schnittmenge (Master ohne Ch-6-Slot) faellt jetzt auf die statische
  Alles-6-Sequenz zurueck statt 'nirgends praesent' zu bewerben.
- TX ist slot-bewusst: PSF/MIF gehen nur in Slots raus, die wir als praesent
  bewerben (awdl_node_tx_slot_ready); so senden wir nicht in als abwesend
  beworbenen Slots und treffen die Overlap-Slots des iPhones. Konsolen-tx
  sendet weiterhin sofort (force). Eigener Sync bleibt empfangsgetrieben.
- Header-Kommentar entschaerft: Akzeptanz einer Nur-Ch6-Sequenz und von
  Slot-0=0 ist auf Hardware noch offen (M2).
Host-Tests ergaenzt (leere Schnittmenge, realistische Slot-0-Infra-Sequenz,
TX-Slot-Gating); inkl. ASan/UBSan gruen, C3-Build sauber. Keine Code-Bugs
im Review, nur diese Design-Punkte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Erster Baustein des M2-Datenpfads: ein COBS-gerahmtes, typ-getaggtes
Nachrichtenprotokoll ueber USB-Serial/JTAG.
- awdl_cobs.{h,c}: plattformunabhaengiger COBS-Encoder/-Decoder + Byte-weiser
  Stream-Reassembler. Host-Test mit Vektoren, Kantenfaellen (254/255-Byte-
  Bloecke, eingebettete Nullen, Overflow, Truncation) und 20000-Iterationen-
  Fuzz, inkl. ASan/UBSan.
- main/bridge.{h,c}: ESP-Transport auf dem nativen usb_serial_jtag-Treiber,
  RX-Task, Dispatch; M2.0 behandelt ECHO (Transporttest) und LOG. Nachrichten-
  typen FRAME/PEER_*/CMD fuer spaetere Stufen schon definiert. Log-Ausgabe
  wird stummgeschaltet, damit sie den Binaerstrom nicht korrumpiert.
- Kconfig-Option ESPDROP_BRIDGE_MODE: Bruecken- statt Konsolenmodus; app_main
  verzweigt. M2.0 bringt WLAN noch nicht hoch (reiner Transporttest).
- tools/bridge_echo.py: Host-Echo-Test; sein COBS stimmt byte-genau mit der
  Firmware ueberein (gegengeprueft, 20k-Fuzz).

Beide C3-Builds sauber (Konsole 822 KB, Bruecke 149 KB); Host-Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Aus dem Design-Review der Bruecke (COBS-Codec war sauber):
- s_started wird vor xTaskCreate gesetzt, damit der hoeher-priorisierte
  RX-Task ein sofort anliegendes ECHO nicht verwirft (haette das erste Echo
  im Test verschlucken koennen).
- bridge_send schreibt den ganzen Frame in einer Schleife ueber Kurz-Writes
  und nutzt ein beschraenktes Write-Timeout (1 s), damit ein nicht leerender
  Host nicht den Mutex und damit alle Sender dauerhaft blockiert.
- s_tx_lock wird auf den Fehlerpfaden freigegeben.
- Log-Stummschaltung ehrlicher kommentiert (ROM-/Panic-Ausgabe bleibt; COBS
  resynchronisiert am naechsten Delimiter).
- RX-Task ignoriert n<=0 defensiv.
Beide C3-Builds sauber, Host-Tests inkl. ASan/UBSan gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
… the bridge

Symptom (host echo test): replies grew cumulatively and never matched, i.e.
the 0x00 frame delimiters were being lost. Cause: in bridge mode the native
usb_serial_jtag driver AND the ESP console (on USB-Serial/JTAG via
sdkconfig.defaults) both owned the same endpoint; the console's echo / line
discipline corrupted the binary COBS stream.

Fix: sdkconfig.bridge.defaults sets CONFIG_ESP_CONSOLE_NONE=y (plus
ESPDROP_BRIDGE_MODE), so only the native driver touches the port. main's
CMake REQUIRES is now config-INDEPENDENT (IDF expands requirements before
CONFIG_* is set, so a config-conditional REQUIRES silently dropped
esp_driver_usb_serial_jtag and broke the build); app_main.c keeps the #if
mode split. Build bridge mode in its own dir with the layered defaults.

Builds verified: bridge 143 KB (console NONE), console 822 KB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
undefined reference to usb_serial_jtag_driver_install on IDF < 5.3: the driver
moved from the legacy 'driver' component (5.1/5.2) to
'esp_driver_usb_serial_jtag' (5.3+). main's REQUIRES now picks the component
from IDF_VERSION (available during the early requirements pass, unlike
CONFIG_*), so it links on 5.1 through 6.x. Verified on 6.0.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Flip the version test so a missing/empty IDF_VERSION picks
esp_driver_usb_serial_jtag (correct on 5.3+) instead of falling through to the
legacy 'driver' (which on 5.3+ lacks the symbol -> undefined reference). Only
positively-detected 5.1/5.2 uses 'driver'. A configure-time message prints the
chosen component and IDF version for diagnosis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
…rial_jtag)

Root cause of the link error on a fresh build_bridge dir: idf.py defaults a
new build to target esp32, which has no USB-Serial/JTAG peripheral, so
CONFIG_SOC_USB_SERIAL_JTAG_SUPPORTED is off and the driver source (with
usb_serial_jtag_driver_install) is not compiled -> undefined reference, even
though the correct component is required. sdkconfig.bridge.defaults now pins
CONFIG_IDF_TARGET=esp32c3; the configure message also prints the resolved
target. Verified: fresh build resolves to esp32c3 and links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
The M2.0 echo test reported growing MISMATCHes (decoded reply +1026 bytes
per echo) even though the C3 was sending correctly delimited frames.

Root cause was entirely host-side, in tools/bridge_echo.py's Reader:
feed() reset self.buf *after* the yield. The echo loop breaks right after
the first ECHO_REPLY, which suspends and abandons the feed() generator at
the yield, so the buffer reset never ran. Each subsequent reply was then
appended onto all prior ones and COBS-decoded merged, giving exactly
(n+1)*1025 + (n-1) - 1 payload bytes at echo n (2050, 3076, 4102, ...).

Fix feed() to consume the whole chunk and return a list of completed
frames, resetting the buffer before any frame is handed out. This also
removes a second latent bug: abandoning the read_for generator on break
dropped any bytes following the delimiter in that chunk. Verified by
simulation (500/500 OK, including adversarially misaligned read chunks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Host:
- tools/espdrop_link.py: shared COBS codec, fixed Reader, frame(),
  open_serial_raw, write_all, read_frames. Single source of truth so the
  device and every host tool agree on the wire format byte-for-byte.
- tools/awdl_bridge.py: creates a Linux TAP 'awdl0', sets its MAC to the
  C3's Wi-Fi MAC (queried via the `mac` command), brings it up, and shuttles
  Ethernet frames between awdl0 and the device over BRIDGE_MSG_FRAME. Also an
  stdin console: mac | stats | ping | testframe.
- tools/bridge_echo.py: refactored onto espdrop_link (no behaviour change).

Device (bridge.c/.h):
- handle BRIDGE_MSG_CMD -> CMD_REPLY (mac via esp_read_mac, stats, ping,
  testframe) and account incoming BRIDGE_MSG_FRAME. No radio yet (M2.1), so
  host->device frames are counted and dropped; M2.2+ transmits them.
- send_test_frame emits one synthetic Ethernet frame (ethertype 0x88b5) so the
  device->host->awdl0 direction is testable before any radio exists.
- CMakeLists: add esp_hw_support for esp_mac.h / esp_read_mac.

Validated in-container against a real kernel TAP (create/teardown, frame write
to an UP interface) and by codec/echo round-trip simulation (500/500, incl.
full-MTU frames and adversarially misaligned reads).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Review follow-up on the M2.1 bridge:

- awdl_bridge.py: the usb read path now treats EOF (empty read) and OSError
  as a device disconnect and breaks cleanly, instead of busy-spinning at 100%
  CPU (EOF) or crashing with a traceback (OSError) when the C3 is unplugged
  or reset. All setup (MAC query, TAP creation, `ip link`) now runs inside the
  try/finally so teardown and the summary always print. The stdin command send
  and query_mac's write are wrapped so a stalled/gone link warns instead of
  crashing. Frame forwarding uses a bounded 1s write timeout so one direction
  under USB backpressure cannot stall the other for long (a queued design is
  deferred to the throughput milestones).

- bridge.c: route CMD replies through a cmd_reply() helper that derives the
  length from strlen, so a truncated snprintf can never be reported as longer
  than its buffer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
…ath)

Bridge mode now brings up the radio and the AWDL node (as M1 validated:
sync, election, peers, slot-gated PSF/MIF) and forwards received AWDL data
frames to the host as Ethernet, so iPhone traffic on channel 6 shows up on
awdl0 in Wireshark/tcpdump.

- radio.c/.h: add a data-frame handler (mirror of the action handler). The
  promiscuous RX callback reconstructs Ethernet from a parsed data frame
  (dst=addr1, src=addr2, carried ethertype + L3 payload) and hands it to the
  consumer from Wi-Fi task context.
- bridge.c/.h: a drop-on-overflow RX-forward queue + dedicated task. The Wi-Fi
  callback (bridge_rx_frame) builds the Ethernet frame into a serialized scratch
  slot and enqueues; the task drains it with bridge_send(FRAME), so the bounded
  USB write never runs on the Wi-Fi or AWDL-timing tasks. `stats` now also
  reports frames_to_host and rx_drops.
- app_main.c: bridge branch does radio_init + awdl_app_start + bridge_start +
  promiscuous RX, in that order so Wi-Fi/PHY boot logs precede the COBS stream.
- awdl_frame.c: awdl_parse_data now accepts QoS-data frames (subtype 8, +2-byte
  QoS control) and ignores 802.11 flag bits, so real Apple data frames parse
  instead of being rejected on a stray flag. Non-QoS behaviour is unchanged.
- test: add test_data_qos_and_flags (QoS offset, flag tolerance, subtype
  rejection). Full native host suite passes under the existing build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
…re visible

Review follow-up:

- app_main.c referenced ESP_OK in the CONFIG_ESPDROP_BRIDGE_MODE branch
  (radio_init / radio_promisc checks) but only esp_log.h was in scope, which
  does not pull in esp_err.h on IDF 5.x. The console branch compiled only
  because esp_console.h includes esp_err.h; the bridge build - M2.2's target -
  would fail with "'ESP_OK' undeclared". Add an explicit esp_err.h include.

- bridge_start: the bridge_rx_task-creation failure path returns after ESP_LOG
  was silenced, which suppressed app_main's "bridge_start failed" error. Restore
  the log level on that path so the failure is diagnosable (nothing has been
  transmitted yet, so no COBS stream to corrupt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
… cmd

First M2.3 increment: frames the host sends on awdl0 are now transmitted over
the radio as AWDL data frames, so the device path is bidirectional.

- bridge.c/.h: BRIDGE_MSG_FRAME now hands the Ethernet frame to a registered
  sink (bridge_set_frame_sink) in addition to accounting it.
- awdl_app.c/.h: awdl_app_tx_ethernet() queues one host IPv6 frame (ethertype
  0x86dd only; awdl0 is IPv6-only). The AWDL task flushes the queue with
  awdl_build_data()+radio_tx() only during slots we advertise as present (the
  master's mirrored ch6 slots), so data TX lands when the peer is on ch6 - the
  same slot-gating used for PSF/MIF. Build+send happen under the node lock that
  guards self state (seq numbers).
- app_main.c: register awdl_app_tx_ethernet as the bridge frame sink.
- bridge.c: add a `radio` command reporting the Wi-Fi counters (rx_action,
  rx_data, tx_ok/err, tx_ack/noack). tx_ack (hardware ACK on a unicast frame)
  tells us whether our TX actually reached the iPhone - the key M2.3 signal.
- tools/awdl_bridge.py: list `radio` in the command hint (no code change).

M2.3b will auto-populate the host neighbour table (PEER_ADD/REMOVE) and tune TX
reliability; for now a static `ip -6 neigh` entry makes ping6 testable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
…o limit

Review follow-up (verified finding):

- awdl_build_data adds 40 bytes of framing (24 B 802.11 + 8 B LLC/SNAP + 8 B
  AWDL data header) on top of the IPv6 payload. esp_wifi_80211_tx rejects
  frames > 1500 bytes, so a full-MTU 1484-byte payload built a 1524-byte frame
  that the driver dropped (counted only as tx_err). Harmless for ping, but it
  would silently drop every near-MTU segment of a bulk transfer (M2.4).
  Lower TX_PAYLOAD_MAX to 1460 (1500 - 40) and the host TAP MTU default to 1460
  so the IP stack never generates packets the device cannot transmit.
- radio command: print the counters with %u (32-bit casts) like `stats`, instead
  of %llu, avoiding any newlib-nano long-long printf doubt. Counts fit 32 bits
  for a test run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
So OpenDrop (M2.4) and ping6 work without manually running `ip -6 neigh`:

- awdl_app.c/.h: the AWDL task diffs its peer table each 1 Hz tick against a
  shadow snapshot and fires a registered callback (awdl_app_set_peer_cb) with
  added/removed peer MACs, outside the node lock. Registering re-syncs so a
  late-registered sink still learns existing peers.
- app_main.c: map peer events to BRIDGE_MSG_PEER_ADD / PEER_REMOVE (6-byte MAC).
- tools/awdl_bridge.py: on PEER_ADD, derive the peer's fe80:: link-local by
  modified EUI-64 and `ip -6 neigh replace <ll> lladdr <mac> dev awdl0 nud
  permanent`; on PEER_REMOVE, delete it. Permanent entries avoid NDP over the
  lossy link. Suppressed with --no-config. EUI-64 verified against a real
  device (a2:77:35:18:28:f3 -> fe80::a077:35ff:fe18:28f3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
claude added 28 commits October 23, 2026 15:58
Receiving no longer needs awdl0, root or a firewall rule either.

Firmware
- sockproxy: TCP_LISTEN opens a listening pcb on the AWDL interface; on
  accept it allocates a device-side id (0x8000+), sends TCP_OPENED with
  accepted=1 and reuses the same data/credit/close path as an outbound
  connection. One listener; reset closes it.

Host
- proxy.Manager.listen + dispatch: a TCP_OPENED for an id the host did not
  allocate (accepted) makes a TcpConn and calls the accept callback.
- device.listen(port, on_accept).
- mdns.Responder: announces _airdrop._tcp (SRV/TXT/AAAA, AAAA = the ESP's
  EUI-64 link-local from the HELLO mac) every 5 s and answers PTR queries,
  unicast for a QU question; goodbye on stop.
- receive.py + espdrop receive: the patched OpenDrop server on 127.0.0.1,
  the device listening on 8771, each accepted connection spliced to a fresh
  loopback connection into that server, announced by the responder.

Tests: an inbound-bridging fake device and a real OpenDrop sender drive
/Discover, /Ask, /Upload against espdrop receive's server; the new
ReceiveEndToEndTest checks the file arrives. 20 host tests pass. C5/C3
bridge and C5 console build. Not yet tested on hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
OpenDrop takes two things from the system that Windows lacks; the host
app now provides both, and Linux behaves as before:
- TLS certificate: made with python-cryptography before OpenDrop would
  run openssl (same files, RSA 2048, self-signed, CN = name).
- libarchive: LIBARCHIVE, a DLL recorded by the installer, archive.dll
  on PATH, then Windows' own System32 archiveint.dll; probed before
  libarchive-c binds it, with advice when none works.
On Windows the archive writer names members with "/" (OpenDrop's
os.path.join gives ".\name") and reads the files in Python, not through
libarchive's code-page paths.

tools/opendrop_install.py builds the venv without bash, git or openssl:
opendrop 0.13.0 plus the 17 airdrop-mt7921 patches at 27edf28, applied
by a strict built-in patcher, every patch and resulting file checked
against pinned digests of the bash install (byte-identical OpenDrop on
both platforms), plus pyserial and cryptography. Same stamps as the bash
script, so either recognises the other's venv. tools/espdrop.cmd starts
the app with that venv in cmd and PowerShell.

espdrop check reports Python, pyserial, zeroconf, libarchive, OpenDrop
and the TLS state, and packs, reads and unpacks a test archive. pyserial
timeouts are only set when they change (each set reconfigures the port
on Windows). 31 host tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Every command on Windows ended in "Write timeout": the ESP took no data
over USB, most likely because the chip was held in reset. The
USB-Serial/JTAG keeps the chip in reset while RTS is on and DTR off, and
with pyserial's default (both on) usbser.sys does not reliably pass DTR
on. On Windows the port is now opened with both lines off, so nothing
from the host can reset the chip; Linux keeps the default, where the
open raises both lines at once.

A write timeout now comes with a hint (replug the ESP, check the
firmware). 32 host tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
On Windows the iPad accepted the /Ask and then reset the connection on
the /Upload (WinError 10054). OpenDrop builds FileBomPath in /Ask with
os.path.join(".", name), so it announced .\test.jpg while the archive
held ./test.jpg. The /Ask plist now gets ./name too on its way out.

The Windows archive writer now also gives the member one link and an
inode number, as OpenDrop's read_disk path does on Linux; a bare
in-memory entry had 0 for both.

send now reports its steps with times (found after, TCP + TLS connect,
accepted after, upload size and rate), and -v (also for receive) logs
everything with milliseconds, to see where the wait before the Accept
prompt goes. 34 host tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
find and send work with the iPad from Windows; receive and non-ASCII
file names are still to be tested there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Session (host/espdrop/session.py) is the core a GUI builds on: one owner
of the serial port that stays. Receiving is ready the whole time, sends
run alongside, browsing is on demand with the ESP's BLE wake, and when
the ESP goes away the session reconnects and restarts receiving. Every
change is an event (device, power, browse, receiver, send with progress,
receive with progress and the files it left).

- Receiving asks first: a handler subclass replaces OpenDrop's accept-all
  /Ask with on_ask(IncomingAsk: sender, files, links, thumbnail); no
  answer in time is a no. The upload reports bytes and the new files.
- Receivers get their names from /Discover; send picks by name, id or
  part of the name, and the CLI lists them to choose from when several
  answer.
- Sends can be cancelled (TcpConn.abort) and report progress against
  the archive OpenDrop actually uploads.
- OpenDrop's HTTPS connections are routed by peer address, so several
  /Discovers and sends can run at once.
- The mDNS browser and responder share UDP 5353 on the device.

CLI: receive asks y/N (--yes accepts all, -d DIR), find shows names,
send -r NAME. 43 host tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
AWDL kept the radio receiving and sending all the time, so the C5 ran
warm even unused. The radio now sleeps (AWDL task paused, esp_wifi_stop)
unless something needs it, and wakes with channel, TX power and
promiscuous mode re-applied.

- Policy in components/awdl/awdl_power.c, with host tests: ON mode never
  sleeps; AUTO keeps the radio on for a host hold (power hold), the BLE
  wake of a send, or an open TCP connection plus 20 s. A host that stays
  silent for 90 s loses its requests.
- main/power.c runs it on a small task; `power` shows the state, `power
  hold <s>` / `power release` / `power on` / `power auto` set it.
- Boot: asleep until a host says otherwise. HELLO L2 (awdl_bridge.py)
  sets ON, as before; HELLO IP sets AUTO.
- Host: the session holds the radio while receiving (with an optional
  window that closes by itself), browsing or sending, and releases it
  otherwise; the CLI holds it for find, send and receive.

No listening for other devices' Bluetooth: receiving works while the app
keeps a receive window open. Builds for the C5 (bridge and console) and
the C3 without warnings. 44 host tests, C host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
espdrop gui (Windows: tools\espdrop-gui.cmd, no console) is one window
that owns the ESP while it runs:
- device and radio state (connected, radio on and why, or asleep);
- receiving in a window that closes by itself (10 min, 1 h, or until
  off), the name others see, and the download folder;
- a dialog before every incoming transfer: sender, files, thumbnail,
  Annehmen / Ablehnen, declined after 60 s without an answer;
- search with the BLE wake, receivers listed by their /Discover names,
  send by dropping files on one or with a file dialog;
- transfers with progress, cancel and open; tray icon, notifications,
  reconnect after unplugging.

Session events reach the GUI thread through a Qt signal; calls that talk
to the device run off it. The installer adds PySide6 and espdrop check
reports it. Offscreen GUI tests drive search and send, accept and decline
through the dialog. 48 host tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
The receive side can now keep the Wi-Fi radio asleep and wake it only
when a phone nearby actually wants to share, so the chip stays cool
without AWDL running all the time.

- An iPhone/iPad with its share sheet open sends a Continuity "AirDrop"
  advert (type 0x05) to find receivers. The firmware runs a passive BLE
  scan (NimBLE observer role) that watches for it; nothing about the
  sender is kept, only "an AirDrop advert is in range". Each advert arms
  a ~25 s wake, re-armed while the share sheet stays open; the busy grace
  then covers the transfer.
- awdl_power gains a listen flag and an advert wake, gated by host
  presence, with host tests. main/power.c starts/stops the scan to match,
  and `power listen on|off` plus a listen=/adverts= status drive it. A
  HELLO cap bit (0x04) tells the host the ESP can wake itself.
- Host: when the ESP reports the cap, receiving uses `power listen on`
  (radio sleeps between adverts) instead of a continuous hold, in the
  session, the CLI and the GUI (which shows "Funk schläft
  (empfangsbereit)"); without it, it falls back to the hold. The mDNS
  responder announces a little faster (2.5 s) so discovery after a wake
  lands inside the phone's share-sheet window.

Builds for the C5 (21% free) and C3 (37% free) with the observer role.
49 host tests, C host tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- Autostart (menu EspDrop): Windows HKCU Run key, Linux XDG autostart.
  It starts `espdrop gui --background`: tray only, receiving until
  switched off; an incoming transfer brings up only its accept dialog
  (its own top-level window), and a notification says when files
  arrived (clicking it opens them). The window stays hidden.
- Sending from the file manager (menu EspDrop): Windows "Senden an ->
  EspDrop (AirDrop)" (a shortcut written through WScript.Shell) and "Mit
  EspDrop senden" in the classic right-click menu (HKCU\Software\Classes\
  *\shell); Linux "Öffnen mit" via a desktop entry. Both run `espdrop gui
  --send FILE...`, which opens "An wen senden?": the devices a search
  finds, one click sends, progress, closes itself when sent.
- One copy per user: a later start hands its files (or "show") to the
  running app over a QLocalServer socket and exits, so the file manager
  and autostart never fight for the serial port; files from one Explorer
  call per file join the same dialog.

Windows' own share sheet ("Teilen") lists only packaged (MSIX) apps; that
waits for a packaged build. All per user, no admin rights. 57 host tests,
incl. the registry and SendTo against a stand-in winreg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
The desktop app now looks like Apple's own apps instead of stock Qt:

- ui.py, a small design system drawn in code (no image or font files):
  light and dark colours that follow the system, the system UI face
  (Segoe UI Variable on Windows 11, Inter or the desktop font on Linux),
  a style sheet for buttons, fields, menus and scroll bars, and the
  controls Qt has no Apple-shaped version of: switch, segmented control,
  round device avatars (outline of iPhone/iPad/Mac or initials) with a
  ring for waiting and progress, a radar while searching, a thin progress
  line, rounded cards with inset separators, line icons, our own mark.
- The window is one column of cards: AirDrop ("Empfangen von: Aus /
  10 Minuten / Für alle" as on the iPhone, name, folder), devices nearby
  as avatars to click or drop files on, transfers with progress, cancel
  and show in folder, and Allgemein with switches for autostart and the
  right-click menu; port, log and quit sit behind the "…" button.
- Incoming transfers ask in a floating card with the picture ("„Bens
  iPhone“ möchte ein Foto teilen."), a countdown line and Ablehnen /
  Annehmen, with no default button so Enter accepts nothing by accident.
- Sending from the file manager opens an AirDrop sheet: the files on top,
  the devices as avatars, one click sends.

Same behaviour and session underneath; 59 host tests (the GUI ones drive
the new controls offscreen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- The receiver's share sheet need not be open when sending (confirmed with
  the ESP's BLE wake): the hints are gone from the app, the CLI, the tools
  and the docs; the old spike notes say they were superseded.
- Autostart and the right-click entries are on from the first start (once;
  switched off they stay off). Every start writes the icon file
  (%LOCALAPPDATA%\EspDrop\espdrop.ico, ~/.local/share/espdrop/espdrop.png)
  and points the entries at it and at the app as it runs now. The ICO holds
  classic bitmaps up to 128 px and PNG at 256 px. The right-click registry
  entry is written before the PowerShell "Send to" shortcut, so it works
  where PowerShell is blocked.
- Windows taskbar: own AppUserModelID, so it shows our icon, not Python's.
- Received files land in Downloads itself. OpenDrop unpacks into a hidden
  staging folder (Downloads/.espdrop-incoming), then each file moves out
  under a free name ('IMG_1 (1).JPG'): nothing gets overwritten; what a
  crash or a locked file leaves there moves out at the next start.
- Allgemein -> Erscheinungsbild: Automatisch / Hell / Dunkel (also sets the
  title bar on Windows with Qt 6.8+).
- AirDrop -> "In die Zwischenablage kopieren" (off by default): received
  files as files, a single photo also as an image, links as text.
- Single instance: the running copy's server no longer deleteLater()s the
  sockets of later starts. A deferred delete still queued when the server
  went deleted the socket a second time (crash at quit, and in the tests);
  finished connections are now deleted explicitly.

68 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- espdrop/flash.py: the firmware is checked before anything is written (an
  image of the whole flash as releases ship it, or an ESP-IDF build folder
  of the bridge; ESP image headers for one chip, the bootloader where that
  chip's ROM looks, the partition table, the app's description with its
  version), then esptool writes it as a library in this process: the C5's
  USB-Serial/JTAG resets it into the ROM bootloader (no buttons), write,
  MD5 verify, restart. The connected chip must match first. Progress comes
  from esptool's documented custom-logger interface, by flash address, so
  it stays monotonic across several parts. Errors are explained in words.
- Desktop app: Allgemein -> "ESP-Firmware" shows what the ESP runs and
  offers "Aktualisieren ..." when the app ships a newer one; an ESP that
  does not answer (no, other or too old firmware) gets "Firmware
  installieren ..." under the title. The dialog lets go of the port,
  flashes in a thread, connects again; it cannot be closed (nor the app
  quit) while writing.
- espdrop flash [IMAGE]; espdrop status shows the firmware version;
  espdrop --version; espdrop check lists esptool; the installer adds it.
- Firmware: version from host/espdrop/version.py (PROJECT_VER, 0.1.0) and a
  `version` command (version, IDF, chip, revision, protocol). Not built
  here (no ESP-IDF in this environment): the CMake regex was run with
  cmake, the command's code compiled with gcc -Wall -Wextra -Werror
  -Wformat=2 against stand-in types.
- Session: the firmware version in the connected event; `silent` counts
  HELLOs in a row an ESP did not answer.
- tools/firmware_image.py: the one-file release image from a build
  (esptool merge-bin), read back and checked the same way.

88 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- tools/build_app.py: Nuitka translates the Python code (ours, the patched
  OpenDrop, every library) to C and compiles it with the Python runtime, Qt
  and the ESP firmware into one folder (EspDrop/EspDrop(.exe)), packed as
  a zip (Windows) or tar.gz (Linux). It checks the build venv (patched
  OpenDrop, PySide6, esptool, Nuitka), draws the icons, takes a firmware
  image or build folder (checked as the app checks it), bundles OpenDrop's
  root certificate and esptool's flasher stubs, on Linux libarchive as a
  fallback, LICENSE and a list of the packages with their licenses.
  --smoke starts the result: --version, check, the window offscreen.
- The compiled program is the whole app: without arguments the window, with
  arguments the command line (Windows: attaches to the console it was
  started from). espdrop/frozen.py finds the program file (Nuitka sets
  __compiled__, not sys.frozen), so autostart and the right-click entries
  start the program itself; libarchive next to it is a last candidate.
- Linux: an application menu entry (espdrop.desktop) at every start, and
  the windows name it as their desktop file, so Wayland docks show our icon.
- espdrop check verifies OpenDrop's root certificate the way AirDropConfig
  looks it up. ESPDROP_INSTANCE and ESPDROP_NO_DESKTOP let a test copy run
  beside the real app without touching the desktop.
- .gitignore: the test_awdl_power binary.

91 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
On pushes that touch the code: the ESP32-C5 bridge firmware with ESP-IDF
6.0.3 (espressif/idf:v6.0.3), merged into one image with
tools/firmware_image.py; the app compiled with Nuitka on Windows and on
Ubuntu 22.04 with that firmware inside, each started with --smoke; the host
tests (offscreen) and the firmware host tests. The files are the run's
artifacts; a tag v* drafts a release with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- frozen.program(): in a Nuitka standalone folder sys.executable is the
  folder plus the build Python's name (python / python.exe), not the
  program, so autostart and the right-click entries would have started a
  file that is not there. Now __compiled__.process_exe (the program file),
  and for a onefile build the binary the user started (containing_dir plus
  argv[0], .exe added on Windows when typed without).
- espdrop check shows that path in the program; the build's smoke test
  requires it to be the built file, so this cannot come back unnoticed.
- Windows: espdrop.cmd next to EspDrop.exe, so cmd and PowerShell wait for
  the program (it opens no console of its own) and get its exit code.
- THIRD_PARTY.txt: a license given as UNKNOWN falls back to the package's
  classifier (OpenDrop: GPLv3). Nuitka: zeroconf's compiled modules as
  they are (--no-prefer-source-code).

91 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
On Windows the path the program reports (Nuitka resolves it) and Python's
realpath may differ in case or by a \\?\ prefix; compare both normalized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- The first local build ran, but the program stopped at once: "'espdrop'
  is not a package". Python, and Nuitka when it compiles, look in the main
  script's folder first, and from tools/ `import espdrop` finds
  tools/espdrop.py rather than the package. The compiled program now
  starts from host/espdrop_app.py, next to the package.
- Nuitka names the program folder <name>.dist: build/app/EspDrop.dist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
The second local build started and checked itself: program path, Qt,
esptool, libarchive fine, but OpenDrop did not load: fleep (its file type
lookup) reads fleep/data.json at import, and Nuitka bundles data files only
for packages it is told about. A scan of every bundled package for data
files missing in the program found only this one that is needed (the
others: markdown_it's port.yaml and pkg_resources' api_tests.txt).

A missing file of OpenDrop's own was reported as a libarchive problem;
now it says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- The CI builds (Windows with MSVC, Linux) compiled and started: program
  path, Qt, esptool, libarchive (Windows' own archiveint.dll) fine. Then
  `check` stopped: it tells the patched OpenDrop by reading
  opendrop/server.py, which the program does not have (the marker is a
  comment, gone when compiled). In the program it now says "checked when
  this program was built": the build refuses an unpatched OpenDrop.
- Qt's X11 plugin needs xcb libraries (since Qt 6.5 libxcb-cursor0 and a
  few more) that minimal installs lack; the window then aborts under X11.
  check names the missing ones with their Debian packages; the build
  refuses to run without them, so the program carries them (CI installs
  them); --smoke on Linux also starts the window under Xvfb through that
  plugin. Wayland does without.

93 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
The build's own check found it missing on Ubuntu 22.04 (Qt's X11 plugin
needs it) and named the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- The name others see is now this PC's name (Windows' computer name; on
  Linux the "pretty" hostname if set, else the hostname, without a
  domain), not "EspDrop". In the window the field stays empty and shows
  that name greyed until the user types one; espdrop -n has it as default.
- Receiving a photo from an iPhone left test.JPG and ._test.JPG: iOS
  sends AppleDouble metadata (extended attributes) beside each file,
  which Apple's own unpacking folds into the file. They are removed in
  the staging folder now, also inside sent folders, recognised by the
  AppleDouble magic so a real file named "._x" stays.
- build_app.py prints the newest glibc the Linux program asks for,
  measured from every file of it.

96 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Measured in the CI build: glibc 2.35 or newer, x86_64. Named the
distributions that have it and those that do not, X11 and Wayland, what
the tray icon needs, the serial port group; Windows 10/11, 64 bit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- build_app.py makes a single file by default (Nuitka onefile):
  EspDrop-<version>-windows-x64.exe and EspDrop-<version>-linux-x86_64,
  plus THIRD_PARTY-<platform>.txt. It unpacks itself once per build into
  the user's cache ({CACHE_DIR}/EspDrop/app/v<version>-<build>) and
  starts from there, so autostart does not unpack 200 MB each time; the
  app removes older builds' folders there (and only there). Licenses,
  the icon and Linux' libarchive go into the file. --folder keeps the
  folder build.
- frozen.bundle_dir(): the program's own files are in the unpacked
  folder, not beside the single file (libarchive fallback, X11 check).
- CI: zstandard for the packing; uploads the single files; a tag v*
  drafts "EspDrop v<x>" with docs/RELEASE.md as its text (checkout
  first, it would clean the downloaded files away).
- docs/RELEASE.md: downloads, Windows and Linux start, which Linux, the
  ESP, use.

98 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
The Windows CI start of the single file failed at the second start:
Windows handed the program its own path in the 8.3 short form
(...\V01~1.0-2), the cleanup compared paths as text, took the running
build's folder for an old one and deleted it, and the next extension
module could not load. Folders are now compared as the same folder
(os.path.samefile); one that cannot be compared is left alone. A test
gives our folder under another spelling.

99 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- espdrop/i18n.py: the texts stay German in the code; _() gives them in
  the language chosen under Allgemein -> Sprache (Automatisch, Deutsch,
  English). Automatisch is German on a German system, English on any
  other. Texts with values use format placeholders, so English can order
  them its own way. A new language shows after a restart; "Jetzt neu
  starten" restarts the app at once.
- Every text the window, the dialogs, the firmware messages and the
  desktop entries (right-click menu text, menu comments) show goes
  through _(); numbers and build dates follow the language. The
  right-click entry is rewritten when the language changed.
- test_i18n.py: every _() text has an English version with the same
  placeholders, and no German text (or text that has a translation) is
  shown without _(); a GUI test runs the window in English.

103 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
- espdrop/install.py: the single-file program, started from anywhere else
  (the download), copies itself to %LOCALAPPDATA%\Programs\EspDrop (with a
  Start menu entry and an entry in "Installed apps" whose Uninstall runs
  `EspDrop uninstall`) or ~/.local/bin/espdrop, starts from there and
  removes the download when it was in the Downloads folder. A running
  copy (an older version) is asked to quit first, so starting a newer
  download updates. Not for the folder build, test runs or
  ESPDROP_PORTABLE=1.
- Uninstall from the app's menu, from "Installed apps" or with
  `EspDrop uninstall`: program, desktop entries, settings and unpacked
  copies go; received files stay. On Windows the running file is deleted
  by a detached cmd once the app has quit.
- The compiled program on Windows no longer sets its own app id, so its
  Start menu entry and taskbar pins belong to the running window.
- README.md: what EspDrop is, what one needs, install, use, building,
  credits and license. docs/RELEASE.md in English (with a German
  paragraph) for the release page.

109 host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
Double-clicked, the program has no console and its standard handles are
not valid ones; a child process that inherits them fails at once with
"WinError 6: The handle is invalid". Installing hit that (the PowerShell
call for the Start menu entry, starting the installed copy), and so would
the language restart and uninstalling. Every child now gets DEVNULL for
stdin, stdout and stderr instead.

The CI smoke test missed it: it starts the program with valid handles and
without installing. --smoke-install (CI only, it installs for real on
Windows) now starts the single file from Downloads the way Explorer does
(os.startfile: no console, no handles) and checks that it installs itself,
removes the download and is in the Start menu and "Installed apps"; on
Linux in a throwaway home folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzgaN4Bv1m8Ka9iqo7QVUN
@AntiWireless
AntiWireless merged commit 0fc91fa into main Nov 1, 2026
7 checks passed
@AntiWireless
AntiWireless deleted the test branch November 1, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants