pyblufi 0.1.0__py3-none-any.whl
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/__init__.py +30 -0
- pyblufi/cli.py +273 -0
- pyblufi/client.py +576 -0
- pyblufi/constants.py +154 -0
- pyblufi/protocol.py +240 -0
- pyblufi-0.1.0.dist-info/METADATA +157 -0
- pyblufi-0.1.0.dist-info/RECORD +10 -0
- pyblufi-0.1.0.dist-info/WHEEL +5 -0
- pyblufi-0.1.0.dist-info/entry_points.txt +2 -0
- pyblufi-0.1.0.dist-info/top_level.txt +1 -0
pyblufi/__init__.py
ADDED
|
@@ -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
|
+
]
|
pyblufi/cli.py
ADDED
|
@@ -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())
|