Skip to content

Commit e1c704a

Browse files
committed
fix(python_sdk): 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 815c046 commit e1c704a

3 files changed

Lines changed: 90 additions & 31 deletions

File tree

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

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -727,26 +727,36 @@ 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
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
745754

746-
# If we're already connected, return
747-
if self.current()[0] == ssid:
748-
logger.info("Wifi already connected")
749-
return True
755+
# If we're already connected, return (skip on macOS 15+ where current() can hang)
756+
if version < Version("15"):
757+
if self.current()[0] == ssid:
758+
logger.info("Wifi already connected")
759+
return True
750760

751761
# Connect now that we found the ssid
752762
logger.info(f"Connecting to {ssid}...")
@@ -755,6 +765,24 @@ def _sync_connect() -> bool:
755765
if "not find" in response.lower():
756766
logger.warning("Network was not found.")
757767
return False
768+
769+
# Check if we're on macOS 15+ where SSID verification doesn't work
770+
if version >= Version("15"):
771+
# On macOS 15+, we can't verify the SSID due to redaction, so we just
772+
# check that we're connected to *some* network after a brief wait
773+
logger.debug("macOS 15+: Skipping SSID verification, checking for any connection...")
774+
time.sleep(2) # Give connection time to establish
775+
current, state = self.current()
776+
if state == SsidState.CONNECTED:
777+
logger.info("Connected to network (SSID redacted by macOS)")
778+
# Additional delay for network to be ready
779+
time.sleep(3)
780+
return True
781+
else:
782+
logger.warning("No connection established after networksetup command")
783+
return False
784+
785+
# For macOS < 15, we can verify the actual SSID
758786
# Now wait for network to actually establish
759787
current = self.current()[0]
760788
logger.debug(f"current wifi: {current}")
@@ -784,31 +812,51 @@ async def disconnect(self) -> bool:
784812
def current(self) -> tuple[str | None, SsidState]:
785813
"""Get the currently connected SSID if there is one.
786814
815+
Note:
816+
On macOS 15+, the SSID is redacted for privacy. In this case, this method
817+
will return (None, SsidState.CONNECTED) when connected to a network, even
818+
though the actual SSID cannot be determined.
819+
787820
Returns:
788-
tuple[str | None, SsidState]: (SSID or None if not connected, SSID state)
821+
tuple[str | None, SsidState]: (SSID or None if not connected/redacted, SSID state)
789822
"""
790823
# attempt to get current network
791824
ssid: str | None = None
825+
is_connected = False
826+
792827
# On MacOS <= 14...
793828
version = Version(platform.mac_ver()[0])
794829
try:
795830
if version <= Version("14"):
796831
if "Current Wi-Fi Network: " in (output := cmd(f"networksetup -getairportnetwork {self.interface}")):
797832
ssid = output.replace("Current Wi-Fi Network: ", "").strip()
833+
is_connected = True
798834
elif version < Version("15.6"):
799835
if match := re.search(r"\n\s+SSID : ([\x20-\x7E]{1,32})", cmd(f"ipconfig getsummary {self.interface}")):
800836
ssid = match.group(1)
801-
except:
837+
is_connected = True
838+
if ssid == "<redacted>":
839+
ssid = None # Redacted, but we know we're connected
840+
except Exception:
841+
# Ignore exceptions here; fallback logic below will attempt to get the SSID using an alternative method.
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)