本站重新定位为
分享技术和经验的个人博客

让Windows 10支持蓝牙连接MagicKeyboard 2并完美使用Fn功能键

本教程针对的用户群体为黑苹果用户 or 使用苹果键盘的Win用户,并且只对Intel芯片的机器适用。

由于没有使用苹果自带的BootCamp,功能缺失的主要表现为在Windows下使用蓝牙连接后正常使用的情况下,切换回Mac系统,键盘就会失灵,需要在Mac系统下重新进行蓝牙配对;并且在Windows下Fn功能键的功能无法实现,即只有默认的Fn功能。

在这里我在网上找到了一篇教程,但是使用时会有些问题,在我稍稍做修改后已经可以正常使用。

注:全程在Windows下操作。

首先,下载Python 2并安装。

下载地址:https://www.python.org/downloads/release/python-2718/

下载后直接安装,具体安装教程不细述,不会的搜索安装(注意将PATH路径写上)。

然后下载并安装7-Zip软件,因为后面的程序会用到。

下载地址:https://sparanoid.com/lab/7z/

安装完成后,使用这个脚本,调用命令为:

python bootcamp.py --model iMac20,2

这里的iMac20,2可以替换成你的机型,也可以不替换,对解决键盘问题没所谓。

白苹果用户可以在这里查询机型:

Mac mini:https://support.apple.com/en-au/HT201894

iMac:https://support.apple.com/en-au/HT201634

将查到的机型替换掉就可。

附上bootcamp.py文件内容:

#!/usr/bin/python

import os
import sys
import subprocess
import urllib2
import plistlib
import re
import tempfile
import shutil
import optparse
import datetime
import platform

from pprint import pprint
from urllib import urlretrieve
from xml.dom import minidom

SUCATALOG_URL = 'https://swscan.apple.com/content/catalogs/others/index-11-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog'
# 7-Zip MSI (15.14)
SEVENZIP_URL = 'http://www.7-zip.org/a/7z1514-x64.msi'

def status(msg):
    print "%s\n" % msg

def getCommandOutput(cmd):
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    out, err = p.communicate()
    return out

# Returns this machine's model identifier, using wmic on Windows,
# system_profiler on OS X
def getMachineModel():
    if platform.system() == 'Windows':
        rawxml = getCommandOutput(['wmic', 'computersystem', 'get', 'model', '/format:RAWXML'])
        dom = minidom.parseString(rawxml)
        results = dom.getElementsByTagName("RESULTS")
        nodes = results[0].getElementsByTagName("CIM")[0].getElementsByTagName("INSTANCE")[0]\
        .getElementsByTagName("PROPERTY")[0].getElementsByTagName("VALUE")[0].childNodes
        model = nodes[0].data
    elif platform.system() == 'Darwin':
        plistxml = getCommandOutput(['system_profiler', 'SPHardwareDataType', '-xml'])
        plist = plistlib.readPlistFromString(plistxml)
        model = plist[0]['_items'][0]['machine_model']
    return model

def downloadFile(url, filename):
    # http://stackoverflow.com/questions/13881092/
    # download-progressbar-for-python-3/13895723#13895723
    def reporthook(blocknum, blocksize, totalsize):
        readsofar = blocknum * blocksize
        if totalsize > 0:
            percent = readsofar * 1e2 / totalsize
            console_out = "\r%5.1f%% %*d / %d bytes" % (
                percent, len(str(totalsize)), readsofar, totalsize)
            sys.stderr.write(console_out)
            if readsofar >= totalsize: # near the end
                sys.stderr.write("\n")
        else: # total size is unknown
            sys.stderr.write("read %d\n" % (readsofar,))

    urlretrieve(url, filename, reporthook=reporthook)

def sevenzipExtract(arcfile, command='e', out_dir=None):
    cmd = [os.path.join(os.environ['SYSTEMDRIVE'] + "\\", "Program Files", "7-Zip", "7z.exe")]
    cmd.append(command)
    if not out_dir:
        out_dir = os.path.dirname(arcfile)
    cmd.append("-o" + out_dir)
    cmd.append("-y")
    cmd.append(arcfile)
    status("Calling 7-Zip command: %s" % ' '.join(cmd))
    retcode = subprocess.call(cmd)
    if retcode:
        sys.exit("Command failure: %s exited %s." % (' '.join(cmd), retcode))

