pyblufi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyblufi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyblufi
3
+ Version: 0.1.0
4
+ Summary: Python BluFi client for ESP32 WiFi provisioning via BLE
5
+ Author-email: ESP32 Solar Manager <dev@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/maybetaken/pyblufi
8
+ Project-URL: Repository, https://github.com/maybetaken/pyblufi.git
9
+ Project-URL: Issues, https://github.com/maybetaken/pyblufi/issues
10
+ Keywords: blufi,esp32,ble,wifi,provisioning
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: License :: OSI Approved :: MIT License
21
+ Classifier: Operating System :: OS Independent
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: bleak>=0.20.0
25
+ Requires-Dist: cryptography>=3.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
29
+ Requires-Dist: black>=23.0; extra == "dev"
30
+ Requires-Dist: flake8>=6.0; extra == "dev"
31
+
32
+ # pyblufi
33
+
34
+ Python implementation of Espressif's BluFi protocol for ESP32 WiFi provisioning over Bluetooth Low Energy (BLE).
35
+
36
+ ## Features
37
+
38
+ - Full BluFi protocol implementation
39
+ - WiFi provisioning via BLE using [bleak](https://github.com/hbldh/bleak)
40
+ - WiFi scanning from ESP32 device
41
+ - WiFi status reporting (connection, SSID, BSSID)
42
+ - Custom data transmission
43
+ - Async/await API
44
+ - Command-line tool for quick provisioning
45
+ - No encryption support (plaintext mode)
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install .
51
+ ```
52
+
53
+ Or from PyPI (once published):
54
+ ```bash
55
+ pip install pyblufi
56
+ ```
57
+
58
+ ## Requirements
59
+
60
+ - Python >= 3.8
61
+ - bleak >= 0.20.0
62
+
63
+ ## Quick Start
64
+
65
+ ### Command Line
66
+
67
+ ```bash
68
+ # Scan for BluFi devices
69
+ blufi-provision scan
70
+
71
+ # Provision a device with WiFi credentials
72
+ blufi-provision provision --ssid "MyWiFi" --password "secret123" --device "BLUFI_DEVICE"
73
+ ```
74
+
75
+ ### Python API
76
+
77
+ ```python
78
+ import asyncio
79
+ from pyblufi import BlufiClient
80
+
81
+ async def main():
82
+ client = BlufiClient(debug=True)
83
+
84
+ # Scan and find device
85
+ device = await client.scan_for_device("BLUFI_DEVICE", timeout=10)
86
+
87
+ # Connect
88
+ await client.connect(device.address)
89
+
90
+ # Set STA mode
91
+ await client.set_sta_mode()
92
+
93
+ # Send SSID and password
94
+ await client.set_ssid("MyWiFi")
95
+ await client.set_password("secret123")
96
+
97
+ # Trigger connection
98
+ await client.connect_to_ap()
99
+
100
+ # Wait for WiFi status via notifications
101
+ status = await client.wait_for_wifi_status(timeout=30)
102
+ print(f"Connected: {status}")
103
+
104
+ await client.disconnect()
105
+
106
+ asyncio.run(main())
107
+ ```
108
+
109
+ ## Protocol
110
+
111
+ BluFi frame format:
112
+ ```
113
+ [TypeSubtype][FrameControl][Sequence][Length][Data...]
114
+ ```
115
+
116
+ - **TypeSubtype**: Combined byte (type in lower 2 bits, subtype in upper 6 bits)
117
+ - **FrameControl**: Control flags (encryption, checksum, fragmentation)
118
+ - **Sequence**: Auto-incrementing sequence number (wraps at 256)
119
+ - **Length**: Payload length (max 255)
120
+ - **Data**: Variable-length payload
121
+
122
+ ### Frame Types
123
+
124
+ | Type | Value | Description |
125
+ |------|-------|-------------|
126
+ | CTRL | 0x00 | Control frame |
127
+ | DATA | 0x01 | Data frame |
128
+
129
+ ### Control Subtypes
130
+
131
+ | Subtype | Command |
132
+ |---------|---------|
133
+ | 0x00 | ACK |
134
+ | 0x02 | SET_WIFI_OPMODE |
135
+ | 0x03 | CONN_TO_AP |
136
+ | 0x05 | GET_WIFI_STATUS |
137
+ | 0x09 | GET_WIFI_LIST |
138
+
139
+ ### Data Subtypes
140
+
141
+ | Subtype | Command |
142
+ |---------|---------|
143
+ | 0x00 | NEG (negotiation) |
144
+ | 0x02 | STA_SSID |
145
+ | 0x03 | STA_PASSWD |
146
+ | 0x0F | WIFI_REP (WiFi status report) |
147
+ | 0x11 | WIFI_LIST (WiFi scan results) |
148
+
149
+ ## Testing
150
+
151
+ ```bash
152
+ pytest tests/ -v
153
+ ```
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,126 @@
1
+ # pyblufi
2
+
3
+ Python implementation of Espressif's BluFi protocol for ESP32 WiFi provisioning over Bluetooth Low Energy (BLE).
4
+
5
+ ## Features
6
+
7
+ - Full BluFi protocol implementation
8
+ - WiFi provisioning via BLE using [bleak](https://github.com/hbldh/bleak)
9
+ - WiFi scanning from ESP32 device
10
+ - WiFi status reporting (connection, SSID, BSSID)
11
+ - Custom data transmission
12
+ - Async/await API
13
+ - Command-line tool for quick provisioning
14
+ - No encryption support (plaintext mode)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install .
20
+ ```
21
+
22
+ Or from PyPI (once published):
23
+ ```bash
24
+ pip install pyblufi
25
+ ```
26
+
27
+ ## Requirements
28
+
29
+ - Python >= 3.8
30
+ - bleak >= 0.20.0
31
+
32
+ ## Quick Start
33
+
34
+ ### Command Line
35
+
36
+ ```bash
37
+ # Scan for BluFi devices
38
+ blufi-provision scan
39
+
40
+ # Provision a device with WiFi credentials
41
+ blufi-provision provision --ssid "MyWiFi" --password "secret123" --device "BLUFI_DEVICE"
42
+ ```
43
+
44
+ ### Python API
45
+
46
+ ```python
47
+ import asyncio
48
+ from pyblufi import BlufiClient
49
+
50
+ async def main():
51
+ client = BlufiClient(debug=True)
52
+
53
+ # Scan and find device
54
+ device = await client.scan_for_device("BLUFI_DEVICE", timeout=10)
55
+
56
+ # Connect
57
+ await client.connect(device.address)
58
+
59
+ # Set STA mode
60
+ await client.set_sta_mode()
61
+
62
+ # Send SSID and password
63
+ await client.set_ssid("MyWiFi")
64
+ await client.set_password("secret123")
65
+
66
+ # Trigger connection
67
+ await client.connect_to_ap()
68
+
69
+ # Wait for WiFi status via notifications
70
+ status = await client.wait_for_wifi_status(timeout=30)
71
+ print(f"Connected: {status}")
72
+
73
+ await client.disconnect()
74
+
75
+ asyncio.run(main())
76
+ ```
77
+
78
+ ## Protocol
79
+
80
+ BluFi frame format:
81
+ ```
82
+ [TypeSubtype][FrameControl][Sequence][Length][Data...]
83
+ ```
84
+
85
+ - **TypeSubtype**: Combined byte (type in lower 2 bits, subtype in upper 6 bits)
86
+ - **FrameControl**: Control flags (encryption, checksum, fragmentation)
87
+ - **Sequence**: Auto-incrementing sequence number (wraps at 256)
88
+ - **Length**: Payload length (max 255)
89
+ - **Data**: Variable-length payload
90
+
91
+ ### Frame Types
92
+
93
+ | Type | Value | Description |
94
+ |------|-------|-------------|
95
+ | CTRL | 0x00 | Control frame |
96
+ | DATA | 0x01 | Data frame |
97
+
98
+ ### Control Subtypes
99
+
100
+ | Subtype | Command |
101
+ |---------|---------|
102
+ | 0x00 | ACK |
103
+ | 0x02 | SET_WIFI_OPMODE |
104
+ | 0x03 | CONN_TO_AP |
105
+ | 0x05 | GET_WIFI_STATUS |
106
+ | 0x09 | GET_WIFI_LIST |
107
+
108
+ ### Data Subtypes
109
+
110
+ | Subtype | Command |
111
+ |---------|---------|
112
+ | 0x00 | NEG (negotiation) |
113
+ | 0x02 | STA_SSID |
114
+ | 0x03 | STA_PASSWD |
115
+ | 0x0F | WIFI_REP (WiFi status report) |
116
+ | 0x11 | WIFI_LIST (WiFi scan results) |
117
+
118
+ ## Testing
119
+
120
+ ```bash
121
+ pytest tests/ -v
122
+ ```
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,30 @@
1
+ """PyBluFi - Python implementation of ESP32 BluFi protocol for WiFi provisioning over BLE."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "ESP32 Solar Manager"
5
+
6
+ from .protocol import BlufiProtocol
7
+ from .client import BlufiClient, WifiStatus, WifiNetwork
8
+ from .constants import (
9
+ BLUFI_TYPE,
10
+ BLUFI_CTRL_SUBTYPE,
11
+ BLUFI_DATA_SUBTYPE,
12
+ WIFI_OP_MODE,
13
+ WIFI_AUTH_MODE,
14
+ WIFI_STATUS,
15
+ SECURITY_MODE,
16
+ )
17
+
18
+ __all__ = [
19
+ "BlufiProtocol",
20
+ "BlufiClient",
21
+ "WifiStatus",
22
+ "WifiNetwork",
23
+ "BLUFI_TYPE",
24
+ "BLUFI_CTRL_SUBTYPE",
25
+ "BLUFI_DATA_SUBTYPE",
26
+ "WIFI_OP_MODE",
27
+ "WIFI_AUTH_MODE",
28
+ "WIFI_STATUS",
29
+ "SECURITY_MODE",
30
+ ]
@@ -0,0 +1,273 @@
1
+ """Command-line interface for PyBluFi."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import sys
6
+ import textwrap
7
+ from typing import List, Optional
8
+
9
+ from .client import BlufiClient, WifiStatus
10
+ from .constants import WIFI_STATUS
11
+
12
+
13
+ def _print_wifi_status(status: WifiStatus) -> None:
14
+ """Print WiFi status in a nice format."""
15
+ status_map = {
16
+ WIFI_STATUS.STA_CONN_SUCCESS: "Connected (with IP)",
17
+ WIFI_STATUS.STA_CONN_FAIL: "Connection failed",
18
+ WIFI_STATUS.STA_CONN_CONNECTING: "Connecting...",
19
+ WIFI_STATUS.STA_CONN_NO_IP: "Connected (no IP)",
20
+ }
21
+ conn_status = status_map.get(status.sta_conn_status, f"Unknown ({status.sta_conn_status})")
22
+
23
+ print(f" Operation Mode: {status.op_mode}")
24
+ print(f" Connection: {conn_status}")
25
+ print(f" SSID: {status.sta_ssid or 'N/A'}")
26
+ print(f" IP: {status.sta_ip or 'N/A'}")
27
+ print(f" BSSID: {status.sta_bssid or 'N/A'}")
28
+ print(f" SoftAP Clients: {status.softap_conn_num}")
29
+
30
+
31
+ async def cmd_scan(args: argparse.Namespace) -> int:
32
+ """Scan for BluFi devices."""
33
+ print("Scanning for BluFi devices...")
34
+ client = BlufiClient(debug=args.debug)
35
+ devices = await client.scan(timeout=args.timeout)
36
+
37
+ if not devices:
38
+ print("No devices found.")
39
+ return 1
40
+
41
+ print(f"\nFound {len(devices)} device(s):")
42
+ print("-" * 60)
43
+ for i, dev in enumerate(devices, 1):
44
+ print(f"{i}. {dev.name or 'Unknown'} ({dev.address})")
45
+ print(f" RSSI: {dev.rssi} dBm")
46
+ print("-" * 60)
47
+ return 0
48
+
49
+
50
+ async def cmd_provision(args: argparse.Namespace) -> int:
51
+ """Provision WiFi credentials to a BluFi device."""
52
+ print(f"Provisioning device: {args.device}")
53
+ print(f" SSID: {args.ssid}")
54
+ print(f" Password: {'*' * len(args.password)}")
55
+ print()
56
+
57
+ client = BlufiClient(debug=args.debug)
58
+
59
+ # Find device
60
+ if args.address:
61
+ address = args.address
62
+ print(f"Using address: {address}")
63
+ else:
64
+ print(f"Scanning for device matching '{args.device}'...")
65
+ device = await client.scan_for_device(args.device, timeout=args.timeout)
66
+ if not device:
67
+ print(f"Device '{args.device}' not found.")
68
+ return 1
69
+ address = device.address
70
+ print(f"Found: {device.name} ({device.address})\n")
71
+
72
+ # Connect
73
+ print("Connecting...")
74
+ if not await client.connect(address, timeout=args.timeout):
75
+ print("Failed to connect.")
76
+ return 1
77
+ print("Connected!\n")
78
+
79
+ try:
80
+ # Send WiFi credentials
81
+ print("Setting STA mode...")
82
+ await client.set_sta_mode()
83
+ await asyncio.sleep(0.5)
84
+
85
+ print(f"Setting SSID: {args.ssid}")
86
+ await client.set_ssid(args.ssid)
87
+ await asyncio.sleep(0.5)
88
+
89
+ print("Setting password...")
90
+ await client.set_password(args.password)
91
+ await asyncio.sleep(0.5)
92
+
93
+ print("Connecting to AP...")
94
+ await client.connect_to_ap()
95
+ await asyncio.sleep(1.0)
96
+
97
+ # Wait for connection
98
+ print(f"\nWaiting for WiFi connection (timeout={args.wait}s)...")
99
+ status = await client.wait_for_wifi_status(timeout=args.wait)
100
+
101
+ if status and status.sta_conn_status == WIFI_STATUS.STA_CONN_SUCCESS:
102
+ print("\nWiFi connected successfully!")
103
+ _print_wifi_status(status)
104
+ return 0
105
+ else:
106
+ print("\nWiFi connection timed out or failed.")
107
+ return 1
108
+
109
+ except Exception as e:
110
+ print(f"Error during provisioning: {e}")
111
+ return 1
112
+ finally:
113
+ await client.disconnect()
114
+ print("\nDisconnected.")
115
+
116
+
117
+ async def cmd_status(args: argparse.Namespace) -> int:
118
+ """Get WiFi status from a connected device."""
119
+ client = BlufiClient(debug=args.debug)
120
+
121
+ print(f"Connecting to {args.address}...")
122
+ if not await client.connect(args.address, timeout=args.timeout):
123
+ print("Failed to connect.")
124
+ return 1
125
+
126
+ try:
127
+ print("Getting WiFi status...")
128
+ status = await client.get_wifi_status()
129
+ if status:
130
+ _print_wifi_status(status)
131
+ else:
132
+ print("No status received.")
133
+ finally:
134
+ await client.disconnect()
135
+
136
+ return 0
137
+
138
+
139
+ async def cmd_scan_wifi(args: argparse.Namespace) -> int:
140
+ """Scan for nearby WiFi networks via the device."""
141
+ client = BlufiClient(debug=args.debug)
142
+
143
+ print(f"Connecting to {args.address}...")
144
+ if not await client.connect(args.address, timeout=args.timeout):
145
+ print("Failed to connect.")
146
+ return 1
147
+
148
+ try:
149
+ print("Scanning for WiFi networks...")
150
+ networks = await client.get_wifi_list()
151
+ if networks:
152
+ print(f"\nFound {len(networks)} network(s):")
153
+ print("-" * 40)
154
+ for net in networks:
155
+ signal = ""
156
+ if net.rssi > -50:
157
+ signal = "Excellent"
158
+ elif net.rssi > -60:
159
+ signal = "Good"
160
+ elif net.rssi > -70:
161
+ signal = "Fair"
162
+ else:
163
+ signal = "Weak"
164
+ print(f" {net.ssid:<32} {net.rssi:>4} dBm ({signal})")
165
+ print("-" * 40)
166
+ else:
167
+ print("No networks found.")
168
+ finally:
169
+ await client.disconnect()
170
+
171
+ return 0
172
+
173
+
174
+ async def cmd_custom(args: argparse.Namespace) -> int:
175
+ """Send custom data to the device."""
176
+ client = BlufiClient(debug=args.debug)
177
+
178
+ print(f"Connecting to {args.address}...")
179
+ if not await client.connect(args.address, timeout=args.timeout):
180
+ print("Failed to connect.")
181
+ return 1
182
+
183
+ try:
184
+ data = args.data.encode("utf-8")
185
+ print(f"Sending custom data: {data.hex()}")
186
+ await client.send_custom_data(data)
187
+ print("Sent.")
188
+ finally:
189
+ await client.disconnect()
190
+
191
+ return 0
192
+
193
+
194
+ def main() -> int:
195
+ """Main CLI entry point."""
196
+ parser = argparse.ArgumentParser(
197
+ prog="blufi-provision",
198
+ description="PyBluFi - ESP32 WiFi provisioning via BLE",
199
+ formatter_class=argparse.RawDescriptionHelpFormatter,
200
+ epilog=textwrap.dedent("""\
201
+ Examples:
202
+ blufi-provision scan
203
+ blufi-provision provision --ssid "MyWiFi" --password "secret123" --device "BLUFI_DEVICE"
204
+ blufi-provision status --address AA:BB:CC:DD:EE:FF
205
+ blufi-provision scan-wifi --address AA:BB:CC:DD:EE:FF
206
+ blufi-provision custom --address AA:BB:CC:DD:EE:FF --data "hello"
207
+ """),
208
+ )
209
+ parser.add_argument(
210
+ "--debug", "-d",
211
+ action="store_true",
212
+ help="Enable debug output",
213
+ )
214
+
215
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
216
+
217
+ # scan
218
+ scan_parser = subparsers.add_parser("scan", help="Scan for BluFi devices")
219
+ scan_parser.add_argument(
220
+ "--timeout", "-t",
221
+ type=float,
222
+ default=5.0,
223
+ help="Scan timeout in seconds (default: 5.0)",
224
+ )
225
+
226
+ # provision
227
+ prov_parser = subparsers.add_parser("provision", help="Provision WiFi credentials")
228
+ prov_parser.add_argument("--ssid", "-s", required=True, help="WiFi SSID")
229
+ prov_parser.add_argument("--password", "-p", default="", help="WiFi password")
230
+ prov_parser.add_argument("--device", "-D", default="BLUFI", help="Device name filter")
231
+ prov_parser.add_argument("--address", "-a", help="BLE address (skip scan)")
232
+ prov_parser.add_argument("--timeout", "-t", type=float, default=10.0, help="Timeout")
233
+ prov_parser.add_argument("--wait", "-w", type=float, default=30.0, help="Wait for WiFi connection")
234
+
235
+ # status
236
+ status_parser = subparsers.add_parser("status", help="Get WiFi status")
237
+ status_parser.add_argument("--address", "-a", required=True, help="BLE address")
238
+ status_parser.add_argument("--timeout", "-t", type=float, default=10.0, help="Timeout")
239
+
240
+ # scan-wifi
241
+ scan_wifi_parser = subparsers.add_parser("scan-wifi", help="Scan for WiFi networks via device")
242
+ scan_wifi_parser.add_argument("--address", "-a", required=True, help="BLE address")
243
+ scan_wifi_parser.add_argument("--timeout", "-t", type=float, default=10.0, help="Timeout")
244
+
245
+ # custom
246
+ custom_parser = subparsers.add_parser("custom", help="Send custom data")
247
+ custom_parser.add_argument("--address", "-a", required=True, help="BLE address")
248
+ custom_parser.add_argument("--data", "-d", required=True, help="Data to send")
249
+ custom_parser.add_argument("--timeout", "-t", type=float, default=10.0, help="Timeout")
250
+
251
+ args = parser.parse_args()
252
+
253
+ if not args.command:
254
+ parser.print_help()
255
+ return 1
256
+
257
+ command_map = {
258
+ "scan": cmd_scan,
259
+ "provision": cmd_provision,
260
+ "status": cmd_status,
261
+ "scan-wifi": cmd_scan_wifi,
262
+ "custom": cmd_custom,
263
+ }
264
+
265
+ if args.command in command_map:
266
+ return asyncio.run(command_map[args.command](args))
267
+ else:
268
+ parser.print_help()
269
+ return 1
270
+
271
+
272
+ if __name__ == "__main__":
273
+ sys.exit(main())