MacからWindows Phoneのテザリングをオンにする

Windows 10 Mobileには、MS-TCCという、bluetoothでリモートからテザリングWindows phone用語的にはホットスポット)をオンにする機能があります。Windows 10 Mobileのスマホをカバンの中にいれたまま、ノートPCからスマホテザリングをオンにできて便利、という機能です。
ただこちら、基本的にはWindows 10のPCからでないと使えないのですが、MS-TCCプロトコルは公開されています。
そこで、Mac OSから、Windows 10 Mobileのテザリングをオンにするツールを自作してみました。
Macbook + Windows phoneなんて組み合わせで使っている人いない気はしますが・・)
以下のリンクからDLできます。
MS-TCC for Mac

上記appを起動するとステータスバーに「H」というマークが追加されます。すでにテザリング済みのWindows 10 mobile端末があれば、Hをクリックすると表示されます。
f:id:shimobepapa:20170124002658p:plain
テザリングをオンにしたい端末を選択することで、テザリングがオンになります。
(処理の結果は、メッセージボックスで表示されます)

ソースコードについて

このツールですが、swift初心者がswiftを勉強しながら書いたのですが、Mac osでswiftでBluetoothをさわっているコードの例が少なく苦労しました。参考に、UI部分以外のソースコードはりつけておきます。

import Foundation
import IOBluetooth

struct MSTCCResult{
    var targetdevice: IOBluetoothDevice? = nil
    var iserror: Bool = false
    var errorMessage: String? = nil
    
    // 成功した時のssidの情報
    var ssid: String? = nil
    var passcode: String? = nil
}

class MSTCCManager: NSObject, IOBluetoothRFCOMMChannelDelegate, IOBluetoothDeviceAsyncCallbacks
{
    var channel: IOBluetoothRFCOMMChannel?
    var isopen: Bool
    var writebuffer: NSMutableData
    var callback: ((MSTCCResult) -> Void)?
    var targetdevice: IOBluetoothDevice?
    
    override init()
    {
        self.channel = IOBluetoothRFCOMMChannel()
        isopen  = false
        writebuffer = NSMutableData(length: 16)!
        callback = nil
        targetdevice = nil
        super.init()
    }
    
    func listMSTCCDevice() -> Array<IOBluetoothDevice>{
        var ans = Array<IOBluetoothDevice>()
        for i in IOBluetoothDevice.pairedDevices() {
            let device = i as! IOBluetoothDevice
            let uuid : [UInt8] = [0x23, 0x2e, 0x51, 0xd8, 0x91, 0xff, 0x4c, 0x24, 0xac, 0x0f, 0x9e, 0xe0, 0x55, 0xda, 0x30, 0xa5]
            let spduuid = IOBluetoothSDPUUID(bytes: uuid, length: 16)
            
            let sr = device.getServiceRecord(for: spduuid)
            if( sr != nil )
            {
                ans.append(device)
            }
        }
        return ans
    }
    
    func connect(devicename: String, callback: ((MSTCCResult) -> Void)? = nil) -> Bool{
        for i in IOBluetoothDevice.pairedDevices() {
            let device = i as! IOBluetoothDevice
            if (device.name == devicename) {
                return connect(device: device, callback: callback)
            }
        }
        simpleerror("the tarfet device is not paired")
        return false
    }
    
    func connect(device: IOBluetoothDevice, callback: ((MSTCCResult) -> Void)? = nil) -> Bool{
        if( self.isopen )
        {
            simpleerror( "Error: in operating", callback: callback )
            return false;
        }
        let ret1 = device.openConnection()
        if( ret1 == kIOReturnSuccess)
        {
            self.isopen  = true
            self.callback = callback
            self.targetdevice = device
            
            device.performSDPQuery(self)
        }
        else
        {
            simpleerror("open connect failed. code = \(ret1)", callback: callback)
        }
        
        return self.isopen
        
    }
    