def postInstallConfig():
    regdata = """Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Apple Inc.\Apple Keyboard Support]
"FirstTimeRun"=dword:00000000"""
    handle, path = tempfile.mkstemp()
    fd = os.fdopen(handle, 'w')
    fd.write(regdata)
    fd.close()
    subprocess.call(['regedit.exe', '/s', path])

def findBootcampMSI(search_dir):
    """Returns the path of the 64-bit BootCamp MSI"""
    # Most ESDs contain 'BootCamp.msi' and 'BootCamp64.msi'
    # Dec. 2012 ESD contains only 'BootCamp.msi' which is 64-bit
    # The top-level structure of the ESD files depends on whether
    # it's an AutoUnattend-based ESD as well, ie:
    # /Drivers/Apple/BootCamp64.msi, or
    # /BootCamp/Drivers/Apple/BootCamp.msi
    candidates = ['BootCamp64.msi', 'BootCamp.msi']
    for root, dirs, files in os.walk(search_dir):
        for msi in candidates:
            if msi in files:
                return os.path.join(root, msi)

def installBootcamp(msipath):
    logpath = os.path.abspath("/BootCamp_Install.log")
    cmd = ['cmd', '/c', 'msiexec', '/i', msipath, '/qb-', '/norestart', '/log', logpath]
    status("Executing command: '%s'" % " ".join(cmd))
    subprocess.call(cmd)
    status("Install log output:")
    with open(logpath, 'r') as logfd:
        logdata = logfd.read()
        print logdata.decode('utf-16')
    postInstallConfig()
    
