Skip to content

Commit 0ab8367

Browse files
committed
fix(wifi): add macOS 15+ support for WiFi scanning and connection
- Skip WiFi pre-scan on macOS 15+ where system_profiler redacts SSID names - Skip current() check before connecting on macOS 15+ to prevent hangs during network transitions - Use connection state verification instead of SSID string matching on macOS 15+ - Fix current() method to only fallback to system_profiler if ipconfig fails - Replace bare except with except Exception for better error handling - Modified NetworksetupWireless.current() to return (None, CONNECTED) when SSID is redacted but connection is active - Updated WifiClient.is_connected to check state only, not SSID - Removed RuntimeError on redacted SSID detection - Added documentation about macOS 15+ privacy limitation
1 parent 185559a commit 0ab8367

File tree

3 files changed

+91
-32
lines changed

3 files changed

+91
-32
lines changed

demos/python/sdk_wireless_camera_control/open_gopro/network/wifi/adapters/wireless.py

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -727,26 +727,37 @@ def _sync_connect() -> bool:
727727
# Escape single quotes
728728
ssid = ssid.replace(r"'", '''"'"''')
729729

730-
logger.info(f"Scanning for {ssid}...")
731-
start = time.time()
732-
discovered = False
733-
while not discovered and (time.time() - start) <= timeout:
734-
# Scan for network
735-
response = cmd(r"/usr/sbin/system_profiler SPAirPortDataType")
736-
regex = re.compile(
737-
r"\n\s+([\x20-\x7E]{1,32}):\n\s+PHY Mode:"
738-
) # 0x20...0x7E --> ASCII for printable characters
739-
if ssid in sorted(regex.findall(response)):
740-
break
741-
time.sleep(1)
730+
# On macOS 15+, system_profiler redacts SSID names, so we skip explicit scanning
731+
# and rely on networksetup's internal scanning when attempting to connect
732+
version = Version(platform.mac_ver()[0])
733+
if version < Version("15"):
734+
logger.info(f"Scanning for {ssid}...")
735+
start = time.time()
736+
discovered = False
737+
while not discovered and (time.time() - start) <= timeout:
738+
# Scan for network
739+
response = cmd(r"/usr/sbin/system_profiler SPAirPortDataType")
740+
regex = re.compile(
741+
r"\n\s+([\x20-\x7E]{1,32}):\n\s+PHY Mode:"
742+
) # 0x20...0x7E --> ASCII for printable characters
743+
if ssid in sorted(regex.findall(response)):
744+
break
745+
time.sleep(1)
746+
else:
747+
logger.warning("Wifi Scan timed out")
748+
return False
742749
else:
743-
logger.warning("Wifi Scan timed out")
744-
return False
745-
746-
# If we're already connected, return
747-
if self.current()[0] == ssid:
748-
logger.info("Wifi already connected")
749-
return True
750+
# On macOS 15+, we can't pre-scan due to SSID redaction, so we'll attempt
751+
# connection directly. networksetup will scan internally and return error if not found.
752+
logger.info(f"Attempting to connect to {ssid} without explicitly scanning first...")
753+
# Check version once for use in connection logic
754+
version = Version(platform.mac_ver()[0])
755+
756+
# If we're already connected, return (skip on macOS 15+ where current() can hang)
757+
if version < Version("15"):
758+
if self.current()[0] == ssid:
759+
logger.info("Wifi already connected")
760+
return True
750761