    func sdpQueryComplete(_ device: IOBluetoothDevice!,
                          status: IOReturn)
    {
        // SPDの情報更新完了
        if( status != kIOReturnSuccess )
        {
            simpleerror("SPD Query Failed. code = \(status)")
            device.closeConnection()
            self.isopen = false
            return
        }
        
        // MS-TCCのGUID
        let uuid : [UInt8] = [0x23, 0x2e, 0x51, 0xd8, 0x91, 0xff, 0x4c, 0x24, 0xac, 0x0f, 0x9e, 0xe0, 0x55, 0xda, 0x30, 0xa5]
        let spduuid = IOBluetoothSDPUUID(bytes: uuid, length: 16)
        
        // MS-TCCのサービス取得
        let sr = device.getServiceRecord(for: spduuid)
        
        if( sr == nil )
        {
            // 更新したらMS-TCCのサービスが見つからなかった
            simpleerror("This Device doesn't support MS-TCC now.")
            device.closeConnection()
            self.isopen = false
            return
        }
        var rfcommid = BluetoothRFCOMMChannelID()
        sr?.getRFCOMMChannelID(&rfcommid)
        let ret2 = device.openRFCOMMChannelAsync( &channel, withChannelID: rfcommid, delegate: self)
        if( ret2 != kIOReturnSuccess)
        {
            simpleerror("open RFCOMM Channel Error. code = \(ret2)")
            device.closeConnection()
            self.isopen = false
            return
        }
    }
    
    func rfcommChannelData(_ rfcommChannel: IOBluetoothRFCOMMChannel!,
                           data dataPointer: UnsafeMutableRawPointer!,
                           length dataLength: Int)
    {
        let buf = UnsafeBufferPointer(start: dataPointer.assumingMemoryBound(to: UInt8.self), count: dataLength)
        let datas = Array(buf)
        var index = 0
        if( datas[0] == 2 )
        {
            var result = MSTCCResult()
            result.iserror = false
            result.errorMessage = ""
            
            index = 3
            while UInt8(index) < datas[2] {
                
                if( datas[index] == 2 )
                {
                    // SSID
                    result.ssid = getStringFromArray( datas, start: index + 3, length: Int(datas[index + 2]))
                    //                    System.Text.Encoding.ASCII.GetString()
                }
                else if( datas[index] == 4 )
                {
                    result.passcode = getStringFromArray( datas, start: index + 3, length: Int(datas[index + 2]))
                    // passphrase
                }
                index = index + Int(datas[index + 2]) + 3
            }
            
            if( self.callback != nil )
            {
                callback!(result)
            }
            self.isopen = false
        }
        else
        {
            simpleerror("MS-TCC Error!\(datas[0])") // todo
        }
        channel!.close()
        self.targetdevice!.closeConnection()
    }
    
    func rfcommChannelWriteComplete(_ rfcommChannel: IOBluetoothRFCOMMChannel!,
                                    refcon: UnsafeMutableRawPointer!,
                                    status error: IOReturn)
    {
        if( error != kIOReturnSuccess)
        {
            simpleerror("Failed to send data. code = \(error)")
            self.targetdevice!.closeConnection()
            self.isopen = false
            return
        }
    }

    func rfcommChannelOpenComplete(_ rfcommChannel: IOBluetoothRFCOMMChannel!,
                                   status error: IOReturn)
    {
        if( error != kIOReturnSuccess)
        {
            simpleerror("open RFCOMM Channel Error. code = \(error)")
            self.targetdevice!.closeConnection()
            self.isopen = false
            return
        }
        if( channel!.isOpen() )
        {
            let cmdata = Data(bytes: [1,0,0])
            writebuffer.setData(cmdata)
            let ret3 = channel!.writeAsync(writebuffer.mutableBytes, length: 3, refcon : nil)
            if( ret3 != kIOReturnSuccess)
            {
                simpleerror("Failed to send data. code = \(ret3)")
                self.targetdevice!.closeConnection()
                self.isopen = false
                return
            }
        }
        else
        {
            // 多分こないはず
            simpleerror("Channel is not opened")
            self.targetdevice!.closeConnection()
            self.isopen = false
            return
        }
    }
    
    func simpleerror(_ errormessage: String, callback: ((MSTCCResult) -> Void)? = nil )
    {
        if( callback != nil)
        {
            var result = MSTCCResult()
            result.iserror = true
            result.errorMessage = errormessage
            callback!( result )
        }
        else if( self.callback != nil )
        {
            var result = MSTCCResult()
            result.iserror = true
            result.errorMessage = errormessage
            self.callback!( result )
            self.callback = nil
        }
    }
    
    func getStringFromArray(_ datas: Array<UInt8>, start: Int, length: Int) -> String
    {
        let strarray = datas[start..<(start+length)]
//        strarray.append(0)
        var byteBuffer = [UInt8]()
        strarray.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
            byteBuffer += bytes
        }
        let str = String(bytes: byteBuffer, encoding: String.Encoding.ascii)
        return str!
    }
    
    func connectionComplete(_ device: IOBluetoothDevice!,
                            status: IOReturn)
    {
    }
    
    func remoteNameRequestComplete(_ device: IOBluetoothDevice!,
                                   status: IOReturn)
    {
    }
}