Skip to content

Commit 890988f

Browse files
committed
macOS: work with macOS >= 14.4 again.
rename get_signal() => scan_signal() rename parse_signal() => get_signal()
1 parent 06d79cf commit 890988f

12 files changed

+140
-110
lines changed

macos_corelocation.py

-59
This file was deleted.

src/mozloc/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .base import log_wifi_loc
2-
from .modules import get_signal, parse_signal, cli_config_check
2+
from .modules import get_signal, scan_signal, config_check
33

4-
__version__ = "1.7.0"
4+
__version__ = "1.8.0"
55

6-
__all__ = ["log_wifi_loc", "get_signal", "parse_signal", "cli_config_check"]
6+
__all__ = ["log_wifi_loc", "get_signal", "scan_signal", "config_check"]

src/mozloc/__main__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pprint import pprint
1212

1313
from .base import log_wifi_loc, process_file
14-
from .modules import parse_signal, get_signal
14+
from .modules import scan_signal, get_signal
1515

1616

1717
p = argparse.ArgumentParser()
@@ -35,7 +35,7 @@
3535
args = p.parse_args()
3636

3737
if args.dump:
38-
pprint(parse_signal(get_signal()))
38+
pprint(get_signal(scan_signal()))
3939
elif args.infile:
4040
process_file(args.infile, mozilla_url=args.url)
4141
else:

src/mozloc/airport.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
""" MacOS airport functions
1+
""" macOS airport functions
22
3-
Airport was removed from macOS 14.4+, so these functions no longer are relevant.
3+
Airport was removed from macOS 14.4+.
4+
Newer macOS use macos_corelocation.py instead.
45
"""
56

67
import logging
@@ -12,7 +13,7 @@
1213
from .exe import get_airport, running_as_root
1314

1415

15-
def cli_config_check() -> bool:
16+
def config_check() -> bool:
1617
# %% check that Airport is available and WiFi is active
1718

1819
try:
@@ -32,7 +33,7 @@ def cli_config_check() -> bool:
3233
return False
3334

3435

35-
def get_signal() -> str:
36+
def scan_signal() -> str:
3637
try:
3738
ret = subprocess.check_output([get_airport(), "--scan"], text=True, timeout=30)
3839
except subprocess.CalledProcessError as err:
@@ -41,7 +42,7 @@ def get_signal() -> str:
4142
return ret
4243

4344

44-
def parse_signal(raw: str) -> pandas.DataFrame:
45+
def get_signal(raw: str):
4546
isroot = running_as_root()
4647

4748
psudo = r"\s*([0-9a-zA-Z\s\-\.]+)\s+([0-9a-f]{2}(?::[0-9a-f]{2}){5})\s+(-\d{2,3})"

src/mozloc/base.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
from __future__ import annotations
12
from time import sleep
23
from pathlib import Path
34
import logging
45
from pprint import pprint
56

6-
from .modules import get_signal, parse_signal, cli_config_check
7+
from .modules import get_signal, scan_signal, config_check
78
from .web import get_loc_mozilla
89

910
HEADER = "time lat lon accuracy NumBSSIDs"
@@ -15,7 +16,7 @@ def process_file(file: Path, mozilla_url: str):
1516
"""
1617

1718
raw = Path(file).expanduser().read_text()
18-
dat = parse_signal(raw)
19+
dat = get_signal(raw)
1920
pprint(dat)
2021
loc = get_loc_mozilla(dat, url=mozilla_url)
2122

