Metadata-Version: 2.4
Name: pi-wifi-setup
Version: 1.0.0
Summary: WiFi configuration captive portal for Raspberry Pi — button-triggered AP + web UI
Author-email: Mikhail Shmarov <inno.menu.ar@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/medvedodesa/pi-wifi-setup
Project-URL: Issues, https://github.com/medvedodesa/pi-wifi-setup/issues
Keywords: raspberry-pi,wifi,captive-portal,iot,networking,setup
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: System :: Networking
Classifier: Topic :: Internet
Classifier: Operating System :: POSIX :: Linux
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: flask>=2.3
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# pi-wifi-setup

WiFi configuration captive portal for Raspberry Pi.  
On first boot (or button press) the device creates a hotspot — connect from your phone, pick a network, enter the password, done.

---

## How it works

1. On startup, if no WiFi credentials are saved, the Pi automatically raises a hotspot
2. `hostapd` creates a virtual `uap0` AP interface alongside the existing `wlan0`
3. `dnsmasq` handles DHCP and redirects all DNS to the captive portal
4. User connects to the hotspot and is directed to `http://192.168.4.1`
5. The web UI lists nearby networks; user selects one and enters the password
6. Credentials are saved via NetworkManager (`nmcli`) and persist across reboots
7. AP shuts down, device resumes normal operation
8. A physical button can trigger setup mode at any time to change the network

---

## Requirements

**System packages:**
```bash
sudo apt install hostapd dnsmasq network-manager
sudo systemctl enable NetworkManager
sudo systemctl disable dnsmasq
```

**Python:** 3.9+  
**Hardware:** Any Raspberry Pi with built-in WiFi:

| Model | Chip | AP mode |
|---|---|---|
| Pi Zero W | CYW43438 | ✓ |
| Pi Zero 2 W | CYW43438 | ✓ |
| Pi 3B / 3B+ | CYW43438 / CYW43455 | ✓ |
| Pi 4B | CYW43455 | ✓ |
| Pi 5 | CYW43455 | ✓ |

> **Pi Zero W / Zero 2 W note:** some units require hostapd to run without
> the `driver=nl80211` line. `pi-wifi-setup` detects this automatically and
> retries without the driver directive if the first attempt fails.

---

## Installation

```bash
pip install pi-wifi-setup
```

---

## Button wiring

Connect a momentary push button between **GPIO 17** (BCM) and **GND**.  
No resistor needed — the internal pull-up is enabled in software.

```
3.3V  ──────────────────┐
                        │  (internal pull-up via lgpio)
GPIO 17 (BCM) ──────────┤
                        │
                [BUTTON]
                        │
GND ────────────────────┘
```

Physical pin reference (Pi 40-pin header):

| Signal   | Physical Pin | BCM |
|----------|-------------|-----|
| GND      | 9, 14, 20 … | —   |
| GPIO 17  | **Pin 11**  | 17  |

You can use any free GPIO pin — pass it as `button_gpio=<pin>`.

---

## Quick start

```python
import threading
import time
from pi_wifi_setup import SetupMode

# --- your application ---

_stop_event = threading.Event()

def main_loop():
    while not _stop_event.is_set():
        print("Working...")   # replace with your actual logic
        time.sleep(1)

def on_pause():
    """Called before the setup AP starts — stop your app here."""
    _stop_event.set()

def on_resume():
    """Called after a successful WiFi connection — restart your app here."""
    _stop_event.clear()
    threading.Thread(target=main_loop, daemon=True).start()

# --- WiFi setup ---

setup = SetupMode(
    ap_ssid="MyDevice",
    ap_password="setup1234",  # WPA2 password, or None for open network
    button_gpio=17,           # BCM pin number (physical pin 11)
    auto_on_no_credentials=True,
)

# Blocks until WiFi is configured if no credentials are saved.
# After that runs in the background, waiting for a button press.
setup.start(on_pause=on_pause, on_resume=on_resume)

# WiFi is ready — start your app
main_loop()
```

If your application does not need to pause during reconfiguration (e.g. it is stateless or handles reconnection itself), `on_pause` and `on_resume` can be omitted:

```python
setup = SetupMode(ap_ssid="MyDevice", ap_password="setup1234")
setup.start()  # blocks until WiFi configured, then continues

main_loop()
```

### First boot (no credentials saved)

```python
setup = SetupMode(ap_ssid="MyDevice", ap_password="setup1234")
setup.start()  # blocks until connected, then continues
```

### Already configured — button only

```python
setup = SetupMode(
    ap_ssid="MyDevice",
    ap_password="setup1234",
    auto_on_no_credentials=False,
)
setup.start(on_pause=app.pause, on_resume=app.resume)
```

### Connect programmatically (no UI)

```python
from pi_wifi_setup import WiFiConnector

wifi = WiFiConnector(interface="wlan0")

if not wifi.has_credentials():
    wifi.connect(ssid="HomeNetwork", password="secret123", timeout=30)

status = wifi.get_status()
print(status["ip_address"])
```

---

## API

### `SetupMode`

| Parameter | Default | Description |
|---|---|---|
| `button_gpio` | `17` | BCM GPIO pin for the setup button |
| `ap_ssid` | `"VendingSetup"` | Hotspot SSID |
| `ap_password` | `None` | WPA2 password, `None` for open network |
| `long_press_seconds` | `3.0` | Hold duration to trigger setup |
| `wifi_interface` | `"wlan0"` | Primary WiFi interface |
| `auto_on_no_credentials` | `True` | Auto-start AP if no network is saved |

| Method | Description |
|---|---|
| `start(on_pause, on_resume)` | Start listener; blocks if auto AP is triggered |
| `run_blocking()` | Start AP immediately and block until connected |
| `stop()` | Stop button listener |
| `has_credentials()` | `True` if a WiFi connection is saved in NetworkManager |

### `WiFiConnector`

| Method | Description |
|---|---|
| `has_credentials()` | Check for saved connections |
| `connect(ssid, password, timeout)` | Connect and persist credentials |
| `get_status()` | Return `{connected, ssid, ip_address, wpa_state}` |

---

## Architecture

```
pi_wifi_setup/
├── setup_mode.py      # Orchestrator
├── ap_manager.py      # hostapd + dnsmasq + iptables
├── web_server.py      # Flask captive portal
├── wifi_connector.py  # nmcli wrapper
├── scanner.py         # WiFi network scanner
├── button_handler.py  # GPIO long-press detector (lgpio)
└── templates/
    └── index.html     # Web UI
```

---

## License

MIT © 2026 [Mikhail Shmarov](mailto:inno.menu.ar@gmail.com)

---