def main():
    scriptdir = os.path.abspath(os.path.dirname(sys.argv[0]))

    o = optparse.OptionParser()
    o.add_option('-m', '--model', action="append",
        help="System model identifier to use (otherwise this machine's \
model is used). This can be specified multiple times to download \
multiple models in a single run.")
    o.add_option('-i', '--install', action="store_true",
        help="After the installer is downloaded, perform the install automatically. \
Can be used on Windows only.")
    o.add_option('-o', '--output-dir',
        help="Base path where the installer files will be extracted into a folder named after the \
product, ie. 'BootCamp-041-1234'. Uses the current directory if this option is omitted.")
    o.add_option('-k', '--keep-files', action="store_true",
        help="Keep the files that were downloaded/extracted. Useful only with the \
'--install' option on Windows.")
    o.add_option('-p', '--product-id',
        help="Specify an exact product ID to download (ie. '031-0787'), currently useful only for cases \
where a model has multiple BootCamp ESDs available and is not downloading the desired version \
according to the post date.")

    opts, args = o.parse_args()
    if opts.install:
        if platform.system() == 'Darwin':
            sys.exit("Installing Boot Camp can only be done on Windows!")
        if platform.system() == 'Windows' and platform.machine() != 'AMD64':
            sys.exit("Installing on anything other than 64-bit Windows is currently not supported!")

    if opts.output_dir:
        if not os.path.isdir(opts.output_dir):
            sys.exit("Output directory %s that was specified doesn't exist!" % opts.output_dir)
        if not os.access(opts.output_dir, os.W_OK):
            sys.exit("Output directory %s is not writable by this user!" % opts.output_dir)
        output_dir = opts.output_dir
    else:
        output_dir = os.getcwd()
        if output_dir.endswith('ystem32') or '\\system32\\' in output_dir.lower():
            output_dir = os.environ['SystemDrive'] + "\\"
            status("Changing output directory to %s to work around an issue \
when running the installer out of 'system32'." % output_dir)

    if opts.keep_files and not opts.install:
        sys.exit("The --keep-files option is only useful when used with --install option!")

    if opts.model:
        if opts.install:
            status("Ignoring '--model' when '--install' is used. The Boot Camp "
                   "installer won't allow other models to be installed, anyway.")
        models = opts.model
    else:
        models = [getMachineModel()]
    if len(models) > 1:
        status("Using Mac models: %s." % ', '.join(models))
    else:
        status("Using Mac model: %s." % ', '.join(models))        

    for model in models:
        sucatalog_url = SUCATALOG_URL
        # check if we defined anything in brigadier.plist
        config_plist = None
        plist_path = os.path.join(scriptdir, 'brigadier.plist')
        if os.path.isfile(plist_path):
            try:
                config_plist = plistlib.readPlist(plist_path)
            except:
                status("Config plist was found at %s but it could not be read. \
                Verify that it is readable and is an XML formatted plist." % plist_path)
        if config_plist:
            if 'CatalogURL' in config_plist.keys():
                sucatalog_url = config_plist['CatalogURL']


        urlfd = urllib2.urlopen(sucatalog_url)
        data = urlfd.read()
        p = plistlib.readPlistFromString(data)
        allprods = p['Products']

        # Get all Boot Camp ESD products
        bc_prods = []
        for (prod_id, prod_data) in allprods.items():
            if 'ServerMetadataURL' in prod_data.keys():
                bc_match = re.search('BootCamp', prod_data['ServerMetadataURL'])
                if bc_match:
                    bc_prods.append((prod_id, prod_data))
        # Find the ESD(s) that applies to our model
        pkg_data = []
        re_model = "([a-zA-Z]{4,12}[1-9]{1,2}\,[1-6])"
        for bc_prod in bc_prods:
            if 'English' in bc_prod[1]['Distributions'].keys():
                disturl = bc_prod[1]['Distributions']['English']
                distfd = urllib2.urlopen(disturl)
                dist_data = distfd.read()
                if re.search(model, dist_data):
                    supported_models = []
                    pkg_data.append({bc_prod[0]: bc_prod[1]})
                    model_matches_in_dist = re.findall(re_model, dist_data)
                    for supported_model in model_matches_in_dist:
                        supported_models.append(supported_model)
                    status("Model supported in package distribution file at %s." % disturl)
                    status("Distribution %s supports the following models: %s." % 
                        (bc_prod[0], ", ".join(supported_models)))
        
        # Ensure we have only one ESD
        if len(pkg_data) == 0:
            sys.exit("Couldn't find a Boot Camp ESD for the model %s in the given software update catalog." % model)
        if len(pkg_data) == 1:
            pkg_data = pkg_data[0]
            if opts.product_id:
                sys.exit("--product-id option is only applicable when multiple ESDs are found for a model.")
        if len(pkg_data) > 1:
            # sys.exit("There is more than one ESD product available for this model: %s. "
            #          "Automically selecting the one with the most recent PostDate.." 
            #          % ", ".join([p.keys()[0] for p in pkg_data]))
            print "There is more than one ESD product available for this model:"
            # Init latest to be epoch start
            latest_date = datetime.datetime.fromtimestamp(0)
            chosen_product = None
            for i, p in enumerate(pkg_data):
                product = p.keys()[0]
                postdate = p[product].get('PostDate')
                print "%s: PostDate %s" % (product, postdate)
                if postdate > latest_date:
                    latest_date = postdate
                    chosen_product = product

            if opts.product_id:
                if opts.product_id not in [k.keys()[0] for k in pkg_data]:
                    sys.exit("Product specified with '--product-id %s' either doesn't exist "
                             "or was not found applicable to models: %s"
                             % (opts.product_id, ", ".join(models)))
                chosen_product = opts.product_id
                print "Selecting manually-chosen product %s." % chosen_product
            else:
                print "Selecting %s as it's the most recently posted." % chosen_product

            for p in pkg_data:
                if p.keys()[0] == chosen_product:
                    selected_pkg = p
            pkg_data = selected_pkg

        pkg_id = pkg_data.keys()[0]
        pkg_url = pkg_data.values()[0]['Packages'][0]['URL']

        # make a sub-dir in the output_dir here, named by product
        landing_dir = os.path.join(output_dir, 'BootCamp-' + pkg_id)
        if os.path.exists(landing_dir):
            status("Final output path %s already exists, removing it..." % landing_dir)
            if platform.system() == 'Windows':
                # using rmdir /qs because shutil.rmtree dies on the Doc files with foreign language characters
                subprocess.call(['cmd', '/c', 'rmdir', '/q', '/s', landing_dir])
            else:
                shutil.rmtree(landing_dir)

        status("Making directory %s.." % landing_dir)
        os.mkdir(landing_dir)

        arc_workdir = tempfile.mkdtemp(prefix="bootcamp-unpack_")
        pkg_dl_path = os.path.join(arc_workdir, pkg_url.split('/')[-1])

        status("Fetching Boot Camp product at URL %s." % pkg_url)
        downloadFile(pkg_url, pkg_dl_path)

        if platform.system() == 'Windows':
            we_installed_7zip = False
            sevenzip_binary = os.path.join(os.environ['SYSTEMDRIVE'] + "\\", 'Program Files', '7-Zip', '7z.exe')
            # fetch and install 7-Zip
            if not os.path.exists(sevenzip_binary):
                tempdir = tempfile.mkdtemp()
                sevenzip_msi_dl_path = os.path.join(tempdir, SEVENZIP_URL.split('/')[-1])
                downloadFile(SEVENZIP_URL, sevenzip_msi_dl_path)
                status("Downloaded 7-zip to %s." % sevenzip_msi_dl_path)
                status("We need to install 7-Zip..")
                retcode = subprocess.call(['msiexec', '/qn', '/i', sevenzip_msi_dl_path])
                status("7-Zip install returned exit code %s." % retcode)
                we_installed_7zip = True

            status("Extracting...")
            # BootCamp.pkg (xar) -> Payload (gzip) -> Payload~ (cpio) -> WindowsSupport.dmg
            for arc in [pkg_dl_path,
                        os.path.join(arc_workdir, 'Payload'),
                        os.path.join(arc_workdir, 'Payload~')]:
                if os.path.exists(arc):
                    sevenzipExtract(arc)
            # finally, 7-Zip also extracts the tree within the DMG to the output dir
            sevenzipExtract(os.path.join(arc_workdir, 'WindowsSupport.dmg'),
                            command='x',
                            out_dir=landing_dir)
            if we_installed_7zip:
                status("Cleaning up the 7-Zip install...")
                subprocess.call(['cmd', '/c', 'msiexec', '/qn', '/x', sevenzip_msi_dl_path])
            if opts.install:
                status("Installing Boot Camp...")
                installBootcamp(findBootcampMSI(landing_dir))
                if not opts.keep_files:
                    subprocess.call(['cmd', '/c', 'rmdir', '/q', '/s', landing_dir])

            # clean up the temp dir always
            subprocess.call(['cmd', '/c', 'rmdir', '/q', '/s', arc_workdir])


        elif platform.system() == 'Darwin':
            status("Expanding flat package...")
            subprocess.call(['/usr/sbin/pkgutil', '--expand', pkg_dl_path,
                            os.path.join(arc_workdir, 'pkg')])
            status("Extracting Payload...")
            subprocess.call(['/usr/bin/tar', '-xz', '-C', arc_workdir, '-f', os.path.join(arc_workdir, 'pkg', 'Payload')])
            output_file = os.path.join(landing_dir, 'WindowsSupport.dmg')
            shutil.move(os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg'),
                output_file)
            status("Extracted to %s." % output_file)

            # If we were to also copy out the contents from the .dmg we might do it like this, but if you're doing this
            # from OS X you probably would rather just burn a disc so we'll stop here..
            # mountxml = getCommandOutput(['/usr/bin/hdiutil', 'attach',
            #     os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg'),
            #     '-mountrandom', '/tmp', '-plist', '-nobrowse'])
            # mountplist = plistlib.readPlistFromString(mountxml)
            # mntpoint = mountplist['system-entities'][0]['mount-point']
            # shutil.copytree(mntpoint, output_dir)
            # subprocess.call(['/usr/bin/hdiutil', 'eject', mntpoint])
            shutil.rmtree(arc_workdir)

        elif platform.system() == 'Linux':
            status("Extracting for Linux...")
            for arc in [pkg_dl_path,
                        os.path.join(arc_workdir, 'Payload'),
                        os.path.join(arc_workdir, 'Payload~')]:
                if os.path.exists(arc):
                    status("Extracting {}".format(arc))
                    subprocess.call(['7z','x',arc, '-o{}'.format(arc_workdir)])

            # BootCamp.pkg (xar) -> Payload (gzip) -> Payload~ (cpio) -> WindowsSupport.dmg
            output_file = os.path.join(arc_workdir, 'WindowsSupport.dmg')
            shutil.move(os.path.join(arc_workdir, 'Library/Application Support/BootCamp/WindowsSupport.dmg'),
                output_file)
            subprocess.call(['7z','-o{}'.format(landing_dir),'x','WindowsSupport.dmg',])
            
            status("Extracted to %s." % output_file)

            if os.path.exists('/mnt/c/Windows/explorer.exe'):
                subprocess.call(['/mnt/c/Windows/explorer.exe','.'])
        else:
            print("Platform not supported! ({})".format(playform.system()))
            pass

    status("Done.")

if __name__ == "__main__":
    main()

成功后,运行设备管理器,就能够看到正确识别了。

赞(0)
这篇文章为天际博客(天朝世纪)原创,转载请注明。天际部落 » 让Windows 10支持蓝牙连接MagicKeyboard 2并完美使用Fn功能键