751762
# Connect now that we found the ssid
752763
logger.info(f"Connecting to {ssid}...")
@@ -755,6 +766,24 @@ def _sync_connect() -> bool:
755766
if "not find" in response.lower():
756767
logger.warning("Network was not found.")
757768
return False
769+
770+
# Check if we're on macOS 15+ where SSID verification doesn't work
771+
if version >= Version("15"):
772+
# On macOS 15+, we can't verify the SSID due to redaction, so we just
773+
# check that we're connected to *some* network after a brief wait
774+
logger.debug("macOS 15+: Skipping SSID verification, checking for any connection...")
775+
time.sleep(2) # Give connection time to establish
776+
current, state = self.current()
777+
if state == SsidState.CONNECTED:
778+
logger.info("Connected to network (SSID redacted by macOS)")
779+
# Additional delay for network to be ready
780+
time.sleep(3)
781+
return True
782+
else:
783+
logger.warning("No connection established after networksetup command")
784+
return False
785+
786+
# For macOS < 15, we can verify the actual SSID
758787
# Now wait for network to actually establish
759788
current = self.current()[0]
760789
logger.debug(f"current wifi: {current}")
@@ -784,31 +813,50 @@ async def disconnect(self) -> bool:
784813
def current(self) -> tuple[str | None, SsidState]:
785814
"""Get the currently connected SSID if there is one.
786815
816+
Note:
817+
On macOS 15+, the SSID is redacted for privacy. In this case, this method
818+
will return (None, SsidState.CONNECTED) when connected to a network, even
819+
though the actual SSID cannot be determined.
820+
787821
Returns:
788-
tuple[str | None, SsidState]: (SSID or None if not connected, SSID state)
822+
tuple[str | None, SsidState]: (SSID or None if not connected/redacted, SSID state)
789823
"""
790824
# attempt to get current network
791825
ssid: str | None = None
826+
is_connected = False
827+
792828
# On MacOS <= 14...
793829
version = Version(platform.mac_ver()[0])
794830
try:
795831
if version <= Version("14"):
796832
if "Current Wi-Fi Network: " in (output := cmd(f"networksetup -getairportnetwork {self.interface}")):
797833
ssid = output.replace("Current Wi-Fi Network: ", "").strip()
834+
is_connected = True
798835
elif version < Version("15.6"):
799836
if match := re.search(r"\n\s+SSID : ([\x20-\x7E]{1,32})", cmd(f"ipconfig getsummary {self.interface}")):
800837
ssid = match.group(1)
801-
except:
838+
is_connected = True
839+
if ssid == "<redacted>":
840+
ssid = None # Redacted, but we know we're connected
841+
except Exception:
802842
pass
843+
803844
# For current MacOs versions or if above failed.
804-
# TODO this should be parsed more generally but Apple is probably going to remove this functionality also...so
805-
# I'm not going to bother. Assuming the current ID is only needed to prevent connecting "better" solution is
806-
# try to communicate with the camera using a raw HTTP endpoint to get the camera name
807-
ssid = cmd(
808-
r"system_profiler SPAirPortDataType | sed -n '/Current Network Information:/,/PHY Mode:/ p' | head -2 | tail -1 | sed 's/^[[:space:]]*//' | sed 's/:$//'"
809-
).strip()
810-
811-
return (ssid, SsidState.CONNECTED) if ssid else (None, SsidState.DISCONNECTED)
845+
if not ssid and not is_connected:
846+
output = cmd(
847+
r"system_profiler SPAirPortDataType | sed -n '/Current Network Information:/,/PHY Mode:/ p' | head -2 | tail -1 | sed 's/^[[:space:]]*//' | sed 's/:$//'"
848+
).strip()
849+
if output and output != "":
850+
is_connected = True
851+
if output != "<redacted>":
852+
ssid = output
853+
# else: connected but redacted, ssid remains None
854+
855+
# Determine state based on connection status
856+
if is_connected:
857+
return (ssid, SsidState.CONNECTED)
858+
else:
859+
return (None, SsidState.DISCONNECTED)
812860

813861
def available_interfaces(self) -> list[str]:
814862
"""Return a list of available Wifi Interface strings

demos/python/sdk_wireless_camera_control/open_gopro/network/wifi/client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,14 @@ async def close(self) -> None:
6161
def is_connected(self) -> bool:
6262
"""Is the WiFi connection currently established?
6363
64+
Note:
65+
On macOS 15+, the SSID name is redacted for privacy, so this method
66+
only checks the connection state, not the SSID name.
67+
6468
Returns:
6569
bool: True if yes, False if no
6670
"""
6771
(ssid, state) = self._controller.current()
68-
return ssid is not None and state is SsidState.CONNECTED
72+
# On modern macOS (15+), SSID may be None even when connected due to privacy redaction
73+
# So we primarily check the connection state
74+
return state is SsidState.CONNECTED

demos/python/sdk_wireless_camera_control/open_gopro/network/wifi/controller.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@ async def disconnect(self) -> bool:
6161
def current(self) -> tuple[Optional[str], SsidState]:
6262
"""Return the SSID and state of the current network.
6363
64+
Note:
65+
On macOS 15+, SSID names are redacted for privacy. In this case, SSID may be None
66+
even when state is CONNECTED. Use the state field to determine connection status.
67+
6468
Returns:
65-
tuple[Optional[str], SsidState]: Tuple of SSID str and state. If SSID is None,
66-
there is no current connection.
69+
tuple[Optional[str], SsidState]: Tuple of SSID str and state. SSID may be None if:
70+
- Not connected to any network
71+
- Connected but SSID is redacted (macOS 15+)
6772
"""
6873

6974
@abstractmethod

0 commit comments

Comments
 (0)