@@ -33,13 +34,13 @@ def log_wifi_loc(cadence_sec: float, mozilla_url: str, logfile: Path | None = No
3334
print(f"updating every {cadence_sec} seconds")
3435
print(HEADER)
3536

36-
if not cli_config_check():
37+
if not config_check():
3738
raise ConnectionError("Could not connect to WiFi hardware")
3839
# nmcli errored for less than about 0.2 sec.
3940
sleep(0.5)
4041
while True:
41-
raw = get_signal()
42-
dat = parse_signal(raw)
42+
raw = scan_signal()
43+
dat = get_signal(raw)
4344
if len(dat) < 2:
4445
logging.warning(f"cannot locate since at least 2 BSSIDs required\n{dat}")
4546
sleep(cadence_sec)

src/mozloc/exe.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import annotations
12
import os
23
import functools
34
import shutil

src/mozloc/macos_corelocation.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# /usr/bin/env python3
2+
"""
3+
MUST USE SYSTEM PYTHON /usr/bin/python3
4+
DON'T USE sudo as LocationServices Python won't pop up.
5+
6+
TODO: even with all this, BSSID is still (null) / None
7+
8+
LocationServices Python app becomes available to enable in
9+
Settings > Privacy > Location Services
10+
11+
pip install pyobjc
12+
13+
from https://forums.developer.apple.com/forums/thread/748161?answerId=782574022#782574022
14+
15+
Ref: https://docs.python.org/3/using/mac.html#gui-programming
16+
"""
17+
18+
import CoreLocation
19+
import time
20+
import logging
21+
import pandas
22+
23+
import objc
24+
25+
26+
def config_check() -> bool:
27+
"""
28+
Need authorization to get BSSID
29+
"""
30+
31+
mgr = CoreLocation.CLLocationManager.alloc().init()
32+
# mgr = CoreLocation.CLLocationManager.new()
33+
# mgr.requestAlwaysAuthorization()
34+
mgr.startUpdatingLocation()
35+
36+
max_wait = 10
37+
# Get the current authorization status for Python
38+
# https://stackoverflow.com/a/75843844
39+
for i in range(1, max_wait):
40+
s = mgr.authorizationStatus()
41+
if s in {3, 4}:
42+
print("Python has been authorized for location services")
43+
return True
44+
if i == max_wait - 1:
45+
logging.error("Unable to obtain authorization")
46+
return False
47+
print(f"Waiting for authorization... do you see the Location Services popup window? {s}")
48+
time.sleep(0.5)
49+
50+
return False
51+
52+
53+
def get_signal(networks):
54+
# Get the current location
55+
56+
dat: list[dict[str, str]] = []
57+
58+
for network in networks:
59+
# print(f"{network.ssid()} {network.bssid()} {network.rssi()} channel {network.channel()}")
60+
d = {"ssid": network.ssid(), "signalStrength": network.rssi()}
61+
if network.bssid() is not None:
62+
d["macAddress"] = network.bssid()
63+
dat.append(d)
64+
65+
return pandas.DataFrame(dat)
66+
67+
68+
def scan_signal():
69+
70+
bundle_path = "/System/Library/Frameworks/CoreWLAN.framework"
71+
72+
objc.loadBundle("CoreWLAN", bundle_path=bundle_path, module_globals=globals())
73+
74+
# https://developer.apple.com/documentation/corewlan/cwinterface
75+
# iface = CWInterface.interface() # not recommended, low-level
76+
# https://developer.apple.com/documentation/corewlan/cwwificlient
77+
iface = CoreLocation.CWWiFiClient.sharedWiFiClient().interface()
78+
79+
logging.info(f"WiFi interface {iface.interfaceName()}")
80+
81+
# need to run once to warmup -- otherwise all SSID are "(null)"
82+
iface.scanForNetworksWithName_includeHidden_error_(None, True, None)
83+
84+
networks, error = iface.scanForNetworksWithName_includeHidden_error_(None, True, None)
85+
86+
return networks

src/mozloc/modules.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import sys
2+
import platform
23

34

4-
match sys.platform:
5-
case "win32":
6-
from .netsh import cli_config_check, get_signal, parse_signal
7-
case "linux":
8-
from .netman import cli_config_check, get_signal, parse_signal
9-
case "darwin":
10-
from .airport import cli_config_check, get_signal, parse_signal
11-
case _:
12-
raise ImportError(f"MozLoc doesn't work with platform {sys.platform}")
5+
if sys.platform == "win32":
6+
from .netsh import config_check, get_signal, scan_signal
7+
elif sys.platform == "linux":
8+
from .netman import config_check, get_signal, scan_signal
9+
elif sys.platform == "darwin":
10+
if tuple(map(int, platform.mac_ver()[0].split("."))) < (14, 4):
11+
from .airport import config_check, get_signal, scan_signal
12+
else:
13+
from .macos_corelocation import config_check, get_signal, scan_signal
14+
else:
15+
raise ImportError(f"MozLoc doesn't work with platform {sys.platform}")
1316

1417

15-
__all__ = ["cli_config_check", "get_signal", "parse_signal"]
18+
__all__ = ["config_check", "get_signal", "scan_signal"]

src/mozloc/netman.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .exe import get_exe
1111

1212

13-
def cli_config_check() -> bool:
13+
def config_check() -> bool:
1414
# %% check that NetworkManager CLI is available and WiFi is active
1515
exe = get_exe("nmcli")
1616

@@ -35,7 +35,7 @@ def cli_config_check() -> bool:
3535
return False
3636

3737

38-
def get_signal() -> str:
38+
def scan_signal() -> str:
3939
exe = get_exe("nmcli")
4040

4141
cmd = [exe, "-g", "SSID,BSSID,FREQ,SIGNAL", "device", "wifi"]
@@ -61,7 +61,7 @@ def get_signal() -> str:
6161
return ret
6262

6363

64-
def parse_signal(raw: str) -> pandas.DataFrame:
64+
def get_signal(raw: str):
6565
dat = pandas.read_csv(
6666
io.StringIO(raw),
6767
sep=r"(?<!\\):",

src/mozloc/netsh.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .exe import get_exe
1010

1111

12-
def cli_config_check() -> bool:
12+
def config_check() -> bool:
1313
# %% check that NetSH EXE is available and WiFi is active
1414
exe = get_exe("netsh")
1515

@@ -35,7 +35,7 @@ def cli_config_check() -> bool:
3535
return False
3636

3737

38-
def get_signal() -> str:
38+
def scan_signal() -> str:
3939
"""
4040
get signal strength using EXE
4141
@@ -52,7 +52,7 @@ def get_signal() -> str:
5252
return ret
5353

5454

55-
def parse_signal(raw: str) -> pandas.DataFrame:
55+
def get_signal(raw: str):
5656
dat: list[dict[str, str]] = []
5757
out = io.StringIO(raw)
5858

src/mozloc/tests/test_mozloc.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
import os
3+
import pandas
4+
import mozloc
5+
6+
is_ci = os.environ.get("CI", "").lower() == "true"
7+
8+
9+
@pytest.mark.skipif(is_ci, reason="CI doesn't usually have WiFi")
10+
def test_signal():
11+
if not mozloc.config_check():
12+
pytest.skip("WiFi not available")
13+
14+
loc = mozloc.get_signal(mozloc.scan_signal())
15+
16+
assert isinstance(loc, pandas.DataFrame)
17+
assert -130 < int(loc["signalStrength"][0]) < 0, "impossible RSSI"

src/mozloc/tests/test_netman.py

-20
This file was deleted.

0 commit comments

Comments
 (0)