Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Created May 26, 2024 00:15
Show Gist options
  • Save ariankordi/378972bfea3b32e6a5fdf3a050f77171 to your computer and use it in GitHub Desktop.
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.
#!/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