├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── characteristics.go ├── descriptors.go ├── emitter.go ├── examples ├── beacon.go ├── discoverer.go ├── explorer.go └── main.go ├── goble.go ├── services.go └── xpc ├── xpc.go ├── xpc_test.go ├── xpc_wrapper.c └── xpc_wrapper.h /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: macOS-latest 8 | steps: 9 | 10 | - name: Set up Go 1.12 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.12 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | if [ -f Gopkg.toml ]; then 23 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 24 | dep ensure 25 | fi 26 | 27 | - name: Build 28 | run: go build -v . 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Raffaele Sena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Documentation](http://godoc.org/github.com/raff/goble?status.svg)](http://godoc.org/github.com/raff/goble) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/raff/goble)](https://goreportcard.com/report/github.com/raff/goble) 3 | 4 | 5 | goble 6 | ===== 7 | 8 | Go implementation of Bluetooth LE support for OSX (derived from noble/bleno) 9 | 10 | This is a port of nodejs [noble](https://github.com/sandeepmistry/noble)/[bleno](https://github.com/sandeepmistry/bleno) for OSX only. 11 | 12 | Once I have something working it can maybe integrated with [github.com/paypal/gatt](https://github.com/paypal/gatt), that right now is Linux only. 13 | 14 | ## Installation 15 | 16 | $ go get github.com/raff/goble 17 | 18 | ## Documentation 19 | http://godoc.org/github.com/raff/goble 20 | 21 | ## Examples 22 | * examples/main.go : an example of how to use most of the APIs 23 | * examples/discoverer.go : a port of nodejs noble "advertisement-discovery.js" example 24 | * examples/explorer.go : a port of nodejs noble "peripheral-explorer.js" example 25 | -------------------------------------------------------------------------------- /characteristics.go: -------------------------------------------------------------------------------- 1 | package goble 2 | 3 | // A dictionary of known characteristic names and type (keyed by characteristic uuid) 4 | var knownCharacteristics = map[string]struct{ Name, Type string }{ 5 | "2a00": {Name: "Device Name", Type: "org.bluetooth.characteristic.gap.device_name"}, 6 | "2a01": {Name: "Appearance", Type: "org.bluetooth.characteristic.gap.appearance"}, 7 | "2a02": {Name: "Peripheral Privacy Flag", Type: "org.bluetooth.characteristic.gap.peripheral_privacy_flag"}, 8 | "2a03": {Name: "Reconnection Address", Type: "org.bluetooth.characteristic.gap.reconnection_address"}, 9 | "2a04": {Name: "Peripheral Preferred Connection Parameters", Type: "org.bluetooth.characteristic.gap.peripheral_preferred_connection_parameters"}, 10 | "2a05": {Name: "Service Changed", Type: "org.bluetooth.characteristic.gatt.service_changed"}, 11 | "2a06": {Name: "Alert Level", Type: "org.bluetooth.characteristic.alert_level"}, 12 | "2a07": {Name: "Tx Power Level", Type: "org.bluetooth.characteristic.tx_power_level"}, 13 | "2a08": {Name: "Date Time", Type: "org.bluetooth.characteristic.date_time"}, 14 | "2a09": {Name: "Day of Week", Type: "org.bluetooth.characteristic.day_of_week"}, 15 | "2a0a": {Name: "Day Date Time", Type: "org.bluetooth.characteristic.day_date_time"}, 16 | "2a0c": {Name: "Exact Time 256", Type: "org.bluetooth.characteristic.exact_time_256"}, 17 | "2a0d": {Name: "DST Offset", Type: "org.bluetooth.characteristic.dst_offset"}, 18 | "2a0e": {Name: "Time Zone", Type: "org.bluetooth.characteristic.time_zone"}, 19 | "2a0f": {Name: "Local Time Information", Type: "org.bluetooth.characteristic.local_time_information"}, 20 | "2a11": {Name: "Time with DST", Type: "org.bluetooth.characteristic.time_with_dst"}, 21 | "2a12": {Name: "Time Accuracy", Type: "org.bluetooth.characteristic.time_accuracy"}, 22 | "2a13": {Name: "Time Source", Type: "org.bluetooth.characteristic.time_source"}, 23 | "2a14": {Name: "Reference Time Information", Type: "org.bluetooth.characteristic.reference_time_information"}, 24 | "2a16": {Name: "Time Update Control Point", Type: "org.bluetooth.characteristic.time_update_control_point"}, 25 | "2a17": {Name: "Time Update State", Type: "org.bluetooth.characteristic.time_update_state"}, 26 | "2a18": {Name: "Glucose Measurement", Type: "org.bluetooth.characteristic.glucose_measurement"}, 27 | "2a19": {Name: "Battery Level", Type: "org.bluetooth.characteristic.battery_level"}, 28 | "2a1c": {Name: "Temperature Measurement", Type: "org.bluetooth.characteristic.temperature_measurement"}, 29 | "2a1d": {Name: "Temperature Type", Type: "org.bluetooth.characteristic.temperature_type"}, 30 | "2a1e": {Name: "Intermediate Temperature", Type: "org.bluetooth.characteristic.intermediate_temperature"}, 31 | "2a21": {Name: "Measurement Interval", Type: "org.bluetooth.characteristic.measurement_interval"}, 32 | "2a22": {Name: "Boot Keyboard Input Report", Type: "org.bluetooth.characteristic.boot_keyboard_input_report"}, 33 | "2a23": {Name: "System ID", Type: "org.bluetooth.characteristic.system_id"}, 34 | "2a24": {Name: "Model Number String", Type: "org.bluetooth.characteristic.model_number_string"}, 35 | "2a25": {Name: "Serial Number String", Type: "org.bluetooth.characteristic.serial_number_string"}, 36 | "2a26": {Name: "Firmware Revision String", Type: "org.bluetooth.characteristic.firmware_revision_string"}, 37 | "2a27": {Name: "Hardware Revision String", Type: "org.bluetooth.characteristic.hardware_revision_string"}, 38 | "2a28": {Name: "Software Revision String", Type: "org.bluetooth.characteristic.software_revision_string"}, 39 | "2a29": {Name: "Manufacturer Name String", Type: "org.bluetooth.characteristic.manufacturer_name_string"}, 40 | "2a2a": {Name: "IEEE 11073-20601 Regulatory Certification Data List", Type: "org.bluetooth.characteristic.ieee_11073-20601_regulatory_certification_data_list"}, 41 | "2a2b": {Name: "Current Time", Type: "org.bluetooth.characteristic.current_time"}, 42 | "2a31": {Name: "Scan Refresh", Type: "org.bluetooth.characteristic.scan_refresh"}, 43 | "2a32": {Name: "Boot Keyboard Output Report", Type: "org.bluetooth.characteristic.boot_keyboard_output_report"}, 44 | "2a33": {Name: "Boot Mouse Input Report", Type: "org.bluetooth.characteristic.boot_mouse_input_report"}, 45 | "2a34": {Name: "Glucose Measurement Context", Type: "org.bluetooth.characteristic.glucose_measurement_context"}, 46 | "2a35": {Name: "Blood Pressure Measurement", Type: "org.bluetooth.characteristic.blood_pressure_measurement"}, 47 | "2a36": {Name: "Intermediate Cuff Pressure", Type: "org.bluetooth.characteristic.intermediate_blood_pressure"}, 48 | "2a37": {Name: "Heart Rate Measurement", Type: "org.bluetooth.characteristic.heart_rate_measurement"}, 49 | "2a38": {Name: "Body Sensor Location", Type: "org.bluetooth.characteristic.body_sensor_location"}, 50 | "2a39": {Name: "Heart Rate Control Point", Type: "org.bluetooth.characteristic.heart_rate_control_point"}, 51 | "2a3f": {Name: "Alert Status", Type: "org.bluetooth.characteristic.alert_status"}, 52 | "2a40": {Name: "Ringer Control Point", Type: "org.bluetooth.characteristic.ringer_control_point"}, 53 | "2a41": {Name: "Ringer Setting", Type: "org.bluetooth.characteristic.ringer_setting"}, 54 | "2a42": {Name: "Alert Category ID Bit Mask", Type: "org.bluetooth.characteristic.alert_category_id_bit_mask"}, 55 | "2a43": {Name: "Alert Category ID", Type: "org.bluetooth.characteristic.alert_category_id"}, 56 | "2a44": {Name: "Alert Notification Control Point", Type: "org.bluetooth.characteristic.alert_notification_control_point"}, 57 | "2a45": {Name: "Unread Alert Status", Type: "org.bluetooth.characteristic.unread_alert_status"}, 58 | "2a46": {Name: "New Alert", Type: "org.bluetooth.characteristic.new_alert"}, 59 | "2a47": {Name: "Supported New Alert Category", Type: "org.bluetooth.characteristic.supported_new_alert_category"}, 60 | "2a48": {Name: "Supported Unread Alert Category", Type: "org.bluetooth.characteristic.supported_unread_alert_category"}, 61 | "2a49": {Name: "Blood Pressure Feature", Type: "org.bluetooth.characteristic.blood_pressure_feature"}, 62 | "2a4a": {Name: "HID Information", Type: "org.bluetooth.characteristic.hid_information"}, 63 | "2a4b": {Name: "Report Map", Type: "org.bluetooth.characteristic.report_map"}, 64 | "2a4c": {Name: "HID Control Point", Type: "org.bluetooth.characteristic.hid_control_point"}, 65 | "2a4d": {Name: "Report", Type: "org.bluetooth.characteristic.report"}, 66 | "2a4e": {Name: "Protocol Mode", Type: "org.bluetooth.characteristic.protocol_mode"}, 67 | "2a4f": {Name: "Scan Interval Window", Type: "org.bluetooth.characteristic.scan_interval_window"}, 68 | "2a50": {Name: "PnP ID", Type: "org.bluetooth.characteristic.pnp_id"}, 69 | "2a51": {Name: "Glucose Feature", Type: "org.bluetooth.characteristic.glucose_feature"}, 70 | "2a52": {Name: "Record Access Control Point", Type: "org.bluetooth.characteristic.record_access_control_point"}, 71 | "2a53": {Name: "RSC Measurement", Type: "org.bluetooth.characteristic.rsc_measurement"}, 72 | "2a54": {Name: "RSC Feature", Type: "org.bluetooth.characteristic.rsc_feature"}, 73 | "2a55": {Name: "SC Control Point", Type: "org.bluetooth.characteristic.sc_control_point"}, 74 | "2a5b": {Name: "CSC Measurement", Type: "org.bluetooth.characteristic.csc_measurement"}, 75 | "2a5c": {Name: "CSC Feature", Type: "org.bluetooth.characteristic.csc_feature"}, 76 | "2a5d": {Name: "Sensor Location", Type: "org.bluetooth.characteristic.sensor_location"}, 77 | } 78 | -------------------------------------------------------------------------------- /descriptors.go: -------------------------------------------------------------------------------- 1 | package goble 2 | 3 | // A dictionary of known descriptor names and type (keyed by descriptor uuid) 4 | var knownDescriptors = map[string]struct{ Name, Type string }{ 5 | "2900": {Name: "Characteristic Extended Properties", Type: "org.bluetooth.descriptor.gatt.characteristic_extended_properties"}, 6 | "2901": {Name: "Characteristic User Description", Type: "org.bluetooth.descriptor.gatt.characteristic_user_description"}, 7 | "2902": {Name: "Client Characteristic Configuration", Type: "org.bluetooth.descriptor.gatt.client_characteristic_configuration"}, 8 | "2903": {Name: "Server Characteristic Configuration", Type: "org.bluetooth.descriptor.gatt.server_characteristic_configuration"}, 9 | "2904": {Name: "Characteristic Presentation Format", Type: "org.bluetooth.descriptor.gatt.characteristic_presentation_format"}, 10 | "2905": {Name: "Characteristic Aggregate Format", Type: "org.bluetooth.descriptor.gatt.characteristic_aggregate_format"}, 11 | "2906": {Name: "Valid Range", Type: "org.bluetooth.descriptor.valid_range"}, 12 | "2907": {Name: "External Report Reference", Type: "org.bluetooth.descriptor.external_report_reference"}, 13 | "2908": {Name: "Report Reference", Type: "org.bluetooth.descriptor.report_reference"}, 14 | } 15 | -------------------------------------------------------------------------------- /emitter.go: -------------------------------------------------------------------------------- 1 | package goble 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/raff/goble/xpc" 7 | ) 8 | 9 | const ( 10 | ALL = "__allEvents__" 11 | ) 12 | 13 | // Event generated by blued, with associated data 14 | type Event struct { 15 | Name string 16 | State string 17 | DeviceUUID xpc.UUID 18 | ServiceUuid string 19 | CharacteristicUuid string 20 | Peripheral Peripheral 21 | Data []byte 22 | Mtu int 23 | IsNotification bool 24 | } 25 | 26 | // The event handler function. 27 | // Return true to terminate 28 | type EventHandlerFunc func(Event) bool 29 | 30 | // Emitter is an object to emit and handle Event(s) 31 | type Emitter struct { 32 | handlers map[string]EventHandlerFunc 33 | event chan Event 34 | verbose bool 35 | } 36 | 37 | // Init initialize the emitter and start a goroutine to execute the event handlers 38 | func (e *Emitter) Init() { 39 | e.handlers = make(map[string]EventHandlerFunc) 40 | e.event = make(chan Event) 41 | 42 | // event handler 43 | go func() { 44 | for { 45 | ev := <-e.event 46 | 47 | if fn, ok := e.handlers[ev.Name]; ok { 48 | if fn(ev) { 49 | break 50 | } 51 | } else if fn, ok := e.handlers[ALL]; ok { 52 | if fn(ev) { 53 | break 54 | } 55 | } else { 56 | if e.verbose { 57 | log.Println("unhandled Emit", ev) 58 | } 59 | } 60 | } 61 | 62 | close(e.event) // TOFIX: this causes new "emits" to panic. 63 | }() 64 | } 65 | 66 | func (e *Emitter) SetVerbose(v bool) { 67 | e.verbose = v 68 | } 69 | 70 | // Emit sends the event on the 'event' channel 71 | func (e *Emitter) Emit(ev Event) { 72 | e.event <- ev 73 | } 74 | 75 | // On(event, cb) registers an handler for the specified event 76 | func (e *Emitter) On(event string, fn EventHandlerFunc) { 77 | if fn == nil { 78 | delete(e.handlers, event) 79 | } else { 80 | e.handlers[event] = fn 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/beacon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "time" 7 | 8 | "github.com/raff/goble" 9 | "github.com/raff/goble/xpc" 10 | ) 11 | 12 | func main() { 13 | uuid := flag.String("uuid", "1BEAC099-BEAC-BEAC-BEAC-BEAC09BEAC09", "iBeacon UUID") 14 | major := flag.Int("major", 0, "iBeacon major value (uint16)") 15 | minor := flag.Int("minor", 0, "iBeacon minor value (uint16)") 16 | power := flag.Int("power", -57, "iBeacon measured power (int8)") 17 | d := flag.Duration("duration", 1*time.Minute, "advertising duration") 18 | verbose := flag.Bool("verbose", false, "dump all events") 19 | flag.Parse() 20 | 21 | ble := goble.New() 22 | ble.SetVerbose(*verbose) 23 | ble.Init() 24 | 25 | var utsname xpc.Utsname 26 | xpc.Uname(&utsname) 27 | log.Println("Release", utsname.Release) 28 | 29 | time.Sleep(1 * time.Second) 30 | 31 | log.Println("Start Advertising", *uuid, *major, *minor, *power) 32 | ble.StartAdvertisingIBeacon(xpc.MustUUID(*uuid), uint16(*major), uint16(*minor), int8(*power)) 33 | 34 | time.Sleep(*d) 35 | 36 | log.Println("Stop Advertising") 37 | ble.StopAdvertising() 38 | } 39 | -------------------------------------------------------------------------------- /examples/discoverer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/raff/goble" 9 | "github.com/raff/goble/xpc" 10 | ) 11 | 12 | func main() { 13 | verbose := flag.Bool("verbose", false, "dump all events") 14 | compact := flag.Bool("compact", true, "compact messages") 15 | dups := flag.Bool("allow-duplicates", false, "allow duplicates when scanning") 16 | flag.Parse() 17 | 18 | var quit chan bool 19 | 20 | ble := goble.New() 21 | ble.SetVerbose(*verbose) 22 | 23 | if *verbose { 24 | var utsname xpc.Utsname 25 | xpc.Uname(&utsname) 26 | log.Println("Release", utsname.Release) 27 | 28 | ble.On(goble.ALL, func(ev goble.Event) (done bool) { 29 | log.Println("Event", ev) 30 | return 31 | }) 32 | } 33 | 34 | ble.On("stateChange", func(ev goble.Event) (done bool) { 35 | if ev.State == "poweredOn" { 36 | ble.StartScanning(nil, *dups) 37 | } else { 38 | ble.StopScanning() 39 | done = true 40 | quit <- true 41 | } 42 | 43 | return 44 | }) 45 | 46 | ble.On("discover", func(ev goble.Event) (done bool) { 47 | if *compact { 48 | fmt.Println("peripheral:", ev.DeviceUUID) 49 | if ev.Peripheral.Advertisement.LocalName != "" { 50 | fmt.Println(" name:", ev.Peripheral.Advertisement.LocalName) 51 | } 52 | if len(ev.Peripheral.Advertisement.ServiceUuids) > 0 { 53 | fmt.Println(" services:", ev.Peripheral.Advertisement.ServiceUuids) 54 | } 55 | } else { 56 | fmt.Println() 57 | fmt.Println("peripheral discovered (", ev.DeviceUUID, "):") 58 | fmt.Println("\thello my local name is:") 59 | fmt.Println("\t\t", ev.Peripheral.Advertisement.LocalName) 60 | fmt.Println("\tcan I interest you in any of the following advertised services:") 61 | fmt.Println("\t\t", ev.Peripheral.Advertisement.ServiceUuids) 62 | } 63 | 64 | serviceData := ev.Peripheral.Advertisement.ServiceData 65 | if len(serviceData) > 0 { 66 | prefix := "\t\t" 67 | 68 | if *compact { 69 | prefix = " " 70 | fmt.Println(" service data:") 71 | } else { 72 | fmt.Println("\there is my service data:") 73 | } 74 | 75 | for _, d := range serviceData { 76 | fmt.Println(prefix, d.Uuid, ":", d.Data) 77 | } 78 | } 79 | 80 | if len(ev.Peripheral.Advertisement.ManufacturerData) > 0 { 81 | if *compact { 82 | fmt.Printf(" manufacturer data: %x\n", ev.Peripheral.Advertisement.ManufacturerData) 83 | } else { 84 | fmt.Println("\there is my manufacturer data:") 85 | fmt.Println("\t\t", ev.Peripheral.Advertisement.ManufacturerData) 86 | } 87 | } 88 | 89 | if ev.Peripheral.Advertisement.TxPowerLevel != 0 { 90 | if *compact { 91 | fmt.Println(" TX power level:", ev.Peripheral.Advertisement.TxPowerLevel) 92 | } else { 93 | fmt.Println("\tmy TX power level is:") 94 | fmt.Println("\t\t", ev.Peripheral.Advertisement.TxPowerLevel) 95 | } 96 | } 97 | 98 | if *compact { 99 | fmt.Println() 100 | } 101 | 102 | return 103 | }) 104 | 105 | if *verbose { 106 | log.Println("Init...") 107 | } 108 | 109 | ble.Init() 110 | 111 | <-quit 112 | } 113 | -------------------------------------------------------------------------------- /examples/explorer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/raff/goble" 11 | ) 12 | 13 | var ( 14 | debug = flag.Bool("debug", false, "log debug messages") 15 | verbose = flag.Bool("verbose", false, "dump all events") 16 | dups = flag.Bool("allow-duplicates", false, "allow duplicates when scanning") 17 | ) 18 | 19 | func DebugPrint(params ...interface{}) { 20 | if *debug { 21 | log.Println(params...) 22 | } 23 | } 24 | 25 | type Result struct { 26 | count int 27 | data string 28 | } 29 | 30 | func explore(ble *goble.BLE, peripheral *goble.Peripheral) { 31 | results := map[string]Result{} 32 | 33 | // connect 34 | ble.On("connect", func(ev goble.Event) (done bool) { 35 | DebugPrint("connected", ev) 36 | ble.DiscoverServices(ev.DeviceUUID, nil) 37 | 38 | go func() { 39 | time.Sleep(2 * time.Minute) 40 | ble.Disconnect(ev.DeviceUUID) 41 | }() 42 | 43 | return 44 | }) 45 | 46 | // discover services 47 | ble.On("servicesDiscover", func(ev goble.Event) (done bool) { 48 | DebugPrint("serviceDiscovered", ev) 49 | for sid, service := range ev.Peripheral.Services { 50 | // this is a map that contains services UUIDs (string) and service startHandle (int) 51 | // for now we only process the "strings" 52 | if _, ok := sid.(string); ok { 53 | serviceInfo := service.Uuid 54 | 55 | if len(service.Name) > 0 { 56 | serviceInfo += " (" + service.Name + ")" 57 | } 58 | 59 | results[service.Uuid] = Result{data: serviceInfo} 60 | ble.DiscoverCharacteristics(ev.DeviceUUID, service.Uuid, nil) 61 | } 62 | } 63 | 64 | return 65 | }) 66 | 67 | // discover characteristics 68 | ble.On("characteristicsDiscover", func(ev goble.Event) (done bool) { 69 | DebugPrint("characteristicsDiscovered", ev) 70 | serviceUuid := ev.ServiceUuid 71 | serviceResult := results[serviceUuid] 72 | 73 | for cid, characteristic := range ev.Peripheral.Services[serviceUuid].Characteristics { 74 | // this is a map that contains services UUIDs (string) and service startHandle (int) 75 | // for now we only process the "strings" 76 | if _, ok := cid.(string); ok { 77 | characteristicInfo := " " + characteristic.Uuid 78 | 79 | if len(characteristic.Name) > 0 { 80 | characteristicInfo += " (" + characteristic.Name + ")" 81 | } 82 | 83 | characteristicInfo += "\n properties " + characteristic.Properties.String() 84 | serviceResult.data += characteristicInfo 85 | 86 | if characteristic.Properties.Readable() { 87 | serviceResult.count += 1 88 | ble.Read(ev.DeviceUUID, serviceUuid, characteristic.Uuid) 89 | } 90 | 91 | //ble.DiscoverDescriptors(ev.DeviceUUID, serviceUuid, characteristic.Uuid) 92 | results[serviceUuid] = serviceResult 93 | 94 | if *verbose { 95 | log.Println(results[serviceUuid]) 96 | } 97 | } 98 | } 99 | 100 | return 101 | }) 102 | 103 | // discover descriptors 104 | ble.On("descriptorsDiscover", func(ev goble.Event) (done bool) { 105 | DebugPrint("descriptorsDiscovered", ev) 106 | fmt.Println(" descriptors ", ev.Peripheral.Services[ev.ServiceUuid].Characteristics[ev.CharacteristicUuid].Descriptors) 107 | return 108 | }) 109 | 110 | // read 111 | ble.On("read", func(ev goble.Event) (done bool) { 112 | DebugPrint("read", ev) 113 | serviceUuid := ev.ServiceUuid 114 | serviceResult := results[serviceUuid] 115 | serviceResult.data += fmt.Sprintf(" value %x | %q\n", ev.Data, ev.Data) 116 | serviceResult.count -= 1 117 | 118 | if serviceResult.count <= 0 { 119 | fmt.Println(serviceResult.data) 120 | return true 121 | } else { 122 | results[serviceUuid] = serviceResult 123 | } 124 | 125 | return 126 | }) 127 | 128 | // disconnect 129 | ble.On("disconnect", func(ev goble.Event) (done bool) { 130 | DebugPrint("disconnected", ev) 131 | os.Exit(0) 132 | return true 133 | }) 134 | 135 | fmt.Println("services and characteristics:") 136 | ble.Connect(peripheral.Uuid) 137 | 138 | /* 139 | async.series([ 140 | function(callback) { 141 | characteristic.discoverDescriptors(function(error, descriptors) { 142 | async.detect( 143 | descriptors, 144 | function(descriptor, callback) { 145 | return callback(descriptor.uuid === '2901") 146 | }, 147 | function(userDescriptionDescriptor){ 148 | if (userDescriptionDescriptor) { 149 | userDescriptionDescriptor.readValue(function(error, data) { 150 | characteristicInfo += ' (' + data.toString() + ')'; 151 | callback(); 152 | }); 153 | } else { 154 | callback(); 155 | } 156 | } 157 | ); 158 | }); 159 | }, 160 | function(callback) { 161 | characteristicInfo += '\n properties ' + characteristic.properties.join(', ") 162 | 163 | if (characteristic.properties.indexOf('read') !== -1) { 164 | characteristic.read(function(error, data) { 165 | if (data) { 166 | var string = data.toString('ascii") 167 | 168 | characteristicInfo += '\n value ' + data.toString('hex') + ' | \'' + string + '\''; 169 | } 170 | callback(); 171 | }); 172 | } else { 173 | callback(); 174 | } 175 | }, 176 | function() { 177 | console.log(characteristicInfo); 178 | characteristicIndex++; 179 | callback(); 180 | } 181 | ]); 182 | }, 183 | function(error) { 184 | serviceIndex++; 185 | callback(); 186 | } 187 | ); 188 | }); 189 | }, 190 | */ 191 | } 192 | 193 | func main() { 194 | flag.Parse() 195 | 196 | if len(flag.Args()) != 1 { 197 | fmt.Println("usage:", os.Args[0], "[options] peripheral-uuid") 198 | os.Exit(1) 199 | } 200 | 201 | peripheralUuid := flag.Args()[0] 202 | 203 | var done chan bool 204 | 205 | ble := goble.New() 206 | ble.SetVerbose(*verbose) 207 | 208 | if *verbose { 209 | ble.On(goble.ALL, func(ev goble.Event) (done bool) { 210 | log.Println("Event", ev) 211 | return 212 | }) 213 | } 214 | 215 | ble.On("stateChange", func(ev goble.Event) (done bool) { 216 | DebugPrint("stateChanged", ev) 217 | if ev.State == "poweredOn" { 218 | ble.StartScanning(nil, *dups) 219 | } else { 220 | ble.StopScanning() 221 | done = true 222 | } 223 | 224 | return 225 | }) 226 | 227 | ble.On("discover", func(ev goble.Event) (done bool) { 228 | DebugPrint("discovered", ev) 229 | if peripheralUuid == ev.DeviceUUID.String() { 230 | ble.StopScanning() 231 | 232 | fmt.Println() 233 | fmt.Println("peripheral with UUID", ev.DeviceUUID, "found") 234 | 235 | advertisement := ev.Peripheral.Advertisement 236 | 237 | DebugPrint("advertised", advertisement) 238 | 239 | localName := advertisement.LocalName 240 | txPowerLevel := advertisement.TxPowerLevel 241 | manufacturerData := advertisement.ManufacturerData 242 | serviceData := advertisement.ServiceData 243 | //serviceUuids := advertisement.ServiceUuids 244 | 245 | if ev.Peripheral.Connectable { 246 | fmt.Println(" Connectable") 247 | } 248 | if len(localName) > 0 { 249 | fmt.Println(" Local Name =", localName) 250 | } 251 | 252 | if txPowerLevel != 0 { 253 | fmt.Println(" TX Power Level =", txPowerLevel) 254 | } 255 | 256 | if len(manufacturerData) > 0 { 257 | fmt.Println(" Manufacturer Data =", manufacturerData) 258 | } 259 | 260 | if len(serviceData) > 0 { 261 | fmt.Println(" Service Data =", serviceData) 262 | } 263 | 264 | fmt.Println() 265 | 266 | DebugPrint("explore", ev.Peripheral) 267 | explore(ble, &ev.Peripheral) 268 | } 269 | 270 | return 271 | }) 272 | 273 | if *verbose { 274 | log.Println("Init...") 275 | } 276 | 277 | ble.Init() 278 | 279 | fmt.Println("waiting...") 280 | <-done 281 | 282 | fmt.Println("goodbye!") 283 | os.Exit(0) 284 | } 285 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | "github.com/raff/goble" 11 | "github.com/raff/goble/xpc" 12 | ) 13 | 14 | func main() { 15 | verbose := flag.Bool("verbose", false, "dump all events") 16 | advertise := flag.Duration("advertise", 0, "Duration of advertising - 0: does not advertise") 17 | dups := flag.Bool("allow-duplicates", false, "allow duplicates when scanning") 18 | ibeacon := flag.Duration("ibeacon", 0, "Duration of IBeacon advertising - 0: does not advertise") 19 | scan := flag.Duration("scan", 10, "Duration of scanning - 0: does not scan") 20 | uuid := flag.String("uuid", "", "device uuid (for ibeacon uuid,major,minor,power)") 21 | connect := flag.Bool("connect", false, "connect to device") 22 | disconnect := flag.Bool("disconnect", false, "disconnect from device") 23 | rssi := flag.Bool("rssi", false, "update rssi for device") 24 | remove := flag.Bool("remove", false, "Remove all services") 25 | discover := flag.Bool("discover", false, "Discover services") 26 | 27 | flag.Parse() 28 | 29 | ble := goble.New() 30 | 31 | ble.SetVerbose(*verbose) 32 | 33 | log.Println("Init...") 34 | ble.Init() 35 | 36 | if *advertise > 0 { 37 | uuids := []xpc.UUID{} 38 | 39 | if len(*uuid) > 0 { 40 | uuids = append(uuids, xpc.MakeUUID(*uuid)) 41 | } 42 | 43 | time.Sleep(1 * time.Second) 44 | log.Println("Start Advertising...") 45 | ble.StartAdvertising("gobble", uuids) 46 | 47 | time.Sleep(*advertise) 48 | log.Println("Stop Advertising...") 49 | ble.StopAdvertising() 50 | } 51 | 52 | if *ibeacon > 0 { 53 | parts := strings.Split(*uuid, ",") 54 | id := parts[0] 55 | 56 | var major, minor uint16 57 | var measuredPower int8 58 | 59 | if len(parts) > 1 { 60 | fmt.Sscanf(parts[1], "%d", &major) 61 | } 62 | if len(parts) > 2 { 63 | fmt.Sscanf(parts[2], "%d", &minor) 64 | } 65 | if len(parts) > 2 { 66 | fmt.Sscanf(parts[3], "%d", &measuredPower) 67 | } 68 | 69 | time.Sleep(1 * time.Second) 70 | log.Println("Start Advertising IBeacon...") 71 | ble.StartAdvertisingIBeacon(xpc.MakeUUID(id), major, minor, measuredPower) 72 | 73 | time.Sleep(*ibeacon) 74 | log.Println("Stop Advertising...") 75 | ble.StopAdvertising() 76 | } 77 | 78 | if *scan > 0 { 79 | time.Sleep(1 * time.Second) 80 | log.Println("Start Scanning...") 81 | ble.StartScanning(nil, *dups) 82 | 83 | time.Sleep(*scan) 84 | log.Println("Stop Scanning...") 85 | ble.StopScanning() 86 | } 87 | 88 | if *connect { 89 | time.Sleep(1 * time.Second) 90 | uuid := xpc.MakeUUID(*uuid) 91 | log.Println("Connect", uuid) 92 | ble.Connect(uuid) 93 | time.Sleep(5 * time.Second) 94 | } 95 | 96 | if *rssi { 97 | time.Sleep(1 * time.Second) 98 | uuid := xpc.MakeUUID(*uuid) 99 | log.Println("UpdateRssi", uuid) 100 | ble.UpdateRssi(uuid) 101 | time.Sleep(5 * time.Second) 102 | } 103 | 104 | if *discover { 105 | time.Sleep(1 * time.Second) 106 | uuid := xpc.MakeUUID(*uuid) 107 | log.Println("DiscoverServices", uuid) 108 | ble.DiscoverServices(uuid, nil) 109 | time.Sleep(5 * time.Second) 110 | } 111 | 112 | if *disconnect { 113 | time.Sleep(1 * time.Second) 114 | uuid := xpc.MakeUUID(*uuid) 115 | log.Println("Disconnect", uuid) 116 | ble.Disconnect(uuid) 117 | time.Sleep(5 * time.Second) 118 | } 119 | 120 | if *remove { 121 | time.Sleep(1 * time.Second) 122 | log.Println("Remove all services") 123 | ble.RemoveServices() 124 | time.Sleep(5 * time.Second) 125 | } 126 | 127 | time.Sleep(5 * time.Second) 128 | log.Println("Goodbye!") 129 | } 130 | -------------------------------------------------------------------------------- /goble.go: -------------------------------------------------------------------------------- 1 | package goble 2 | 3 | import "C" 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "fmt" 9 | "log" 10 | "time" 11 | 12 | "github.com/raff/goble/xpc" 13 | ) 14 | 15 | // "github.com/raff/goble/xpc" 16 | // 17 | // BLE support 18 | // 19 | 20 | var STATES = []string{"unknown", "resetting", "unsupported", "unauthorized", "poweredOff", "poweredOn"} 21 | 22 | type Property int 23 | 24 | const ( 25 | Broadcast Property = 1 << iota 26 | Read = 1 << iota 27 | WriteWithoutResponse = 1 << iota 28 | Write = 1 << iota 29 | Notify = 1 << iota 30 | Indicate = 1 << iota 31 | AuthenticatedSignedWrites = 1 << iota 32 | ExtendedProperties = 1 << iota 33 | ) 34 | 35 | func (p Property) Readable() bool { 36 | return (p & Read) != 0 37 | } 38 | 39 | func (p Property) String() (result string) { 40 | if (p & Broadcast) != 0 { 41 | result += "broadcast " 42 | } 43 | if (p & Read) != 0 { 44 | result += "read " 45 | } 46 | if (p & WriteWithoutResponse) != 0 { 47 | result += "writeWithoutResponse " 48 | } 49 | if (p & Write) != 0 { 50 | result += "write " 51 | } 52 | if (p & Notify) != 0 { 53 | result += "notify " 54 | } 55 | if (p & Indicate) != 0 { 56 | result += "indicate " 57 | } 58 | if (p & AuthenticatedSignedWrites) != 0 { 59 | result += "authenticateSignedWrites " 60 | } 61 | if (p & ExtendedProperties) != 0 { 62 | result += "extendedProperties " 63 | } 64 | 65 | return 66 | } 67 | 68 | type ServiceData struct { 69 | Uuid string 70 | Data []byte 71 | } 72 | 73 | type CharacteristicDescriptor struct { 74 | Uuid string 75 | Handle int 76 | } 77 | 78 | type ServiceCharacteristic struct { 79 | Uuid string 80 | Name string 81 | Type string 82 | Properties Property 83 | Descriptors map[interface{}]*CharacteristicDescriptor 84 | Handle int 85 | ValueHandle int 86 | } 87 | 88 | type ServiceHandle struct { 89 | Uuid string 90 | Name string 91 | Type string 92 | Characteristics map[interface{}]*ServiceCharacteristic 93 | startHandle int 94 | endHandle int 95 | } 96 | 97 | type Advertisement struct { 98 | LocalName string 99 | TxPowerLevel int 100 | ManufacturerData []byte 101 | ServiceData []ServiceData 102 | ServiceUuids []string 103 | } 104 | 105 | type Peripheral struct { 106 | Uuid xpc.UUID 107 | Address string 108 | AddressType string 109 | Connectable bool 110 | Advertisement Advertisement 111 | Rssi int 112 | Services map[interface{}]*ServiceHandle 113 | } 114 | 115 | // GATT Descriptor 116 | type Descriptor struct { 117 | uuid xpc.UUID 118 | value []byte 119 | } 120 | 121 | // GATT Characteristic 122 | type Characteristic struct { 123 | uuid xpc.UUID 124 | properties Property 125 | secure Property 126 | descriptors []Descriptor 127 | value []byte 128 | } 129 | 130 | // GATT Service 131 | type Service struct { 132 | uuid xpc.UUID 133 | characteristics []Characteristic 134 | } 135 | 136 | type BLE struct { 137 | Emitter 138 | conn xpc.XPC 139 | verbose bool 140 | 141 | peripherals map[string]*Peripheral 142 | attributes xpc.Array 143 | lastServiceAttributeId int 144 | allowDuplicates bool 145 | 146 | utsname xpc.Utsname 147 | } 148 | 149 | func New() *BLE { 150 | ble := &BLE{peripherals: map[string]*Peripheral{}, Emitter: Emitter{}} 151 | ble.Emitter.Init() 152 | ble.conn = xpc.XpcConnect("com.apple.blued", ble) 153 | xpc.Uname(&ble.utsname) 154 | return ble 155 | } 156 | 157 | func (ble *BLE) SetVerbose(v bool) { 158 | ble.verbose = v 159 | ble.Emitter.SetVerbose(v) 160 | } 161 | 162 | // process BLE events and asynchronous errors 163 | // (implements XpcEventHandler) 164 | func (ble *BLE) HandleXpcEvent(event xpc.Dict, err error) { 165 | if err != nil { 166 | log.Println("error:", err) 167 | if event == nil { 168 | return 169 | } 170 | } 171 | 172 | id := event.MustGetInt("kCBMsgId") 173 | args := event.MustGetDict("kCBMsgArgs") 174 | 175 | retry_switch: 176 | if ble.verbose { 177 | log.Printf("event: %v %#v\n", id, args) 178 | defer log.Printf("done event: %v", id) 179 | } 180 | 181 | switch id { 182 | case 4, 6: // state change 183 | if id == 6 && ble.utsname.Release >= "19." { 184 | if _, ok := args["kCBMsgArgState"]; !ok { 185 | // this is not a state change event 186 | // 187 | // event: 6 xpc.Dict{"kCBMsgArgConnectableState":1, "kCBMsgArgDiscoverableState":1} 188 | // event: 6 xpc.Dict{"kCBMsgArgInquiryState":1} 189 | return 190 | } 191 | } 192 | state := args.MustGetInt("kCBMsgArgState") 193 | ble.Emit(Event{Name: "stateChange", State: STATES[state]}) 194 | 195 | case 16: // advertising start 196 | result := args.MustGetInt("kCBMsgArgResult") 197 | if result != 0 { 198 | log.Printf("event: error in advertisingStart %v\n", result) 199 | } else { 200 | ble.Emit(Event{Name: "advertisingStart"}) 201 | } 202 | 203 | case 17: // advertising stop 204 | result := args.MustGetInt("kCBMsgArgResult") 205 | if result != 0 { 206 | log.Printf("event: error in advertisingStop %v\n", result) 207 | } else { 208 | ble.Emit(Event{Name: "advertisingStop"}) 209 | } 210 | 211 | case 37, 48, 51, 57, 60: // discover 212 | advdata := args.MustGetDict("kCBMsgArgAdvertisementData") 213 | if len(advdata) == 0 { 214 | //log.Println("event: discover with no advertisment data") 215 | break 216 | } 217 | 218 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 219 | 220 | advertisement := Advertisement{ 221 | LocalName: advdata.GetString("kCBAdvDataLocalName", args.GetString("kCBMsgArgName", "")), 222 | TxPowerLevel: advdata.GetInt("kCBAdvDataTxPowerLevel", 0), 223 | ManufacturerData: advdata.GetBytes("kCBAdvDataManufacturerData", nil), 224 | ServiceData: []ServiceData{}, 225 | ServiceUuids: []string{}, 226 | } 227 | 228 | connectable := advdata.GetInt("kCBAdvDataIsConnectable", 0) > 0 229 | rssi := args.GetInt("kCBMsgArgRssi", 0) 230 | 231 | if uuids, ok := advdata["kCBAdvDataServiceUUIDs"]; ok { 232 | for _, uuid := range uuids.(xpc.Array) { 233 | advertisement.ServiceUuids = append(advertisement.ServiceUuids, fmt.Sprintf("%x", uuid)) 234 | } 235 | } 236 | 237 | if data, ok := advdata["kCBAdvDataServiceData"]; ok { 238 | sdata := data.(xpc.Array) 239 | 240 | for i := 0; i < len(sdata); i += 2 { 241 | sd := ServiceData{ 242 | Uuid: fmt.Sprintf("%x", sdata[i+0].([]byte)), 243 | Data: sdata[i+1].([]byte), 244 | } 245 | 246 | advertisement.ServiceData = append(advertisement.ServiceData, sd) 247 | } 248 | } 249 | 250 | pid := deviceUuid.String() 251 | p := ble.peripherals[pid] 252 | emit := ble.allowDuplicates || p == nil 253 | 254 | if p == nil { 255 | // add new peripheral 256 | p = &Peripheral{ 257 | Uuid: deviceUuid, 258 | Connectable: connectable, 259 | Advertisement: advertisement, 260 | Rssi: rssi, 261 | Services: map[interface{}]*ServiceHandle{}, 262 | } 263 | 264 | ble.peripherals[pid] = p 265 | } else { 266 | // update peripheral 267 | p.Advertisement = advertisement 268 | p.Rssi = rssi 269 | } 270 | 271 | if emit { 272 | ble.Emit(Event{Name: "discover", DeviceUUID: deviceUuid, Peripheral: *p}) 273 | } 274 | 275 | case 38, 58, 67, 86: // connect 276 | if id == 38 && ble.utsname.Release >= "18." { 277 | // this is not a connect (it doesn't have kCBMsgArgDeviceUUID, 278 | // but instead has kCBAdvDataDeviceAddress) 279 | break 280 | } 281 | 282 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 283 | ble.Emit(Event{Name: "connect", DeviceUUID: deviceUuid}) 284 | 285 | case 40: // disconnect (+ 53 see next) 286 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 287 | ble.Emit(Event{Name: "disconnect", DeviceUUID: deviceUuid}) 288 | 289 | case 53: // mtuChange 290 | if ble.utsname.Release >= "14." { 291 | // this is actually a disconnect 292 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 293 | ble.Emit(Event{Name: "disconnect", DeviceUUID: deviceUuid}) 294 | break 295 | } 296 | 297 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 298 | mtu := args.MustGetInt("kCBMsgArgATTMTU") 299 | 300 | // bleno here converts the deviceUuid to an address 301 | if p, ok := ble.peripherals[deviceUuid.String()]; ok { 302 | ble.Emit(Event{Name: "mtuChange", DeviceUUID: deviceUuid, Peripheral: *p, Mtu: mtu}) 303 | } 304 | 305 | case 54, 82: // serviceDiscover 306 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 307 | servicesUuids := []string{} 308 | servicesHandles := map[interface{}]*ServiceHandle{} 309 | 310 | if dservices, ok := args["kCBMsgArgServices"]; ok { 311 | for _, s := range dservices.(xpc.Array) { 312 | service := s.(xpc.Dict) 313 | serviceHandle := ServiceHandle{ 314 | Uuid: service.MustGetHexBytes("kCBMsgArgUUID"), 315 | startHandle: service.MustGetInt("kCBMsgArgServiceStartHandle"), 316 | endHandle: service.MustGetInt("kCBMsgArgServiceEndHandle"), 317 | Characteristics: map[interface{}]*ServiceCharacteristic{}} 318 | 319 | if nameType, ok := knownServices[serviceHandle.Uuid]; ok { 320 | serviceHandle.Name = nameType.Name 321 | serviceHandle.Type = nameType.Type 322 | } 323 | 324 | servicesHandles[serviceHandle.Uuid] = &serviceHandle 325 | servicesHandles[serviceHandle.startHandle] = &serviceHandle 326 | 327 | servicesUuids = append(servicesUuids, serviceHandle.Uuid) 328 | } 329 | } 330 | 331 | if p, ok := ble.peripherals[deviceUuid.String()]; ok { 332 | p.Services = servicesHandles 333 | ble.Emit(Event{Name: "servicesDiscover", DeviceUUID: deviceUuid, Peripheral: *p}) 334 | } 335 | 336 | case 55: // rssiUpdate 337 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 338 | rssi := args.MustGetInt("kCBMsgArgData") 339 | 340 | if p, ok := ble.peripherals[deviceUuid.String()]; ok { 341 | p.Rssi = rssi 342 | ble.Emit(Event{Name: "rssiUpdate", DeviceUUID: deviceUuid, Peripheral: *p}) 343 | } 344 | 345 | case 63, 89: // characteristicsDiscover 346 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 347 | serviceStartHandle := args.MustGetInt("kCBMsgArgServiceStartHandle") 348 | 349 | if p, ok := ble.peripherals[deviceUuid.String()]; ok { 350 | service := p.Services[serviceStartHandle] 351 | 352 | //result := args.MustGetInt("kCBMsgArgResult") 353 | 354 | for _, c := range args.MustGetArray("kCBMsgArgCharacteristics") { 355 | cDict := c.(xpc.Dict) 356 | 357 | characteristic := ServiceCharacteristic{ 358 | Uuid: cDict.MustGetHexBytes("kCBMsgArgUUID"), 359 | Handle: cDict.MustGetInt("kCBMsgArgCharacteristicHandle"), 360 | ValueHandle: cDict.MustGetInt("kCBMsgArgCharacteristicValueHandle"), 361 | Descriptors: map[interface{}]*CharacteristicDescriptor{}, 362 | } 363 | 364 | if nameType, ok := knownCharacteristics[characteristic.Uuid]; ok { 365 | characteristic.Name = nameType.Name 366 | characteristic.Type = nameType.Type 367 | } 368 | 369 | properties := cDict.MustGetInt("kCBMsgArgCharacteristicProperties") 370 | 371 | if (properties & 0x01) != 0 { 372 | characteristic.Properties |= Broadcast 373 | } 374 | 375 | if (properties & 0x02) != 0 { 376 | characteristic.Properties |= Read 377 | } 378 | 379 | if (properties & 0x04) != 0 { 380 | characteristic.Properties |= WriteWithoutResponse 381 | } 382 | 383 | if (properties & 0x08) != 0 { 384 | characteristic.Properties |= Write 385 | } 386 | 387 | if (properties & 0x10) != 0 { 388 | characteristic.Properties |= Notify 389 | } 390 | 391 | if (properties & 0x20) != 0 { 392 | characteristic.Properties |= Indicate 393 | } 394 | 395 | if (properties & 0x40) != 0 { 396 | characteristic.Properties |= AuthenticatedSignedWrites 397 | } 398 | 399 | if (properties & 0x80) != 0 { 400 | characteristic.Properties |= ExtendedProperties 401 | } 402 | 403 | if service != nil { 404 | service.Characteristics[characteristic.Uuid] = &characteristic 405 | service.Characteristics[characteristic.Handle] = &characteristic 406 | service.Characteristics[characteristic.ValueHandle] = &characteristic 407 | } 408 | } 409 | 410 | if service != nil { 411 | ble.Emit(Event{Name: "characteristicsDiscover", DeviceUUID: deviceUuid, ServiceUuid: service.Uuid, Peripheral: *p}) 412 | } else { 413 | log.Println("no service", serviceStartHandle) 414 | } 415 | } else { 416 | log.Println("no peripheral", deviceUuid) 417 | } 418 | 419 | case 75, 99: // descriptorsDiscover 420 | if id == 99 && ble.utsname.Release >= "19." { 421 | if _, ok := args["kCBMsgArgServiceStartHandle"]; ok { 422 | // this is actually a service discover event 423 | id = 82 424 | goto retry_switch 425 | } 426 | } 427 | 428 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 429 | characteristicsHandle := args.MustGetInt("kCBMsgArgCharacteristicHandle") 430 | //result := args.MustGetInt("kCBMsgArgResult") 431 | 432 | if p, ok := ble.peripherals[deviceUuid.String()]; ok { 433 | for _, s := range p.Services { 434 | if c, ok := s.Characteristics[characteristicsHandle]; ok { 435 | for _, d := range args.MustGetArray("kCBMsgArgDescriptors") { 436 | dDict := d.(xpc.Dict) 437 | descriptor := CharacteristicDescriptor{ 438 | Uuid: dDict.MustGetHexBytes("kCBMsgArgUUID"), 439 | Handle: dDict.MustGetInt("kCBMsgArgDescriptorHandle"), 440 | } 441 | 442 | c.Descriptors[descriptor.Uuid] = &descriptor 443 | c.Descriptors[descriptor.Handle] = &descriptor 444 | } 445 | 446 | ble.Emit(Event{Name: "descriptorsDiscover", DeviceUUID: deviceUuid, ServiceUuid: s.Uuid, CharacteristicUuid: c.Uuid, Peripheral: *p}) 447 | break 448 | } 449 | } 450 | } else { 451 | log.Println("no peripheral", deviceUuid) 452 | } 453 | 454 | case 70, 95, 115: // read 455 | deviceUuid := args.MustGetUUID("kCBMsgArgDeviceUUID") 456 | characteristicsHandle := args.MustGetInt("kCBMsgArgCharacteristicHandle") 457 | //result := args.MustGetInt("kCBMsgArgResult") 458 | isNotification := args.GetInt("kCBMsgArgIsNotification", 0) != 0 459 | data := args.MustGetBytes("kCBMsgArgData") 460 | 461 | if p, ok := ble.peripherals[deviceUuid.String()]; ok { 462 | for _, s := range p.Services { 463 | if c, ok := s.Characteristics[characteristicsHandle]; ok { 464 | ble.Emit(Event{Name: "read", DeviceUUID: deviceUuid, ServiceUuid: s.Uuid, CharacteristicUuid: c.Uuid, Peripheral: *p, Data: data, IsNotification: isNotification}) 465 | break 466 | } 467 | } 468 | } 469 | } 470 | } 471 | 472 | // send a message to Blued 473 | func (ble *BLE) sendCBMsg(id int, args xpc.Dict) { 474 | message := xpc.Dict{"kCBMsgId": id, "kCBMsgArgs": args} 475 | if ble.verbose { 476 | log.Printf("sendCBMsg %#v\n", message) 477 | } 478 | 479 | ble.conn.Send(message, ble.verbose) 480 | } 481 | 482 | // initialize BLE 483 | func (ble *BLE) Init() { 484 | ble.sendCBMsg(1, xpc.Dict{"kCBMsgArgName": fmt.Sprintf("goble-%v", time.Now().Unix()), 485 | "kCBMsgArgOptions": xpc.Dict{"kCBInitOptionShowPowerAlert": 0}, "kCBMsgArgType": 0}) 486 | } 487 | 488 | // start advertising 489 | func (ble *BLE) StartAdvertising(name string, serviceUuids []xpc.UUID) { 490 | uuids := make([][]byte, len(serviceUuids)) 491 | for i, uuid := range serviceUuids { 492 | uuids[i] = []byte(uuid[:]) 493 | } 494 | ble.sendCBMsg(8, xpc.Dict{"kCBAdvDataLocalName": name, "kCBAdvDataServiceUUIDs": uuids}) 495 | } 496 | 497 | // start advertising as IBeacon (raw data) 498 | func (ble *BLE) StartAdvertisingIBeaconData(data []byte) { 499 | if ble.utsname.Release >= "14." { 500 | l := len(data) 501 | buf := bytes.NewBuffer([]byte{byte(l + 5), 0xFF, 0x4C, 0x00, 0x02, byte(l)}) 502 | buf.Write(data) 503 | ble.sendCBMsg(8, xpc.Dict{"kCBAdvDataAppleMfgData": buf.Bytes()}) 504 | } else { 505 | ble.sendCBMsg(8, xpc.Dict{"kCBAdvDataAppleBeaconKey": data}) 506 | } 507 | } 508 | 509 | // start advertising as IBeacon 510 | func (ble *BLE) StartAdvertisingIBeacon(uuid xpc.UUID, major, minor uint16, measuredPower int8) { 511 | var buf bytes.Buffer 512 | binary.Write(&buf, binary.BigEndian, uuid[:]) 513 | binary.Write(&buf, binary.BigEndian, major) 514 | binary.Write(&buf, binary.BigEndian, minor) 515 | binary.Write(&buf, binary.BigEndian, measuredPower) 516 | 517 | ble.StartAdvertisingIBeaconData(buf.Bytes()) 518 | } 519 | 520 | // stop advertising 521 | func (ble *BLE) StopAdvertising() { 522 | ble.sendCBMsg(9, nil) 523 | } 524 | 525 | // start scanning 526 | func (ble *BLE) StartScanning(serviceUuids []xpc.UUID, allowDuplicates bool) { 527 | uuids := []string{} 528 | 529 | for _, uuid := range serviceUuids { 530 | uuids = append(uuids, uuid.String()) 531 | } 532 | 533 | args := xpc.Dict{"kCBMsgArgUUIDs": uuids} 534 | if allowDuplicates { 535 | args["kCBMsgArgOptions"] = xpc.Dict{"kCBScanOptionAllowDuplicates": 1} 536 | } else { 537 | args["kCBMsgArgOptions"] = xpc.Dict{} 538 | } 539 | 540 | ble.allowDuplicates = allowDuplicates 541 | msg := 29 542 | if ble.utsname.Release >= "19.4" { 543 | msg = 53 544 | } else if ble.utsname.Release >= "19." { 545 | msg = 51 546 | } else if ble.utsname.Release >= "18." { 547 | msg = 46 548 | } else if ble.utsname.Release >= "17." { 549 | msg = 44 550 | } 551 | ble.sendCBMsg(msg, args) 552 | } 553 | 554 | // stop scanning 555 | func (ble *BLE) StopScanning() { 556 | msg := 30 557 | if ble.utsname.Release >= "19." { 558 | msg = 52 559 | } else if ble.utsname.Release >= "18." { 560 | msg = 47 561 | } 562 | ble.sendCBMsg(msg, nil) 563 | } 564 | 565 | // connect 566 | func (ble *BLE) Connect(deviceUuid xpc.UUID) { 567 | uuid := deviceUuid.String() 568 | msg := 31 569 | if ble.utsname.Release >= "19." { 570 | msg = 53 571 | } else if ble.utsname.Release >= "18." { 572 | msg = 48 573 | } 574 | if p, ok := ble.peripherals[uuid]; ok { 575 | ble.sendCBMsg(msg, xpc.Dict{"kCBMsgArgOptions": xpc.Dict{"kCBConnectOptionNotifyOnDisconnection": 1}, "kCBMsgArgDeviceUUID": p.Uuid}) 576 | } else { 577 | log.Println("no peripheral", deviceUuid) 578 | } 579 | } 580 | 581 | // disconnect 582 | func (ble *BLE) Disconnect(deviceUuid xpc.UUID) { 583 | uuid := deviceUuid.String() 584 | msg := 32 585 | if ble.utsname.Release >= "19." { 586 | msg = 45 587 | } else if ble.utsname.Release >= "18." { 588 | msg = 49 589 | } 590 | if p, ok := ble.peripherals[uuid]; ok { 591 | ble.sendCBMsg(msg, xpc.Dict{"kCBMsgArgDeviceUUID": p.Uuid}) 592 | } else { 593 | log.Println("no peripheral", deviceUuid) 594 | } 595 | } 596 | 597 | // update rssi 598 | func (ble *BLE) UpdateRssi(deviceUuid xpc.UUID) { 599 | uuid := deviceUuid.String() 600 | msg := 43 601 | if ble.utsname.Release >= "19." { 602 | msg = 76 // or 93 ? 603 | } else if ble.utsname.Release >= "18." { 604 | msg = 71 605 | } 606 | if p, ok := ble.peripherals[uuid]; ok { 607 | ble.sendCBMsg(msg, xpc.Dict{"kCBMsgArgDeviceUUID": p.Uuid}) 608 | } else { 609 | log.Println("no peripheral", deviceUuid) 610 | } 611 | } 612 | 613 | // discover services 614 | func (ble *BLE) DiscoverServices(deviceUuid xpc.UUID, uuids []xpc.UUID) { 615 | sUuid := deviceUuid.String() 616 | msg := 44 617 | if ble.utsname.Release >= "19." { 618 | msg = 77 619 | } else if ble.utsname.Release >= "18." { 620 | msg = 72 621 | } 622 | if p, ok := ble.peripherals[sUuid]; ok { 623 | sUuids := make([]string, len(uuids)) 624 | for i, uuid := range uuids { 625 | sUuids[i] = uuid.String() // uuids may be a list of []byte (2 bytes) 626 | } 627 | ble.sendCBMsg(msg, xpc.Dict{"kCBMsgArgDeviceUUID": p.Uuid, "kCBMsgArgUUIDs": sUuids}) 628 | } else { 629 | log.Println("no peripheral", deviceUuid) 630 | } 631 | } 632 | 633 | // discover characteristics 634 | func (ble *BLE) DiscoverCharacteristics(deviceUuid xpc.UUID, serviceUuid string, characteristicUuids []string) { 635 | sUuid := deviceUuid.String() 636 | msg := 61 637 | if ble.utsname.Release >= "19." { 638 | msg = 92 639 | } else if ble.utsname.Release >= "18." { 640 | msg = 87 641 | } 642 | if p, ok := ble.peripherals[sUuid]; ok { 643 | cUuids := make([]string, len(characteristicUuids)) 644 | for i, cuuid := range characteristicUuids { 645 | cUuids[i] = cuuid // characteristicUuids may be a list of []byte (2 bytes) 646 | } 647 | 648 | ble.sendCBMsg(msg, xpc.Dict{ 649 | "kCBMsgArgDeviceUUID": p.Uuid, 650 | "kCBMsgArgServiceStartHandle": p.Services[serviceUuid].startHandle, 651 | "kCBMsgArgServiceEndHandle": p.Services[serviceUuid].endHandle, 652 | "kCBMsgArgUUIDs": cUuids, 653 | }) 654 | 655 | } else { 656 | log.Println("no peripheral", deviceUuid) 657 | } 658 | } 659 | 660 | // discover descriptors 661 | func (ble *BLE) DiscoverDescriptors(deviceUuid xpc.UUID, serviceUuid, characteristicUuid string) { 662 | sUuid := deviceUuid.String() 663 | msg := 69 664 | if ble.utsname.Release >= "19." { 665 | msg = 99 666 | } else if ble.utsname.Release >= "18." { 667 | msg = 94 668 | } 669 | if p, ok := ble.peripherals[sUuid]; ok { 670 | s := p.Services[serviceUuid] 671 | c := s.Characteristics[characteristicUuid] 672 | 673 | ble.sendCBMsg(msg, xpc.Dict{ 674 | "kCBMsgArgDeviceUUID": p.Uuid, 675 | "kCBMsgArgCharacteristicHandle": c.Handle, 676 | "kCBMsgArgCharacteristicValueHandle": c.ValueHandle, 677 | }) 678 | } else { 679 | log.Println("no peripheral", deviceUuid) 680 | } 681 | } 682 | 683 | // read 684 | func (ble *BLE) Read(deviceUuid xpc.UUID, serviceUuid, characteristicUuid string) { 685 | sUuid := deviceUuid.String() 686 | msg := 64 687 | if ble.utsname.Release >= "19.4" { 688 | msg = 90 689 | } else if ble.utsname.Release >= "19." { 690 | msg = 105 691 | } else if ble.utsname.Release >= "18." { 692 | msg = 100 693 | } 694 | if p, ok := ble.peripherals[sUuid]; ok { 695 | s := p.Services[serviceUuid] 696 | c := s.Characteristics[characteristicUuid] 697 | 698 | ble.sendCBMsg(msg, xpc.Dict{ 699 | "kCBMsgArgDeviceUUID": p.Uuid, 700 | "kCBMsgArgCharacteristicHandle": c.Handle, 701 | "kCBMsgArgCharacteristicValueHandle": c.ValueHandle, 702 | }) 703 | } else { 704 | log.Println("no peripheral", deviceUuid) 705 | } 706 | } 707 | 708 | // remove all services 709 | func (ble *BLE) RemoveServices() { 710 | ble.sendCBMsg(12, nil) 711 | } 712 | 713 | // set services 714 | func (ble *BLE) SetServices(services []Service) { 715 | ble.sendCBMsg(12, nil) // remove all services 716 | ble.attributes = xpc.Array{nil} 717 | 718 | attributeId := 1 719 | 720 | for _, service := range services { 721 | arg := xpc.Dict{ 722 | "kCBMsgArgAttributeID": attributeId, 723 | "kCBMsgArgAttributeIDs": []int{}, 724 | "kCBMsgArgCharacteristics": nil, 725 | "kCBMsgArgType": 1, // 1 => primary, 0 => excluded 726 | "kCBMsgArgUUID": service.uuid.String(), 727 | } 728 | 729 | ble.attributes = append(ble.attributes, service) 730 | ble.lastServiceAttributeId = attributeId 731 | attributeId += 1 732 | 733 | characteristics := xpc.Array{} 734 | 735 | for _, characteristic := range service.characteristics { 736 | properties := 0 737 | permissions := 0 738 | 739 | if Read&characteristic.properties != 0 { 740 | properties |= 0x02 741 | 742 | if Read&characteristic.secure != 0 { 743 | permissions |= 0x04 744 | } else { 745 | permissions |= 0x01 746 | } 747 | } 748 | 749 | if WriteWithoutResponse&characteristic.properties != 0 { 750 | properties |= 0x04 751 | 752 | if WriteWithoutResponse&characteristic.secure != 0 { 753 | permissions |= 0x08 754 | } else { 755 | permissions |= 0x02 756 | } 757 | } 758 | 759 | if Write&characteristic.properties != 0 { 760 | properties |= 0x08 761 | 762 | if WriteWithoutResponse&characteristic.secure != 0 { 763 | permissions |= 0x08 764 | } else { 765 | permissions |= 0x02 766 | } 767 | } 768 | 769 | if Notify&characteristic.properties != 0 { 770 | if Notify&characteristic.secure != 0 { 771 | properties |= 0x100 772 | } else { 773 | properties |= 0x10 774 | } 775 | } 776 | 777 | if Indicate&characteristic.properties != 0 { 778 | if Indicate&characteristic.secure != 0 { 779 | properties |= 0x200 780 | } else { 781 | properties |= 0x20 782 | } 783 | } 784 | 785 | descriptors := xpc.Array{} 786 | for _, descriptor := range characteristic.descriptors { 787 | descriptors = append(descriptors, xpc.Dict{"kCBMsgArgData": descriptor.value, "kCBMsgArgUUID": descriptor.uuid.String()}) 788 | } 789 | 790 | characteristicArg := xpc.Dict{ 791 | "kCBMsgArgAttributeID": attributeId, 792 | "kCBMsgArgAttributePermissions": permissions, 793 | "kCBMsgArgCharacteristicProperties": properties, 794 | "kCBMsgArgData": characteristic.value, 795 | "kCBMsgArgDescriptors": descriptors, 796 | "kCBMsgArgUUID": characteristic.uuid.String(), 797 | } 798 | 799 | ble.attributes = append(ble.attributes, characteristic) 800 | characteristics = append(characteristics, characteristicArg) 801 | 802 | attributeId += 1 803 | } 804 | 805 | arg["kCBMsgArgCharacteristics"] = characteristics 806 | ble.sendCBMsg(10, arg) // remove all services 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /services.go: -------------------------------------------------------------------------------- 1 | package goble 2 | 3 | // A dictionary of known service names and type (keyed by service uuid) 4 | var knownServices = map[string]struct{ Name, Type string }{ 5 | "1800": {Name: "Generic Access", Type: "org.bluetooth.service.generic_access"}, 6 | "1801": {Name: "Generic Attribute", Type: "org.bluetooth.service.generic_attribute"}, 7 | "1802": {Name: "Immediate Alert", Type: "org.bluetooth.service.immediate_alert"}, 8 | "1803": {Name: "Link Loss", Type: "org.bluetooth.service.link_loss"}, 9 | "1804": {Name: "Tx Power", Type: "org.bluetooth.service.tx_power"}, 10 | "1805": {Name: "Current Time Service", Type: "org.bluetooth.service.current_time"}, 11 | "1806": {Name: "Reference Time Update Service", Type: "org.bluetooth.service.reference_time_update"}, 12 | "1807": {Name: "Next DST Change Service", Type: "org.bluetooth.service.next_dst_change"}, 13 | "1808": {Name: "Glucose", Type: "org.bluetooth.service.glucose"}, 14 | "1809": {Name: "Health Thermometer", Type: "org.bluetooth.service.health_thermometer"}, 15 | "180a": {Name: "Device Information", Type: "org.bluetooth.service.device_information"}, 16 | "180d": {Name: "Heart Rate", Type: "org.bluetooth.service.heart_rate"}, 17 | "180e": {Name: "Phone Alert Status Service", Type: "org.bluetooth.service.phone_alert_service"}, 18 | "180f": {Name: "Battery Service", Type: "org.bluetooth.service.battery_service"}, 19 | "1810": {Name: "Blood Pressure", Type: "org.bluetooth.service.blood_pressuer"}, 20 | "1811": {Name: "Alert Notification Service", Type: "org.bluetooth.service.alert_notification"}, 21 | "1812": {Name: "Human Interface Device", Type: "org.bluetooth.service.human_interface_device"}, 22 | "1813": {Name: "Scan Parameters", Type: "org.bluetooth.service.scan_parameters"}, 23 | "1814": {Name: "Running Speed and Cadence", Type: "org.bluetooth.service.running_speed_and_cadence"}, 24 | "1815": {Name: "Cycling Speed and Cadence", Type: "org.bluetooth.service.cycling_speed_and_cadence"}, 25 | } 26 | -------------------------------------------------------------------------------- /xpc/xpc.go: -------------------------------------------------------------------------------- 1 | package xpc 2 | 3 | /* 4 | #include "xpc_wrapper.h" 5 | */ 6 | import "C" 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "log" 12 | r "reflect" 13 | "strings" 14 | "unsafe" 15 | ) 16 | 17 | type XPC struct { 18 | conn C.xpc_connection_t 19 | } 20 | 21 | func (x *XPC) Send(msg interface{}, verbose bool) { 22 | C.XpcSendMessage(x.conn, goToXpc(msg), C.bool(true), C.bool(verbose)) 23 | } 24 | 25 | // 26 | // minimal XPC support required for BLE 27 | // 28 | 29 | // a dictionary of things 30 | type Dict map[string]interface{} 31 | 32 | func (d Dict) Contains(k string) bool { 33 | _, ok := d[k] 34 | return ok 35 | } 36 | 37 | func (d Dict) MustGetDict(k string) Dict { 38 | if v, ok := d[k]; ok { 39 | return v.(Dict) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (d Dict) MustGetArray(k string) Array { 46 | if v, ok := d[k]; ok { 47 | return v.(Array) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (d Dict) MustGetBytes(k string) []byte { 54 | return d[k].([]byte) 55 | } 56 | 57 | func (d Dict) MustGetHexBytes(k string) string { 58 | return fmt.Sprintf("%x", d[k].([]byte)) 59 | } 60 | 61 | func (d Dict) MustGetInt(k string) int { 62 | return int(d[k].(int64)) 63 | } 64 | 65 | func (d Dict) MustGetUUID(k string) UUID { 66 | return d[k].(UUID) 67 | } 68 | 69 | func (d Dict) GetString(k, defv string) string { 70 | if v := d[k]; v != nil { 71 | //log.Printf("GetString %s %#v\n", k, v) 72 | return v.(string) 73 | } else { 74 | //log.Printf("GetString %s default %#v\n", k, defv) 75 | return defv 76 | } 77 | } 78 | 79 | func (d Dict) GetBytes(k string, defv []byte) []byte { 80 | if v := d[k]; v != nil { 81 | //log.Printf("GetBytes %s %#v\n", k, v) 82 | return v.([]byte) 83 | } else { 84 | //log.Printf("GetBytes %s default %#v\n", k, defv) 85 | return defv 86 | } 87 | } 88 | 89 | func (d Dict) GetInt(k string, defv int) int { 90 | if v := d[k]; v != nil { 91 | //log.Printf("GetString %s %#v\n", k, v) 92 | return int(v.(int64)) 93 | } else { 94 | //log.Printf("GetString %s default %#v\n", k, defv) 95 | return defv 96 | } 97 | } 98 | 99 | func (d Dict) GetUUID(k string) UUID { 100 | return GetUUID(d[k]) 101 | } 102 | 103 | // an Array of things 104 | type Array []interface{} 105 | 106 | func (a Array) GetUUID(k int) UUID { 107 | return GetUUID(a[k]) 108 | } 109 | 110 | // a UUID 111 | type UUID [16]byte 112 | 113 | func MakeUUID(s string) UUID { 114 | var sl []byte 115 | 116 | s = strings.Replace(s, "-", "", -1) 117 | fmt.Sscanf(s, "%32x", &sl) 118 | 119 | var uuid [16]byte 120 | copy(uuid[:], sl) 121 | return UUID(uuid) 122 | } 123 | 124 | func MustUUID(s string) UUID { 125 | var sl []byte 126 | 127 | s = strings.Replace(s, "-", "", -1) 128 | if len(s) != 32 { 129 | log.Fatal("invalid UUID") 130 | } 131 | if n, err := fmt.Sscanf(s, "%32x", &sl); err != nil || n != 1 { 132 | log.Fatal("invalid UUID ", s, " len ", n, " error ", err) 133 | } 134 | 135 | var uuid [16]byte 136 | copy(uuid[:], sl) 137 | return UUID(uuid) 138 | } 139 | 140 | func (uuid UUID) String() string { 141 | return fmt.Sprintf("%x", [16]byte(uuid)) 142 | } 143 | 144 | func GetUUID(v interface{}) UUID { 145 | if v == nil { 146 | return UUID{} 147 | } 148 | 149 | if uuid, ok := v.(UUID); ok { 150 | return uuid 151 | } 152 | 153 | if bytes, ok := v.([]byte); ok { 154 | uuid := UUID{} 155 | 156 | for i, b := range bytes { 157 | uuid[i] = b 158 | } 159 | 160 | return uuid 161 | } 162 | 163 | if bytes, ok := v.([]uint8); ok { 164 | uuid := UUID{} 165 | 166 | for i, b := range bytes { 167 | uuid[i] = b 168 | } 169 | 170 | return uuid 171 | } 172 | 173 | log.Fatalf("invalid type for UUID: %#v", v) 174 | return UUID{} 175 | } 176 | 177 | var ( 178 | CONNECTION_INVALID = errors.New("connection invalid") 179 | CONNECTION_INTERRUPTED = errors.New("connection interrupted") 180 | CONNECTION_TERMINATED = errors.New("connection terminated") 181 | 182 | TYPE_OF_UUID = r.TypeOf(UUID{}) 183 | TYPE_OF_BYTES = r.TypeOf([]byte{}) 184 | 185 | handlers = map[uintptr]XpcEventHandler{} 186 | ) 187 | 188 | type XpcEventHandler interface { 189 | HandleXpcEvent(event Dict, err error) 190 | } 191 | 192 | func XpcConnect(service string, eh XpcEventHandler) XPC { 193 | // func XpcConnect(service string, eh XpcEventHandler) C.xpc_connection_t { 194 | ctx := uintptr(unsafe.Pointer(&eh)) 195 | handlers[ctx] = eh 196 | 197 | cservice := C.CString(service) 198 | defer C.free(unsafe.Pointer(cservice)) 199 | // return C.XpcConnect(cservice, C.uintptr_t(ctx)) 200 | return XPC{conn: C.XpcConnect(cservice, C.uintptr_t(ctx))} 201 | } 202 | 203 | //export handleXpcEvent 204 | func handleXpcEvent(event C.xpc_object_t, p C.ulong) { 205 | //log.Printf("handleXpcEvent %#v %#v\n", event, p) 206 | 207 | t := C.xpc_get_type(event) 208 | 209 | eh := handlers[uintptr(p)] 210 | if eh == nil { 211 | //log.Println("no handler for", p) 212 | return 213 | } 214 | 215 | if t == C.TYPE_ERROR { 216 | if event == C.ERROR_CONNECTION_INVALID { 217 | // The client process on the other end of the connection has either 218 | // crashed or cancelled the connection. After receiving this error, 219 | // the connection is in an invalid state, and you do not need to 220 | // call xpc_connection_cancel(). Just tear down any associated state 221 | // here. 222 | //log.Println("connection invalid") 223 | eh.HandleXpcEvent(nil, CONNECTION_INVALID) 224 | } else if event == C.ERROR_CONNECTION_INTERRUPTED { 225 | //log.Println("connection interrupted") 226 | eh.HandleXpcEvent(nil, CONNECTION_INTERRUPTED) 227 | } else if event == C.ERROR_CONNECTION_TERMINATED { 228 | // Handle per-connection termination cleanup. 229 | //log.Println("connection terminated") 230 | eh.HandleXpcEvent(nil, CONNECTION_TERMINATED) 231 | } else { 232 | //log.Println("got some error", event) 233 | eh.HandleXpcEvent(nil, fmt.Errorf("%v", event)) 234 | } 235 | } else { 236 | eh.HandleXpcEvent(xpcToGo(event).(Dict), nil) 237 | } 238 | } 239 | 240 | // goToXpc converts a go object to an xpc object 241 | func goToXpc(o interface{}) C.xpc_object_t { 242 | return valueToXpc(r.ValueOf(o)) 243 | } 244 | 245 | // valueToXpc converts a go Value to an xpc object 246 | // 247 | // note that not all the types are supported, but only the subset required for Blued 248 | func valueToXpc(val r.Value) C.xpc_object_t { 249 | if !val.IsValid() { 250 | return nil 251 | } 252 | 253 | var xv C.xpc_object_t 254 | 255 | switch val.Kind() { 256 | case r.Int, r.Int8, r.Int16, r.Int32, r.Int64: 257 | xv = C.xpc_int64_create(C.int64_t(val.Int())) 258 | 259 | case r.Uint, r.Uint8, r.Uint16, r.Uint32: 260 | xv = C.xpc_int64_create(C.int64_t(val.Uint())) 261 | 262 | case r.String: 263 | xv = C.xpc_string_create(C.CString(val.String())) 264 | 265 | case r.Map: 266 | xv = C.xpc_dictionary_create(nil, nil, 0) 267 | for _, k := range val.MapKeys() { 268 | v := valueToXpc(val.MapIndex(k)) 269 | C.xpc_dictionary_set_value(xv, C.CString(k.String()), v) 270 | if v != nil { 271 | C.xpc_release(v) 272 | } 273 | } 274 | 275 | case r.Array, r.Slice: 276 | if val.Type() == TYPE_OF_UUID { 277 | // Array of bytes 278 | var uuid [16]byte 279 | r.Copy(r.ValueOf(uuid[:]), val) 280 | xv = C.xpc_uuid_create(C.ptr_to_uuid(unsafe.Pointer(&uuid[0]))) 281 | } else if val.Type() == TYPE_OF_BYTES { 282 | // slice of bytes 283 | xv = C.xpc_data_create(unsafe.Pointer(val.Pointer()), C.size_t(val.Len())) 284 | } else { 285 | xv = C.xpc_array_create(nil, 0) 286 | l := val.Len() 287 | 288 | for i := 0; i < l; i++ { 289 | v := valueToXpc(val.Index(i)) 290 | C.xpc_array_append_value(xv, v) 291 | if v != nil { 292 | C.xpc_release(v) 293 | } 294 | } 295 | } 296 | 297 | case r.Interface, r.Ptr: 298 | xv = valueToXpc(val.Elem()) 299 | 300 | default: 301 | log.Fatalf("unsupported %#v", val.String()) 302 | } 303 | 304 | return xv 305 | } 306 | 307 | //export arraySet 308 | func arraySet(u C.uintptr_t, i C.int, v C.xpc_object_t) { 309 | a := *(*Array)(unsafe.Pointer(uintptr(u))) 310 | a[i] = xpcToGo(v) 311 | } 312 | 313 | //export dictSet 314 | func dictSet(u C.uintptr_t, k *C.char, v C.xpc_object_t) { 315 | d := *(*Dict)(unsafe.Pointer(uintptr(u))) 316 | d[C.GoString(k)] = xpcToGo(v) 317 | } 318 | 319 | // xpcToGo converts an xpc object to a go object 320 | // 321 | // note that not all the types are supported, but only the subset required for Blued 322 | func xpcToGo(v C.xpc_object_t) interface{} { 323 | t := C.xpc_get_type(v) 324 | 325 | switch t { 326 | case C.TYPE_ARRAY: 327 | a := make(Array, C.int(C.xpc_array_get_count(v))) 328 | p := uintptr(unsafe.Pointer(&a)) 329 | C.XpcArrayApply(C.uintptr_t(p), v) 330 | return a 331 | 332 | case C.TYPE_DATA: 333 | return C.GoBytes(C.xpc_data_get_bytes_ptr(v), C.int(C.xpc_data_get_length(v))) 334 | 335 | case C.TYPE_DICT: 336 | d := make(Dict) 337 | p := uintptr(unsafe.Pointer(&d)) 338 | C.XpcDictApply(C.uintptr_t(p), v) 339 | return d 340 | 341 | case C.TYPE_INT64: 342 | return int64(C.xpc_int64_get_value(v)) 343 | 344 | case C.TYPE_STRING: 345 | return C.GoString(C.xpc_string_get_string_ptr(v)) 346 | 347 | case C.TYPE_UUID: 348 | a := [16]byte{} 349 | C.XpcUUIDGetBytes(unsafe.Pointer(&a), v) 350 | return UUID(a) 351 | 352 | default: 353 | log.Fatalf("unexpected type %#v, value %#v", t, v) 354 | } 355 | 356 | return nil 357 | } 358 | 359 | // xpc_release is needed by tests, since they can't use CGO 360 | func xpc_release(xv C.xpc_object_t) { 361 | C.xpc_release(xv) 362 | } 363 | 364 | // this is used to check the OS version 365 | 366 | type Utsname struct { 367 | Sysname string 368 | Nodename string 369 | Release string 370 | Version string 371 | Machine string 372 | } 373 | 374 | func Uname(utsname *Utsname) error { 375 | var cstruct C.struct_utsname 376 | if err := C.uname(&cstruct); err != 0 { 377 | return errors.New("utsname error") 378 | } 379 | 380 | // XXX: this may crash if any value is exactly 256 characters (no 0 terminator) 381 | utsname.Sysname = C.GoString(&cstruct.sysname[0]) 382 | utsname.Nodename = C.GoString(&cstruct.nodename[0]) 383 | utsname.Release = C.GoString(&cstruct.release[0]) 384 | utsname.Version = C.GoString(&cstruct.version[0]) 385 | utsname.Machine = C.GoString(&cstruct.machine[0]) 386 | 387 | return nil 388 | } 389 | -------------------------------------------------------------------------------- /xpc/xpc_test.go: -------------------------------------------------------------------------------- 1 | package xpc 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func CheckUUID(t *testing.T, v interface{}) UUID { 8 | if uuid, ok := v.(UUID); ok { 9 | return uuid 10 | } else { 11 | t.Errorf("not a UUID: %#v\n", v) 12 | return uuid 13 | } 14 | } 15 | 16 | func TestConvertUUID(t *testing.T) { 17 | uuid := MakeUUID("00112233445566778899aabbccddeeff") 18 | 19 | xv := goToXpc(uuid) 20 | v := xpcToGo(xv) 21 | 22 | xpc_release(xv) 23 | 24 | uuid2 := CheckUUID(t, v) 25 | 26 | if uuid != uuid2 { 27 | t.Errorf("expected %#v got %#v\n", uuid, uuid2) 28 | } 29 | } 30 | 31 | func TestConvertSlice(t *testing.T) { 32 | arr := []string{"one", "two", "three"} 33 | 34 | xv := goToXpc(arr) 35 | v := xpcToGo(xv) 36 | 37 | xpc_release(xv) 38 | 39 | if arr2, ok := v.(Array); !ok { 40 | t.Errorf("not an array: %#v\n", v) 41 | } else if len(arr) != len(arr2) { 42 | t.Errorf("expected %#v got %#v\n", arr, arr2) 43 | } else { 44 | for i := range arr { 45 | if arr[i] != arr2[i] { 46 | t.Errorf("expected array[%d]: %#v got %#v\n", i, arr[i], arr2[i]) 47 | } 48 | } 49 | } 50 | } 51 | 52 | func TestConvertSliceUUID(t *testing.T) { 53 | arr := []UUID{MakeUUID("0000000000000000"), MakeUUID("1111111111111111"), MakeUUID("2222222222222222")} 54 | 55 | xv := goToXpc(arr) 56 | v := xpcToGo(xv) 57 | 58 | xpc_release(xv) 59 | 60 | if arr2, ok := v.(Array); !ok { 61 | t.Errorf("not an array: %#v\n", v) 62 | } else if len(arr) != len(arr2) { 63 | t.Errorf("expected %#v got %#v\n", arr, arr2) 64 | } else { 65 | for i := range arr { 66 | uuid1 := CheckUUID(t, arr[i]) 67 | uuid2 := CheckUUID(t, arr2[i]) 68 | 69 | if uuid1 != uuid2 { 70 | t.Errorf("expected array[%d]: %#v got %#v\n", i, arr[i], arr2[i]) 71 | } 72 | } 73 | } 74 | } 75 | 76 | func TestConvertMap(t *testing.T) { 77 | d := Dict{ 78 | "number": int64(42), 79 | "text": "hello gopher", 80 | "uuid": MakeUUID("aabbccddeeff00112233445566778899"), 81 | } 82 | 83 | xv := goToXpc(d) 84 | v := xpcToGo(xv) 85 | 86 | xpc_release(xv) 87 | 88 | if d2, ok := v.(Dict); !ok { 89 | t.Errorf("not a map: %#v", v) 90 | } else if len(d) != len(d2) { 91 | t.Errorf("expected %#v got %#v\n", d, d2) 92 | } else { 93 | fail := false 94 | 95 | for k, v := range d { 96 | if v != d2[k] { 97 | t.Logf("expected map[%s]: %#v got %#v\n", k, v, d2[k]) 98 | fail = true 99 | } 100 | } 101 | 102 | if fail { 103 | t.Error("test failed") 104 | } 105 | } 106 | } 107 | 108 | func TestUname(t *testing.T) { 109 | var uname Utsname 110 | 111 | if err := Uname(&uname); err != nil { 112 | t.Errorf("Uname error %v", err) 113 | } else { 114 | t.Logf("Uname: %#v", uname) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /xpc/xpc_wrapper.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "_cgo_export.h" 9 | 10 | // 11 | // types and errors are implemented as macros 12 | // create some real objects to make them accessible to Go 13 | // 14 | xpc_type_t TYPE_ERROR = XPC_TYPE_ERROR; 15 | 16 | xpc_type_t TYPE_ARRAY = XPC_TYPE_ARRAY; 17 | xpc_type_t TYPE_DATA = XPC_TYPE_DATA; 18 | xpc_type_t TYPE_DICT = XPC_TYPE_DICTIONARY; 19 | xpc_type_t TYPE_INT64 = XPC_TYPE_INT64; 20 | xpc_type_t TYPE_STRING = XPC_TYPE_STRING; 21 | xpc_type_t TYPE_UUID = XPC_TYPE_UUID; 22 | 23 | xpc_object_t ERROR_CONNECTION_INVALID = (xpc_object_t) XPC_ERROR_CONNECTION_INVALID; 24 | xpc_object_t ERROR_CONNECTION_INTERRUPTED = (xpc_object_t) XPC_ERROR_CONNECTION_INTERRUPTED; 25 | xpc_object_t ERROR_CONNECTION_TERMINATED = (xpc_object_t) XPC_ERROR_TERMINATION_IMMINENT; 26 | 27 | const ptr_to_uuid_t ptr_to_uuid(void *p) { return (ptr_to_uuid_t)p; } 28 | 29 | 30 | // 31 | // connect to XPC service 32 | // 33 | xpc_connection_t XpcConnect(char *service, uintptr_t ctx) { 34 | dispatch_queue_t queue = dispatch_queue_create(service, 0); 35 | xpc_connection_t conn = xpc_connection_create_mach_service(service, queue, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED); 36 | 37 | // making a local copy, that should be made "persistent" with the following Block_copy 38 | //GoInterface ictx = *((GoInterface*)ctx); 39 | 40 | xpc_connection_set_event_handler(conn, 41 | Block_copy(^(xpc_object_t event) { 42 | handleXpcEvent(event, ctx); //(void *)&ictx); 43 | }) 44 | ); 45 | 46 | xpc_connection_resume(conn); 47 | return conn; 48 | } 49 | 50 | void XpcSendMessage(xpc_connection_t conn, xpc_object_t message, bool release, bool reportDelivery) { 51 | xpc_connection_send_message(conn, message); 52 | xpc_connection_send_barrier(conn, ^{ 53 | // Block is invoked on connection's target queue 54 | // when 'message' has been sent. 55 | if (reportDelivery) { // maybe this could be a callback 56 | puts("message delivered"); 57 | } 58 | }); 59 | if (release) { 60 | xpc_release(message); 61 | } 62 | } 63 | 64 | void XpcArrayApply(uintptr_t v, xpc_object_t arr) { 65 | xpc_array_apply(arr, ^bool(size_t index, xpc_object_t value) { 66 | arraySet(v, index, value); 67 | return true; 68 | }); 69 | } 70 | 71 | void XpcDictApply(uintptr_t v, xpc_object_t dict) { 72 | xpc_dictionary_apply(dict, ^bool(const char *key, xpc_object_t value) { 73 | dictSet(v, (char *)key, value); 74 | return true; 75 | }); 76 | } 77 | 78 | void XpcUUIDGetBytes(void *v, xpc_object_t uuid) { 79 | const uint8_t *src = xpc_uuid_get_bytes(uuid); 80 | uint8_t *dest = (uint8_t *)v; 81 | 82 | for (int i=0; i < sizeof(uuid_t); i++) { 83 | dest[i] = src[i]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /xpc/xpc_wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef _XPC_WRAPPER_H_ 2 | #define _XPC_WRAPPER_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | extern xpc_type_t TYPE_ERROR; 10 | 11 | extern xpc_type_t TYPE_ARRAY; 12 | extern xpc_type_t TYPE_DATA; 13 | extern xpc_type_t TYPE_DICT; 14 | extern xpc_type_t TYPE_INT64; 15 | extern xpc_type_t TYPE_STRING; 16 | extern xpc_type_t TYPE_UUID; 17 | 18 | extern xpc_object_t ERROR_CONNECTION_INVALID; 19 | extern xpc_object_t ERROR_CONNECTION_INTERRUPTED; 20 | extern xpc_object_t ERROR_CONNECTION_TERMINATED; 21 | 22 | extern xpc_connection_t XpcConnect(char *, uintptr_t); 23 | extern void XpcSendMessage(xpc_connection_t, xpc_object_t, bool, bool); 24 | extern void XpcArrayApply(uintptr_t, xpc_object_t); 25 | extern void XpcDictApply(uintptr_t, xpc_object_t); 26 | extern void XpcUUIDGetBytes(void *, xpc_object_t); 27 | 28 | // the input type for xpc_uuid_create should be uuid_t but CGO instists on unsigned char * 29 | // typedef uuid_t * ptr_to_uuid_t; 30 | typedef unsigned char * ptr_to_uuid_t; 31 | extern const ptr_to_uuid_t ptr_to_uuid(void *p); 32 | 33 | #endif 34 | --------------------------------------------------------------------------------