Created
May 26, 2024 00:15
-
-
Save ariankordi/378972bfea3b32e6a5fdf3a050f77171 to your computer and use it in GitHub Desktop.
Do you want to transfer screenshots from your Switch to your PC easier, and already have a capture card set up? This is a script that scans the QR code for the "Send to Smartphone" feature and opens the browser for you automatically. Should be convenient... if you get it working, that is.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import sys # for executable and exit | |
# OpenCV opens the camera and provides basic QR scanning. | |
# pip3 install opencv-python | |
try: | |
import cv2 | |
except ImportError as e: | |
# red then bold | |
print('\033[91m' | |
'Python OpenCV is not installed.\n' | |
# reset then set bold | |
'Install it and pyzbar (also needed) with:\033[0m\033[1m\n' | |
f'{sys.executable} -m pip install opencv-python pyzbar\033[0m') | |
raise e | |
# For running commands for notifications and Wi-Fi | |
import subprocess | |
# Provide CLI arguments | |
import argparse | |
import os # For os.path | |
import platform # Detect OS, platform.system() | |
import time # Current time for timeout and sleep | |
import tempfile # Make temporary file for Windows WLAN profile | |
import webbrowser # Open browser to URL after connecting | |
has_zbar = True | |
try: | |
import pyzbar.pyzbar | |
except ImportError: | |
print('\033[91m' | |
'pyzbar is not installed, and QR codes may not scan. ' | |
'Install it with:\033[0m\033[1m\n' | |
f'{sys.executable} -m pip install pyzbar\n\033[0m') | |
has_zbar = False | |
# Helper for printing color code format. | |
def print_color(message, color_code): | |
print(f'\033[{color_code}m{message}\033[0m') # Color then reset | |
# Import win10toast on Windows | |
if platform.system() == 'Windows': | |
try: | |
from win10toast import ToastNotifier | |
except ImportError as e: | |
print_color('Install win10toast for notifications on Windows.', '0;1') | |
print(e) | |
else: | |
toaster = ToastNotifier() | |
# Function to display a notification (cross-platform) | |
def notify(title, message): | |
# Just title and message for now. | |
system = platform.system() | |
if system == 'Darwin': # macOS | |
# NOTE: NOT ESCAPED HERE but should be fine for our purposes | |
subprocess.run(['osascript', '-e', f'display notification "{message}" with title "{title}"']) | |
elif system == 'Linux': | |
subprocess.run(['notify-send', title, message]) | |
elif system == 'Windows': | |
# Pop up a toast notification with win10toast ToastNotifier | |
try: | |
toaster.show_toast(title, message) | |
except NameError: | |
print_color('Install win10toast for notifications on Windows.', '0;1') | |
# Default URL to open after connecting to Wi-Fi network | |
INDEX_URL = 'http://192.168.0.1/index.html' | |
# Function to connect to Wi-Fi | |
def connect_to_wifi(ssid, password): | |
system = platform.system() | |
if system == 'Darwin': # macOS | |
# NOTE: Hardcoded network interface "en0", should be default Wi-Fi interface on Mac | |
# Turn on Wi-Fi if it is turned off | |
print_color('Turning on Wi-Fi on en0', '0;2') | |
subprocess.run(['networksetup', '-setairportpower', 'en0', 'on']) | |
# Remove preferred wireless network so we can recreate it with a new password | |
print_color(f'Removing network {ssid}', '0;2') | |
subprocess.run(['networksetup', '-removepreferredwirelessnetwork', 'en0', ssid]) | |
# Run the command to connect to the network | |
print_color(f'Now connecting to network {ssid} with password {password}', '0;2') | |
result = subprocess.run(['networksetup', '-setairportnetwork', | |
'en0', ssid, password], capture_output=True, text=True) | |
if result.returncode != 0: | |
# Failed to connect to the network | |
print_color(f'networksetup FAILED with code {result.returncode}, output:', '1;31') | |
print(result.stdout) | |
notify(f'networksetup return code {result.returncode}', result.stdout) | |
else: | |
# If successful, open the index URL in the web browser. | |
print_color(f'Now opening browser to {INDEX_URL}', '0;2') | |
webbrowser.open(INDEX_URL, new=0, autoraise=True) | |
elif system == 'Linux': | |
# TODO: ONLY REALLY TESTED IF NETWORK IS ALREADY ADDED | |
# Change network's password if it already exists | |
print_color('NOTE: the Linux implementation here assumes you have already ' | |
'connected to the Switch\'s network, so if you haven\'t, ' | |
'go ahead and do so with the credentials above.', '0;1') | |
print_color(f'Modifying password on connection {ssid}', '0;2') | |
subprocess.run(['nmcli', 'connection', 'modify', ssid, 'wifi-sec.psk', password]) | |
# Enable Wi-Fi if it's not already enabled | |
print_color('Enabling Wi-Fi', '0;2') | |
subprocess.run(['nmcli', 'r', 'wifi', 'on']) | |
# Disconnect from the network if it is already connected | |
print_color(f'Disconnecting from network {ssid}', '0;2') | |
subprocess.run(['nmcli', 'c', 'down', ssid]) | |
# Finally, connect to the network | |
print_color(f'Reconnecting to network {ssid}', '0;2') | |
result = subprocess.run(['nmcli', 'c', 'up', ssid], capture_output=True, text=True) | |
if result.returncode != 0: | |
# Failed to connect to the network | |
print_color(f'nmcli c up {ssid} FAILED with code {result.returncode}, output:', '1;31') | |
print(result.stdout) | |
notify(f'nmcli return code {result.returncode}', result.stdout) | |
else: | |
# If successful, open the index URL in the web browser. | |
print_color(f'Now opening browser to {INDEX_URL}', '0;2') | |
webbrowser.open(INDEX_URL, new=0, autoraise=True) | |
elif system == 'Windows': | |
# Write a temporary file containing the Wi-Fi config | |
# NOTE: This puts the filename of the script into the XML's path name | |
try: | |
temp_xml_name_prefix = os.path.basename(__file__) | |
except NameError: | |
temp_xml_name_prefix = 'switch_qr_connect_script' | |
temp_xml_name = f'{temp_xml_name_prefix}_wifi_config.xml' | |
temp_xml = os.path.join(tempfile.gettempdir(), temp_xml_name) | |
# XML template (NOTE: NO SAFE ESCAPING, SHOULDN'T BE A PROBLEM THO) | |
print_color(f'Writing WLAN profile to {temp_xml}', '0;2') | |
with open(temp_xml, 'w') as f: | |
f.write(f'''<?xml version="1.0"?> | |
<WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1"> | |
<name>{ssid}</name> | |
<SSIDConfig> | |
<SSID> | |
<name>{ssid}</name> | |
</SSID> | |
</SSIDConfig> | |
<connectionType>ESS</connectionType> | |
<connectionMode>auto</connectionMode> | |
<MSM> | |
<security> | |
<authEncryption> | |
<authentication>WPA2PSK</authentication> | |
<encryption>AES</encryption> | |
<useOneX>false</useOneX> | |
</authEncryption> | |
<sharedKey> | |
<keyType>passPhrase</keyType> | |
<protected>false</protected> | |
<keyMaterial>{password}</keyMaterial> | |
</sharedKey> | |
</security> | |
</MSM> | |
</WLANProfile>''') | |
# Add the Wi-Fi profile in the temporary folder | |
print_color('Adding profile with netsh wlan', '0;2') | |
subprocess.run(['netsh', 'wlan', 'add', 'profile', f'filename="{temp_xml}"']) | |
# Attempt to connect to the network | |
# TODO: NOT TESTED UP TO THIS POINT | |
print_color('Connecting to network', '0;2') | |
result = subprocess.run(['netsh', 'wlan', 'connect', f'name={ssid}'], capture_output=True, text=True) | |
if result.returncode != 0: | |
# Failed to connect to the network | |
print(f'netsh wlan connect return code: {result.returncode}, output:') | |
print(result.stdout) | |
#notify(f'netsh error code {result.returncode}', result.stdout) | |
#else: | |
# NOTE: netsh returns a return code even if it.. works? UNTESTED UNTESTED | |
# If successful, open the index URL in the web browser. | |
print_color(f'Now opening browser to {INDEX_URL}', '0;2') | |
webbrowser.open(INDEX_URL, new=0, autoraise=True) | |
# Finally, remove the temporary Wi-Fi profile XML | |
os.remove(temp_xml) | |
# Fill "cameras" with list of cameras | |
# TODO: INEFFICIENT, OPENS EVERY WEBCAM AND ONLY LISTS INDEX | |
def list_cameras(): | |
index = 0 | |
cameras = [] | |
# Iterate through opening every camera (I think) | |
while True: | |
# See if this index opens | |
cap = cv2.VideoCapture(index) | |
if not cap.read()[0]: | |
# If it failed to read then break out of the loop | |
break | |
# Append its index to our list | |
cameras.append(index) | |
# Turn off the camera | |
cap.release() | |
# Increment index | |
index += 1 | |
return cameras | |
# Argument parsing | |
parser = argparse.ArgumentParser(description='Connects to a Wi-Fi QR code captured through a capture ' | |
'card or a webcam. Intended to be used with the Switch\'s "Send to Smartphone" feature.') | |
parser.add_argument('--list-cameras', action='store_true', help='List all available cameras') | |
parser.add_argument('--camera', type=int, default=0, help='Specify which camera to use (default is 0)') | |
args = parser.parse_args() | |
# Handle listing cameras | |
if args.list_cameras: | |
# Add Linux note | |
if platform.system() == 'Linux': | |
print('\033[1;31m' | |
'On Linux this doesn\'t work reliably, ' | |
'however, you can actually just use: \033[0m\033[2m\n' | |
'\tls -l /dev/v4l/by-id/' | |
'\n\033[0m\033[1;31m' | |
'This will show you the names of your capture cards (use index0), ' | |
'you can use the ID after "../../video" in this program.\033[0m') | |
cameras = list_cameras() | |
# Enumerate through the list we just created | |
for idx, cam in enumerate(cameras): | |
# Webcam index | |
print_color(f'Webcam {idx}: {cam}', '1;34') | |
print_color(f'\t* Use --camera {idx} to select this camera', '0;1') | |
# Exit with code 0 | |
sys.exit(0) | |
print_color('To list all cameras, use --list-cameras', '1;33') | |
# Open the camera using args.camera as index | |
cap = cv2.VideoCapture(args.camera) | |
if not cap.isOpened(): | |
print_color('Error: Could not open camera/cap.isOpened() returned false', '1;31') | |
notify('Could not open camera', 'cap.isOpened() returned false') | |
# Fail and stop | |
sys.exit(1) | |
# Use MJPEG for webcam. Very undocumented and may not work. | |
cap.set(cv2.CAP_PROP_FOURCC, 1196444237) | |
# Set capture resolution to 1280x720 | |
# Should be supported by most if not all capture cards & webcams. | |
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) | |
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) | |
# Set to low 15 fps | |
#cap.set(cv2.CAP_PROP_FPS, 15) | |
print_color(f'Using camera {args.camera}', '1;34') | |
# Set a timeout in case a code is not seen for too long | |
start_time = time.time() | |
TIMEOUT_MINUTES = 3 # 3 minutes | |
timeout = TIMEOUT_MINUTES * 60 # Minute | |
# Initialize OpenCV QR code detector | |
if not has_zbar: | |
qr_code_detector = cv2.QRCodeDetector() | |
# Begin loop to continuously read the camera | |
while True: | |
# Capture a frame from the camera | |
ret, frame = cap.read() | |
if not ret: | |
# Failed to capture, break out of loop and exit | |
print_color('Error: Failed to capture image from camera/cap.read() returned false', '1;31') | |
notify('Failed to capture image from camera', 'cap.read() returned false') | |
break | |
# ZBar may or may not be enabled. Case where it is enabled | |
if has_zbar: | |
# Grab a grayscale version of the frame and try to decode with ZBar | |
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) | |
zbar_codes = pyzbar.pyzbar.decode(frame_gray) | |
has_code = False | |
if len(zbar_codes) > 0: | |
# When zbar_codes has one element, there is a code | |
has_code = True | |
# Store in data just like without zbar | |
data = zbar_codes[0].data.decode('utf-8') | |
else: | |
# Without ZBar, using built in OpenCV detector | |
# Detect and decode QR code | |
data, bbox, _ = qr_code_detector.detectAndDecode(frame) | |
# If there is a QR Code on screen... | |
has_code = bbox is not None and data | |
# There is a QR code in "data" | |
if has_code: | |
# Print QR content to console | |
# Green, then no color for data | |
print('\033[1;32mRead QR Code:\033[0m', data) | |
# Check that it is a Wi-Fi QR code | |
if data.startswith('WIFI:'): | |
# Decode fields and obtain SSID and password | |
qr_fields = data.split(';') | |
ssid = qr_fields[0][7:] | |
password = qr_fields[2][2:] | |
# Check that they are not null or empty | |
if ssid and password: | |
# Good SSID & password at this point | |
print(f'\033[1;32mSwitch SSID:\033[0m {ssid}, ' | |
f'\033[1;32mPassword:\033[0m {password}') | |
# Stop camera | |
cap.release() | |
# Close window | |
cv2.destroyAllWindows() | |
# Pop up a notification with SSID and password, BEFORE connecting | |
notify('Now connecting to Switch Wi-Fi...', f'Found SSID: {ssid}, Password: {password}') | |
# Run our function to connect to network and open URL | |
connect_to_wifi(ssid, password) | |
# Break to end of script | |
break | |
else: | |
# Notify about blank SSID or password | |
notify('QR Format Error', 'SSID or password are blank in Wi-Fi QR code') | |
print_color('SSID or password are blank in Wi-Fi QR code..???', '1;31') | |
else: | |
# Not a Wi-Fi QR code | |
notify('QR Format Error', 'This is not a Wi-Fi QR code.') | |
print_color('Saw a QR code but it is not a Wi-Fi QR code.', '1;33') | |
# Wait one few seconds before repeating loop to debounce | |
# i.e., avoid displaying this error a ton of times | |
time.sleep(1) | |
# Code reaches here at end of loop or if nothing was detected | |
# On each tick, check if timeout has been reached | |
if time.time() - start_time > timeout: | |
# Timeout reached, break out of loop and exit | |
print_color(f'Error: Timeout. No QR code detected within {TIMEOUT_MINUTES} minutes.', '1;31') | |
notify('Timeout', f'No QR code detected within {TIMEOUT_MINUTES} minutes, exiting.') | |
# break out of loop/exit | |
break | |
# Name of window | |
cv2.imshow('Waiting for Switch Wi-Fi QR code to show on screen (Hit q or Esc to exit)', frame) | |
# Detect any pressed key in the window | |
key = cv2.waitKey(1) & 0xFF | |
# 17 = Ctrl (to detect Ctrl+C), 27 = Escape, 8 = Backspace, | |
if key in [ord('q'), 17, 27, 8]: | |
# Close program | |
break | |
# All "break"s listed above reach here | |
# At end of main function, close camera and close window. | |
cap.release() | |
cv2.destroyAllWindows() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment