├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── _examples ├── cmd │ ├── agent.go │ ├── beacon.go │ ├── btmgmt.go │ ├── discovery.go │ ├── err.go │ ├── hci.go │ ├── obexPush.go │ ├── root.go │ ├── sensortagInfo.go │ ├── sensortagTemperature.go │ └── service.go ├── go.mod ├── go.sum └── main.go ├── api ├── advertisement.go ├── api.go ├── api_test.go ├── beacon │ ├── beacon.go │ ├── beacon_cast.go │ ├── beacon_create.go │ ├── beacon_create_test.go │ ├── beacon_eddystone.go │ ├── beacon_eddystone_test.go │ ├── beacon_expose.go │ ├── beacon_ibeacon.go │ └── beacon_ibeacon_test.go ├── discover.go ├── discover_test.go ├── object_manager_service.go ├── properties_service.go ├── properties_service_test.go ├── service │ ├── agent.go │ ├── agent_test.go │ ├── app.go │ ├── app_advertise.go │ ├── app_char.go │ ├── app_char_rw.go │ ├── app_descr.go │ ├── app_descr_rw.go │ ├── app_getter.go │ ├── app_service.go │ ├── app_service_mgm.go │ ├── app_test.go │ └── gatt.go └── service_expose.go ├── bin └── docker-btmgmt ├── bluez-5.50.json ├── bluez-5.53.json ├── bluez-5.54.json ├── bluez-5.55.json ├── bluez-5.60.json ├── bluez-5.62.json ├── bluez-5.64.json ├── bluez-5.65.json ├── bluez ├── bluez.go ├── client.go ├── dbus.go ├── events.go ├── introspect.go ├── object_manager.go ├── profile │ ├── adapter │ │ ├── Adapter1_methods.go │ │ ├── adapter.go │ │ ├── adapter_devices.go │ │ ├── adapter_devices_test.go │ │ ├── adapter_discovery.go │ │ ├── adapter_discovery_test.go │ │ ├── adapter_filter.go │ │ ├── adapter_filter_test.go │ │ ├── adapter_gatt.go │ │ ├── adapter_gatt_test.go │ │ ├── adapter_test.go │ │ ├── gen_Adapter1.go │ │ └── gen_adapter.go │ ├── admin_policy │ │ ├── gen_AdminPolicySet1.go │ │ ├── gen_AdminPolicyStatus1.go │ │ └── gen_admin_policy.go │ ├── advertisement_monitor │ │ ├── advertisement_monitor.go │ │ ├── gen_AdvertisementMonitor1.go │ │ ├── gen_AdvertisementMonitorManager1.go │ │ └── gen_advertisement_monitor.go │ ├── advertising │ │ ├── advertising.go │ │ ├── gen_LEAdvertisement1.go │ │ ├── gen_LEAdvertisingManager1.go │ │ └── gen_advertising.go │ ├── agent │ │ ├── agent.go │ │ ├── agent_simple.go │ │ ├── gen_Agent1.go │ │ ├── gen_AgentManager1.go │ │ └── gen_agent.go │ ├── battery │ │ ├── gen_Battery1.go │ │ ├── gen_BatteryProvider1.go │ │ ├── gen_BatteryProviderManager1.go │ │ └── gen_battery.go │ ├── device │ │ ├── device.go │ │ ├── gen_Device1.go │ │ ├── gen_device.go │ │ └── types.go │ ├── gatt │ │ ├── gatt.go │ │ ├── gen_GattCharacteristic1.go │ │ ├── gen_GattDescriptor1.go │ │ ├── gen_GattManager1.go │ │ ├── gen_GattProfile1.go │ │ ├── gen_GattService1.go │ │ └── gen_gatt.go │ ├── gen_errors.go │ ├── gen_version.go │ ├── health │ │ ├── gen_HealthChannel1.go │ │ ├── gen_HealthDevice1.go │ │ ├── gen_HealthManager1.go │ │ └── gen_health.go │ ├── input │ │ ├── gen_Input1.go │ │ └── gen_input.go │ ├── media │ │ ├── gen_Media1.go │ │ ├── gen_MediaControl1.go │ │ ├── gen_MediaEndpoint1.go │ │ ├── gen_MediaFolder1.go │ │ ├── gen_MediaItem1.go │ │ ├── gen_MediaPlayer1.go │ │ ├── gen_MediaTransport1.go │ │ ├── gen_media.go │ │ └── types.go │ ├── mesh │ │ ├── gen_Application1.go │ │ ├── gen_Attention1.go │ │ ├── gen_Element1.go │ │ ├── gen_Management1.go │ │ ├── gen_Network1.go │ │ ├── gen_Node1.go │ │ ├── gen_ProvisionAgent1.go │ │ ├── gen_Provisioner1.go │ │ ├── gen_mesh.go │ │ └── types.go │ ├── network │ │ ├── gen_Network1.go │ │ ├── gen_NetworkServer1.go │ │ └── gen_network.go │ ├── obex │ │ ├── Client1.go │ │ ├── Client1_test.go │ │ ├── ObjectPush1.go │ │ ├── Session1.go │ │ ├── Transfer1.go │ │ ├── gen_FileTransfer.go │ │ ├── gen_Message1.go │ │ ├── gen_MessageAccess1.go │ │ ├── gen_PhonebookAccess1.go │ │ ├── gen_Synchronization1.go │ │ ├── gen_obex.go │ │ └── types.go │ ├── obex_agent │ │ ├── gen_Agent1.go │ │ ├── gen_AgentManager1.go │ │ └── gen_obex_agent.go │ ├── profile.go │ ├── profile │ │ ├── gen_Profile1.go │ │ ├── gen_ProfileManager1.go │ │ └── gen_profile.go │ ├── sap │ │ ├── gen_SimAccess1.go │ │ └── gen_sap.go │ └── thermometer │ │ ├── gen_Thermometer1.go │ │ ├── gen_ThermometerManager1.go │ │ ├── gen_ThermometerWatcher1.go │ │ └── gen_thermometer.go └── props.go ├── devices └── sensortag │ ├── barometric.go │ ├── calc.go │ ├── humidity.go │ ├── luxometer.go │ ├── mpu.go │ ├── sensortag.go │ ├── st.go │ └── temperature.go ├── docs └── index.md ├── env └── bluez │ ├── Dockerfile │ └── entrypoint.sh ├── examples ├── agent │ └── agent.go ├── beacon │ └── beacon.go ├── btmgmt │ └── btmgmt.go ├── discovery │ └── discovery.go ├── hci_updown │ └── hci_updown.go ├── obex_push │ └── obex_push.go ├── sensortag_info │ └── sensortag_info.go ├── sensortag_temperature │ └── sensortag_temperature.go └── service │ ├── client.go │ ├── main.go │ └── server.go ├── gen ├── Makefile ├── README.md ├── bluez_api.go ├── filters │ └── filter.go ├── generator │ ├── generator.go │ ├── generator_test.go │ ├── generator_tpl.go │ ├── generator_tpl_api.go │ ├── generator_tpl_constr.go │ ├── generator_tpl_test.go │ ├── generator_types.go │ ├── generator_util.go │ ├── generator_version.go │ └── tpl │ │ ├── api.go.tpl │ │ ├── errors.go.tpl │ │ ├── interfaces.go.tpl │ │ └── root.go.tpl ├── override │ ├── constructor.go │ ├── expose_props.go │ ├── filename.go │ ├── properties.go │ └── types.go ├── parser.go ├── parser │ ├── api.go │ ├── api_group.go │ ├── api_methods.go │ ├── api_properties.go │ ├── api_signals.go │ ├── method.go │ └── property.go ├── parser_test.go ├── srcgen │ └── main.go ├── types │ ├── generator.go │ └── parser.go └── util │ └── util.go ├── go.mod ├── go.sum ├── gopher.png ├── hw ├── hw.go └── linux │ ├── btmgmt │ ├── btmgmt.go │ └── btmgmt_test.go │ ├── cmd │ └── cmd.go │ ├── hci │ ├── hci.go │ └── hci_test.go │ ├── hciconfig │ ├── hciconfig.go │ └── hciconfig_test.go │ ├── hcitool │ ├── hcitool.go │ └── hcitool_test.go │ ├── linux.go │ ├── linux_test.go │ └── rfkill │ ├── rfkill.go │ ├── rfkill_test.go │ └── switch.go ├── props ├── props.go └── to_map.go ├── tests └── watch_properties_routines_num.go └── util ├── map_struct.go └── map_struct_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: muka # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.code-workspace 2 | .vscode 3 | # /bluez/profile/*/gen_* 4 | # /bluez/profile/gen_* 5 | 6 | /go-bluetooth 7 | logs/*.log 8 | debug 9 | 10 | /vendor 11 | /test 12 | /src/gen* 13 | 14 | 15 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 16 | *.o 17 | *.a 18 | *.so 19 | 20 | # Folders 21 | _obj 22 | _test 23 | 24 | # Architecture specific extensions/prefixes 25 | *.[568vq] 26 | [568vq].out 27 | 28 | *.cgo1.go 29 | *.cgo2.c 30 | _cgo_defun.c 31 | _cgo_gotypes.go 32 | _cgo_export.* 33 | 34 | _testmain.go 35 | 36 | *.exe 37 | *.test 38 | *.prof 39 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/bluez"] 2 | path = src/bluez 3 | url = https://git.kernel.org/pub/scm/bluetooth/bluez.git 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at luca.capra+github@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: gen 3 | 4 | BLUEZ_VERSION ?= 5.60 5 | FILTER ?= 6 | 7 | DOCKER_PARAMS := --privileged -it --rm \ 8 | --net=host \ 9 | -v /dev:/dev \ 10 | -v /var/run/dbus:/var/run/dbus \ 11 | -v /sys/class/bluetooth:/sys/class/bluetooth \ 12 | -v /var/lib/bluetooth:/var/lib/bluetooth \ 13 | opny/bluez-${BLUEZ_VERSION} 14 | 15 | all: bluez/checkout gen/clean gen/run 16 | 17 | bluez/init: 18 | git submodule init 19 | git submodule update 20 | 21 | bluez/checkout: 22 | cd src/bluez && git fetch && git checkout ${BLUEZ_VERSION} 23 | 24 | service/bluetoothd/logs: 25 | journalctl -u bluetooth -f 26 | 27 | service/bluetoothd/start: service/bluetoothd/stop 28 | sudo bluetoothd -E -d -n -P hostname 29 | 30 | service/bluetoothd/stop: 31 | sudo killall bluetoothd || true 32 | 33 | gen/clean: 34 | rm -f `ls bluez/profile/*/gen_* -1` || true 35 | rm -f `ls bluez/profile/gen_* -1` || true 36 | 37 | gen/run: bluez/checkout 38 | BLUEZ_VERSION=${BLUEZ_VERSION} FILTER=${FILTER} go run gen/srcgen/main.go full 39 | 40 | gen: gen/run 41 | 42 | build: gen 43 | CGO_ENABLED=0 go build -o go-bluetooth ./main.go 44 | 45 | dev/kill: 46 | ssh ${DEV_HOST} "killall go-bluetooth" || true 47 | 48 | docker/bluetoothd/init: 49 | sudo addgroup bluetooth || true 50 | sudo adduser `id -nu` bluetooth || true 51 | sudo ln -s `pwd`/src/bluetooth.conf /etc/dbus-1/system.d/ 52 | 53 | docker/service/setup: 54 | ./bin/btmgmt power off 55 | ./bin/btmgmt le on 56 | ./bin/btmgmt bredr off 57 | ./bin/btmgmt power on 58 | 59 | docker/btmgmt: 60 | ./bin/btmgmt 61 | 62 | docker/bluetoothd/build: 63 | docker build ./env/bluez --build-arg BLUEZ_VERSION=${BLUEZ_VERSION} -t opny/bluez-${BLUEZ_VERSION} 64 | 65 | docker/bluetoothd/push: 66 | docker push opny/bluez-${BLUEZ_VERSION} 67 | 68 | docker/bluetoothd/run: service/bluetoothd/stop 69 | docker run --name bluez_bluetoothd \ 70 | ${DOCKER_PARAMS} 71 | 72 | bluez-5.50/gen: 73 | BLUEZ_VERSION=5.50 make gen/clean gen 74 | 75 | bluez-5.53/gen: 76 | BLUEZ_VERSION=5.53 make gen/clean gen 77 | 78 | bluez-5.54/gen: 79 | BLUEZ_VERSION=5.54 make gen/clean gen 80 | 81 | bluez-5.55/gen: 82 | BLUEZ_VERSION=5.55 make gen/clean gen 83 | 84 | bluez-5.62/gen: 85 | BLUEZ_VERSION=5.62 make gen/clean gen 86 | 87 | bluez-5.60/gen: 88 | BLUEZ_VERSION=5.60 make gen/clean gen 89 | 90 | bluez-5.64/gen: 91 | BLUEZ_VERSION=5.64 make gen/clean gen 92 | 93 | bluez-5.65/gen: 94 | BLUEZ_VERSION=5.65 make gen/clean gen 95 | -------------------------------------------------------------------------------- /_examples/cmd/agent.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 luca capra 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | agent_example "github.com/muka/go-bluetooth/examples/agent" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | // agentCmd represents the agent command 23 | var agentCmd = &cobra.Command{ 24 | Use: "agent", 25 | Short: "A bluez Agent1 example", 26 | Long: `An example of agent interaction to exchange a passkey during pairing`, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | 29 | // id, err := cmd.Flags().GetString("id") 30 | // if err != nil { 31 | // fail(err) 32 | // } 33 | 34 | adapterID, err := cmd.Flags().GetString("adapterID") 35 | if err != nil { 36 | fail(err) 37 | } 38 | 39 | if len(args) == 0 { 40 | failArg("Device mac") 41 | } 42 | id := args[0] 43 | 44 | fail(agent_example.Run(id, adapterID)) 45 | }, 46 | } 47 | 48 | func init() { 49 | rootCmd.AddCommand(agentCmd) 50 | 51 | agentCmd.Flags().String("id", "", "Help message for toggle") 52 | } 53 | -------------------------------------------------------------------------------- /_examples/cmd/beacon.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | beacon_example "github.com/muka/go-bluetooth/examples/beacon" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // beaconCmd represents the beacon command 22 | var beaconCmd = &cobra.Command{ 23 | Use: "beacon", 24 | Short: "Advertising example", 25 | Long: ``, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | 28 | adapterID, err := cmd.Flags().GetString("adapterID") 29 | if err != nil { 30 | fail(err) 31 | } 32 | 33 | if len(args) == 0 { 34 | failArg("type: ibeacon or eddystone") 35 | } 36 | 37 | var beaconType string = "URL" 38 | if len(args) == 2 { 39 | beaconType = args[1] 40 | } 41 | 42 | fail(beacon_example.Run(args[0], beaconType, adapterID)) 43 | 44 | }, 45 | } 46 | 47 | func init() { 48 | rootCmd.AddCommand(beaconCmd) 49 | } 50 | -------------------------------------------------------------------------------- /_examples/cmd/btmgmt.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package cmd 14 | 15 | import ( 16 | btmgmt_example "github.com/muka/go-bluetooth/examples/btmgmt" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | // btmgmtCmd represents the btmgmt command 21 | var btmgmtCmd = &cobra.Command{ 22 | Use: "btmgmt", 23 | Short: "btmgmt shell command wrapper example", 24 | Long: ``, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | fail(btmgmt_example.Run()) 27 | }, 28 | } 29 | 30 | func init() { 31 | rootCmd.AddCommand(btmgmtCmd) 32 | } 33 | -------------------------------------------------------------------------------- /_examples/cmd/discovery.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | discovery_example "github.com/muka/go-bluetooth/examples/discovery" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // discoveryCmd represents the discovery command 22 | var discoveryCmd = &cobra.Command{ 23 | Use: "discovery", 24 | Short: "bluetooth discovery example", 25 | Long: ``, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | 28 | adapterID, err := cmd.Flags().GetString("adapterID") 29 | if err != nil { 30 | fail(err) 31 | } 32 | 33 | onlyBeacon, err := cmd.Flags().GetBool("beacon") 34 | if err != nil { 35 | fail(err) 36 | } 37 | 38 | fail(discovery_example.Run(adapterID, onlyBeacon)) 39 | }, 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(discoveryCmd) 44 | discoveryCmd.Flags().BoolP("beacon", "b", false, "Only report beacons") 45 | } 46 | -------------------------------------------------------------------------------- /_examples/cmd/err.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func failArg(arg string) { 11 | failArgs([]string{arg}) 12 | } 13 | 14 | func failArgs(args []string) { 15 | fail(fmt.Errorf("Missing arguments: %s", args)) 16 | } 17 | 18 | func fail(err error) { 19 | if err != nil { 20 | log.Errorf("Error: %s", err) 21 | os.Exit(1) 22 | } 23 | os.Exit(0) 24 | } 25 | -------------------------------------------------------------------------------- /_examples/cmd/hci.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | hci_updown_example "github.com/muka/go-bluetooth/examples/hci_updown" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // hciCmd represents the hci command 22 | var hciCmd = &cobra.Command{ 23 | Use: "hci", 24 | Short: "Example usage of the HCI interface", 25 | Long: ``, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | adapterID, err := cmd.Flags().GetString("adapterID") 28 | if err != nil { 29 | fail(err) 30 | } 31 | fail(hci_updown_example.Run(adapterID)) 32 | }, 33 | } 34 | 35 | func init() { 36 | rootCmd.AddCommand(hciCmd) 37 | } 38 | -------------------------------------------------------------------------------- /_examples/cmd/obexPush.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | obex_push_example "github.com/muka/go-bluetooth/examples/obex_push" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // obexPushCmd represents the obexPush command 22 | var obexPushCmd = &cobra.Command{ 23 | Use: "obex-push", 24 | Short: "Obex push example", 25 | Long: ``, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | 28 | adapterID, err := cmd.Flags().GetString("adapterID") 29 | if err != nil { 30 | fail(err) 31 | } 32 | 33 | if len(args) < 2 { 34 | failArgs([]string{"target_address", "file_path"}) 35 | } 36 | 37 | fail(obex_push_example.Run(args[0], args[1], adapterID)) 38 | }, 39 | } 40 | 41 | func init() { 42 | rootCmd.AddCommand(obexPushCmd) 43 | } 44 | -------------------------------------------------------------------------------- /_examples/cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 NAME HERE 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | log "github.com/sirupsen/logrus" 22 | "github.com/spf13/cobra" 23 | "github.com/spf13/viper" 24 | ) 25 | 26 | // rootCmd represents the base command when called without any subcommands 27 | var rootCmd = &cobra.Command{ 28 | Use: "go-bluetooth", 29 | Short: "Usage examples for the go-bluetooth library", 30 | Long: ``, 31 | // Uncomment the following line if your bare application 32 | // has an action associated with it: 33 | // Run: func(cmd *cobra.Command, args []string) { }, 34 | } 35 | 36 | // Execute adds all child commands to the root command and sets flags appropriately. 37 | // This is called by main.main(). It only needs to happen once to the rootCmd. 38 | func Execute() { 39 | if err := rootCmd.Execute(); err != nil { 40 | fmt.Println(err) 41 | os.Exit(1) 42 | } 43 | } 44 | 45 | func init() { 46 | cobra.OnInitialize(initConfig) 47 | 48 | rootCmd.PersistentFlags().String("adapterID", "hci0", "Set the adapterID to use") 49 | } 50 | 51 | // initConfig reads in config file and ENV variables if set. 52 | func initConfig() { 53 | 54 | viper.SetConfigName("config") 55 | viper.AutomaticEnv() // read in environment variables that match 56 | 57 | // If a config file is found, read it in. 58 | if err := viper.ReadInConfig(); err == nil { 59 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 60 | } 61 | 62 | logLevel := log.DebugLevel 63 | if viper.IsSet("LOG_LEVEL") { 64 | lvl, err := log.ParseLevel(viper.GetString("LOG_LEVEL")) 65 | if err != nil { 66 | panic(err) 67 | } 68 | logLevel = lvl 69 | } 70 | log.SetLevel(logLevel) 71 | 72 | } 73 | -------------------------------------------------------------------------------- /_examples/cmd/sensortagInfo.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | sensortag_info_example "github.com/muka/go-bluetooth/examples/sensortag_info" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // sensortagInfoCmd represents the sensortagInfo command 22 | var sensortagInfoCmd = &cobra.Command{ 23 | Use: "sensortag-info", 24 | Short: "Retrieve TI SensorTag sensors informations", 25 | Long: ``, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | 28 | adapterID, err := cmd.Flags().GetString("adapterID") 29 | if err != nil { 30 | fail(err) 31 | } 32 | 33 | if len(args) < 1 { 34 | failArgs([]string{"sensortag_address"}) 35 | } 36 | 37 | fail(sensortag_info_example.Run(args[0], adapterID)) 38 | }, 39 | } 40 | 41 | func init() { 42 | rootCmd.AddCommand(sensortagInfoCmd) 43 | 44 | // Here you will define your flags and configuration settings. 45 | 46 | // Cobra supports Persistent Flags which will work for this command 47 | // and all subcommands, e.g.: 48 | // sensortagInfoCmd.PersistentFlags().String("foo", "", "A help for foo") 49 | 50 | // Cobra supports local flags which will only run when this command 51 | // is called directly, e.g.: 52 | // sensortagInfoCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 53 | } 54 | -------------------------------------------------------------------------------- /_examples/cmd/sensortagTemperature.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | sensortag_temperature_example "github.com/muka/go-bluetooth/examples/sensortag_temperature" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // sensortagTemperatureCmd represents the sensortagTemperature command 22 | var sensortagTemperatureCmd = &cobra.Command{ 23 | Use: "sensortag-temperature", 24 | Short: "Receives SensorTag temperature updates", 25 | Long: ``, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | 28 | adapterID, err := cmd.Flags().GetString("adapterID") 29 | if err != nil { 30 | fail(err) 31 | } 32 | 33 | if len(args) < 1 { 34 | failArgs([]string{"sensortag_address"}) 35 | } 36 | 37 | fail(sensortag_temperature_example.Run(args[0], adapterID)) 38 | }, 39 | } 40 | 41 | func init() { 42 | rootCmd.AddCommand(sensortagTemperatureCmd) 43 | 44 | // Here you will define your flags and configuration settings. 45 | 46 | // Cobra supports Persistent Flags which will work for this command 47 | // and all subcommands, e.g.: 48 | // sensortagTemperatureCmd.PersistentFlags().String("foo", "", "A help for foo") 49 | 50 | // Cobra supports local flags which will only run when this command 51 | // is called directly, e.g.: 52 | // sensortagTemperatureCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 53 | } 54 | -------------------------------------------------------------------------------- /_examples/cmd/service.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | service_example "github.com/muka/go-bluetooth/examples/service" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | // serviceCmd represents the service command 22 | var serviceCmd = &cobra.Command{ 23 | Use: "service", 24 | Short: "A service / client example", 25 | Long: ``, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | 28 | adapterID, err := cmd.Flags().GetString("adapterID") 29 | if err != nil { 30 | fail(err) 31 | } 32 | 33 | if len(args) < 1 { 34 | failArgs([]string{"mode [server|client]"}) 35 | } 36 | 37 | if args[0] == "client" { 38 | if len(args) < 2 { 39 | failArgs([]string{ 40 | "please specify the adapter HW address that expose the service (eg. using hciconfig)", 41 | }) 42 | } 43 | } else { 44 | args = append(args, "") 45 | } 46 | 47 | fail(service_example.Run(adapterID, args[0], args[1])) 48 | }, 49 | } 50 | 51 | func init() { 52 | rootCmd.AddCommand(serviceCmd) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /_examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muka/go-bluetooth/_examples 2 | 3 | go 1.14 4 | 5 | replace github.com/muka/go-bluetooth v0.0.0 => ../ 6 | 7 | require ( 8 | github.com/muka/go-bluetooth v0.0.0 9 | github.com/sirupsen/logrus v1.6.0 10 | github.com/spf13/cobra v0.0.7 11 | github.com/spf13/viper v1.6.2 12 | ) 13 | -------------------------------------------------------------------------------- /_examples/main.go: -------------------------------------------------------------------------------- 1 | //go:generate go run ./gen/srcgen/main.go 2 | 3 | // Copyright © 2020 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package main 18 | 19 | import "github.com/muka/go-bluetooth/_examples/cmd" 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | -------------------------------------------------------------------------------- /api/advertisement.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/godbus/dbus/v5" 7 | "github.com/muka/go-bluetooth/bluez" 8 | "github.com/muka/go-bluetooth/bluez/profile/advertising" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // const baseAdvertismentPath = "/org/bluez/%s/apps/advertisement%d" 13 | const BaseAdvertismentPath = "/go_bluetooth/%s/advertisement/%d" 14 | 15 | var advertisingCount int = -1 16 | 17 | func nextAdvertismentPath(adapterID string) dbus.ObjectPath { 18 | advertisingCount++ 19 | return dbus.ObjectPath(fmt.Sprintf(BaseAdvertismentPath, adapterID, advertisingCount)) 20 | } 21 | 22 | func decreaseAdvertismentCounter() { 23 | advertisingCount-- 24 | if advertisingCount < -1 { 25 | advertisingCount = -1 26 | } 27 | } 28 | 29 | type Advertisement struct { 30 | path dbus.ObjectPath 31 | objectManager *DBusObjectManager 32 | iprops *DBusProperties 33 | conn *dbus.Conn 34 | props *advertising.LEAdvertisement1Properties 35 | } 36 | 37 | func (a *Advertisement) DBusConn() *dbus.Conn { 38 | return a.conn 39 | } 40 | 41 | func (a *Advertisement) DBusObjectManager() *DBusObjectManager { 42 | return a.objectManager 43 | } 44 | 45 | func (a *Advertisement) DBusProperties() *DBusProperties { 46 | return a.iprops 47 | } 48 | 49 | func (a *Advertisement) GetProperties() bluez.Properties { 50 | return a.props 51 | } 52 | 53 | func (a *Advertisement) Path() dbus.ObjectPath { 54 | return a.path 55 | } 56 | 57 | func (a *Advertisement) Interface() string { 58 | return advertising.LEAdvertisement1Interface 59 | } 60 | 61 | func NewAdvertisement(adapterID string, props *advertising.LEAdvertisement1Properties) (*Advertisement, error) { 62 | 63 | adv := new(Advertisement) 64 | 65 | adv.props = props 66 | adv.path = nextAdvertismentPath(adapterID) 67 | 68 | conn, err := dbus.SystemBus() 69 | if err != nil { 70 | return nil, err 71 | } 72 | adv.conn = conn 73 | 74 | om, err := NewDBusObjectManager(conn) 75 | if err != nil { 76 | return nil, err 77 | } 78 | adv.objectManager = om 79 | 80 | iprops, err := NewDBusProperties(conn) 81 | if err != nil { 82 | return nil, err 83 | } 84 | adv.iprops = iprops 85 | 86 | return adv, nil 87 | } 88 | 89 | // Expose to bluez an advertisment instance via the adapter advertisement manager 90 | func ExposeAdvertisement(adapterID string, props *advertising.LEAdvertisement1Properties, discoverableTimeout uint32) (func(), error) { 91 | 92 | log.Tracef("Retrieving adapter instance %s", adapterID) 93 | a, err := GetAdapter(adapterID) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | adv, err := NewAdvertisement(adapterID, props) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | err = ExposeDBusService(adv) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | log.Debug("Setup adapter") 109 | err = a.SetDiscoverable(true) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | err = a.SetDiscoverableTimeout(discoverableTimeout) 115 | if err != nil { 116 | return nil, err 117 | } 118 | err = a.SetPowered(true) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | log.Trace("Registering LEAdvertisement1 instance") 124 | advManager, err := advertising.NewLEAdvertisingManager1FromAdapterID(adapterID) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | err = advManager.RegisterAdvertisement(adv.Path(), map[string]interface{}{}) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | cancel := func() { 135 | decreaseAdvertismentCounter() 136 | err := advManager.UnregisterAdvertisement(adv.Path()) 137 | if err != nil { 138 | log.Warn(err) 139 | } 140 | err = a.SetProperty("Discoverable", false) 141 | if err != nil { 142 | log.Warn(err) 143 | } 144 | } 145 | 146 | return cancel, nil 147 | } 148 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | // package api wraps an high level API to simplify interaction 2 | package api 3 | 4 | import ( 5 | "github.com/muka/go-bluetooth/bluez" 6 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 7 | ) 8 | 9 | var adapters = map[string]*adapter.Adapter1{} 10 | 11 | //Exit performs a clean exit 12 | func Exit() error { 13 | 14 | for _, a := range adapters { 15 | a.Close() 16 | } 17 | 18 | adapters = map[string]*adapter.Adapter1{} 19 | 20 | return bluez.CloseConnections() 21 | } 22 | 23 | func GetAdapter(adapterID string) (*adapter.Adapter1, error) { 24 | 25 | if _, ok := adapters[adapterID]; ok { 26 | return adapters[adapterID], nil 27 | } 28 | 29 | a, err := adapter.GetAdapter(adapterID) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | adapters[adapterID] = a 35 | 36 | return a, nil 37 | } 38 | 39 | func GetDefaultAdapter() (*adapter.Adapter1, error) { 40 | return GetAdapter(GetDefaultAdapterID()) 41 | } 42 | 43 | func GetDefaultAdapterID() string { 44 | return adapter.GetDefaultAdapterID() 45 | } 46 | -------------------------------------------------------------------------------- /api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetAdapterID(t *testing.T) { 11 | 12 | defaultAdapterID := adapter.GetDefaultAdapterID() 13 | 14 | adapter.SetDefaultAdapterID("foo") 15 | adapterID := GetDefaultAdapterID() 16 | 17 | if adapterID != "foo" { 18 | t.Fatalf("Wrong adapter ID: %s", adapterID) 19 | } 20 | 21 | adapter.SetDefaultAdapterID(defaultAdapterID) 22 | adapterID = GetDefaultAdapterID() 23 | 24 | if adapterID != defaultAdapterID { 25 | t.Fatalf("Wrong adapter ID: %s", adapterID) 26 | } 27 | 28 | } 29 | 30 | func TestGetAdapter(t *testing.T) { 31 | 32 | a1, err := GetDefaultAdapter() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | id := GetDefaultAdapterID() 38 | a2, err := GetAdapter(id) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | assert.Equal(t, a1.Properties.Address, a2.Properties.Address) 44 | 45 | err = Exit() 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /api/beacon/beacon_cast.go: -------------------------------------------------------------------------------- 1 | package beacon 2 | 3 | // source https://github.com/suapapa/go_eddystone/blob/master/cast.go 4 | func fixTofloat32(a uint16) float32 { 5 | if a&0x8000 == 0 { 6 | return float32(a) / 256.0 7 | } 8 | return -(float32(^a) + 1) / 256.0 9 | } 10 | 11 | func bytesToUint16(a []byte) (v uint16) { 12 | _ = a[1] 13 | v = uint16(a[0])<<8 | uint16(a[1]) 14 | return 15 | } 16 | 17 | func byteToInt(a byte) (v int) { 18 | v = int(a) 19 | if v&0x80 != 0 { 20 | v = -((^v + 1) & 0xff) 21 | } 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /api/beacon/beacon_create_test.go: -------------------------------------------------------------------------------- 1 | package beacon 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/muka/go-bluetooth/bluez/profile/device" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCreateIBeacon(t *testing.T) { 11 | 12 | uuid := "AAAABBBBCCCCDDDDAAAABBBBCCCCDDDD" 13 | maj := uint16(32000) 14 | min := uint16(11000) 15 | txPwr := uint16(0xB3) 16 | 17 | b, err := CreateIBeacon(uuid, maj, min, txPwr) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | b.Device = &device.Device1{ 23 | Properties: &device.Device1Properties{}, 24 | } 25 | 26 | b.Device.Properties.ServiceData = b.props.ServiceData 27 | 28 | isBeacon := b.Parse() 29 | assert.True(t, isBeacon) 30 | assert.Equal(t, BeaconTypeIBeacon, string(b.Type)) 31 | 32 | assert.Equal(t, uuid, b.GetIBeacon().ProximityUUID) 33 | assert.Equal(t, maj, b.GetIBeacon().Major) 34 | assert.Equal(t, min, b.GetIBeacon().Minor) 35 | assert.Equal(t, txPwr, b.GetIBeacon().MeasuredPower) 36 | 37 | } 38 | 39 | func TestCreateEddystoneURL(t *testing.T) { 40 | 41 | url := "http://example.com" 42 | b, err := CreateEddystoneURL(url, 99) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | b.Device = &device.Device1{ 48 | Properties: &device.Device1Properties{}, 49 | } 50 | 51 | b.Device.Properties.ManufacturerData = b.props.ManufacturerData 52 | b.Device.Properties.UUIDs = b.props.ServiceUUIDs 53 | 54 | isBeacon := b.Parse() 55 | 56 | assert.True(t, isBeacon) 57 | assert.True(t, b.IsEddystone()) 58 | assert.Equal(t, string(b.Type), string(BeaconTypeEddystone)) 59 | assert.IsType(t, BeaconEddystone{}, b.GetEddystone()) 60 | 61 | assert.Equal(t, url, b.GetEddystone().URL) 62 | } 63 | 64 | func TestCreateEddystoneTLM(t *testing.T) { 65 | 66 | batt := uint16(89) 67 | b, err := CreateEddystoneTLM(batt, 10.0, 10, 10) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | b.Device = &device.Device1{ 73 | Properties: &device.Device1Properties{}, 74 | } 75 | 76 | b.Device.Properties.ManufacturerData = b.props.ManufacturerData 77 | b.Device.Properties.UUIDs = b.props.ServiceUUIDs 78 | 79 | isBeacon := b.Parse() 80 | 81 | assert.True(t, isBeacon) 82 | assert.True(t, b.IsEddystone()) 83 | assert.Equal(t, string(b.Type), string(BeaconTypeEddystone)) 84 | assert.IsType(t, BeaconEddystone{}, b.GetEddystone()) 85 | 86 | assert.Equal(t, batt, b.GetEddystone().TLMBatteryVoltage) 87 | } 88 | 89 | func TestCreateEddystoneUID(t *testing.T) { 90 | 91 | nsUID := "AAAAAAAAAABBBBBBBBBB" 92 | b, err := CreateEddystoneUID(nsUID, "123456123456", 99) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | b.Device = &device.Device1{ 98 | Properties: &device.Device1Properties{}, 99 | } 100 | 101 | b.Device.Properties.ManufacturerData = b.props.ManufacturerData 102 | b.Device.Properties.UUIDs = b.props.ServiceUUIDs 103 | 104 | isBeacon := b.Parse() 105 | 106 | assert.True(t, isBeacon) 107 | assert.True(t, b.IsEddystone()) 108 | assert.Equal(t, string(b.Type), string(BeaconTypeEddystone)) 109 | assert.IsType(t, BeaconEddystone{}, b.GetEddystone()) 110 | 111 | assert.Equal(t, nsUID, b.GetEddystone().UID) 112 | } 113 | -------------------------------------------------------------------------------- /api/beacon/beacon_eddystone_test.go: -------------------------------------------------------------------------------- 1 | package beacon 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/muka/go-bluetooth/bluez/profile/device" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | eddystone "github.com/suapapa/go_eddystone" 11 | ) 12 | 13 | func testNewBeacon(t *testing.T, frame eddystone.Frame) Beacon { 14 | 15 | dev := &device.Device1{ 16 | Properties: &device.Device1Properties{ 17 | Name: "test_eddystone", 18 | // FEAA, full UUID 19 | UUIDs: []string{"0000feaa-0000-1000-8000-00805f9b34fb"}, 20 | ServiceData: map[string]interface{}{ 21 | "0000feaa-0000-1000-8000-00805f9b34fb": []byte(frame), 22 | }, 23 | }, 24 | } 25 | 26 | beacon, err := NewBeacon(dev) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | if frame == nil { 32 | return beacon 33 | } 34 | 35 | isBeacon := beacon.Parse() 36 | 37 | assert.True(t, isBeacon) 38 | assert.True(t, beacon.IsEddystone()) 39 | assert.Equal(t, string(beacon.Type), string(BeaconTypeEddystone)) 40 | assert.IsType(t, BeaconEddystone{}, beacon.GetEddystone()) 41 | 42 | return beacon 43 | } 44 | 45 | func TestParseEddystoneUID(t *testing.T) { 46 | 47 | log.SetLevel(log.DebugLevel) 48 | 49 | uid := "EDD1EBEAC04E5DEFA017" 50 | instanceUid := "0BDB87539B67" 51 | txpower := 120 52 | frame, err := eddystone.MakeUIDFrame( 53 | strings.Replace(uid, "-", "", -1), 54 | strings.Replace(instanceUid, "-", "", -1), 55 | txpower, 56 | ) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | beacon := testNewBeacon(t, frame) 62 | 63 | assert.Equal(t, uid, beacon.GetEddystone().UID) 64 | assert.Equal(t, instanceUid, beacon.GetEddystone().InstanceUID) 65 | assert.Equal(t, txpower, beacon.GetEddystone().CalibratedTxPower) 66 | } 67 | 68 | func TestParseEddystoneTLM(t *testing.T) { 69 | 70 | log.SetLevel(log.DebugLevel) 71 | 72 | var batt uint16 = 1000 73 | var temp float32 = 25 74 | var advCnt uint32 = 10 75 | var secCnt uint32 = 50 76 | frame, err := eddystone.MakeTLMFrame(batt, temp, advCnt, secCnt) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | beacon := testNewBeacon(t, frame) 82 | e := beacon.GetEddystone() 83 | 84 | assert.Equal(t, batt, e.TLMBatteryVoltage) 85 | assert.Equal(t, temp, e.TLMTemperature) 86 | assert.Equal(t, advCnt, e.TLMAdvertisingPDU) 87 | assert.Equal(t, secCnt, e.TLMLastRebootedTime) 88 | 89 | // log.Debugf("%+v", e) 90 | 91 | } 92 | 93 | func TestParseEddystoneURL(t *testing.T) { 94 | 95 | log.SetLevel(log.DebugLevel) 96 | 97 | url := "https://bit.ly/2OCrFK2" 98 | txPwr := 89 99 | frame, err := eddystone.MakeURLFrame(url, txPwr) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | beacon := testNewBeacon(t, frame) 105 | e := beacon.GetEddystone() 106 | 107 | assert.Equal(t, url, e.URL) 108 | assert.Equal(t, txPwr, e.CalibratedTxPower) 109 | 110 | } 111 | -------------------------------------------------------------------------------- /api/beacon/beacon_expose.go: -------------------------------------------------------------------------------- 1 | package beacon 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/api" 5 | "github.com/muka/go-bluetooth/bluez/profile/advertising" 6 | ) 7 | 8 | // Expose the beacon 9 | func (b *Beacon) Expose(adapterID string, timeout uint16) (func(), error) { 10 | 11 | props := b.props 12 | props.Type = advertising.AdvertisementTypeBroadcast 13 | 14 | if b.Name != "" { 15 | props.LocalName = b.Name 16 | } 17 | 18 | b.props.Includes = nil 19 | b.props.ManufacturerData = nil 20 | 21 | // Duration is set to 2sec by default 22 | // Not sure if duration can be mapped to interval. 23 | // props.Duration = 1 24 | props.Timeout = timeout 25 | 26 | cancel, err := api.ExposeAdvertisement(adapterID, props, uint32(timeout)) 27 | 28 | return cancel, err 29 | } 30 | -------------------------------------------------------------------------------- /api/beacon/beacon_ibeacon.go: -------------------------------------------------------------------------------- 1 | package beacon 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "strings" 7 | ) 8 | 9 | type BeaconIBeacon struct { 10 | Type string 11 | ProximityUUID string 12 | Major uint16 13 | Minor uint16 14 | MeasuredPower uint16 15 | } 16 | 17 | // From Apple specifications 18 | // Byte(s) Name Value Notes 19 | // 0 Flags[0] 0x02 See Bluetooth 4.0 Core Specification , Volume 3, Appendix C, 18.1. 20 | // 1 Flags[1] 0x01 See Bluetooth 4.0 Core Specification , Volume 3, Appendix C, 18.1. 21 | // 2 Flags[2] 0x06 See Bluetooth 4.0 Core Specification , Volume 3, Appendix C, 18.1. 22 | // 3 Length 0x1A See Bluetooth 4.0 Core Specification 23 | // 4 Type 0xFF See Bluetooth 4.0 Core Specification 24 | // 5 Company ID[0] 0x4C Must not be used for any purposes not specified by Apple. 25 | // 6 Company ID[1] 0x00 Must not be used for any purposes not specified by Apple. 26 | // ---- Bluez data starts here ---- 27 | // 7 Beacon Type[0] 0x02 Must be set to 0x02 for all Proximity Beacons 28 | // 8 Beacon Type[1] 0x15 Must be set to 0x15 for all Proximity Beacons 29 | // 9-24 Proximity UUID 0xnn..nn See CLBeaconRegion class in iOS Developer Library. Must not be set to all 0s. 30 | // 25-26 Major 0xnnnn See CLBeaconRegion class in iOS Developer Library. 0x0000 = unset. 31 | // 27-28 Minor 0xnnnn See CLBeaconRegion class in iOS Developer Library. 0x0000 = unset. 32 | // 29 Measured Power 0xnn See Measured Power (page 7) 33 | func (b *Beacon) ParseIBeacon(frames []uint8) BeaconIBeacon { 34 | 35 | info := BeaconIBeacon{} 36 | 37 | if frames[7-7] == 0x02 && frames[8-7] == 0x15 { 38 | info.Type = "proximity" 39 | } 40 | 41 | uuid := strings.ToUpper(hex.EncodeToString(frames[9-7 : 25-7])) 42 | info.ProximityUUID = strings.ToUpper(uuid) 43 | 44 | info.Major = binary.BigEndian.Uint16(frames[25-7 : 27-7]) 45 | info.Minor = binary.BigEndian.Uint16(frames[27-7 : 29-7]) 46 | 47 | if len(frames) < 23 { 48 | frames = append(frames, 0xb3) 49 | } 50 | 51 | info.MeasuredPower = uint16(frames[29-7]) 52 | 53 | return info 54 | } 55 | -------------------------------------------------------------------------------- /api/beacon/beacon_ibeacon_test.go: -------------------------------------------------------------------------------- 1 | package beacon 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/muka/go-bluetooth/bluez/profile/device" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestParseIBeacon(t *testing.T) { 12 | 13 | log.SetLevel(log.DebugLevel) 14 | 15 | uuid := "010203040506070809101112131415" 16 | major := uint16(999) 17 | minor := uint16(111) 18 | measuredPower := uint16(80) 19 | 20 | b1, err := CreateIBeacon(uuid, major, minor, measuredPower) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | frames := b1.GetFrames() 26 | 27 | dev := &device.Device1{ 28 | Properties: &device.Device1Properties{ 29 | Name: "test_ibeacon", 30 | ManufacturerData: map[uint16]interface{}{ 31 | appleBit: frames, 32 | }, 33 | }, 34 | } 35 | 36 | beacon, err := NewBeacon(dev) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | isBeacon := beacon.Parse() 42 | 43 | assert.True(t, isBeacon) 44 | assert.True(t, beacon.IsIBeacon()) 45 | assert.Equal(t, string(beacon.Type), string(BeaconTypeIBeacon)) 46 | assert.IsType(t, BeaconIBeacon{}, beacon.GetIBeacon()) 47 | } 48 | 49 | func TestParseInvalidIBeacon(t *testing.T) { 50 | log.SetLevel(log.DebugLevel) 51 | 52 | dev := &device.Device1{ 53 | Properties: &device.Device1Properties{ 54 | Name: "test_ibeacon", 55 | ManufacturerData: map[uint16]interface{}{ 56 | // this is an invalid package 57 | appleBit: []byte{16, 5, 1, 24, 128, 123, 77}, 58 | }, 59 | }, 60 | } 61 | 62 | beacon, err := NewBeacon(dev) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | isBeacon := beacon.Parse() 68 | 69 | assert.False(t, isBeacon) 70 | assert.False(t, beacon.IsIBeacon()) 71 | assert.Equal(t, string(beacon.Type), "") 72 | } 73 | -------------------------------------------------------------------------------- /api/discover.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // Discover start device discovery 9 | func Discover( 10 | a *adapter.Adapter1, filter *adapter.DiscoveryFilter, 11 | ) ( 12 | chan *adapter.DeviceDiscovered, func(), error, 13 | ) { 14 | 15 | err := a.SetPairable(false) 16 | if err != nil { 17 | return nil, nil, err 18 | } 19 | err = a.SetDiscoverable(false) 20 | if err != nil { 21 | return nil, nil, err 22 | } 23 | err = a.SetPowered(true) 24 | if err != nil { 25 | return nil, nil, err 26 | } 27 | 28 | filterMap := make(map[string]interface{}) 29 | if filter != nil { 30 | filterMap = filter.ToMap() 31 | } 32 | err = a.SetDiscoveryFilter(filterMap) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | 37 | err = a.StartDiscovery() 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | 42 | ch, discoveryCancel, err := a.OnDeviceDiscovered() 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | cancel := func() { 48 | err := a.StopDiscovery() 49 | if err != nil { 50 | log.Warnf("Error stopping discovery: %s", err) 51 | } 52 | discoveryCancel() 53 | } 54 | 55 | return ch, cancel, nil 56 | } 57 | -------------------------------------------------------------------------------- /api/discover_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // 4 | // func TestDiscoverDevice(t *testing.T) { 5 | // 6 | // a, err := GetDefaultAdapter() 7 | // if err != nil { 8 | // t.Fatal(err) 9 | // } 10 | // 11 | // discovery, cancel, err := Discover(a, nil) 12 | // if err != nil { 13 | // t.Fatal(err) 14 | // } 15 | // 16 | // defer cancel() 17 | // 18 | // wait := make(chan error) 19 | // 20 | // go func() { 21 | // for dev := range discovery { 22 | // if dev == nil { 23 | // return 24 | // } 25 | // wait <- nil 26 | // } 27 | // }() 28 | // 29 | // go func() { 30 | // sleep := 5 31 | // time.Sleep(time.Duration(sleep) * time.Second) 32 | // log.Debugf("Discovery timeout exceeded (%ds)", sleep) 33 | // wait <- nil 34 | // }() 35 | // 36 | // err = <-wait 37 | // if err != nil { 38 | // t.Fatal(err) 39 | // } 40 | // 41 | // } 42 | -------------------------------------------------------------------------------- /api/object_manager_service.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/godbus/dbus/v5" 7 | "github.com/muka/go-bluetooth/bluez" 8 | "github.com/muka/go-bluetooth/bluez/profile" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // NewDBusObjectManager create a new instance 13 | func NewDBusObjectManager(conn *dbus.Conn) (*DBusObjectManager, error) { 14 | 15 | o := &DBusObjectManager{ 16 | conn: conn, 17 | objects: make(map[dbus.ObjectPath]map[string]bluez.Properties), 18 | } 19 | 20 | return o, nil 21 | } 22 | 23 | // DBusObjectManager interface implementation 24 | type DBusObjectManager struct { 25 | conn *dbus.Conn 26 | objects map[dbus.ObjectPath]map[string]bluez.Properties 27 | } 28 | 29 | // SignalAdded notify of interfaces being added 30 | func (o *DBusObjectManager) SignalAdded(path dbus.ObjectPath) error { 31 | 32 | props, err := o.GetManagedObject(path) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return o.conn.Emit(path, bluez.InterfacesAdded, props) 38 | } 39 | 40 | // SignalRemoved notify of interfaces being removed 41 | func (o *DBusObjectManager) SignalRemoved(path dbus.ObjectPath, ifaces []string) error { 42 | if ifaces == nil { 43 | ifaces = make([]string, 0) 44 | } 45 | return o.conn.Emit(path, bluez.InterfacesRemoved, ifaces) 46 | } 47 | 48 | // GetManagedObject return an up to date view of a single object state 49 | func (o *DBusObjectManager) GetManagedObject(objpath dbus.ObjectPath) (map[string]map[string]dbus.Variant, error) { 50 | props, err := o.GetManagedObjects() 51 | if err != nil { 52 | return nil, err 53 | } 54 | if p, ok := props[objpath]; ok { 55 | return p, nil 56 | } 57 | return nil, errors.New("Object not found") 58 | } 59 | 60 | // GetManagedObjects return an up to date view of the object state 61 | func (o *DBusObjectManager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, *dbus.Error) { 62 | 63 | props := make(map[dbus.ObjectPath]map[string]map[string]dbus.Variant) 64 | for path, ifs := range o.objects { 65 | if _, ok := props[path]; !ok { 66 | props[path] = make(map[string]map[string]dbus.Variant) 67 | } 68 | for i, m := range ifs { 69 | if _, ok := props[path][i]; !ok { 70 | props[path][i] = make(map[string]dbus.Variant) 71 | } 72 | l, err := m.ToMap() 73 | if err != nil { 74 | log.Errorf("Failed to serialize properties: %s", err.Error()) 75 | return nil, &profile.ErrInvalidArguments 76 | } 77 | for k, v := range l { 78 | vrt := dbus.MakeVariant(v) 79 | props[path][i][k] = vrt 80 | } 81 | } 82 | } 83 | log.Tracef("ObjectManager.GetManagedObjects \n %v", props) 84 | return props, nil 85 | } 86 | 87 | //AddObject add an object to the list 88 | func (o *DBusObjectManager) AddObject(path dbus.ObjectPath, val map[string]bluez.Properties) error { 89 | log.Tracef("ObjectManager.AddObject: %s", path) 90 | o.objects[path] = val 91 | return o.SignalAdded(path) 92 | } 93 | 94 | //RemoveObject remove an object from the list 95 | func (o *DBusObjectManager) RemoveObject(path dbus.ObjectPath) error { 96 | log.Tracef("ObjectManager.RemoveObject: %s", path) 97 | if s, ok := o.objects[path]; ok { 98 | delete(o.objects, path) 99 | ifaces := make([]string, len(s)) 100 | for i := range s { 101 | ifaces = append(ifaces, i) 102 | } 103 | return o.SignalRemoved(path, ifaces) 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /api/properties_service.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/fatih/structs" 5 | "github.com/godbus/dbus/v5" 6 | "github.com/godbus/dbus/v5/introspect" 7 | "github.com/godbus/dbus/v5/prop" 8 | "github.com/muka/go-bluetooth/bluez" 9 | "github.com/muka/go-bluetooth/bluez/profile" 10 | "github.com/muka/go-bluetooth/props" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // NewDBusProperties create a new instance 15 | func NewDBusProperties(conn *dbus.Conn) (*DBusProperties, error) { 16 | 17 | o := &DBusProperties{ 18 | conn: conn, 19 | props: make(map[string]bluez.Properties), 20 | propsConfig: make(map[string]map[string]*props.PropInfo), 21 | } 22 | 23 | err := o.parseProperties() 24 | return o, err 25 | } 26 | 27 | // DBus Properties interface implementation 28 | type DBusProperties struct { 29 | conn *dbus.Conn 30 | props map[string]bluez.Properties 31 | propsConfig map[string]map[string]*props.PropInfo 32 | instance *prop.Properties 33 | } 34 | 35 | func (p *DBusProperties) parseProperties() error { 36 | for iface, ifaceVal := range p.props { 37 | if _, ok := p.propsConfig[iface]; !ok { 38 | p.propsConfig[iface] = make(map[string]*props.PropInfo) 39 | } 40 | p.propsConfig[iface] = props.ParseProperties(ifaceVal) 41 | } 42 | return nil 43 | } 44 | 45 | func (p *DBusProperties) onChange(ev *prop.Change) *dbus.Error { 46 | if _, ok := p.propsConfig[ev.Iface]; ok { 47 | if conf, ok := p.propsConfig[ev.Iface][ev.Name]; ok { 48 | if conf.Writable { 49 | log.Debugf("Set %s.%s", ev.Iface, ev.Name) 50 | prop := p.props[ev.Iface] 51 | s := structs.New(prop) 52 | err := s.Field(ev.Name).Set(ev.Value) 53 | if err != nil { 54 | log.Errorf("Failed to set %s.%s: %s", ev.Iface, ev.Name, err.Error()) 55 | return &profile.ErrRejected 56 | } 57 | } 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | //Instance return the props instance 64 | func (p *DBusProperties) Instance() *prop.Properties { 65 | return p.instance 66 | } 67 | 68 | //Introspection return the props instance 69 | func (p *DBusProperties) Introspection(iface string) []introspect.Property { 70 | res := p.instance.Introspection(iface) 71 | // log.Debug("Introspect", res) 72 | return res 73 | } 74 | 75 | //Expose expose the properties interface 76 | func (p *DBusProperties) Expose(path dbus.ObjectPath) { 77 | propsConfig := make(map[string]map[string]*prop.Prop) 78 | for iface1, props1 := range p.propsConfig { 79 | propsConfig[iface1] = make(map[string]*prop.Prop) 80 | for k, v := range props1 { 81 | if v.Skip { 82 | continue 83 | } 84 | propsConfig[iface1][k] = &v.Prop 85 | } 86 | } 87 | 88 | p.instance = prop.New(p.conn, path, propsConfig) 89 | } 90 | 91 | //AddProperties add a property set 92 | func (p *DBusProperties) AddProperties(iface string, props bluez.Properties) error { 93 | p.props[iface] = props 94 | return p.parseProperties() 95 | } 96 | 97 | //RemoveProperties remove a property set 98 | func (p *DBusProperties) RemoveProperties(iface string) { 99 | delete(p.props, iface) 100 | delete(p.propsConfig, iface) 101 | } 102 | -------------------------------------------------------------------------------- /api/properties_service_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/muka/go-bluetooth/bluez" 8 | "github.com/muka/go-bluetooth/props" 9 | "github.com/muka/go-bluetooth/util" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type testStruct struct { 14 | IgnoreFlag bool `dbus:"ignore"` 15 | ToOmit map[string]interface{} `dbus:"omitEmpty,writable"` 16 | Ignored string `dbus:"ignore"` 17 | IgnoredByProperty []string `dbus:"ignore=IgnoreFlag"` 18 | Avail string 19 | } 20 | 21 | func (s testStruct) ToMap() (map[string]interface{}, error) { 22 | m := map[string]interface{}{} 23 | util.StructToMap(s, m) 24 | return m, nil 25 | } 26 | 27 | func (s testStruct) Lock() {} 28 | func (s testStruct) Unlock() {} 29 | 30 | func TestParseTag(t *testing.T) { 31 | 32 | s := testStruct{ 33 | IgnoreFlag: true, 34 | Ignored: "foo", 35 | IgnoredByProperty: []string{"bar"}, 36 | Avail: "foo", 37 | } 38 | 39 | prop := &DBusProperties{ 40 | props: make(map[string]bluez.Properties), 41 | propsConfig: make(map[string]map[string]*props.PropInfo), 42 | } 43 | 44 | err := prop.AddProperties("test", s) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | err = prop.parseProperties() 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | cfg := prop.propsConfig["test"] 55 | 56 | for field, cfg := range cfg { 57 | fmt.Printf("%s: %++v\n", field, cfg) 58 | } 59 | 60 | assert.True(t, cfg["ToOmit"].Skip) 61 | assert.True(t, cfg["ToOmit"].Writable) 62 | assert.True(t, cfg["Ignored"].Skip) 63 | assert.True(t, cfg["IgnoredByProperty"].Skip) 64 | assert.Equal(t, "foo", cfg["Avail"].Value) 65 | 66 | } 67 | -------------------------------------------------------------------------------- /api/service/agent.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/muka/go-bluetooth/bluez/profile/agent" 4 | 5 | func (app *App) createAgent() (agent.Agent1Client, error) { 6 | a := agent.NewDefaultSimpleAgent() 7 | return a, nil 8 | } 9 | 10 | // Expose app agent on DBus 11 | func (app *App) ExposeAgent(caps string, setAsDefaultAgent bool) error { 12 | return agent.ExposeAgent(app.DBusConn(), app.agent, caps, setAsDefaultAgent) 13 | } 14 | -------------------------------------------------------------------------------- /api/service/agent_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // 4 | // func TestAppAgent(t *testing.T) { 5 | // a := createTestApp(t) 6 | // defer a.Close() 7 | // 8 | // ag, err := agent.NewAgent1("org.bluez", "/org/bluez/agent/simple0") 9 | // if err != nil { 10 | // t.Fatal(err) 11 | // } 12 | // 13 | // err = ag.RequestConfirmation("/org/bluez/hci0/app0", 123456) 14 | // if err != nil { 15 | // t.Fatal(err) 16 | // } 17 | // 18 | // } 19 | -------------------------------------------------------------------------------- /api/service/app_advertise.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/api" 5 | "github.com/muka/go-bluetooth/bluez/profile/advertising" 6 | ) 7 | 8 | func (app *App) GetAdvertisement() *advertising.LEAdvertisement1Properties { 9 | return app.advertisement 10 | } 11 | 12 | func (app *App) Advertise(timeout uint32) (func(), error) { 13 | 14 | adv := app.GetAdvertisement() 15 | 16 | for _, svc := range app.GetServices() { 17 | adv.ServiceUUIDs = append(adv.ServiceUUIDs, svc.UUID) 18 | } 19 | 20 | cancel, err := api.ExposeAdvertisement(app.adapterID, adv, timeout) 21 | return cancel, err 22 | } 23 | -------------------------------------------------------------------------------- /api/service/app_descr.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/go-bluetooth/api" 6 | "github.com/muka/go-bluetooth/bluez" 7 | "github.com/muka/go-bluetooth/bluez/profile/gatt" 8 | ) 9 | 10 | type DescrReadCallback func(c *Descr, options map[string]interface{}) ([]byte, error) 11 | type DescrWriteCallback func(c *Descr, value []byte) ([]byte, error) 12 | 13 | type Descr struct { 14 | UUID string 15 | app *App 16 | char *Char 17 | 18 | path dbus.ObjectPath 19 | 20 | Properties *gatt.GattDescriptor1Properties 21 | iprops *api.DBusProperties 22 | 23 | readCallback DescrReadCallback 24 | writeCallback DescrWriteCallback 25 | } 26 | 27 | func (s *Descr) DBusProperties() *api.DBusProperties { 28 | return s.iprops 29 | } 30 | 31 | func (s *Descr) DBusObjectManager() *api.DBusObjectManager { 32 | return s.App().DBusObjectManager() 33 | } 34 | 35 | func (s *Descr) DBusConn() *dbus.Conn { 36 | return s.App().DBusConn() 37 | } 38 | 39 | func (s *Descr) Path() dbus.ObjectPath { 40 | return s.path 41 | } 42 | 43 | func (s *Descr) Char() *Char { 44 | return s.char 45 | } 46 | 47 | func (s *Descr) Interface() string { 48 | return gatt.GattDescriptor1Interface 49 | } 50 | 51 | func (s *Descr) GetProperties() bluez.Properties { 52 | s.Properties.Characteristic = s.char.Path() 53 | return s.Properties 54 | } 55 | 56 | func (s *Descr) App() *App { 57 | return s.app 58 | } 59 | 60 | // Expose descr to dbus 61 | func (s *Descr) Expose() error { 62 | return api.ExposeDBusService(s) 63 | } 64 | 65 | // Remove descr from dbus 66 | func (s *Descr) Remove() error { 67 | return api.RemoveDBusService(s) 68 | } 69 | -------------------------------------------------------------------------------- /api/service/app_descr_rw.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // Set the Read callback, called when a client attempt to read 9 | func (s *Descr) OnRead(fx DescrReadCallback) *Descr { 10 | s.readCallback = fx 11 | return s 12 | } 13 | 14 | // Set the Write callback, called when a client attempt to write 15 | func (s *Descr) OnWrite(fx DescrWriteCallback) *Descr { 16 | s.writeCallback = fx 17 | return s 18 | } 19 | 20 | //ReadValue read a value 21 | func (s *Descr) ReadValue(options map[string]interface{}) ([]byte, *dbus.Error) { 22 | 23 | log.Trace("Descr.ReadValue") 24 | 25 | if s.readCallback != nil { 26 | b, err := s.readCallback(s, options) 27 | if err != nil { 28 | return nil, dbus.MakeFailedError(err) 29 | } 30 | return b, nil 31 | } 32 | 33 | return s.Properties.Value, nil 34 | } 35 | 36 | //WriteValue write a value 37 | func (s *Descr) WriteValue(value []byte, options map[string]interface{}) *dbus.Error { 38 | 39 | log.Trace("Descr.WriteValue") 40 | 41 | val := value 42 | if s.writeCallback != nil { 43 | log.Trace("Used write callback") 44 | b, err := s.writeCallback(s, value) 45 | val = b 46 | if err != nil { 47 | return dbus.MakeFailedError(err) 48 | } 49 | } else { 50 | log.Trace("Store directly to value (no callback)") 51 | } 52 | 53 | // TODO update on Properties interface 54 | s.Properties.Value = val 55 | err := s.iprops.Instance().Set(s.Interface(), "Value", dbus.MakeVariant(value)) 56 | 57 | return err 58 | } 59 | -------------------------------------------------------------------------------- /api/service/app_getter.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/go-bluetooth/api" 6 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 7 | "github.com/muka/go-bluetooth/bluez/profile/advertising" 8 | "github.com/muka/go-bluetooth/bluez/profile/agent" 9 | ) 10 | 11 | func (app *App) AdapterID() string { 12 | return app.adapterID 13 | } 14 | 15 | func (app *App) Adapter() *adapter.Adapter1 { 16 | return app.adapter 17 | } 18 | 19 | func (app *App) Agent() agent.Agent1Client { 20 | return app.agent 21 | } 22 | 23 | // return the app dbus path 24 | func (app *App) Path() dbus.ObjectPath { 25 | return app.path 26 | } 27 | 28 | // return the dbus connection 29 | func (app *App) DBusConn() *dbus.Conn { 30 | return app.conn 31 | } 32 | 33 | func (app *App) DBusObjectManager() *api.DBusObjectManager { 34 | return app.objectManager 35 | } 36 | 37 | func (app *App) SetName(name string) { 38 | app.advertisement.LocalName = name 39 | } 40 | 41 | func (app *App) SetLEAdvertisement(adv *advertising.LEAdvertisement1Properties) { 42 | app.advertisement = adv 43 | } 44 | -------------------------------------------------------------------------------- /api/service/app_service_mgm.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/godbus/dbus/v5" 8 | "github.com/muka/go-bluetooth/api" 9 | "github.com/muka/go-bluetooth/bluez" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func (app *App) GetServices() map[dbus.ObjectPath]*Service { 14 | return app.services 15 | } 16 | 17 | func (app *App) NewService(uuid string) (*Service, error) { 18 | 19 | s := new(Service) 20 | s.UUID = app.GenerateUUID(uuid) 21 | 22 | s.app = app 23 | s.chars = make(map[dbus.ObjectPath]*Char) 24 | s.path = dbus.ObjectPath(fmt.Sprintf("%s/service%s", app.Path(), strings.Replace(s.UUID, "-", "_", -1)[:8])) 25 | s.Properties = NewGattService1Properties(s.UUID) 26 | 27 | iprops, err := api.NewDBusProperties(s.App().DBusConn()) 28 | if err != nil { 29 | return nil, err 30 | } 31 | s.iprops = iprops 32 | 33 | return s, nil 34 | } 35 | 36 | func (app *App) AddService(s *Service) error { 37 | 38 | app.services[s.Path()] = s 39 | 40 | err := s.Expose() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | err = app.DBusObjectManager().AddObject(s.Path(), map[string]bluez.Properties{ 46 | s.Interface(): s.GetProperties(), 47 | }) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | log.Tracef("Added GATT Service UUID=%s %s", s.UUID, s.Path()) 53 | 54 | return nil 55 | } 56 | 57 | //RemoveService remove an exposed service 58 | func (app *App) RemoveService(service *Service) error { 59 | if _, ok := app.services[service.Path()]; !ok { 60 | return nil 61 | } 62 | 63 | for _, char := range service.GetChars() { 64 | err := service.RemoveChar(char) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | 70 | err := api.RemoveDBusService(service) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | delete(app.services, service.Path()) 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /api/service/app_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/muka/go-bluetooth/api" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func createTestApp(t *testing.T) *App { 11 | 12 | log.SetLevel(log.TraceLevel) 13 | 14 | a, err := NewApp(AppOptions{ 15 | AdapterID: api.GetDefaultAdapterID(), 16 | }) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | s1, err := a.NewService("2233") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | c1, err := s1.NewChar("3344") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | c1. 32 | OnRead(CharReadCallback(func(c *Char, options map[string]interface{}) ([]byte, error) { 33 | return nil, nil 34 | })). 35 | OnWrite(CharWriteCallback(func(c *Char, value []byte) ([]byte, error) { 36 | return nil, nil 37 | })) 38 | 39 | d1, err := c1.NewDescr("4455") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | err = c1.AddDescr(d1) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | err = s1.AddChar(c1) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | err = a.AddService(s1) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | err = a.Run() 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | return a 65 | } 66 | 67 | func TestApp(t *testing.T) { 68 | a := createTestApp(t) 69 | defer a.Close() 70 | } 71 | -------------------------------------------------------------------------------- /api/service/gatt.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/bluez/profile/gatt" 5 | ) 6 | 7 | // Create a new GattService1Properties 8 | func NewGattService1Properties(uuid string) *gatt.GattService1Properties { 9 | return &gatt.GattService1Properties{ 10 | IsService: true, 11 | Primary: true, 12 | UUID: uuid, 13 | } 14 | } 15 | 16 | // Create a new GattCharacteristic1Properties 17 | func NewGattCharacteristic1Properties(uuid string) *gatt.GattCharacteristic1Properties { 18 | return &gatt.GattCharacteristic1Properties{ 19 | UUID: uuid, 20 | Flags: []string{}, 21 | } 22 | } 23 | 24 | // Create a new GattDescriptor1Properties 25 | func NewGattDescriptor1Properties(uuid string) *gatt.GattDescriptor1Properties { 26 | return &gatt.GattDescriptor1Properties{ 27 | UUID: uuid, 28 | Flags: []string{ 29 | gatt.FlagDescriptorRead, 30 | gatt.FlagDescriptorWrite, 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /api/service_expose.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/godbus/dbus/v5/introspect" 6 | "github.com/godbus/dbus/v5/prop" 7 | "github.com/muka/go-bluetooth/bluez" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type ExposedDBusService interface { 12 | Path() dbus.ObjectPath 13 | Interface() string 14 | GetProperties() bluez.Properties 15 | // App() *App 16 | DBusProperties() *DBusProperties 17 | DBusObjectManager() *DBusObjectManager 18 | DBusConn() *dbus.Conn 19 | // ExportTree() error 20 | } 21 | 22 | func RemoveDBusService(s ExposedDBusService) error { 23 | 24 | err := s.DBusObjectManager().RemoveObject(s.Path()) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | 32 | // Expose 33 | // - Interface() (Service, Char or Descr) 34 | // - Properties interface 35 | func ExposeDBusService(s ExposedDBusService) (err error) { 36 | 37 | conn := s.DBusConn() 38 | 39 | if conn == nil { 40 | conn, err = dbus.SystemBus() 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | 46 | log.Tracef("Expose %s (%s)", s.Path(), s.Interface()) 47 | err = conn.Export(s, s.Path(), s.Interface()) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | err = s.DBusProperties().AddProperties(s.Interface(), s.GetProperties()) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | log.Tracef("Expose Properties interface (%s)", s.Path()) 58 | s.DBusProperties().Expose(s.Path()) 59 | 60 | node := &introspect.Node{ 61 | Interfaces: []introspect.Interface{ 62 | //Introspect 63 | introspect.IntrospectData, 64 | //Properties 65 | prop.IntrospectData, 66 | // Exposed service introspectable 67 | { 68 | Name: s.Interface(), 69 | Methods: introspect.Methods(s), 70 | Properties: s.DBusProperties().Introspection(s.Interface()), 71 | }, 72 | }, 73 | } 74 | 75 | err = conn.Export( 76 | introspect.NewIntrospectable(node), 77 | s.Path(), 78 | "org.freedesktop.DBus.Introspectable", 79 | ) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // v, _ := intrsp.Introspect() 85 | // fmt.Println("service_expose introspection", v) 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /bin/docker-btmgmt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker run -it --name bluez_btmgmt --privileged --rm --net=host -v /dev:/dev -v /var/run/dbus:/var/run/dbus -v /sys/class/bluetooth:/sys/class/bluetooth -v /var/lib/bluetooth:/var/lib/bluetooth opny/bluez-5.54 /bluez/tools/btmgmt $@ 3 | 4 | -------------------------------------------------------------------------------- /bluez/bluez.go: -------------------------------------------------------------------------------- 1 | package bluez 2 | 3 | import "github.com/godbus/dbus/v5/introspect" 4 | 5 | const ( 6 | OrgBluezPath = "/org/bluez" 7 | OrgBluezInterface = "org.bluez" 8 | 9 | //ObjectManagerInterface the dbus object manager interface 10 | ObjectManagerInterface = "org.freedesktop.DBus.ObjectManager" 11 | //InterfacesRemoved the DBus signal member for InterfacesRemoved 12 | InterfacesRemoved = "org.freedesktop.DBus.ObjectManager.InterfacesRemoved" 13 | //InterfacesAdded the DBus signal member for InterfacesAdded 14 | InterfacesAdded = "org.freedesktop.DBus.ObjectManager.InterfacesAdded" 15 | 16 | //PropertiesInterface the DBus properties interface 17 | PropertiesInterface = "org.freedesktop.DBus.Properties" 18 | //PropertiesChanged the DBus properties interface and member 19 | PropertiesChanged = "org.freedesktop.DBus.Properties.PropertiesChanged" 20 | 21 | // Introspectable introspectable interface 22 | Introspectable = "org.freedesktop.DBus.Introspectable" 23 | ) 24 | 25 | // ObjectManagerIntrospectDataString introspect ObjectManager description 26 | const ObjectManagerIntrospectDataString = ` 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ` 40 | 41 | // ObjectManagerIntrospectData introspect ObjectManager description 42 | var ObjectManagerIntrospectData = introspect.Interface{ 43 | Name: "org.freedesktop.DBus.ObjectManager", 44 | Methods: []introspect.Method{ 45 | { 46 | Name: "GetManagedObjects", 47 | Args: []introspect.Arg{ 48 | { 49 | Name: "objects", 50 | Type: "a{oa{sa{sv}}}", 51 | Direction: "out", 52 | }, 53 | }, 54 | }, 55 | }, 56 | Signals: []introspect.Signal{ 57 | { 58 | Name: "InterfacesAdded", 59 | Args: []introspect.Arg{ 60 | { 61 | Name: "object", 62 | Type: "o", 63 | }, 64 | { 65 | Name: "interfaces", 66 | Type: "a{sa{sv}}", 67 | }, 68 | }, 69 | }, 70 | { 71 | Name: "InterfacesRemoved", 72 | Args: []introspect.Arg{ 73 | { 74 | Name: "object", 75 | Type: "o", 76 | }, 77 | { 78 | Name: "interfaces", 79 | Type: "as", 80 | }, 81 | }, 82 | }, 83 | }, 84 | } 85 | -------------------------------------------------------------------------------- /bluez/dbus.go: -------------------------------------------------------------------------------- 1 | package bluez 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/godbus/dbus/v5" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | //Properties dbus serializable struct 11 | // Use struct tags to control how the field is handled by Properties interface 12 | // Example: field `dbus:writable,emit,myCallback` 13 | // See Prop in github.com/godbus/dbus/v5/prop for configuration details 14 | // Options: 15 | // - writable: set the property as writable (Set will updated it). Omit for read-only 16 | // - emit|invalidates: emit PropertyChanged, invalidates emit without disclosing the value. Omit for read-only 17 | // - callback: a callable function in the struct compatible with the signature of Prop.Callback. Omit for no callback 18 | type Properties interface { 19 | ToMap() (map[string]interface{}, error) 20 | Lock() 21 | Unlock() 22 | } 23 | 24 | //BusType a type of DBus connection 25 | type BusType int 26 | 27 | const ( 28 | // SessionBus uses the session bus 29 | SessionBus BusType = iota 30 | // SystemBus uses the system bus 31 | SystemBus 32 | ) 33 | 34 | var conns = make([]*dbus.Conn, 2) 35 | 36 | // Config pass configuration to a DBUS client 37 | type Config struct { 38 | Name string 39 | Iface string 40 | Path dbus.ObjectPath 41 | Bus BusType 42 | } 43 | 44 | // CloseConnections close all open connection to DBus 45 | func CloseConnections() (err error) { 46 | for _, conn := range conns { 47 | if conn != nil { 48 | err = conn.Close() 49 | if err != nil { 50 | log.Warnf("Close: %s", err) 51 | } 52 | } 53 | } 54 | conns = make([]*dbus.Conn, 2) 55 | return err 56 | } 57 | 58 | //GetConnection get a DBus connection 59 | func GetConnection(connType BusType) (*dbus.Conn, error) { 60 | switch connType { 61 | case SystemBus: 62 | if conns[SystemBus] == nil { 63 | // c.logger.Debug("Connecting to SystemBus") 64 | conn, err := dbus.SystemBus() 65 | if err != nil { 66 | return nil, err 67 | } 68 | conns[SystemBus] = conn 69 | } 70 | return conns[SystemBus], nil 71 | case SessionBus: 72 | if conns[SessionBus] == nil { 73 | // c.logger.Debug("Connecting to SessionBus") 74 | conn, err := dbus.SessionBus() 75 | if err != nil { 76 | return nil, err 77 | } 78 | conns[SessionBus] = conn 79 | } 80 | return conns[SessionBus], nil 81 | default: 82 | return nil, errors.New("Unmanged DBus type code") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bluez/events.go: -------------------------------------------------------------------------------- 1 | package bluez 2 | 3 | // PropertyChanged indicates that a change is notified 4 | type PropertyChanged struct { 5 | Interface string 6 | Name string 7 | Value interface{} 8 | } 9 | -------------------------------------------------------------------------------- /bluez/object_manager.go: -------------------------------------------------------------------------------- 1 | package bluez 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | ) 6 | 7 | var objectManager *ObjectManager 8 | 9 | // GetObjectManager return a client instance of the Bluez object manager 10 | func GetObjectManager() (*ObjectManager, error) { 11 | if objectManager != nil { 12 | return objectManager, nil 13 | } 14 | 15 | om, err := NewObjectManager(OrgBluezInterface, "/") 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | objectManager = om 21 | return om, nil 22 | } 23 | 24 | // NewObjectManager create a new ObjectManager client 25 | func NewObjectManager(name string, path string) (*ObjectManager, error) { 26 | om := new(ObjectManager) 27 | om.client = NewClient( 28 | &Config{ 29 | Name: name, 30 | Iface: "org.freedesktop.DBus.ObjectManager", 31 | Path: dbus.ObjectPath(path), 32 | Bus: SystemBus, 33 | }, 34 | ) 35 | return om, nil 36 | } 37 | 38 | // ObjectManager manges the list of all available objects 39 | type ObjectManager struct { 40 | client *Client 41 | } 42 | 43 | // Close the connection 44 | func (o *ObjectManager) Close() { 45 | o.client.Disconnect() 46 | } 47 | 48 | // GetManagedObjects return a list of all available objects registered 49 | func (o *ObjectManager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, error) { 50 | var objs map[dbus.ObjectPath]map[string]map[string]dbus.Variant 51 | err := o.client.Call("GetManagedObjects", 0).Store(&objs) 52 | return objs, err 53 | } 54 | 55 | //Register watch for signal events 56 | func (o *ObjectManager) Register() (chan *dbus.Signal, error) { 57 | path := o.client.Config.Path 58 | iface := o.client.Config.Iface 59 | return o.client.Register(dbus.ObjectPath(path), iface) 60 | } 61 | 62 | //Unregister watch for signal events 63 | func (o *ObjectManager) Unregister(signal chan *dbus.Signal) error { 64 | path := o.client.Config.Path 65 | iface := o.client.Config.Iface 66 | return o.client.Unregister(dbus.ObjectPath(path), iface, signal) 67 | } 68 | 69 | // GetManagedObject return an up to date view of a single object state. 70 | // object is nil if the object path is not found 71 | func (o *ObjectManager) GetManagedObject(objpath dbus.ObjectPath) (map[string]map[string]dbus.Variant, error) { 72 | objects, err := o.GetManagedObjects() 73 | if err != nil { 74 | return nil, err 75 | } 76 | if p, ok := objects[objpath]; ok { 77 | return p, nil 78 | } 79 | // return nil, errors.New("Object not found") 80 | return nil, nil 81 | } 82 | -------------------------------------------------------------------------------- /bluez/profile/adapter/Adapter1_methods.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | // GetAdapterID return the Id of the adapter 4 | func (a *Adapter1) GetAdapterID() (string, error) { 5 | return ParseAdapterID(a.Path()) 6 | } 7 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/godbus/dbus/v5" 8 | "github.com/muka/go-bluetooth/bluez" 9 | "github.com/muka/go-bluetooth/bluez/profile/device" 10 | ) 11 | 12 | var defaultAdapterID = "hci0" 13 | 14 | // SetDefaultAdapterID set the default adapter 15 | func SetDefaultAdapterID(a string) { 16 | defaultAdapterID = a 17 | } 18 | 19 | // GetDefaultAdapterID get the default adapter 20 | func GetDefaultAdapterID() string { 21 | return defaultAdapterID 22 | } 23 | 24 | // ParseAdapterID read the adapterID from an object path in the form /org/bluez/hci[0-9]*[/...] 25 | func ParseAdapterID(path dbus.ObjectPath) (string, error) { 26 | 27 | spath := string(path) 28 | 29 | if !strings.HasPrefix(spath, bluez.OrgBluezPath) { 30 | return "", fmt.Errorf("Failed to parse adapterID from %s", path) 31 | } 32 | 33 | parts := strings.Split(spath[len(bluez.OrgBluezPath)+1:], "/") 34 | adapterID := parts[0] 35 | 36 | if adapterID[:3] != "hci" { 37 | return "", fmt.Errorf("adapterID missing hci* prefix from %s", path) 38 | } 39 | 40 | return adapterID, nil 41 | } 42 | 43 | // AdapterExists checks if an adapter is available 44 | func AdapterExists(adapterID string) (bool, error) { 45 | 46 | om, err := bluez.GetObjectManager() 47 | if err != nil { 48 | return false, err 49 | } 50 | 51 | objects, err := om.GetManagedObjects() 52 | if err != nil { 53 | return false, err 54 | } 55 | 56 | path := dbus.ObjectPath(fmt.Sprintf("%s/%s", bluez.OrgBluezPath, adapterID)) 57 | _, exists := objects[path] 58 | 59 | return exists, nil 60 | } 61 | 62 | func GetDefaultAdapter() (*Adapter1, error) { 63 | return GetAdapter(GetDefaultAdapterID()) 64 | } 65 | 66 | // GetAdapter return an adapter object instance 67 | func GetAdapter(adapterID string) (*Adapter1, error) { 68 | 69 | if exists, err := AdapterExists(adapterID); !exists { 70 | if err != nil { 71 | return nil, fmt.Errorf("AdapterExists: %s", err) 72 | } 73 | return nil, fmt.Errorf("Adapter %s not found", adapterID) 74 | } 75 | 76 | return NewAdapter1FromAdapterID(adapterID) 77 | } 78 | 79 | // GetAdapterFromDevicePath Return an adapter based on a device path 80 | func GetAdapterFromDevicePath(path dbus.ObjectPath) (*Adapter1, error) { 81 | 82 | d, err := device.NewDevice1(path) 83 | if err != nil { 84 | return nil, fmt.Errorf("Failed to load device %s", path) 85 | } 86 | 87 | a, err := NewAdapter1(d.Properties.Adapter) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return a, nil 93 | } 94 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_devices.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/godbus/dbus/v5" 8 | "github.com/muka/go-bluetooth/bluez" 9 | "github.com/muka/go-bluetooth/bluez/profile/device" 10 | "github.com/muka/go-bluetooth/util" 11 | ) 12 | 13 | //GetDeviceByAddress return a Device object based on its address 14 | func (a *Adapter1) GetDeviceByAddress(address string) (*device.Device1, error) { 15 | 16 | list, err := a.GetDeviceList() 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | for _, path := range list { 22 | 23 | dev, err := device.NewDevice1(path) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | if dev.Properties.Address == address { 29 | return dev, nil 30 | } 31 | } 32 | 33 | return nil, nil 34 | } 35 | 36 | //GetDevices returns a list of bluetooth discovered Devices 37 | func (a *Adapter1) GetDevices() ([]*device.Device1, error) { 38 | 39 | list, err := a.GetDeviceList() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | om, err := bluez.GetObjectManager() 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | objects, err := om.GetManagedObjects() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | devices := []*device.Device1{} 55 | 56 | for _, path := range list { 57 | 58 | object, ok := objects[path] 59 | if !ok { 60 | return nil, fmt.Errorf("Path %s does not exists", path) 61 | } 62 | 63 | props := object[device.Device1Interface] 64 | dev, err := parseDevice(path, props) 65 | if err != nil { 66 | return nil, err 67 | } 68 | devices = append(devices, dev) 69 | } 70 | 71 | // log.Debugf("%d cached devices", len(devices)) 72 | return devices, nil 73 | } 74 | 75 | // GetDeviceList returns a list of cached device paths 76 | func (a *Adapter1) GetDeviceList() ([]dbus.ObjectPath, error) { 77 | 78 | om, err := bluez.GetObjectManager() 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | objects, err := om.GetManagedObjects() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | devices := []dbus.ObjectPath{} 89 | for path, ifaces := range objects { 90 | for iface := range ifaces { 91 | switch iface { 92 | case device.Device1Interface: 93 | { 94 | if strings.Contains(string(path), string(a.Path())) { 95 | devices = append(devices, path) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | return devices, nil 103 | } 104 | 105 | // FlushDevices removes device from bluez cache 106 | func (a *Adapter1) FlushDevices() error { 107 | 108 | devices, err := a.GetDevices() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | for _, dev := range devices { 114 | err = a.RemoveDevice(dev.Path()) 115 | if err != nil { 116 | return fmt.Errorf("FlushDevices.RemoveDevice %s: %s", dev.Path(), err) 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | 123 | // ParseDevice parse a Device from a ObjectManager map 124 | func parseDevice(path dbus.ObjectPath, propsMap map[string]dbus.Variant) (*device.Device1, error) { 125 | 126 | dev, err := device.NewDevice1(path) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | err = util.MapToStruct(dev.Properties, propsMap) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return dev, nil 137 | } 138 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_devices_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func getDefaultAdapter(t *testing.T) *Adapter1 { 10 | a, err := GetDefaultAdapter() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | return a 15 | } 16 | 17 | func TestGetDeviceByAddress(t *testing.T) { 18 | a := getDefaultAdapter(t) 19 | list, err := a.GetDeviceByAddress("foobar") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | assert.Empty(t, list) 24 | } 25 | 26 | func TestGetDevices(t *testing.T) { 27 | a := getDefaultAdapter(t) 28 | _, err := a.GetDevices() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | 34 | func TestGetDeviceList(t *testing.T) { 35 | a := getDefaultAdapter(t) 36 | _, err := a.GetDeviceList() 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | 42 | func TestFlushDevices(t *testing.T) { 43 | a := getDefaultAdapter(t) 44 | err := a.FlushDevices() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | list, err := a.GetDevices() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | assert.Zero(t, len(list)) 53 | } 54 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_discovery.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/go-bluetooth/bluez" 6 | "github.com/muka/go-bluetooth/bluez/profile/device" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | const ( 11 | // DeviceRemoved a device has been removed from local cache 12 | DeviceRemoved DeviceActions = iota 13 | // DeviceAdded new device found, eg. via discovery 14 | DeviceAdded 15 | ) 16 | 17 | type DeviceActions uint8 18 | 19 | // DeviceDiscovered event emitted when a device is added or removed from Object Manager 20 | type DeviceDiscovered struct { 21 | Path dbus.ObjectPath 22 | Type DeviceActions 23 | } 24 | 25 | // OnDeviceDiscovered monitor for new devices and send updates via channel. Use cancel to close the monitoring process 26 | func (a *Adapter1) OnDeviceDiscovered() (chan *DeviceDiscovered, func(), error) { 27 | 28 | signal, omSignalCancel, err := a.GetObjectManagerSignal() 29 | if err != nil { 30 | return nil, nil, err 31 | } 32 | 33 | var ( 34 | ch = make(chan *DeviceDiscovered) 35 | ) 36 | 37 | go func() { 38 | // Recover from panic on write to closed channel which happens 39 | // very often when there's too many BLE advertisements to process 40 | // in timly manner by bluez+dbus and advertising reports come in 41 | // after scanning was stopped 42 | defer func() { 43 | if err := recover(); err != nil { 44 | log.Warnf("Recovering from panic: %s", err) 45 | } 46 | }() 47 | 48 | for v := range signal { 49 | 50 | if v == nil { 51 | return 52 | } 53 | 54 | var op DeviceActions 55 | if v.Name == bluez.InterfacesAdded { 56 | op = DeviceAdded 57 | } else { 58 | if v.Name == bluez.InterfacesRemoved { 59 | op = DeviceRemoved 60 | } else { 61 | continue 62 | } 63 | } 64 | 65 | path := v.Body[0].(dbus.ObjectPath) 66 | 67 | if op == DeviceRemoved { 68 | ifaces := v.Body[1].([]string) 69 | for _, iface := range ifaces { 70 | if iface == device.Device1Interface { 71 | log.Tracef("Removed device %s", path) 72 | ch <- &DeviceDiscovered{path, op} 73 | } 74 | } 75 | continue 76 | } 77 | 78 | ifaces := v.Body[1].(map[string]map[string]dbus.Variant) 79 | if p, ok := ifaces[device.Device1Interface]; ok { 80 | if p == nil { 81 | continue 82 | } 83 | log.Tracef("Added device %s", path) 84 | 85 | if ch == nil { 86 | return 87 | } 88 | 89 | ch <- &DeviceDiscovered{path, op} 90 | 91 | } 92 | 93 | } 94 | }() 95 | 96 | cancel := func() { 97 | omSignalCancel() 98 | if ch != nil { 99 | close(ch) 100 | } 101 | ch = nil 102 | log.Trace("OnDeviceDiscovered: cancel() called") 103 | } 104 | 105 | return ch, cancel, nil 106 | } 107 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_discovery_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestDiscovery(t *testing.T) { 11 | a := getDefaultAdapter(t) 12 | 13 | err := a.StartDiscovery() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer func() { 18 | err := a.StopDiscovery() 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | }() 23 | 24 | discovery, cancel, err := a.OnDeviceDiscovered() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | var ( 30 | wait = make(chan error, 2) 31 | wg sync.WaitGroup 32 | ) 33 | 34 | wg.Add(2) 35 | 36 | go func() { 37 | defer wg.Done() 38 | for dev := range discovery { 39 | if dev == nil { 40 | return 41 | } 42 | wait <- nil 43 | } 44 | }() 45 | 46 | go func() { 47 | defer wg.Done() 48 | sleep := 5 49 | time.Sleep(time.Duration(sleep) * time.Second) 50 | wait <- fmt.Errorf("Discovery timeout exceeded (%ds)", sleep) 51 | }() 52 | 53 | err = <-wait 54 | cancel() 55 | 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | wg.Wait() 61 | } 62 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_filter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/util" 5 | ) 6 | 7 | const ( 8 | DiscoveryFilterTransportAuto = "auto" 9 | DiscoveryFilterTransportBrEdr = "bredr" 10 | DiscoveryFilterTransportLE = "le" 11 | ) 12 | 13 | // Filter applied to discovery 14 | type DiscoveryFilter struct { 15 | 16 | // Filter by service UUIDs, empty means match 17 | // _any_ UUID. 18 | // 19 | // When a remote device is found that advertises 20 | // any UUID from UUIDs, it will be reported if: 21 | // - Pathloss and RSSI are both empty. 22 | // - only Pathloss param is set, device advertise 23 | // TX pwer, and computed pathloss is less than 24 | // Pathloss param. 25 | // - only RSSI param is set, and received RSSI is 26 | // higher than RSSI param. 27 | UUIDs []string 28 | 29 | // RSSI threshold value. 30 | // 31 | // PropertiesChanged signals will be emitted 32 | // for already existing Device objects, with 33 | // updated RSSI value. If one or more discovery 34 | // filters have been set, the RSSI delta-threshold, 35 | // that is imposed by StartDiscovery by default, 36 | // will not be applied. 37 | RSSI int16 38 | 39 | // Pathloss threshold value. 40 | // 41 | // PropertiesChanged signals will be emitted 42 | // for already existing Device objects, with 43 | // updated Pathloss value. 44 | Pathloss uint16 45 | 46 | // Transport parameter determines the type of 47 | // scan. 48 | // 49 | // Possible values: 50 | // "auto" - interleaved scan 51 | // "bredr" - BR/EDR inquiry 52 | // "le" - LE scan only 53 | // 54 | // If "le" or "bredr" Transport is requested, 55 | // and the controller doesn't support it, 56 | // org.bluez.Error.Failed error will be returned. 57 | // If "auto" transport is requested, scan will use 58 | // LE, BREDR, or both, depending on what's 59 | // currently enabled on the controller. 60 | Transport string 61 | 62 | // Disables duplicate detection of advertisement 63 | // data. 64 | // 65 | // When enabled PropertiesChanged signals will be 66 | // generated for either ManufacturerData and 67 | // ServiceData everytime they are discovered. 68 | DuplicateData bool 69 | } 70 | 71 | func (a *DiscoveryFilter) uuidExists(uuid string) bool { 72 | for _, uiid1 := range a.UUIDs { 73 | if uiid1 == uuid { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | 80 | // Add an UUID to filter if it does not exists 81 | func (a *DiscoveryFilter) AddUUIDs(uuids ...string) { 82 | for _, uuid := range uuids { 83 | if !a.uuidExists(uuid) { 84 | a.UUIDs = append(a.UUIDs, uuid) 85 | } 86 | } 87 | } 88 | 89 | // ToMap convert to a format compatible with SetDiscoveryFilter method call 90 | func (a *DiscoveryFilter) ToMap() map[string]interface{} { 91 | 92 | m := make(map[string]interface{}) 93 | util.StructToMap(a, m) 94 | 95 | if len(a.UUIDs) == 0 { 96 | delete(m, "UUIDs") 97 | } 98 | if a.RSSI == 0 { 99 | delete(m, "RSSI") 100 | } 101 | if a.Pathloss == 0 { 102 | delete(m, "Pathloss") 103 | } 104 | 105 | return m 106 | } 107 | 108 | // initialize a new DiscoveryFilter 109 | func NewDiscoveryFilter() DiscoveryFilter { 110 | return DiscoveryFilter{ 111 | // defaults 112 | DuplicateData: true, 113 | Transport: DiscoveryFilterTransportAuto, 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_filter_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDiscoveryFilter(t *testing.T) { 10 | 11 | f := NewDiscoveryFilter() 12 | f.AddUUIDs("AAAA", "BBBB") 13 | f.RSSI = 10 14 | f.Pathloss = 1 15 | f.DuplicateData = false 16 | f.Transport = DiscoveryFilterTransportLE 17 | 18 | m := f.ToMap() 19 | 20 | assert.EqualValues(t, m["UUIDs"].([]string), f.UUIDs) 21 | assert.EqualValues(t, m["RSSI"].(int16), f.RSSI) 22 | assert.EqualValues(t, m["Pathloss"].(uint16), f.Pathloss) 23 | assert.EqualValues(t, m["DuplicateData"].(bool), f.DuplicateData) 24 | assert.EqualValues(t, m["Transport"].(string), f.Transport) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_gatt.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/bluez/profile/gatt" 5 | ) 6 | 7 | //GetGattManager return a GattManager1 instance 8 | func (a *Adapter1) GetGattManager() (*gatt.GattManager1, error) { 9 | adapterID, err := ParseAdapterID(a.Path()) 10 | if err != nil { 11 | return nil, err 12 | } 13 | return gatt.NewGattManager1FromAdapterID(adapterID) 14 | } 15 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_gatt_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import "testing" 4 | 5 | //GetGattManager return a GattManager1 instance 6 | func TestGetGattManager(t *testing.T) { 7 | 8 | a := getDefaultAdapter(t) 9 | 10 | _, err := a.GetGattManager() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /bluez/profile/adapter/adapter_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/godbus/dbus/v5" 9 | "github.com/muka/go-bluetooth/bluez" 10 | ) 11 | 12 | func TestGetAdapterIDFromPath(t *testing.T) { 13 | 14 | hci := "hci999" 15 | path := fmt.Sprintf("%s/%s", bluez.OrgBluezPath, hci) 16 | adapterID, err := ParseAdapterID(dbus.ObjectPath(path)) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | if hci != adapterID { 21 | t.Fatal(fmt.Errorf("%s != %s", hci, adapterID)) 22 | } 23 | 24 | path += "/dev_AA_BB_CC" 25 | adapterID, err = ParseAdapterID(dbus.ObjectPath(path)) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if hci != adapterID { 30 | t.Fatal(fmt.Errorf("%s != %s", hci, adapterID)) 31 | } 32 | 33 | } 34 | 35 | func TestGetAdapterIDFromPathFail(t *testing.T) { 36 | hci := "foo1234" 37 | path := fmt.Sprintf("%s/%s", bluez.OrgBluezPath, hci) 38 | _, err := ParseAdapterID(dbus.ObjectPath(path)) 39 | if err == nil { 40 | t.Fatal("Expected error parsing hci device name") 41 | } 42 | 43 | path = "foo/hci1" 44 | _, err = ParseAdapterID(dbus.ObjectPath(path)) 45 | if err == nil { 46 | t.Fatal("Expected error parsing bluez base path") 47 | } 48 | } 49 | 50 | func TestDefaultAdapterSetGet(t *testing.T) { 51 | testAdapterID := "hci1" 52 | SetDefaultAdapterID(testAdapterID) 53 | adapterID := GetDefaultAdapterID() 54 | if adapterID != testAdapterID { 55 | log.Fatal("Failed to set default adapter") 56 | } 57 | } 58 | 59 | func TestAdapterExists(t *testing.T) { 60 | 61 | adapterID := GetDefaultAdapterID() 62 | 63 | exists, err := AdapterExists(adapterID) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | if exists == false { 69 | t.Errorf("Expected %s to exists", adapterID) 70 | t.Fatal() 71 | } 72 | 73 | } 74 | 75 | func TestGetAdapterNotExists(t *testing.T) { 76 | _, err := GetAdapter("foobar") 77 | if err == nil { 78 | t.Fatal("adapter should not exists") 79 | } 80 | } 81 | 82 | func TestGetAdapterFromDevicePath(t *testing.T) { 83 | 84 | a := getDefaultAdapter(t) 85 | 86 | list, err := a.GetDeviceList() 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | if len(list) == 0 { 92 | t.Log("Cannot test GetAdapterFromDevicePath, empty device list") 93 | return 94 | } 95 | 96 | _, err = GetAdapterFromDevicePath(list[0]) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | } 102 | 103 | func TestGetAdapterFromDevicePathFail(t *testing.T) { 104 | _, err := GetAdapterFromDevicePath(dbus.ObjectPath("foo/test/hci1/dev_AA_BB_CC_DD_EE_FF")) 105 | if err == nil { 106 | t.Fatal("Expected failure parsing device path") 107 | } 108 | } 109 | 110 | func TestGetAdapter(t *testing.T) { 111 | a := getDefaultAdapter(t) 112 | if a.Properties.Address == "" { 113 | log.Fatal("Properties should not be empty") 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /bluez/profile/adapter/gen_adapter.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Adapter API description [adapter-api.txt] 5 | 6 | 7 | */ 8 | package adapter 9 | -------------------------------------------------------------------------------- /bluez/profile/admin_policy/gen_admin_policy.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Admin Policy API description [admin-policy-api.txt] 5 | This API provides methods to control the behavior of bluez as an administrator. 6 | 7 | Interface AdminPolicySet1 provides methods to set policies. Once the policy is 8 | set successfully, it will affect all clients and stay persistently even after 9 | restarting Bluetooth Daemon. The only way to clear it is to overwrite the 10 | policy with the same method. 11 | 12 | Interface AdminPolicyStatus1 provides readonly properties to indicate the 13 | current values of admin policy. 14 | 15 | 16 | 17 | */ 18 | package admin_policy 19 | -------------------------------------------------------------------------------- /bluez/profile/advertisement_monitor/advertisement_monitor.go: -------------------------------------------------------------------------------- 1 | package advertisement_monitor 2 | 3 | // array{(uint8, uint8, array{byte})} 4 | type Pattern struct { 5 | v1 uint8 6 | v2 uint8 7 | v3 []byte 8 | } 9 | 10 | type Patterns = []Pattern 11 | -------------------------------------------------------------------------------- /bluez/profile/advertisement_monitor/gen_advertisement_monitor.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Advertisement Monitor API Description [advertisement-monitor-api.txt] 5 | This API allows an client to specify a job of monitoring advertisements by 6 | registering the root of hierarchy and then exposing advertisement monitors 7 | under the root with filtering conditions, thresholds of RSSI and timers 8 | of RSSI thresholds. 9 | 10 | Once a monitoring job is activated by BlueZ, the client can expect to get 11 | notified on the targeted advertisements no matter if there is an ongoing 12 | discovery session (a discovery session is started/stopped with methods in 13 | org.bluez.Adapter1 interface). 14 | 15 | 16 | */ 17 | package advertisement_monitor 18 | -------------------------------------------------------------------------------- /bluez/profile/advertising/advertising.go: -------------------------------------------------------------------------------- 1 | package advertising 2 | 3 | const ( 4 | SecondaryChannel1M = "1M" 5 | SecondaryChannel2M = "2M" 6 | SecondaryChannelCoded = "Coded" 7 | ) 8 | 9 | // "tx-power" 10 | // "appearance" 11 | // "local-name" 12 | const ( 13 | SupportedIncludesTxPower = "tx-power" 14 | SupportedIncludesAppearance = "appearance" 15 | SupportedIncludesLocalName = "local-name" 16 | ) 17 | 18 | const ( 19 | AdvertisementTypeBroadcast = "broadcast" 20 | AdvertisementTypePeripheral = "peripheral" 21 | ) 22 | 23 | func (a *LEAdvertisement1Properties) AddServiceUUID(uuids ...string) { 24 | if a.ServiceUUIDs == nil { 25 | a.ServiceUUIDs = make([]string, 0) 26 | } 27 | for _, uuid := range uuids { 28 | for _, uuid1 := range a.ServiceUUIDs { 29 | if uuid1 == uuid { 30 | continue 31 | } 32 | } 33 | a.ServiceUUIDs = append(a.ServiceUUIDs, uuid) 34 | } 35 | } 36 | 37 | func (a *LEAdvertisement1Properties) AddData(code byte, data []uint8) { 38 | if a.Data == nil { 39 | a.Data = make(map[byte]interface{}) 40 | } 41 | a.Data[code] = data 42 | } 43 | 44 | func (a *LEAdvertisement1Properties) AddServiceData(code string, data []uint8) { 45 | if a.ServiceData == nil { 46 | a.ServiceData = make(map[string]interface{}) 47 | } 48 | a.ServiceData[code] = data 49 | } 50 | 51 | func (a *LEAdvertisement1Properties) AddManifacturerData(code uint16, data []uint8) { 52 | if a.ManufacturerData == nil { 53 | a.ManufacturerData = make(map[uint16]interface{}) 54 | } 55 | a.ManufacturerData[code] = data 56 | } 57 | -------------------------------------------------------------------------------- /bluez/profile/advertising/gen_advertising.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus LE Advertising API Description [advertising-api.txt] 5 | Advertising packets are structured data which is broadcast on the LE Advertising 6 | channels and available for all devices in range. Because of the limited space 7 | available in LE Advertising packets (31 bytes), each packet's contents must be 8 | carefully controlled. 9 | 10 | BlueZ acts as a store for the Advertisement Data which is meant to be sent. 11 | It constructs the correct Advertisement Data from the structured 12 | data and configured the kernel to send the correct advertisement. 13 | 14 | Advertisement Data objects are registered freely and then referenced by BlueZ 15 | when constructing the data sent to the kernel. 16 | 17 | 18 | */ 19 | package advertising 20 | -------------------------------------------------------------------------------- /bluez/profile/agent/gen_agent.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Agent API description [agent-api.txt] 5 | 6 | 7 | */ 8 | package agent 9 | -------------------------------------------------------------------------------- /bluez/profile/battery/gen_battery.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Battery API description [battery-api.txt] 5 | 6 | 7 | */ 8 | package battery 9 | -------------------------------------------------------------------------------- /bluez/profile/device/gen_device.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Device API description [device-api.txt] 5 | 6 | 7 | */ 8 | package device 9 | -------------------------------------------------------------------------------- /bluez/profile/device/types.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import "github.com/godbus/dbus/v5" 4 | 5 | type SetsItem struct { 6 | Object dbus.ObjectPath 7 | Dict map[string]byte 8 | } 9 | -------------------------------------------------------------------------------- /bluez/profile/gatt/gatt.go: -------------------------------------------------------------------------------- 1 | package gatt 2 | 3 | // Defines how the characteristic value can be used. See 4 | // Core spec "Table 3.5: Characteristic Properties bit 5 | // field", and "Table 3.8: Characteristic Extended 6 | // Properties bit field" 7 | const ( 8 | FlagCharacteristicBroadcast = "broadcast" 9 | FlagCharacteristicRead = "read" 10 | FlagCharacteristicWriteWithoutResponse = "write-without-response" 11 | FlagCharacteristicWrite = "write" 12 | FlagCharacteristicNotify = "notify" 13 | FlagCharacteristicIndicate = "indicate" 14 | FlagCharacteristicAuthenticatedSignedWrites = "authenticated-signed-writes" 15 | FlagCharacteristicReliableWrite = "reliable-write" 16 | FlagCharacteristicWritableAuxiliaries = "writable-auxiliaries" 17 | FlagCharacteristicEncryptRead = "encrypt-read" 18 | FlagCharacteristicEncryptWrite = "encrypt-write" 19 | FlagCharacteristicEncryptAuthenticatedRead = "encrypt-authenticated-read" 20 | FlagCharacteristicEncryptAuthenticatedWrite = "encrypt-authenticated-write" 21 | FlagCharacteristicSecureRead = "secure-read" 22 | FlagCharacteristicSecureWrite = "secure-write" 23 | ) 24 | 25 | // Descriptor specific flags 26 | const ( 27 | FlagDescriptorRead = "read" 28 | FlagDescriptorWrite = "write" 29 | FlagDescriptorEncryptRead = "encrypt-read" 30 | FlagDescriptorEncryptWrite = "encrypt-write" 31 | FlagDescriptorEncryptAuthenticatedRead = "encrypt-authenticated-read" 32 | FlagDescriptorEncryptAuthenticatedWrite = "encrypt-authenticated-write" 33 | FlagDescriptorSecureRead = "secure-read" 34 | FlagDescriptorSecureWrite = "secure-write" 35 | ) 36 | -------------------------------------------------------------------------------- /bluez/profile/gatt/gen_gatt.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus GATT API description [gatt-api.txt] 5 | GATT local and remote services share the same high-level D-Bus API. Local 6 | refers to GATT based service exported by a BlueZ plugin or an external 7 | application. Remote refers to GATT services exported by the peer. 8 | 9 | BlueZ acts as a proxy, translating ATT operations to D-Bus method calls and 10 | Properties (or the opposite). Support for D-Bus Object Manager is mandatory for 11 | external services to allow seamless GATT declarations (Service, Characteristic 12 | and Descriptors) discovery. Each GATT service tree is required to export a D-Bus 13 | Object Manager at its root that is solely responsible for the objects that 14 | belong to that service. 15 | 16 | Releasing a registered GATT service is not defined yet. Any API extension 17 | should avoid breaking the defined API, and if possible keep an unified GATT 18 | remote and local services representation. 19 | 20 | 21 | */ 22 | package gatt 23 | -------------------------------------------------------------------------------- /bluez/profile/gen_errors.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | package profile 4 | 5 | import ( 6 | "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | var ( 10 | // NotReady map to org.bluez.Error.NotReady 11 | ErrNotReady = dbus.Error{ 12 | Name: "org.bluez.Error.NotReady", 13 | Body: []interface{}{"NotReady"}, 14 | } 15 | // InvalidArguments map to org.bluez.Error.InvalidArguments 16 | ErrInvalidArguments = dbus.Error{ 17 | Name: "org.bluez.Error.InvalidArguments", 18 | Body: []interface{}{"InvalidArguments"}, 19 | } 20 | // Failed map to org.bluez.Error.Failed 21 | ErrFailed = dbus.Error{ 22 | Name: "org.bluez.Error.Failed", 23 | Body: []interface{}{"Failed"}, 24 | } 25 | // DoesNotExist map to org.bluez.Error.DoesNotExist 26 | ErrDoesNotExist = dbus.Error{ 27 | Name: "org.bluez.Error.DoesNotExist", 28 | Body: []interface{}{"DoesNotExist"}, 29 | } 30 | // Rejected map to org.bluez.Error.Rejected 31 | ErrRejected = dbus.Error{ 32 | Name: "org.bluez.Error.Rejected", 33 | Body: []interface{}{"Rejected"}, 34 | } 35 | // NotConnected map to org.bluez.Error.NotConnected 36 | ErrNotConnected = dbus.Error{ 37 | Name: "org.bluez.Error.NotConnected", 38 | Body: []interface{}{"NotConnected"}, 39 | } 40 | // NotAcquired map to org.bluez.Error.NotAcquired 41 | ErrNotAcquired = dbus.Error{ 42 | Name: "org.bluez.Error.NotAcquired", 43 | Body: []interface{}{"NotAcquired"}, 44 | } 45 | // NotSupported map to org.bluez.Error.NotSupported 46 | ErrNotSupported = dbus.Error{ 47 | Name: "org.bluez.Error.NotSupported", 48 | Body: []interface{}{"NotSupported"}, 49 | } 50 | // NotAuthorized map to org.bluez.Error.NotAuthorized 51 | ErrNotAuthorized = dbus.Error{ 52 | Name: "org.bluez.Error.NotAuthorized", 53 | Body: []interface{}{"NotAuthorized"}, 54 | } 55 | // NotAvailable map to org.bluez.Error.NotAvailable 56 | ErrNotAvailable = dbus.Error{ 57 | Name: "org.bluez.Error.NotAvailable", 58 | Body: []interface{}{"NotAvailable"}, 59 | } 60 | // AlreadyConnected map to org.bluez.Error.AlreadyConnected 61 | ErrAlreadyConnected = dbus.Error{ 62 | Name: "org.bluez.Error.AlreadyConnected", 63 | Body: []interface{}{"AlreadyConnected"}, 64 | } 65 | ) 66 | -------------------------------------------------------------------------------- /bluez/profile/gen_version.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | package profile 4 | 5 | const Version = 5.64 6 | -------------------------------------------------------------------------------- /bluez/profile/health/gen_health.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Health API description [health-api.txt] 5 | 6 | 7 | */ 8 | package health 9 | -------------------------------------------------------------------------------- /bluez/profile/input/gen_input.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Input API description [input-api.txt] 5 | 6 | */ 7 | package input 8 | -------------------------------------------------------------------------------- /bluez/profile/media/gen_media.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Media API description [media-api.txt] 5 | 6 | 7 | */ 8 | package media 9 | -------------------------------------------------------------------------------- /bluez/profile/media/types.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import "github.com/godbus/dbus/v5" 4 | 5 | // Item map to array{objects, properties} 6 | type Item struct { 7 | Object dbus.ObjectPath 8 | Property map[string]interface{} 9 | } 10 | 11 | // Track map to a media track 12 | type Track struct { 13 | // Track title name 14 | Title string 15 | // Track artist name 16 | Artist string 17 | // Track album name 18 | Album string 19 | // Track genre name 20 | Genre string 21 | // Number of tracks in total 22 | NumberOfTracks uint32 23 | // Track number 24 | TrackNumber uint32 25 | // Track duration in milliseconds 26 | Duration uint32 27 | } 28 | -------------------------------------------------------------------------------- /bluez/profile/mesh/gen_mesh.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Mesh API description [mesh-api.txt] 5 | 6 | */ 7 | package mesh 8 | -------------------------------------------------------------------------------- /bluez/profile/mesh/types.go: -------------------------------------------------------------------------------- 1 | package mesh 2 | 3 | import "github.com/godbus/dbus/v5" 4 | 5 | //VendorItem array{(uint16, uint16)} 6 | type VendorItem struct { 7 | Vendor uint16 8 | ModelID uint16 9 | } 10 | 11 | //VendorItem array{(uint16, uint16, dict)} 12 | type VendorOptionsItem struct { 13 | VendorItem 14 | Options map[string]interface{} 15 | } 16 | 17 | // ModelConfig 18 | type ModelConfig struct { 19 | Bindings []uint16 20 | PublicationPeriod uint32 21 | Vendor uint16 22 | Subscriptions []dbus.Variant 23 | } 24 | 25 | // Model 26 | type Model struct { 27 | Identifier uint16 28 | Config ModelConfig 29 | } 30 | 31 | // ConfigurationItem array{byte, array{(uint16, dict)}} 32 | type ConfigurationItem struct { 33 | Index byte 34 | Models []Model 35 | } 36 | -------------------------------------------------------------------------------- /bluez/profile/network/gen_network.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Network API description [network-api.txt] 5 | 6 | 7 | */ 8 | package network 9 | -------------------------------------------------------------------------------- /bluez/profile/obex/Client1.go: -------------------------------------------------------------------------------- 1 | package obex 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/go-bluetooth/bluez" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // TODO: https://github.com/blueman-project/blueman/issues/218#issuecomment-89315974 10 | // NewObexClient1 create a new ObexClient1 client 11 | func NewObexClient1() *ObexClient1 { 12 | a := new(ObexClient1) 13 | a.client = bluez.NewClient( 14 | &bluez.Config{ 15 | Name: "org.bluez.obex", 16 | Iface: "org.bluez.obex.Client1", 17 | Path: "/org/bluez/obex", 18 | Bus: bluez.SessionBus, 19 | }, 20 | ) 21 | return a 22 | } 23 | 24 | // ObexClient1 client 25 | type ObexClient1 struct { 26 | client *bluez.Client 27 | } 28 | 29 | // Close the connection 30 | func (a *ObexClient1) Close() { 31 | a.client.Disconnect() 32 | } 33 | 34 | // Create a new OBEX session for the given remote address. 35 | // 36 | // The last parameter is a dictionary to hold optional or 37 | // type-specific parameters. Typical parameters that can 38 | // be set in this dictionary include the following: 39 | // 40 | // string "Target" : type of session to be created 41 | // string "Source" : local address to be used 42 | // byte "Channel" 43 | // 44 | // The currently supported targets are the following: 45 | // 46 | // - "ftp" 47 | // - "map" 48 | // - "opp" 49 | // - "pbap" 50 | // - "sync" 51 | // 52 | // Possible errors: 53 | // - org.bluez.obex.Error.InvalidArguments 54 | // - org.bluez.obex.Error.Failed 55 | // 56 | // TODO: Use ObexSession1 struct instead of generic map for options 57 | func (a *ObexClient1) CreateSession(destination string, options map[string]interface{}) (string, error) { 58 | log.Debugf("CreateSession to %s", destination) 59 | var sessionPath string 60 | err := a.client.Call("CreateSession", 0, destination, options).Store(&sessionPath) 61 | return sessionPath, err 62 | } 63 | 64 | // Unregister session and abort pending transfers. 65 | // 66 | // Possible errors: 67 | // - org.bluez.obex.Error.InvalidArguments 68 | // - org.bluez.obex.Error.NotAuthorized 69 | // 70 | func (a *ObexClient1) RemoveSession(session string) error { 71 | return a.client.Call("RemoveSession", 0, dbus.ObjectPath(session)).Store() 72 | } 73 | -------------------------------------------------------------------------------- /bluez/profile/obex/Client1_test.go: -------------------------------------------------------------------------------- 1 | package obex 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewObexClient1(t *testing.T) { 8 | t.Log("Create ObexClient1") 9 | 10 | a := NewObexClient1() 11 | 12 | t.Log("Start CreateSession") 13 | 14 | temp := map[string]interface{}{} 15 | //temp := map[string]dbus.Variant{} 16 | temp["Target"] = "opp" 17 | 18 | _, err := a.CreateSession("98:4E:97:00:3F:3C", temp) 19 | if err != nil { 20 | t.Logf("Error on CreateSession") 21 | t.Fatal(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bluez/profile/obex/ObjectPush1.go: -------------------------------------------------------------------------------- 1 | package obex 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/go-bluetooth/bluez" 6 | "github.com/muka/go-bluetooth/util" 7 | ) 8 | 9 | // NewObjectPush1 create a new ObjectPush1 client 10 | func NewObjectPush1(sessionPath string) *ObjectPush1 { 11 | a := new(ObjectPush1) 12 | a.client = bluez.NewClient( 13 | &bluez.Config{ 14 | Name: "org.bluez.obex", 15 | Iface: "org.bluez.obex.ObjectPush1", 16 | Path: dbus.ObjectPath(sessionPath), 17 | Bus: bluez.SessionBus, 18 | }, 19 | ) 20 | return a 21 | } 22 | 23 | // ObjectPush1 client 24 | type ObjectPush1 struct { 25 | client *bluez.Client 26 | } 27 | 28 | // Close the connection 29 | func (d *ObjectPush1) Close() { 30 | d.client.Disconnect() 31 | } 32 | 33 | // 34 | // Send one local file to the remote device. 35 | // 36 | // The returned path represents the newly created transfer, 37 | // which should be used to find out if the content has been 38 | // successfully transferred or if the operation fails. 39 | // 40 | // The properties of this transfer are also returned along 41 | // with the object path, to avoid a call to GetProperties. 42 | // 43 | // Possible errors: 44 | // - org.bluez.obex.Error.InvalidArguments 45 | // - org.bluez.obex.Error.Failed 46 | // 47 | func (a *ObjectPush1) SendFile(sourcefile string) (string, *ObexTransfer1Properties, error) { 48 | 49 | result := make(map[string]dbus.Variant) 50 | var sessionPath string 51 | err := a.client.Call("SendFile", 0, sourcefile).Store(&sessionPath, &result) 52 | if err != nil { 53 | return "", nil, err 54 | } 55 | 56 | transportProps := new(ObexTransfer1Properties) 57 | err = util.MapToStruct(transportProps, result) 58 | 59 | return sessionPath, transportProps, err 60 | } 61 | -------------------------------------------------------------------------------- /bluez/profile/obex/Session1.go: -------------------------------------------------------------------------------- 1 | package obex 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/go-bluetooth/bluez" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // NewObexSession1 create a new ObexSession1 client 10 | func NewObexSession1(path string) *ObexSession1 { 11 | a := new(ObexSession1) 12 | a.client = bluez.NewClient( 13 | &bluez.Config{ 14 | Name: "org.bluez.obex", 15 | Iface: "org.bluez.obex.Session1", 16 | Path: dbus.ObjectPath(path), 17 | Bus: bluez.SessionBus, 18 | }, 19 | ) 20 | a.Properties = new(ObexSession1Properties) 21 | _, err := a.GetProperties() 22 | if err != nil { 23 | log.Warn(err) 24 | } 25 | return a 26 | } 27 | 28 | // ObexSession1 client 29 | type ObexSession1 struct { 30 | client *bluez.Client 31 | Properties *ObexSession1Properties 32 | } 33 | 34 | // ObexSession1Properties exposed properties for ObexSession1 35 | type ObexSession1Properties struct { 36 | Source string // [readonly] Bluetooth adapter address 37 | Destination string // [readonly] Bluetooth device address 38 | Channel byte // [readonly] Bluetooth channel 39 | Target string // [readonly] Target UUID 40 | Root string // [readonly] Root path 41 | } 42 | 43 | // Close the connection 44 | func (d *ObexSession1) Close() { 45 | d.client.Disconnect() 46 | } 47 | 48 | //GetProperties load all available properties 49 | func (d *ObexSession1) GetProperties() (*ObexSession1Properties, error) { 50 | err := d.client.GetProperties(d.Properties) 51 | return d.Properties, err 52 | } 53 | 54 | //GetProperty get a property 55 | func (d *ObexSession1) GetProperty(name string) (dbus.Variant, error) { 56 | return d.client.GetProperty(name) 57 | } 58 | -------------------------------------------------------------------------------- /bluez/profile/obex/Transfer1.go: -------------------------------------------------------------------------------- 1 | package obex 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/go-bluetooth/bluez" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // NewObexTransfer1 create a new ObexTransfer1 client 10 | func NewObexTransfer1(path string) *ObexTransfer1 { 11 | a := new(ObexTransfer1) 12 | a.client = bluez.NewClient( 13 | &bluez.Config{ 14 | Name: "org.bluez.obex", 15 | Iface: "org.bluez.obex.Transfer1", 16 | Path: dbus.ObjectPath(path), 17 | Bus: bluez.SessionBus, 18 | }, 19 | ) 20 | a.Properties = new(ObexTransfer1Properties) 21 | _, err := a.GetProperties() 22 | if err != nil { 23 | log.Warn(err) 24 | } 25 | return a 26 | } 27 | 28 | // ObexTransfer1 client 29 | type ObexTransfer1 struct { 30 | client *bluez.Client 31 | Properties *ObexTransfer1Properties 32 | } 33 | 34 | // ObexTransfer1Properties exposed properties for ObexTransfer1 35 | type ObexTransfer1Properties struct { 36 | Status string 37 | Session dbus.ObjectPath 38 | Name string 39 | Type string 40 | Time uint64 41 | Size uint64 42 | Transferred uint64 43 | Filename string 44 | } 45 | 46 | // Close the connection 47 | func (d *ObexTransfer1) Close() { 48 | d.client.Disconnect() 49 | } 50 | 51 | //GetProperties load all available properties 52 | func (d *ObexTransfer1) GetProperties() (*ObexTransfer1Properties, error) { 53 | err := d.client.GetProperties(d.Properties) 54 | return d.Properties, err 55 | } 56 | 57 | //GetProperty get a property 58 | func (d *ObexTransfer1) GetProperty(name string) (dbus.Variant, error) { 59 | return d.client.GetProperty(name) 60 | } 61 | 62 | // 63 | // Stops the current transference. 64 | // 65 | // Possible errors: org.bluez.obex.Error.NotAuthorized 66 | // - org.bluez.obex.Error.InProgress 67 | // - org.bluez.obex.Error.Failed 68 | // 69 | func (a *ObexTransfer1) Cancel() error { 70 | return a.client.Call("Cancel", 0).Store() 71 | } 72 | 73 | // 74 | // Suspend transference. 75 | // 76 | // Possible errors: org.bluez.obex.Error.NotAuthorized 77 | // org.bluez.obex.Error.NotInProgress 78 | // 79 | // Note that it is not possible to suspend transfers 80 | // which are queued which is why NotInProgress is listed 81 | // as possible error. 82 | // 83 | func (a *ObexTransfer1) Suspend() error { 84 | return a.client.Call("Suspend", 0).Store() 85 | } 86 | 87 | // 88 | // Resume transference. 89 | // 90 | // Possible errors: org.bluez.obex.Error.NotAuthorized 91 | // org.bluez.obex.Error.NotInProgress 92 | // 93 | // Note that it is not possible to resume transfers 94 | // which are queued which is why NotInProgress is listed 95 | // as possible error. 96 | // 97 | func (a *ObexTransfer1) Resume() error { 98 | return a.client.Call("Resume", 0).Store() 99 | } 100 | -------------------------------------------------------------------------------- /bluez/profile/obex/gen_obex.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | OBEX D-Bus API description [obex-api.txt] 5 | 6 | 7 | */ 8 | package obex 9 | -------------------------------------------------------------------------------- /bluez/profile/obex/types.go: -------------------------------------------------------------------------------- 1 | package obex 2 | 3 | import "github.com/godbus/dbus/v5" 4 | 5 | //VCardItem vcard-listing data where every entry consists of a pair of strings containing the vcard handle and the contact name. For example:"1.vcf" : "John" 6 | type VCardItem struct { 7 | Vcard string 8 | Name string 9 | } 10 | 11 | //Message map to array{object, dict} 12 | type Message struct { 13 | Path dbus.ObjectPath 14 | Dict map[string]interface{} 15 | } 16 | -------------------------------------------------------------------------------- /bluez/profile/obex_agent/gen_obex_agent.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | OBEX D-Bus Agent API description [obex-agent-api.txt] 5 | 6 | 7 | */ 8 | package obex_agent 9 | -------------------------------------------------------------------------------- /bluez/profile/profile.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import "github.com/godbus/dbus/v5" 4 | 5 | // BluezApi is the shared interface for the Bluez API implmentation 6 | type BluezApi interface { 7 | Path() dbus.ObjectPath 8 | Interface() string 9 | Close() 10 | } 11 | -------------------------------------------------------------------------------- /bluez/profile/profile/gen_profile.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Profile API description [profile-api.txt] 5 | 6 | 7 | */ 8 | package profile 9 | -------------------------------------------------------------------------------- /bluez/profile/sap/gen_sap.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Sim Access API description [sap-api.txt] 5 | 6 | 7 | */ 8 | package sap 9 | -------------------------------------------------------------------------------- /bluez/profile/thermometer/gen_thermometer.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | BlueZ D-Bus Thermometer API description [thermometer-api.txt] 5 | Santiago Carot-Nemesio 6 | 7 | 8 | */ 9 | package thermometer 10 | -------------------------------------------------------------------------------- /bluez/props.go: -------------------------------------------------------------------------------- 1 | package bluez 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/godbus/dbus/v5" 7 | "github.com/muka/go-bluetooth/util" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type WatchableClient interface { 12 | Client() *Client 13 | Path() dbus.ObjectPath 14 | ToProps() Properties 15 | GetWatchPropertiesChannel() chan *dbus.Signal 16 | SetWatchPropertiesChannel(chan *dbus.Signal) 17 | } 18 | 19 | // WatchProperties updates on property changes 20 | func WatchProperties(wprop WatchableClient) (chan *PropertyChanged, error) { 21 | 22 | channel, err := wprop.Client().Register(wprop.Path(), PropertiesInterface) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | wprop.SetWatchPropertiesChannel(channel) 28 | ch := make(chan *PropertyChanged) 29 | 30 | go (func() { 31 | defer func() { 32 | if err := recover(); err != nil { 33 | log.Warnf("Recovering from panic in SetWatchPropertiesChannel: %s", err) 34 | } 35 | }() 36 | 37 | for { 38 | 39 | if channel == nil { 40 | break 41 | } 42 | 43 | sig := <-channel 44 | 45 | if sig == nil { 46 | return 47 | } 48 | 49 | if sig.Name != PropertiesChanged { 50 | continue 51 | } 52 | if sig.Path != wprop.Path() { 53 | continue 54 | } 55 | 56 | iface := sig.Body[0].(string) 57 | changes := sig.Body[1].(map[string]dbus.Variant) 58 | 59 | for field, val := range changes { 60 | 61 | // updates [*]Properties struct when a property change 62 | s := reflect.ValueOf(wprop.ToProps()).Elem() 63 | // exported field 64 | f := s.FieldByName(field) 65 | if f.IsValid() { 66 | // A Value can be changed only if it is 67 | // addressable and was not obtained by 68 | // the use of unexported struct fields. 69 | if f.CanSet() { 70 | x := reflect.ValueOf(val.Value()) 71 | wprop.ToProps().Lock() 72 | // map[*]variant -> map[*]interface{} 73 | ok, err := util.AssignMapVariantToInterface(f, x) 74 | if err != nil { 75 | log.Errorf("Failed to set %s: %s", f.String(), err) 76 | continue 77 | } 78 | // direct assignment 79 | if !ok { 80 | f.Set(x) 81 | } 82 | wprop.ToProps().Unlock() 83 | } 84 | } 85 | 86 | propChanged := &PropertyChanged{ 87 | Interface: iface, 88 | Name: field, 89 | Value: val.Value(), 90 | } 91 | ch <- propChanged 92 | } 93 | 94 | } 95 | })() 96 | 97 | return ch, nil 98 | } 99 | 100 | func UnwatchProperties(wprop WatchableClient, ch chan *PropertyChanged) error { 101 | defer func() { 102 | if err := recover(); err != nil { 103 | log.Warnf("Recovering from panic in UnwatchProperties: %s", err) 104 | } 105 | }() 106 | if wprop.GetWatchPropertiesChannel() != nil { 107 | wprop.GetWatchPropertiesChannel() <- nil 108 | err := wprop.Client().Unregister(wprop.Path(), PropertiesInterface, wprop.GetWatchPropertiesChannel()) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | ch <- nil 114 | close(ch) 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /devices/sensortag/calc.go: -------------------------------------------------------------------------------- 1 | package sensortag 2 | 3 | import "math" 4 | 5 | // Port from http://processors.wiki.ti.com/index.php/SensorTag_User_Guide#IR_Temperature_Sensor 6 | var calcBarometricPressure = func(raw uint32) float64 { 7 | //barometric pressure...... 8 | pressureMask := (int(raw) >> 8) & 0x00ffffff 9 | return float64(pressureMask) / 100.0 10 | } 11 | 12 | var calcBarometricTemperature = func(raw uint32) float64 { 13 | //TEMPERATURE calibiration data coming from barometric sensor 14 | tempMask := int(raw) & 0x00ffffff 15 | return float64(tempMask) / 100.0 16 | } 17 | 18 | // Port from http://processors.wiki.ti.com/index.php/SensorTag_User_Guide#IR_Temperature_Sensor 19 | var calcHumidLocal = func(raw uint16) float64 { 20 | //humidity calibiration......... 21 | return float64(raw) * 100 / 65536.0 22 | } 23 | 24 | var calcTmpFromHumidSensor = func(raw uint16) float64 { 25 | //TEMPERATURE calibiration for data coming from humidity sensor 26 | return -40 + ((165 * float64(raw)) / 65536.0) 27 | } 28 | 29 | // Port from http://processors.wiki.ti.com/index.php/SensorTag_User_Guide#IR_Temperature_Sensor 30 | var calcTmpLocal = func(raw uint16) float64 { 31 | //ambient temperature calberation 32 | return float64(raw) / 128.0 33 | } 34 | 35 | /* Conversion algorithm for target temperature */ 36 | var calcTmpTarget = func(raw uint16) float64 { 37 | //..object temperature caliberation........... 38 | return float64(raw) / 128.0 39 | } 40 | 41 | // Port from http://processors.wiki.ti.com/index.php/SensorTag_User_Guide#IR_Temperature_Sensor 42 | 43 | var calcMpuGyroscope = func(rawX, rawY, rawZ uint16) (float64, float64, float64) { 44 | 45 | Xg := float64(rawX) / 128.0 46 | Yg := float64(rawY) / 128.0 47 | Zg := float64(rawZ) / 128.0 48 | 49 | return Xg, Yg, Zg 50 | } 51 | var calcMpuAccelerometer = func(rawX, rawY, rawZ uint16) (float64, float64, float64) { 52 | 53 | Xg := float64(rawX) / 4096.0 54 | Yg := float64(rawY) / 4096.0 55 | Zg := float64(rawZ) / 4096.0 56 | 57 | return Xg, Yg, Zg 58 | } 59 | var calcMpuMagnetometer = func(rawX, rawY, rawZ uint16) (float64, float64, float64) { 60 | 61 | Xg := float64(rawX) * 4912.0 / 32768.0 62 | Yg := float64(rawY) * 4912.0 / 32768.0 63 | Zg := float64(rawZ) * 4912.0 / 32768.0 64 | 65 | return Xg, Yg, Zg 66 | } 67 | 68 | // Port from http://processors.wiki.ti.com/index.php/SensorTag_User_Guide#IR_Temperature_Sensor 69 | 70 | var calcLuxometer = func(raw uint16) float64 { 71 | 72 | exponent := (int(raw) & 0xF000) >> 12 73 | mantissa := (int(raw) & 0x0FFF) 74 | exp := float64(exponent) 75 | man := float64(mantissa) 76 | flLux := man * math.Pow(2, exp) / 100 77 | return float64(flLux) 78 | } 79 | -------------------------------------------------------------------------------- /devices/sensortag/st.go: -------------------------------------------------------------------------------- 1 | package sensortag 2 | 3 | func getOptions() map[string]interface{} { 4 | options := make(map[string]interface{}) 5 | return options 6 | } 7 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Examples 4 | 5 | ## Running the service example 6 | 7 | Requires two adapters. In two shell run in order 8 | 9 | - `go run main.go service server` 10 | - `go run main.go service client 00:1A:7D:DA:71:15 --adapterID hci1 ` 11 | 12 | 13 | ## Development 14 | 15 | Edit `/etc/systemd/system/bluetooth.target.wants/bluetooth.service` and update the `ExecStart` command adding those optios 16 | 17 | `ExecStart=/usr/lib/bluetooth/bluetoothd -E -d -P hostname` 18 | 19 | - `-E` experimental mode 20 | - `-d` debug mode 21 | - `-P` no plugin 22 | 23 | Afterwards run `systemctl daemon-reload` to reload the config and `service bluetooth restart` to restart the service. 24 | 25 | To view logs `journalctl -u bluetooth -f` 26 | -------------------------------------------------------------------------------- /env/bluez/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:19.10 2 | ARG BLUEZ_VERSION=5.54 3 | WORKDIR /bluez 4 | RUN apt update -qq && apt remove --purge -y bluetooth && apt install -y && \ 5 | DEBIAN_FRONTEND=noninteractive apt install -y \ 6 | git libdbus-1-dev libudev-dev libical-dev libreadline-dev \ 7 | autotools-dev automake libtool libglib2.0-dev udev 8 | 9 | ENV BLUEZ_VERSION=$BLUEZ_VERSION 10 | RUN cd / && git clone https://git.kernel.org/pub/scm/bluetooth/bluez.git && cd bluez && git checkout $BLUEZ_VERSION && \ 11 | ./bootstrap && ./configure --disable-systemd && make && make install 12 | COPY ./entrypoint.sh /entrypoint.sh 13 | RUN chmod +x /entrypoint.sh 14 | 15 | CMD ["sh", "/entrypoint.sh"] 16 | -------------------------------------------------------------------------------- /env/bluez/entrypoint.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | /usr/local/libexec/bluetooth/bluetoothd -E -d -n -P hostname 4 | -------------------------------------------------------------------------------- /examples/agent/agent.go: -------------------------------------------------------------------------------- 1 | package agent_example 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/godbus/dbus/v5" 7 | "github.com/muka/go-bluetooth/api" 8 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 9 | "github.com/muka/go-bluetooth/bluez/profile/agent" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // ToDo: allow enabling "simple pairing" (sspmode set via hcitool) 14 | func Run(deviceAddress, adapterID string) error { 15 | 16 | defer api.Exit() 17 | 18 | //Connect DBus System bus 19 | conn, err := dbus.SystemBus() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | ag := agent.NewSimpleAgent() 25 | err = agent.ExposeAgent(conn, ag, agent.CapKeyboardDisplay, true) 26 | if err != nil { 27 | return fmt.Errorf("SimpleAgent: %s", err) 28 | } 29 | 30 | a, err := adapter.GetAdapter(adapterID) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | devices, err := a.GetDevices() 36 | if err != nil { 37 | return fmt.Errorf("GetDevices: %s", err) 38 | } 39 | 40 | found := false 41 | for _, dev := range devices { 42 | 43 | if dev.Properties.Address != deviceAddress { 44 | continue 45 | } 46 | 47 | if dev.Properties.Paired { 48 | continue 49 | } 50 | 51 | found = true 52 | // log.Info(i, v.Path) 53 | log.Infof("Pairing with %s", dev.Properties.Address) 54 | 55 | err := dev.Pair() 56 | if err != nil { 57 | return fmt.Errorf("Pair failed: %s", err) 58 | } 59 | 60 | log.Info("Pair succeed, connecting...") 61 | agent.SetTrusted(adapterID, dev.Path()) 62 | 63 | err = dev.Connect() 64 | if err != nil { 65 | return fmt.Errorf("Connect failed: %s", err) 66 | } 67 | 68 | } 69 | 70 | if !found { 71 | return fmt.Errorf("No device found that need to be paired on %s", adapterID) 72 | } 73 | 74 | log.Info("Working...") 75 | select {} 76 | } 77 | -------------------------------------------------------------------------------- /examples/beacon/beacon.go: -------------------------------------------------------------------------------- 1 | package beacon_example 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/muka/go-bluetooth/api/beacon" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func Run(beaconType, eddystoneBeaconType, adapterID string) error { 12 | 13 | var b *beacon.Beacon 14 | if beaconType == "ibeacon" { 15 | b1, err := beacon.CreateIBeacon("AAAABBBBCCCCDDDDAAAABBBBCCCCDDDD", 111, 999, 89) 16 | if err != nil { 17 | return err 18 | } 19 | b = b1 20 | } else { 21 | 22 | if eddystoneBeaconType == "URL" { 23 | log.Infof("Exposing eddystone URL") 24 | b1, err := beacon.CreateEddystoneURL("https://bit.ly/2OCrFK2", 99) 25 | if err != nil { 26 | return err 27 | } 28 | b = b1 29 | } else { 30 | // UID 31 | log.Infof("Exposing eddystone UID") 32 | b1, err := beacon.CreateEddystoneUID("AAAAAAAAAABBBBBBBBBB", "123456123456", -59) 33 | if err != nil { 34 | return err 35 | } 36 | b = b1 37 | } 38 | } 39 | 40 | // A timeout of 0 cause an immediate timeout and advertisement deregistration 41 | // see https://www.spinics.net/lists/linux-bluetooth/msg79915.html 42 | // In seconds 43 | timeout := uint16(60 * 60 * 18) 44 | 45 | cancel, err := b.Expose(adapterID, timeout) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | defer cancel() 51 | 52 | log.Debugf("%s ready", beaconType) 53 | 54 | go func() { 55 | time.Sleep(time.Duration(timeout) * time.Second) 56 | os.Exit(0) 57 | }() 58 | 59 | select {} 60 | } 61 | -------------------------------------------------------------------------------- /examples/btmgmt/btmgmt.go: -------------------------------------------------------------------------------- 1 | // Example use of the btmgmt wrapper 2 | package btmgmt_example 3 | 4 | import ( 5 | "github.com/muka/go-bluetooth/hw/linux/btmgmt" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func Run() error { 10 | 11 | list, err := btmgmt.GetAdapters() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | for i, a := range list { 17 | log.Infof("%d) %s (%v)", i+1, a.Name, a.Addr) 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /examples/discovery/discovery.go: -------------------------------------------------------------------------------- 1 | //shows how to watch for new devices and list them 2 | package discovery_example 3 | 4 | import ( 5 | "context" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/muka/go-bluetooth/api" 10 | "github.com/muka/go-bluetooth/api/beacon" 11 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 12 | "github.com/muka/go-bluetooth/bluez/profile/device" 13 | log "github.com/sirupsen/logrus" 14 | eddystone "github.com/suapapa/go_eddystone" 15 | ) 16 | 17 | func Run(adapterID string, onlyBeacon bool) error { 18 | 19 | //clean up connection on exit 20 | defer api.Exit() 21 | 22 | a, err := adapter.GetAdapter(adapterID) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | log.Debug("Flush cached devices") 28 | err = a.FlushDevices() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | log.Debug("Start discovery") 34 | discovery, cancel, err := api.Discover(a, nil) 35 | if err != nil { 36 | return err 37 | } 38 | defer cancel() 39 | 40 | go func() { 41 | 42 | for ev := range discovery { 43 | 44 | if ev.Type == adapter.DeviceRemoved { 45 | continue 46 | } 47 | 48 | dev, err := device.NewDevice1(ev.Path) 49 | if err != nil { 50 | log.Errorf("%s: %s", ev.Path, err) 51 | continue 52 | } 53 | 54 | if dev == nil { 55 | log.Errorf("%s: not found", ev.Path) 56 | continue 57 | } 58 | 59 | log.Infof("name=%s addr=%s rssi=%d", dev.Properties.Name, dev.Properties.Address, dev.Properties.RSSI) 60 | 61 | go func(ev *adapter.DeviceDiscovered) { 62 | err = handleBeacon(dev) 63 | if err != nil { 64 | log.Errorf("%s: %s", ev.Path, err) 65 | } 66 | }(ev) 67 | } 68 | 69 | }() 70 | 71 | ch := make(chan os.Signal) 72 | signal.Notify(ch, os.Interrupt, os.Kill) // get notified of all OS signals 73 | 74 | sig := <-ch 75 | log.Infof("Received signal [%v]; shutting down...\n", sig) 76 | 77 | return nil 78 | } 79 | 80 | func handleBeacon(dev *device.Device1) error { 81 | 82 | b, err := beacon.NewBeacon(dev) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | beaconUpdated, err := b.WatchDeviceChanges(context.Background()) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | isBeacon := <-beaconUpdated 93 | if !isBeacon { 94 | return nil 95 | } 96 | 97 | name := b.Device.Properties.Alias 98 | if name == "" { 99 | name = b.Device.Properties.Name 100 | } 101 | 102 | log.Debugf("Found beacon %s %s", b.Type, name) 103 | 104 | if b.IsEddystone() { 105 | ed := b.GetEddystone() 106 | switch ed.Frame { 107 | case eddystone.UID: 108 | log.Debugf( 109 | "Eddystone UID %s instance %s (%ddbi)", 110 | ed.UID, 111 | ed.InstanceUID, 112 | ed.CalibratedTxPower, 113 | ) 114 | break 115 | case eddystone.TLM: 116 | log.Debugf( 117 | "Eddystone TLM temp:%.0f batt:%d last reboot:%d advertising pdu:%d (%ddbi)", 118 | ed.TLMTemperature, 119 | ed.TLMBatteryVoltage, 120 | ed.TLMLastRebootedTime, 121 | ed.TLMAdvertisingPDU, 122 | ed.CalibratedTxPower, 123 | ) 124 | break 125 | case eddystone.URL: 126 | log.Debugf( 127 | "Eddystone URL %s (%ddbi)", 128 | ed.URL, 129 | ed.CalibratedTxPower, 130 | ) 131 | break 132 | } 133 | 134 | } 135 | if b.IsIBeacon() { 136 | ibeacon := b.GetIBeacon() 137 | log.Debugf( 138 | "IBeacon %s (%ddbi) (major=%d minor=%d)", 139 | ibeacon.ProximityUUID, 140 | ibeacon.MeasuredPower, 141 | ibeacon.Major, 142 | ibeacon.Minor, 143 | ) 144 | } 145 | 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /examples/hci_updown/hci_updown.go: -------------------------------------------------------------------------------- 1 | package hci_updown_example 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/muka/go-bluetooth/hw/linux/hci" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | //HciUpDownExample hciconfig up / down 13 | func Run(rawAdapterID string) error { 14 | 15 | log.Info("Turn down") 16 | 17 | adapterID, err := strconv.Atoi(strings.Replace(rawAdapterID, "hci", "", 1)) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | err = hci.Down(adapterID) 23 | if err != nil { 24 | return fmt.Errorf("Failed to stop device hci%d: %s", adapterID, err.Error()) 25 | } 26 | 27 | log.Info("Turn on") 28 | err = hci.Up(adapterID) 29 | if err != nil { 30 | return fmt.Errorf("Failed to start device hci%d: %s", adapterID, err.Error()) 31 | } 32 | 33 | log.Info("Done.") 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /examples/obex_push/obex_push.go: -------------------------------------------------------------------------------- 1 | //this example starts discovery on adapter 2 | //after discovery process GetDevices method 3 | //returns list of discovered devices 4 | //then with the help of mac address 5 | //connectivity starts 6 | //once sensors are connected it will 7 | //fetch sensor name,manufacturer detail, 8 | //firmware version, hardware version, model 9 | //and sensor data... 10 | 11 | package obex_push_example 12 | 13 | import ( 14 | "sync" 15 | "time" 16 | 17 | "github.com/muka/go-bluetooth/api" 18 | "github.com/muka/go-bluetooth/bluez/profile/obex" 19 | log "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var wg sync.WaitGroup 23 | 24 | func Run(targetAddress, filePath, adapterID string) error { 25 | 26 | a, err := api.GetAdapter(adapterID) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | dev, err := a.GetDeviceByAddress(targetAddress) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | log.Debugf("device %s (%s)", dev.Properties.Name, dev.Properties.Address) 37 | 38 | if dev == nil { 39 | return err 40 | } 41 | 42 | props, err := dev.GetProperties() 43 | if err != nil { 44 | return err 45 | } 46 | if !props.Paired { 47 | log.Debug("not paired") 48 | 49 | err = dev.Pair() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | } else { 55 | log.Debug("already paired") 56 | } 57 | 58 | sessionArgs := map[string]interface{}{} 59 | sessionArgs["Target"] = "opp" 60 | 61 | obexClient := obex.NewObexClient1() 62 | 63 | tries := 1 64 | maxRetry := 20 65 | var sessionPath string 66 | for tries < maxRetry { 67 | 68 | log.Debug("Create Session...") 69 | sessionPath, err = obexClient.CreateSession(targetAddress, sessionArgs) 70 | if err == nil { 71 | break 72 | } 73 | 74 | tries++ 75 | 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | if tries >= maxRetry { 81 | log.Fatal("Max tries reached") 82 | } 83 | 84 | log.Debug("Session created: ", sessionPath) 85 | 86 | obexSession := obex.NewObexSession1(sessionPath) 87 | sessionProps, err := obexSession.GetProperties() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | log.Debug("Source : ", sessionProps.Source) 93 | log.Debug("Destination : ", sessionProps.Destination) 94 | log.Debug("Channel : ", sessionProps.Channel) 95 | log.Debug("Target : ", sessionProps.Target) 96 | log.Debug("Root : ", sessionProps.Root) 97 | 98 | log.Debug("Init transmission on ", sessionPath) 99 | obexObjectPush := obex.NewObjectPush1(sessionPath) 100 | log.Debug("Send File: ", filePath) 101 | transPath, transProps, err := obexObjectPush.SendFile(filePath) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | log.Debug("Transmission initiated: ", transPath) 107 | log.Debug("Status : ", transProps.Status) 108 | log.Debug("Session : ", transProps.Session) 109 | log.Debug("Name : ", transProps.Name) 110 | log.Debug("Type : ", transProps.Type) 111 | log.Debug("Time : ", transProps.Time) 112 | log.Debug("Size : ", transProps.Size) 113 | log.Debug("Transferred : ", transProps.Transferred) 114 | log.Debug("Filename : ", transProps.Filename) 115 | 116 | for transProps.Transferred < transProps.Size { 117 | time.Sleep(1 * time.Second) 118 | 119 | obexTransfer := obex.NewObexTransfer1(transPath) 120 | transProps, err = obexTransfer.GetProperties() 121 | if err != nil { 122 | return err 123 | } 124 | transferedPercent := (100 / float64(transProps.Size)) * float64(transProps.Transferred) 125 | 126 | log.Debug("Progress : ", transferedPercent) 127 | } 128 | 129 | obexClient.RemoveSession(sessionPath) 130 | log.Debug(sessionPath) 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /examples/sensortag_info/sensortag_info.go: -------------------------------------------------------------------------------- 1 | //this example starts discovery on adapter 2 | //after discovery process GetDevices method 3 | //returns list of discovered devices 4 | //then with the help of mac address 5 | //connectivity starts 6 | //once sensors are connected it will 7 | //fetch sensor name,manufacturer detail, 8 | //firmware version, hardware version, model 9 | //and sensor data... 10 | 11 | package sensortag_info_example 12 | 13 | import ( 14 | "fmt" 15 | 16 | "github.com/muka/go-bluetooth/api" 17 | "github.com/muka/go-bluetooth/bluez/profile/battery" 18 | "github.com/muka/go-bluetooth/devices/sensortag" 19 | log "github.com/sirupsen/logrus" 20 | ) 21 | 22 | func Run(address, adapterID string) error { 23 | 24 | a, err := api.GetAdapter(adapterID) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | dev, err := a.GetDeviceByAddress(address) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if dev == nil { 35 | return fmt.Errorf("device %s not found", address) 36 | } 37 | 38 | sensorTag, err := sensortag.NewSensorTag(dev) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | name := sensorTag.Temperature.GetName() 44 | log.Debugf("sensor name: %s", name) 45 | 46 | name1 := sensorTag.Humidity.GetName() 47 | log.Debugf("sensor name: %s", name1) 48 | 49 | mpu := sensorTag.Mpu.GetName() 50 | log.Debugf("sensor name: %s", mpu) 51 | 52 | barometric := sensorTag.Barometric.GetName() 53 | log.Debugf("sensor name: %s", barometric) 54 | 55 | luxometer := sensorTag.Luxometer.GetName() 56 | log.Debugf("sensor name: %s", luxometer) 57 | 58 | devInfo, err := sensorTag.DeviceInfo.Read() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | log.Debug("FirmwareVersion: ", devInfo.FirmwareVersion) 64 | log.Debug("HardwareVersion: ", devInfo.HardwareVersion) 65 | log.Debug("Manufacturer: ", devInfo.Manufacturer) 66 | log.Debug("Model: ", devInfo.Model) 67 | 68 | batt, err := battery.NewBattery1(sensorTag.Device1.Path()) 69 | if err != nil { 70 | log.Errorf("Cannot load battery profile: %s", err) 71 | } else { 72 | perc, err1 := batt.GetPercentage() 73 | if err1 != nil { 74 | log.Errorf("Cannot load battery percentage: %s", err) 75 | } else { 76 | log.Debugf("Battery: %d%%", perc) 77 | } 78 | } 79 | 80 | err = sensorTag.Temperature.StartNotify() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = sensorTag.Humidity.StartNotify() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | err = sensorTag.Mpu.StartNotify(address) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | err = sensorTag.Barometric.StartNotify(address) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | err = sensorTag.Luxometer.StartNotify(address) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | go func() { 106 | // sensortag.SensorTagDataEvent 107 | for data := range sensorTag.Data() { 108 | log.Debugf("data received: %++v", data) 109 | } 110 | }() 111 | 112 | log.Debug("Waiting for data") 113 | 114 | select {} 115 | } 116 | -------------------------------------------------------------------------------- /examples/sensortag_temperature/sensortag_temperature.go: -------------------------------------------------------------------------------- 1 | package sensortag_temperature_example 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/muka/go-bluetooth/api" 9 | "github.com/muka/go-bluetooth/devices/sensortag" 10 | ) 11 | 12 | // example of reading temperature from a TI sensortag 13 | func Run(tagAddress, adapterID string) error { 14 | 15 | a, err := api.GetAdapter(adapterID) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | dev, err := a.GetDeviceByAddress(tagAddress) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if dev == nil { 26 | return fmt.Errorf("Device %s not found", tagAddress) 27 | } 28 | 29 | err = dev.Connect() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | sensorTag, err := sensortag.NewSensorTag(dev) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | // var readTemperature = func() { 40 | // temp, err := sensorTag.Temperature.Read() 41 | // if err != nil { 42 | // panic(err) 43 | // } 44 | // log.Printf("Temperature %v°", temp) 45 | // } 46 | 47 | var notifyTemperature = func(fn func(temperature float64)) { 48 | sensorTag.Temperature.StartNotify() 49 | select {} 50 | } 51 | 52 | // readTemperature() 53 | notifyTemperature(func(t float64) { 54 | log.Infof("Temperature update: %f", t) 55 | }) 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /examples/service/main.go: -------------------------------------------------------------------------------- 1 | package service_example 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/muka/go-bluetooth/hw" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func Run(adapterID string, mode string, hwaddr string) error { 11 | 12 | log.SetLevel(log.TraceLevel) 13 | 14 | btmgmt := hw.NewBtMgmt(adapterID) 15 | if len(os.Getenv("DOCKER")) > 0 { 16 | btmgmt.BinPath = "./bin/docker-btmgmt" 17 | } 18 | 19 | // set LE mode 20 | btmgmt.SetPowered(false) 21 | btmgmt.SetLe(true) 22 | btmgmt.SetBredr(false) 23 | btmgmt.SetPowered(true) 24 | 25 | if mode == "client" { 26 | return client(adapterID, hwaddr) 27 | } else { 28 | return serve(adapterID) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/service/server.go: -------------------------------------------------------------------------------- 1 | package service_example 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/muka/go-bluetooth/api/service" 7 | "github.com/muka/go-bluetooth/bluez/profile/agent" 8 | "github.com/muka/go-bluetooth/bluez/profile/gatt" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func serve(adapterID string) error { 13 | 14 | options := service.AppOptions{ 15 | AdapterID: adapterID, 16 | AgentCaps: agent.CapNoInputNoOutput, 17 | UUIDSuffix: "-0000-1000-8000-00805F9B34FB", 18 | UUID: "1234", 19 | } 20 | 21 | a, err := service.NewApp(options) 22 | if err != nil { 23 | return err 24 | } 25 | defer a.Close() 26 | 27 | a.SetName("go_bluetooth") 28 | 29 | log.Infof("HW address %s", a.Adapter().Properties.Address) 30 | 31 | if !a.Adapter().Properties.Powered { 32 | err = a.Adapter().SetPowered(true) 33 | if err != nil { 34 | log.Fatalf("Failed to power the adapter: %s", err) 35 | } 36 | } 37 | 38 | service1, err := a.NewService("2233") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = a.AddService(service1) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | char1, err := service1.NewChar("3344") 49 | if err != nil { 50 | return err 51 | } 52 | 53 | char1.Properties.Flags = []string{ 54 | gatt.FlagCharacteristicRead, 55 | gatt.FlagCharacteristicWrite, 56 | } 57 | 58 | char1.OnRead(service.CharReadCallback(func(c *service.Char, options map[string]interface{}) ([]byte, error) { 59 | log.Warnf("GOT READ REQUEST") 60 | return []byte{42}, nil 61 | })) 62 | 63 | char1.OnWrite(service.CharWriteCallback(func(c *service.Char, value []byte) ([]byte, error) { 64 | log.Warnf("GOT WRITE REQUEST") 65 | return value, nil 66 | })) 67 | 68 | err = service1.AddChar(char1) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | descr1, err := char1.NewDescr("4455") 74 | if err != nil { 75 | return err 76 | } 77 | 78 | descr1.Properties.Flags = []string{ 79 | gatt.FlagDescriptorRead, 80 | gatt.FlagDescriptorWrite, 81 | } 82 | 83 | descr1.OnRead(service.DescrReadCallback(func(c *service.Descr, options map[string]interface{}) ([]byte, error) { 84 | log.Warnf("GOT READ REQUEST") 85 | return []byte{42}, nil 86 | })) 87 | descr1.OnWrite(service.DescrWriteCallback(func(d *service.Descr, value []byte) ([]byte, error) { 88 | log.Warnf("GOT WRITE REQUEST") 89 | return value, nil 90 | })) 91 | 92 | err = char1.AddDescr(descr1) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | err = a.Run() 98 | if err != nil { 99 | return err 100 | } 101 | 102 | log.Infof("Exposed service %s", service1.Properties.UUID) 103 | 104 | timeout := uint32(6 * 3600) // 6h 105 | log.Infof("Advertising for %ds...", timeout) 106 | cancel, err := a.Advertise(timeout) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | defer cancel() 112 | 113 | wait := make(chan bool) 114 | go func() { 115 | time.Sleep(time.Duration(timeout) * time.Second) 116 | wait <- true 117 | }() 118 | 119 | <-wait 120 | 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /gen/Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: adapter advertising 3 | 4 | adapter: 5 | BASEDIR=.. \ 6 | FILE_FILTER=adapter \ 7 | xAPI_FILTER="Provisioner" \ 8 | xMETHOD_FILTER=Attach LOG_LEVEL=trace \ 9 | go run ./srcgen/main.go full --debug 10 | 11 | advertising: 12 | BASEDIR=.. \ 13 | FILE_FILTER=advertising-api \ 14 | xAPI_FILTER="Provisioner" \ 15 | xMETHOD_FILTER=Attach LOG_LEVEL=trace \ 16 | go run ./srcgen/main.go full --debug 17 | 18 | mesh: 19 | BASEDIR=.. \ 20 | FILE_FILTER=mesh-api \ 21 | xAPI_FILTER="Provisioner" \ 22 | xMETHOD_FILTER=Attach LOG_LEVEL=trace \ 23 | go run ./srcgen/main.go full --debug 24 | 25 | advertisement-monitor: 26 | BASEDIR=.. \ 27 | FILE_FILTER=advertisement-monitor-api \ 28 | xAPI_FILTER="Provisioner" \ 29 | xMETHOD_FILTER=Attach LOG_LEVEL=trace \ 30 | go run ./srcgen/main.go full --debug -------------------------------------------------------------------------------- /gen/README.md: -------------------------------------------------------------------------------- 1 | # BLueZ docs parser & generator 2 | 3 | This software parse `doc` bluez folder and output a set of struct to interact with the bluez DBus API. 4 | 5 | 6 | ## Usage: 7 | 8 | Env variables 9 | - `BASEDIR` base directory for docs and generation output, default ./ 10 | - `FILE_FILTER` filter docs files by name 11 | - `API_FILTER` filter docs API by name 12 | - `METHOD_FILTER` filter docs API method by name 13 | - `LOG_LEVEL` tune CLI log level output 14 | 15 | ## Notes 16 | 17 | - Generated files have a `gen_` prefix, followed by the API name 18 | - If a `.go` file exists, it will be skipped from the generation. This to allow custom code to live with generated one. 19 | - Generation process does not overwrite existing files, ensure to remove previously generated files. 20 | -------------------------------------------------------------------------------- /gen/bluez_api.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | 7 | "github.com/muka/go-bluetooth/gen/types" 8 | ) 9 | 10 | type BluezAPI struct { 11 | Version string 12 | Api []*types.ApiGroup 13 | } 14 | 15 | // Serialize store the structure as JSON 16 | func (g *BluezAPI) Serialize(destFile string) error { 17 | 18 | data, err := json.Marshal(g) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return ioutil.WriteFile(destFile, data, 0755) 24 | } 25 | 26 | // LoadJSON parse an ApiGroup from JSON definition 27 | func LoadJSON(srcFile string) (*BluezAPI, error) { 28 | 29 | b, err := ioutil.ReadFile(srcFile) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | a := new(BluezAPI) 35 | err = json.Unmarshal(b, a) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return a, nil 41 | } 42 | -------------------------------------------------------------------------------- /gen/filters/filter.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | type FilterContext int 10 | 11 | const ( 12 | FilterFile FilterContext = 0 13 | FilterApi FilterContext = iota 14 | FilterMethod FilterContext = iota 15 | ) 16 | 17 | const ( 18 | ParamFileFilter = "file_filter" 19 | ParamApiFilter = "api_filter" 20 | ParamMethodFilter = "method_filter" 21 | ) 22 | 23 | type Filter struct { 24 | Context FilterContext 25 | Value string 26 | } 27 | 28 | func NewFilter(value string, context FilterContext) Filter { 29 | return Filter{context, value} 30 | } 31 | 32 | func extractFilters(param string, paramType FilterContext) []Filter { 33 | list := []Filter{} 34 | 35 | // parse from env vars 36 | rawFilters := strings.Split(os.Getenv(strings.ToUpper(param)), ",") 37 | for _, filter := range rawFilters { 38 | filter = strings.Trim(filter, " ") 39 | if len(filter) == 0 { 40 | continue 41 | } 42 | list = append(list, NewFilter(filter, paramType)) 43 | } 44 | 45 | // parse from args 46 | if len(os.Args) > 1 { 47 | args := os.Args[1:] 48 | for _, arg := range args { 49 | if strings.Contains(arg, fmt.Sprintf("%s=", param)) { 50 | filters2 := strings.Split(strings.Split(arg, "=")[1], ",") 51 | for _, filter := range filters2 { 52 | filter = strings.Trim(filter, " ") 53 | if len(filter) == 0 { 54 | continue 55 | } 56 | 57 | list = append(list, NewFilter(filter, paramType)) 58 | } 59 | } 60 | } 61 | } 62 | 63 | return list 64 | } 65 | 66 | func ParseCliFilters() []Filter { 67 | filters := []Filter{} 68 | filters = append(filters, extractFilters(ParamFileFilter, FilterFile)...) 69 | filters = append(filters, extractFilters(ParamApiFilter, FilterApi)...) 70 | filters = append(filters, extractFilters(ParamMethodFilter, FilterMethod)...) 71 | return filters 72 | } 73 | -------------------------------------------------------------------------------- /gen/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path" 7 | "strings" 8 | 9 | "github.com/muka/go-bluetooth/gen" 10 | "github.com/muka/go-bluetooth/gen/util" 11 | log "github.com/sirupsen/logrus" 12 | "golang.org/x/tools/imports" 13 | ) 14 | 15 | // Generate go code from the API definition 16 | func Generate(bluezApi gen.BluezAPI, outDir string, debug bool, forceOverwrite bool) error { 17 | 18 | apiGroups := bluezApi.Api 19 | 20 | err := util.Mkdir(outDir) 21 | if err != nil { 22 | log.Errorf("Failed to mkdir %s: %s", outDir, err) 23 | return err 24 | } 25 | 26 | outDir += "/profile" 27 | err = util.Mkdir(outDir) 28 | if err != nil { 29 | log.Errorf("Failed to mkdir %s: %s", outDir, err) 30 | return err 31 | } 32 | 33 | errorsFile := path.Join(outDir, "gen_errors.go") 34 | if forceOverwrite || !util.Exists(errorsFile) { 35 | err = ErrorsTemplate(errorsFile, apiGroups) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | 41 | versionFile := path.Join(outDir, "gen_version.go") 42 | if forceOverwrite || !util.Exists(versionFile) { 43 | err = VersionTemplate(versionFile, bluezApi.Version) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | // filename = filepath.Join(outDir, "interfaces.go") 50 | // err = InterfacesTemplate(filename, apiGroups) 51 | // if err != nil { 52 | // return err 53 | // } 54 | 55 | for _, apiGroup := range apiGroups { 56 | 57 | if apiGroup == nil { 58 | continue 59 | } 60 | 61 | apiName := getApiPackage(apiGroup) 62 | dirpath := path.Join(outDir, apiName) 63 | err := util.Mkdir(dirpath) 64 | if err != nil { 65 | log.Errorf("Failed to mkdir %s: %s", dirpath, err) 66 | continue 67 | } 68 | 69 | rootFile := path.Join(dirpath, fmt.Sprintf("gen_%s.go", apiName)) 70 | 71 | if forceOverwrite || !util.Exists(rootFile) { 72 | err = RootTemplate(rootFile, apiGroup) 73 | if err != nil { 74 | log.Errorf("Failed to create %s: %s", rootFile, err) 75 | continue 76 | } 77 | if debug { 78 | log.Tracef("Wrote %s", rootFile) 79 | } 80 | } 81 | 82 | for _, api := range apiGroup.Api { 83 | 84 | if api == nil { 85 | continue 86 | } 87 | 88 | pts := strings.Split(api.Interface, ".") 89 | apiBaseName := pts[len(pts)-1] 90 | apiBaseName = strings.Replace(apiBaseName, " [experimental]", "", -1) 91 | 92 | apiFilename := path.Join(dirpath, fmt.Sprintf("%s.go", apiBaseName)) 93 | apiGenFilename := path.Join(dirpath, fmt.Sprintf("gen_%s.go", apiBaseName)) 94 | 95 | if util.Exists(apiFilename) { 96 | // log.Debugf("Skipped generation, API file exists: %s", apiFilename) 97 | continue 98 | } 99 | 100 | if !forceOverwrite && util.Exists(apiGenFilename) { 101 | // log.Debugf("Skipped, file exists: %s", apiGenFilename) 102 | continue 103 | } 104 | 105 | err1 := ApiTemplate(apiGenFilename, api, apiGroup) 106 | if err1 != nil { 107 | log.Errorf("Api generation failed %s: %s", api.Title, err1) 108 | return err1 109 | } 110 | if debug { 111 | log.Tracef("Wrote %s", apiGenFilename) 112 | } 113 | 114 | code, err := imports.Process(apiGenFilename, nil, nil) 115 | if err != nil { 116 | log.Tracef("format code: %s: %v", apiGenFilename, err) 117 | } 118 | 119 | if err := ioutil.WriteFile(apiGenFilename, code, 0644); err != nil { 120 | log.Tracef("rewrite with formatted code: %s", apiGenFilename) 121 | return err 122 | } 123 | } 124 | } 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /gen/generator/generator_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/muka/go-bluetooth/gen" 8 | "github.com/muka/go-bluetooth/gen/filters" 9 | "github.com/muka/go-bluetooth/gen/util" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGenerate(t *testing.T) { 14 | 15 | TplPath = "../../gen/generator/tpl/%s.go.tpl" 16 | outdir := "../../test/out" 17 | 18 | bluezApi, err := gen.Parse("../../src/bluez/doc", []filters.Filter{}, false) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | err = util.Mkdir("../../test") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | err = util.Mkdir(outdir) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | err = Generate(bluezApi, outdir, true, true) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | assert.DirExists(t, outdir) 38 | assert.DirExists(t, fmt.Sprintf("%s/profile/adapter", outdir)) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /gen/generator/generator_tpl.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/muka/go-bluetooth/gen/types" 9 | ) 10 | 11 | func RootTemplate(filename string, api *types.ApiGroup) error { 12 | 13 | fw, err := os.Create(filename) 14 | if err != nil { 15 | return fmt.Errorf("create file: %s", err) 16 | } 17 | 18 | apidoc := types.ApiGroupDoc{ 19 | ApiGroup: api, 20 | Package: getApiPackage(api), 21 | } 22 | 23 | apidoc.ApiGroup.Description = prepareDocs(apidoc.ApiGroup.Description, false, 0) 24 | 25 | tmpl := loadtpl("root") 26 | err = tmpl.Execute(fw, apidoc) 27 | if err != nil { 28 | return fmt.Errorf("write tpl: %s", err) 29 | } 30 | 31 | // log.Debugf("Created %s", filename) 32 | return nil 33 | } 34 | 35 | func ErrorsTemplate(filename string, apis []*types.ApiGroup) error { 36 | 37 | fw, err := os.Create(filename) 38 | if err != nil { 39 | return fmt.Errorf("create file: %s", err) 40 | } 41 | 42 | errors := []string{} 43 | for _, apiGroup := range apis { 44 | if apiGroup == nil { 45 | continue 46 | } 47 | for _, api := range apiGroup.Api { 48 | if api == nil { 49 | continue 50 | } 51 | for _, method := range api.Methods { 52 | if method == nil { 53 | continue 54 | } 55 | for _, err := range method.Errors { 56 | errors = appendIfMissing(errors, err) 57 | } 58 | } 59 | } 60 | } 61 | 62 | errorsList := types.BluezErrors{ 63 | List: make([]types.BluezError, len(errors)), 64 | } 65 | 66 | for i, err := range errors { 67 | base := err[strings.LastIndex(err, ".")+1:] 68 | if strings.Contains(err, "obex") { 69 | base = "Obex" + base 70 | } 71 | errorsList.List[i] = types.BluezError{ 72 | Name: err, 73 | Base: base, 74 | } 75 | } 76 | 77 | tmpl := loadtpl("errors") 78 | err = tmpl.Execute(fw, errorsList) 79 | if err != nil { 80 | return fmt.Errorf("tpl write: %s", err) 81 | } 82 | 83 | // log.Debugf("Created %s", filename) 84 | return nil 85 | } 86 | 87 | func InterfacesTemplate(filename string, apis []types.ApiGroup) error { 88 | 89 | fw, err := os.Create(filename) 90 | if err != nil { 91 | return fmt.Errorf("create file: %s", err) 92 | } 93 | 94 | interfaces := []types.InterfaceDoc{} 95 | for _, apiGroup := range apis { 96 | for _, api := range apiGroup.Api { 97 | 98 | pts := strings.Split(api.Interface, ".") 99 | ifaceName := pts[len(pts)-1] 100 | // org.bluez.obex.AgentManager1 101 | if len(pts) > 3 { 102 | ifaceName = "" 103 | for _, pt := range pts[2:] { 104 | ifaceName += strings.ToUpper(string(pt[0])) + pt[1:] 105 | } 106 | } 107 | 108 | iface := types.InterfaceDoc{ 109 | Title: api.Title, 110 | Name: ifaceName, 111 | Interface: api.Interface, 112 | } 113 | interfaces = append(interfaces, iface) 114 | } 115 | } 116 | 117 | ifaces := types.InterfacesDoc{ 118 | Interfaces: interfaces, 119 | } 120 | 121 | tmpl := loadtpl("interfaces") 122 | err = tmpl.Execute(fw, ifaces) 123 | if err != nil { 124 | return fmt.Errorf("tpl write: %s", err) 125 | } 126 | 127 | // log.Debugf("Created %s", filename) 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /gen/generator/generator_tpl_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func TestCastType(t *testing.T) { 11 | 12 | log.SetLevel(log.DebugLevel) 13 | 14 | typedef := "object" 15 | res := castType(typedef) 16 | 17 | if res != "dbus.ObjectPath" { 18 | t.Fatal(fmt.Sprintf("%s != %s", typedef, res)) 19 | } 20 | 21 | typedef = "array{objects, properties}" 22 | res = castType(typedef) 23 | 24 | if res != "[]dbus.ObjectPath, string" { 25 | t.Fatal(fmt.Sprintf("%s != %s", typedef, res)) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /gen/generator/generator_util.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "text/template" 8 | 9 | "github.com/muka/go-bluetooth/gen/override" 10 | "github.com/muka/go-bluetooth/gen/types" 11 | ) 12 | 13 | var TplPath = "gen/generator/tpl/%s.go.tpl" 14 | 15 | // rename variable name to avoid collision with Go languages 16 | func renameReserved(varname string) string { 17 | switch varname { 18 | case "type": 19 | return "type1" 20 | default: 21 | return varname 22 | } 23 | } 24 | 25 | func getBaseDir() string { 26 | baseDir := os.Getenv("BASEDIR") 27 | if baseDir == "" { 28 | baseDir = "." 29 | } 30 | return baseDir 31 | } 32 | 33 | func getTplPath() string { 34 | return fmt.Sprintf("%s/%s", getBaseDir(), TplPath) 35 | } 36 | 37 | func loadtpl(name string) *template.Template { 38 | return template.Must(template.ParseFiles(fmt.Sprintf(getTplPath(), name))) 39 | } 40 | 41 | func prepareDocs(src string, skipFirstComment bool, leftpad int) string { 42 | return src 43 | // lines := strings.Split(src, "\n") 44 | // result := []string{} 45 | // // comment := "// " 46 | // comment := "" 47 | // prefixLen := leftpad + len(comment) 48 | // fmtt := fmt.Sprintf("%%%ds%%s", prefixLen) 49 | // 50 | // for _, line := range lines { 51 | // line = strings.Trim(line, " \t\r") 52 | // if len(line) == 0 { 53 | // continue 54 | // } 55 | // 56 | // result = append(result, fmt.Sprintf(fmtt, comment, line)) 57 | // } 58 | // if skipFirstComment && len(result) > 0 && len(result[0]) > 3 { 59 | // result[0] = result[0][prefixLen:] 60 | // } 61 | // return strings.Join(result, "\n") 62 | } 63 | 64 | func getApiPackage(apiGroup *types.ApiGroup) string { 65 | apiName, ok := override.MapFile(apiGroup.FileName) 66 | if !ok { 67 | apiName = apiGroup.FileName 68 | } 69 | apiName = strings.ReplaceAll(apiName, "-api.txt", "") 70 | apiName = strings.ReplaceAll(apiName, "_api.txt", "") 71 | apiName = strings.ReplaceAll(apiName, "org.bluez.", "") 72 | apiName = strings.ReplaceAll(apiName, ".rst", "") 73 | apiName = strings.ReplaceAll(apiName, "-", "_") 74 | apiName = strings.ReplaceAll(apiName, " [experimental]", "") 75 | apiName = strings.ToLower(apiName) 76 | return apiName 77 | } 78 | 79 | func appendIfMissing(slice []string, i string) []string { 80 | for _, ele := range slice { 81 | if ele == i { 82 | return slice 83 | } 84 | } 85 | return append(slice, i) 86 | } 87 | -------------------------------------------------------------------------------- /gen/generator/generator_version.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | ) 7 | 8 | // VersionTemplate generate the version file 9 | func VersionTemplate(filename, version string) error { 10 | 11 | tpl := `// Code generated by go-bluetooth generator DO NOT EDIT. 12 | 13 | package profile 14 | 15 | const Version = %s 16 | ` 17 | 18 | err := ioutil.WriteFile(filename, []byte(fmt.Sprintf(tpl, version)), 0644) 19 | if err != nil { 20 | return fmt.Errorf("tpl write: %s", err) 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /gen/generator/tpl/errors.go.tpl: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | package profile 4 | 5 | import ( 6 | "github.com/godbus/dbus/v5" 7 | ) 8 | 9 | var ( 10 | {{- range .List }} 11 | // {{.Base}} map to {{.Name}} 12 | Err{{.Base}} = dbus.Error{ 13 | Name: "{{.Name}}", 14 | Body: []interface{}{"{{.Base}}"}, 15 | } 16 | {{- end }} 17 | ) 18 | -------------------------------------------------------------------------------- /gen/generator/tpl/interfaces.go.tpl: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | package profile 4 | 5 | const ( 6 | OrgBluezInterface = "org.bluez" 7 | {{- range .Interfaces }} 8 | //{{.Name}}Interface {{.Title}} 9 | {{.Name}}Interface = "{{.Interface}}" 10 | {{- end }} 11 | ) 12 | -------------------------------------------------------------------------------- /gen/generator/tpl/root.go.tpl: -------------------------------------------------------------------------------- 1 | // Code generated by go-bluetooth generator DO NOT EDIT. 2 | 3 | /* 4 | {{.Name}} [{{.FileName}}] 5 | {{.Description}} 6 | */ 7 | package {{.Package}} 8 | -------------------------------------------------------------------------------- /gen/override/constructor.go: -------------------------------------------------------------------------------- 1 | package override 2 | 3 | type ConstructorOverride struct { 4 | AdapterAsArgument bool 5 | } 6 | 7 | var constructorOverrides = map[string][]ConstructorOverride{ 8 | "org.bluez.Adapter1": { 9 | ConstructorOverride{ 10 | AdapterAsArgument: true, 11 | }, 12 | }, 13 | "org.bluez.GattManager1": { 14 | ConstructorOverride{ 15 | AdapterAsArgument: true, 16 | }, 17 | }, 18 | "org.bluez.LEAdvertisingManager1": { 19 | ConstructorOverride{ 20 | AdapterAsArgument: true, 21 | }, 22 | }, 23 | "org.bluez.MediaControl1": { 24 | ConstructorOverride{ 25 | AdapterAsArgument: true, 26 | }, 27 | }, 28 | } 29 | 30 | func GetConstructorsOverrides(iface string) ([]ConstructorOverride, bool) { 31 | if val, ok := constructorOverrides[iface]; ok { 32 | return val, ok 33 | } 34 | return []ConstructorOverride{}, false 35 | } 36 | -------------------------------------------------------------------------------- /gen/override/expose_props.go: -------------------------------------------------------------------------------- 1 | package override 2 | 3 | var ExposePropertiesInterface = map[string]bool{ 4 | "org.bluez.AgentManager1": false, 5 | "org.bluez.Agent1": false, 6 | "org.bluez.ProfileManager1": false, 7 | "org.bluez.Profile1": false, 8 | } 9 | 10 | // ExposeProperties expose Properties interface to the struct 11 | func ExposeProperties(iface string) bool { 12 | if val, ok := ExposePropertiesInterface[iface]; ok { 13 | return val 14 | } 15 | return true 16 | } 17 | -------------------------------------------------------------------------------- /gen/override/properties.go: -------------------------------------------------------------------------------- 1 | package override 2 | 3 | func GetPropertiesOverride(iface string) (map[string]string, bool) { 4 | if props, ok := PropertyTypes[iface]; ok { 5 | return props, ok 6 | } 7 | return map[string]string{}, false 8 | } 9 | 10 | var PropertyTypes = map[string]map[string]string{ 11 | "org.bluez.Device1": { 12 | "ServiceData": "map[string]interface{}", 13 | "ManufacturerData": "map[uint16]interface{}", 14 | "Sets": "[]SetsItem", 15 | }, 16 | "org.bluez.DeviceSet1": { 17 | "Devices": "[]dbus.ObjectPath", 18 | }, 19 | "org.bluez.GattCharacteristic1": { 20 | "Value": "[]byte `dbus:\"emit\"`", 21 | "Descriptors": "[]dbus.ObjectPath", 22 | "WriteAcquired": "bool `dbus:\"ignore\"`", 23 | "NotifyAcquired": "bool `dbus:\"ignore\"`", 24 | }, 25 | "org.bluez.GattDescriptor1": { 26 | "Value": "[]byte `dbus:\"emit\"`", 27 | "Characteristic": "dbus.ObjectPath", 28 | }, 29 | "org.bluez.GattService1": { 30 | "Characteristics": "[]dbus.ObjectPath `dbus:\"emit\"`", 31 | "Includes": "[]dbus.ObjectPath `dbus:\"omitEmpty\"`", 32 | "Device": "dbus.ObjectPath `dbus:\"ignore=IsService\"`", 33 | "IsService": "bool `dbus:\"ignore\"`", 34 | }, 35 | "org.bluez.LEAdvertisement1": { 36 | // dbus type: (yv) dict of byte variant (array of bytes) 37 | "Data": "map[byte]interface{}", 38 | // dbus type: (qv) dict of uint16 variant (array of bytes) 39 | "ManufacturerData": "map[uint16]interface{}", 40 | // dbus type: (s[v]) dict of string variant (array of bytes) 41 | "ServiceData": "map[string]interface{}", 42 | // SecondaryChannel, if set on 5.54 cause a parsing exception 43 | "SecondaryChannel": "string `dbus:\"omitEmpty\"`", 44 | }, 45 | "org.bluez.AdvertisementMonitor1": { 46 | // array{(uint8, uint8, array{byte})} 47 | "Patterns": "[]Pattern", 48 | }, 49 | "org.bluez.MediaPlayer1": { 50 | "Track": "Track", 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /gen/override/types.go: -------------------------------------------------------------------------------- 1 | package override 2 | 3 | var typesMap = map[string]string{ 4 | //mesh-api object node, array{byte, array{(uint16, dict)}} configuration Attach(object app_root, uint64 token) 5 | "object node, array{byte, array{(uint16, dict)}} configuration": "dbus.ObjectPath, []ConfigurationItem", 6 | 7 | "array{(uint16 id, dict caps)}": "[]ConfigurationItem", 8 | // mesh-api array{(uint16, uint16)} VendorModels [read-only] 9 | "array{(uint16, uint16)}": "[]VendorItem", 10 | 11 | "array{(uint16 vendor, uint16 id, dict options)}": "[]VendorOptionsItem", 12 | // obex-api array{string vcard, string name} List(dict filters) 13 | "array{string vcard, string name}": "[]VCardItem", 14 | // obex-api 15 | "object, dict": "dbus.ObjectPath, map[string]interface{}", 16 | // obex-api array{object, dict} ListMessages(string folder, dict filter) 17 | "array{object, dict}": "[]Message", 18 | // gatt AcquireWrite 19 | "fd, uint16": "dbus.UnixFD, uint16", 20 | // media-api array{objects, properties} ListItems(dict filter) 21 | "array{objects, properties}": "[]Item", 22 | // media-api fd, uint16, uint16 Acquire() 23 | "fd, uint16, uint16": "dbus.UnixFD, uint16, uint16", 24 | // advertisement_monitor array{(uint8, uint8, array{byte})} Patterns [read-only, optional] 25 | "array{(uint8, uint8, array{byte})}": "[]Pattern", 26 | // advertisement_monitor Uint/Int with uppercase 27 | "Uint16": "uint16", 28 | "Int16": "int16", 29 | } 30 | 31 | //MapType map a raw type literal otherwise difficult to parse 32 | func MapType(rawtype string) (string, bool) { 33 | res, ok := typesMap[rawtype] 34 | return res, ok 35 | } 36 | -------------------------------------------------------------------------------- /gen/parser.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/muka/go-bluetooth/gen/filters" 7 | "github.com/muka/go-bluetooth/gen/parser" 8 | "github.com/muka/go-bluetooth/gen/types" 9 | "github.com/muka/go-bluetooth/gen/util" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Parse bluez DBus API docs 14 | func Parse(docsDir string, filtersList []filters.Filter, debug bool) (BluezAPI, error) { 15 | files, err := util.ListFiles(docsDir) 16 | if err != nil { 17 | return BluezAPI{}, err 18 | } 19 | apis := make([]*types.ApiGroup, 0) 20 | for _, file := range files { 21 | 22 | keep := true 23 | if len(filtersList) > 0 { 24 | keep = false 25 | for _, filter1 := range filtersList { 26 | if filter1.Context != filters.FilterFile { 27 | continue 28 | } 29 | if strings.Contains(file, filter1.Value) { 30 | keep = true 31 | if debug { 32 | log.Debugf("[filter %s] Keep %s", filter1.Value, file) 33 | } 34 | break 35 | } 36 | } 37 | } 38 | 39 | if !keep { 40 | continue 41 | } 42 | 43 | apiGroupParser := parser.NewApiGroupParser(debug, filtersList) 44 | apiGroup, err := apiGroupParser.Parse(file) 45 | if err != nil { 46 | log.Errorf("Failed to load %s, skipped", file) 47 | continue 48 | } 49 | apis = append(apis, apiGroup) 50 | } 51 | 52 | version, err := util.GetGitVersion(docsDir) 53 | if err != nil { 54 | log.Errorf("Failed to parse version: %s", err) 55 | } 56 | 57 | return BluezAPI{ 58 | Version: version, 59 | Api: apis, 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /gen/parser/api.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/muka/go-bluetooth/gen/filters" 8 | "github.com/muka/go-bluetooth/gen/types" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ApiParser struct { 13 | model *types.Api 14 | debug bool 15 | filter []filters.Filter 16 | } 17 | 18 | // NewApiParser parser for Api 19 | func NewApiParser(debug bool, filter []filters.Filter) ApiParser { 20 | parser := ApiParser{ 21 | filter: filter, 22 | debug: debug, 23 | model: new(types.Api), 24 | } 25 | return parser 26 | } 27 | 28 | func (g *ApiParser) log(msg string) { 29 | log.Debugf("%s: %s", g.model.Title, msg) 30 | } 31 | 32 | func (g *ApiParser) Parse(raw []byte) (*types.Api, error) { 33 | 34 | var err error 35 | api := g.model 36 | 37 | // title & description 38 | re := regexp.MustCompile(`(?s)(.+)\n[=]+\n?(.*)\nService|Interface *`) 39 | matches := re.FindSubmatchIndex(raw) 40 | 41 | api.Title = string(raw[matches[2]:matches[3]]) 42 | api.Description = string(raw[matches[4]:matches[5]]) 43 | 44 | if g.debug { 45 | log.Debugf("= %s", api.Title) 46 | } 47 | 48 | if len(g.filter) > 0 { 49 | skipItem := false 50 | for _, filter := range g.filter { 51 | if filter.Context != filters.FilterApi { 52 | continue 53 | } 54 | skipItem = !strings.Contains( 55 | strings.ToLower(api.Title), strings.ToLower(filter.Value)) 56 | } 57 | if skipItem { 58 | log.Debugf("Skip filtered API %s", api.Title) 59 | return nil, nil 60 | } else { 61 | log.Debugf("Keep filtered API %s", api.Title) 62 | } 63 | 64 | } 65 | 66 | raw = raw[matches[5]:] 67 | 68 | // service interface object 69 | re = regexp.MustCompile(`Service[ \t]*((?s).+)\nInterface[ \t]*((?s).+)\nObject path[ \t]*((?s).+?)\n\n`) 70 | matches = re.FindSubmatchIndex(raw) 71 | 72 | g.model = api 73 | api.Service = string(raw[matches[2]:matches[3]]) 74 | api.Interface = strings.Replace(string(raw[matches[4]:matches[5]]), " [experimental]", "", -1) 75 | api.ObjectPath = string(raw[matches[6]:matches[7]]) 76 | 77 | if g.debug { 78 | log.Debugf("\tService %s", api.Service) 79 | log.Debugf("\tInterface %s", api.Interface) 80 | log.Debugf("\tObjectPath %s", api.ObjectPath) 81 | } 82 | 83 | raw = raw[matches[7]:] 84 | 85 | methods, err := g.ParseMethods(raw) 86 | if err != nil { 87 | return api, err 88 | } 89 | api.Methods = methods 90 | 91 | properties, err := g.ParseProperties(raw) 92 | if err != nil { 93 | return api, err 94 | } 95 | api.Properties = properties 96 | 97 | signals, err := g.ParseSignals(raw) 98 | if err != nil { 99 | return api, err 100 | } 101 | api.Signals = signals 102 | 103 | return api, nil 104 | } 105 | -------------------------------------------------------------------------------- /gen/parser/api_properties.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/muka/go-bluetooth/gen/types" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func (g *ApiParser) ParseProperties(raw []byte) ([]*types.Property, error) { 11 | 12 | var err error = nil 13 | props := make([]*types.Property, 0) 14 | slices := make([][]byte, 0) 15 | 16 | re := regexp.MustCompile(`(?s)\nProperties(.+)\n\n?(Filters|)?[ \t]?`) 17 | matches1 := re.FindSubmatch(raw) 18 | 19 | if len(matches1) == 0 { 20 | return props, err 21 | } 22 | 23 | for _, propsRaw := range matches1[1:] { 24 | 25 | // string Modalias [readonly, optional] 26 | re1 := regexp.MustCompile(`(?s)[ \t]*` + propBaseRegexp + `.*?\n`) 27 | matches2 := re1.FindAllSubmatchIndex(propsRaw, -1) 28 | 29 | // log.Debugf("1*** %d", matches2) 30 | 31 | // if len(matches2) == 0 { 32 | // re1 := regexp.MustCompile(`[ \t]*(bool|byte|string|uint|dict|array.*) ([A-Za-z0-9_]+?)( ?) *\n`) 33 | // matches2 := re1.FindAllSubmatchIndex(propsRaw, -1) 34 | // } 35 | 36 | // log.Debugf("2 *** %d", matches2) 37 | 38 | if len(matches2) == 1 { 39 | if len(propsRaw) > 0 { 40 | // log.Debugf("ADD single *** %s", propsRaw) 41 | slices = append(slices, propsRaw) 42 | } 43 | } else { 44 | prevPos := -1 45 | for i := 0; i < len(matches2); i++ { 46 | 47 | if prevPos == -1 { 48 | prevPos = matches2[i][0] 49 | continue 50 | } 51 | 52 | nextPos := matches2[i][0] 53 | propRaw := propsRaw[prevPos:nextPos] 54 | prevPos = nextPos 55 | 56 | if len(propRaw) > 0 { 57 | slices = append(slices, propRaw) 58 | } 59 | 60 | // keep the last one 61 | lastItem := len(matches2) - 1 62 | if i == lastItem { 63 | propRaw = propsRaw[matches2[lastItem][0]:] 64 | if len(propRaw) > 0 { 65 | slices = append(slices, propRaw) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | if g.debug { 73 | log.Debug("\tProperties:") 74 | } 75 | 76 | for _, propRaw := range slices { 77 | propertyParser := NewPropertyParser(g.debug) 78 | prop, err := propertyParser.Parse(propRaw) 79 | if err != nil { 80 | log.Warnf("Skipped property: %s", err) 81 | continue 82 | } 83 | props = append(props, prop) 84 | } 85 | 86 | return props, nil 87 | } 88 | -------------------------------------------------------------------------------- /gen/parser/api_signals.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/muka/go-bluetooth/gen/types" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func (g *ApiParser) ParseSignals(raw []byte) ([]*types.Method, error) { 11 | 12 | var err error 13 | methods := make([]*types.Method, 0) 14 | slices := make([][]byte, 0) 15 | 16 | re := regexp.MustCompile(`(?s)Signals(.+)\n\nProperties`) 17 | matches1 := re.FindSubmatch(raw) 18 | 19 | if len(matches1) == 0 { 20 | return methods, err 21 | } 22 | 23 | // if len(matches1) == 0 { 24 | // re = regexp.MustCompile(`(?s)[ \t\n]+(.+)`) 25 | // matches1 = re.FindSubmatch(raw) 26 | // if len(matches1) == 1 { 27 | // matches1 = append(matches1, matches1[0]) 28 | // } 29 | // } 30 | 31 | // log.Debugf("matches1 %s", matches1[1:]) 32 | // log.Debugf("%s", matches1) 33 | 34 | for _, methodsRaw := range matches1[1:] { 35 | 36 | re1 := regexp.MustCompile(`[ \t]*?(.*?)? ?([^ ]+)\(([^)]+?)?\) ?(.*)`) 37 | matches2 := re1.FindAllSubmatchIndex(methodsRaw, -1) 38 | 39 | if len(matches2) == 1 { 40 | if len(methodsRaw) > 0 { 41 | slices = append(slices, methodsRaw) 42 | } 43 | } else { 44 | prevPos := -1 45 | for i := 0; i < len(matches2); i++ { 46 | 47 | if prevPos == -1 { 48 | prevPos = matches2[i][0] 49 | continue 50 | } 51 | 52 | nextPos := matches2[i][0] 53 | methodRaw := methodsRaw[prevPos:nextPos] 54 | prevPos = nextPos 55 | 56 | if len(methodRaw) > 0 { 57 | slices = append(slices, methodRaw) 58 | } 59 | 60 | // keep the last one 61 | lastItem := len(matches2) - 1 62 | if i == lastItem { 63 | methodRaw = methodsRaw[matches2[lastItem][0]:] 64 | if len(methodRaw) > 0 { 65 | slices = append(slices, methodRaw) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | if g.debug { 73 | log.Debug("\nSignals:") 74 | } 75 | for _, methodRaw := range slices { 76 | methodParser := NewMethodParser(g.debug) 77 | method, err := methodParser.Parse(methodRaw) 78 | if err != nil { 79 | log.Debugf("Skip signal: %s", err) 80 | continue 81 | } 82 | methods = append(methods, method) 83 | } 84 | 85 | return methods, nil 86 | } 87 | -------------------------------------------------------------------------------- /gen/parser/method.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/muka/go-bluetooth/gen/types" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // NewMethodParser init a MethodParser 14 | func NewMethodParser(debug bool) MethodParser { 15 | return MethodParser{ 16 | model: new(types.Method), 17 | debug: debug, 18 | } 19 | } 20 | 21 | //MethodParser wrap a parsable method 22 | type MethodParser struct { 23 | model *types.Method 24 | debug bool 25 | } 26 | 27 | //Parse a method text 28 | func (g *MethodParser) Parse(raw []byte) (*types.Method, error) { 29 | 30 | var err error = nil 31 | method := g.model 32 | 33 | re := regexp.MustCompile(`[\t]{1,}(.*?)(?: |\n\t{2,})?(\w+)\(([^)]*)\) ?(.*?)\n((?s).+)`) 34 | matches1 := re.FindAllSubmatch(raw, -1) 35 | 36 | for _, matches2 := range matches1 { 37 | 38 | rtype := string(matches2[1]) 39 | if len(rtype) > 7 && rtype[:7] == "Methods" { 40 | rtype = rtype[7:] 41 | } 42 | 43 | rtype = strings.Trim(rtype, " \t") 44 | 45 | for _, srtype := range strings.Split(rtype, ",") { 46 | if len(strings.Split(strings.Trim(srtype, " "), " ")) > 2 { 47 | // log.Warnf("****** %s | %s", strings.Trim(srtype, " "), strings.Split(strings.Trim(srtype, " "), " ")) 48 | return g.model, fmt.Errorf("Method %s return type contains space: `%s`", method.Name, rtype) 49 | } 50 | } 51 | 52 | if len(rtype) > 20 { 53 | log.Warnf("Return type value is too long? `%s`", rtype) 54 | } 55 | 56 | method.ReturnType = rtype 57 | 58 | name := string(matches2[2]) 59 | method.Name = strings.Trim(name, " \t") 60 | 61 | args := []types.Arg{} 62 | if len(matches2[3]) > 0 { 63 | 64 | args1 := string(matches2[3]) 65 | if args1 == "void" { 66 | continue 67 | } 68 | 69 | argslist := strings.Split(args1, ",") 70 | for _, arg := range argslist { 71 | arg = strings.Trim(arg, " ") 72 | argsparts := strings.Split(arg, " ") 73 | if argsparts[0] == "void" { 74 | continue 75 | } 76 | if len(argsparts) < 2 { 77 | if argsparts[0] == "fd" { 78 | argsparts = []string{"int32", argsparts[0]} 79 | } else { 80 | argsparts = []string{"", argsparts[0]} 81 | } 82 | } 83 | 84 | argType := strings.Trim(argsparts[0], " \t\n") 85 | arg := types.Arg{ 86 | Type: argType, 87 | Name: argsparts[1], 88 | } 89 | args = append(args, arg) 90 | } 91 | } 92 | method.Args = args 93 | method.Docs = string(matches2[5]) 94 | } 95 | 96 | // 97 | re2 := regexp.MustCompile(`(?s)(org\.bluez\.Error\.\w+)`) 98 | matches2 := re2.FindAllSubmatch(raw, -1) 99 | 100 | if len(matches2) >= 1 { 101 | for _, merr := range matches2[0] { 102 | method.Errors = append(method.Errors, string(merr)) 103 | } 104 | } 105 | 106 | if method.Name == "" { 107 | return method, errors.New("Empty method name") 108 | } 109 | 110 | // if strings.Contains(method.ReturnType, "Handle") { 111 | // fmt.Println(method) 112 | // os.Exit(1) 113 | // } 114 | 115 | if g.debug { 116 | log.Debugf("\t - %s", method) 117 | } 118 | 119 | return method, err 120 | } 121 | -------------------------------------------------------------------------------- /gen/parser/property.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/muka/go-bluetooth/gen/types" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const propBaseRegexp = `(bool|boolean|byte|string|[i|I]nt16|[U|u]int16|uint16_t|uint32|dict|object|array\{.*?) ([A-Z].+?)` 13 | 14 | type PropertyParser struct { 15 | model *types.Property 16 | debug bool 17 | } 18 | 19 | // NewPropertyParser 20 | func NewPropertyParser(debug bool) PropertyParser { 21 | p := PropertyParser{ 22 | model: new(types.Property), 23 | debug: debug, 24 | } 25 | return p 26 | } 27 | 28 | func (g *PropertyParser) Parse(raw []byte) (*types.Property, error) { 29 | 30 | var err error 31 | property := g.model 32 | // log.Debugf("prop raw -> %s", raw) 33 | 34 | re1 := regexp.MustCompile(`[ \t]*?` + propBaseRegexp + `( \[[^\]]*\].*)?\n((?s).+)`) 35 | matches2 := re1.FindAllSubmatch(raw, -1) 36 | 37 | // log.Warnf("m1 %s", matches2) 38 | 39 | if len(matches2) == 0 || len(matches2[0]) == 1 { 40 | re1 = regexp.MustCompile(`[ \t]*?` + propBaseRegexp + `\n((?s).+)`) 41 | matches2 = re1.FindAllSubmatch(raw, -1) 42 | // log.Warnf("m2 %s", matches2) 43 | } 44 | 45 | if len(matches2) == 0 { 46 | log.Debugf("prop raw -> %s", raw) 47 | return property, errors.New("no property found") 48 | } 49 | 50 | flags := []types.Flag{} 51 | flagListRaw := string(matches2[0][3]) 52 | flagList := strings.Split(strings.Trim(flagListRaw, "[] "), ",") 53 | 54 | for _, f := range flagList { 55 | 56 | // track server-only flags for gatt API 57 | if strings.Contains(f, "Server Only") { 58 | flags = append(flags, types.FlagServerOnly) 59 | } 60 | 61 | // int16 Handle [read-write, optional] (Server Only) 62 | if strings.Contains(f, "]") { 63 | f = strings.Split(f, "]")[0] 64 | } 65 | 66 | f = strings.Trim(f, " []") 67 | if f != "" { 68 | var flag types.Flag = 0 69 | switch f { 70 | case "readonly": 71 | case "read-only": 72 | flag = types.FlagReadOnly 73 | case "writeonly": 74 | case "write-only": 75 | flag = types.FlagWriteOnly 76 | case "readwrite": 77 | case "read-write": 78 | case "read/write": 79 | flag = types.FlagReadWrite 80 | case "experimental": 81 | case "Experimental": 82 | flag = types.FlagExperimental 83 | case "optional": 84 | flag = types.FlagOptional 85 | default: 86 | log.Warnf("Unknown flag %s", f) 87 | } 88 | 89 | if flag > 0 { 90 | flags = append(flags, flag) 91 | } 92 | } 93 | } 94 | 95 | docs := string(matches2[0][4]) 96 | docs = strings.Replace(docs, " \t\n", "", -1) 97 | docs = strings.Trim(docs, " \t\n") 98 | 99 | name := string(matches2[0][2]) 100 | 101 | if strings.Contains(name, "optional") { 102 | name = strings.Replace(name, " (optional)", "", -1) 103 | docs = "(optional) " + docs 104 | flags = append(flags, types.FlagOptional) 105 | } 106 | 107 | name = strings.Replace(name, " \t\n", "", -1) 108 | 109 | // theese bastards fucks up with properties names 110 | if nameParts := strings.Split(name, " "); len(nameParts) > 1 { 111 | name = nameParts[0] 112 | } 113 | 114 | property.Type = string(matches2[0][1]) 115 | property.Name = name 116 | property.Flags = flags 117 | property.Docs = docs 118 | 119 | if g.debug { 120 | log.Debugf("\t - %s", property) 121 | } 122 | return property, err 123 | } 124 | -------------------------------------------------------------------------------- /gen/parser_test.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/muka/go-bluetooth/gen/filters" 8 | "github.com/muka/go-bluetooth/gen/util" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestParse(t *testing.T) { 13 | api, err := Parse("../src/bluez/doc", []filters.Filter{}, false) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | assert.NotEmpty(t, api.Version) 18 | assert.NotEmpty(t, api.Api) 19 | } 20 | 21 | func TestSerialization(t *testing.T) { 22 | 23 | api, err := Parse("../src/bluez/doc", []filters.Filter{}, false) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | destDir := "../test/" 29 | jsonFile := path.Join(destDir, "test.json") 30 | 31 | err = util.Mkdir(destDir) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | err = api.Serialize(jsonFile) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | api1, err := LoadJSON(jsonFile) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | assert.Equal(t, api.Version, api1.Version) 47 | } 48 | -------------------------------------------------------------------------------- /gen/types/generator.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type BluezError struct { 4 | Name string 5 | Base string 6 | Error string 7 | } 8 | 9 | type BluezErrors struct { 10 | List []BluezError 11 | } 12 | 13 | type MethodDoc struct { 14 | *Method 15 | ArgsList string 16 | ParamsList string 17 | SingleReturn bool 18 | ReturnVarsDefinition string 19 | ReturnVarsRefs string 20 | ReturnVarsList string 21 | } 22 | 23 | type InterfaceDoc struct { 24 | Title string 25 | Name string 26 | Interface string 27 | } 28 | 29 | type InterfacesDoc struct { 30 | Interfaces []InterfaceDoc 31 | } 32 | 33 | type PropertyDoc struct { 34 | *Property 35 | RawType string 36 | RawTypeInitializer string 37 | ReadOnly bool 38 | WriteOnly bool 39 | ReadWrite bool 40 | } 41 | 42 | type ApiGroupDoc struct { 43 | *ApiGroup 44 | Package string 45 | } 46 | 47 | type ApiDoc struct { 48 | Api *Api 49 | InterfaceName string 50 | Package string 51 | Properties []PropertyDoc 52 | Methods []MethodDoc 53 | Imports string 54 | Constructors []Constructor 55 | ExposeProperties bool 56 | } 57 | 58 | type Constructor struct { 59 | Service string 60 | Role string 61 | ObjectPath string 62 | Args string 63 | ArgsDocs string 64 | Docs []string 65 | } 66 | -------------------------------------------------------------------------------- /gen/types/parser.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type ApiGroup struct { 9 | FileName string 10 | Name string 11 | Description string 12 | Api []*Api 13 | debug bool 14 | } 15 | 16 | type Api struct { 17 | Title string 18 | Description string 19 | Service string 20 | Interface string 21 | ObjectPath string 22 | Methods []*Method 23 | // those are currently avail only in health-api 24 | Signals []*Method 25 | Properties []*Property 26 | } 27 | 28 | type Flag int 29 | 30 | const ( 31 | FlagReadOnly Flag = iota + 1 32 | FlagWriteOnly 33 | FlagReadWrite 34 | FlagExperimental 35 | FlagOptional 36 | FlagServerOnly 37 | ) 38 | 39 | type Arg struct { 40 | Type string 41 | Name string 42 | } 43 | 44 | func (a *Arg) String() string { 45 | return fmt.Sprintf("%s %s", a.Type, a.Name) 46 | } 47 | 48 | type Method struct { 49 | Name string 50 | ReturnType string 51 | Args []Arg 52 | Errors []string 53 | Docs string 54 | } 55 | 56 | func (m *Method) String() string { 57 | args := []string{} 58 | for _, arg := range m.Args { 59 | args = append(args, arg.String()) 60 | } 61 | return fmt.Sprintf("%s %s(%s)", m.ReturnType, m.Name, strings.Join(args, ", ")) 62 | } 63 | 64 | type Property struct { 65 | Name string 66 | Type string 67 | Docs string 68 | Flags []Flag 69 | } 70 | 71 | func (p *Property) String() string { 72 | flags := []string{} 73 | for _, flag := range p.Flags { 74 | flagLabel := "" 75 | switch flag { 76 | case FlagReadOnly: 77 | flagLabel = "readonly" 78 | break 79 | case FlagWriteOnly: 80 | flagLabel = "writeonly" 81 | break 82 | case FlagReadWrite: 83 | flagLabel = "readwrite" 84 | break 85 | case FlagExperimental: 86 | flagLabel = "experimental" 87 | break 88 | case FlagOptional: 89 | flagLabel = "optional" 90 | break 91 | case FlagServerOnly: 92 | flagLabel = "server-only" 93 | break 94 | } 95 | if flagLabel != "" { 96 | flags = append(flags, flagLabel) 97 | } 98 | } 99 | 100 | flagsStr := "" 101 | if len(flags) > 0 { 102 | flagsStr = fmt.Sprintf("[%s]", strings.Join(flags, ", ")) 103 | } 104 | 105 | return fmt.Sprintf("%s %s %s", p.Type, p.Name, flagsStr) 106 | } 107 | -------------------------------------------------------------------------------- /gen/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Mkdir Create a dir if not exists 15 | func Mkdir(dirpath string) error { 16 | err := os.Mkdir(dirpath, 0755) 17 | if err != nil && !os.IsExist(err) { 18 | return err 19 | } 20 | return nil 21 | } 22 | 23 | // ListFiles return a list of bluez api txt 24 | func ListFiles(dir string) ([]string, error) { 25 | 26 | list := make([]string, 0) 27 | 28 | if !Exists(dir) { 29 | return list, fmt.Errorf("Doc dir not found %s", dir) 30 | } 31 | 32 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 33 | if info.IsDir() { 34 | return nil 35 | } 36 | 37 | if strings.HasSuffix(path, "mgmt-api.txt") { 38 | return nil 39 | } 40 | 41 | if !strings.HasSuffix(path, "-api.txt") && !strings.HasPrefix(filepath.Base(path), "org.bluez.") { 42 | return nil 43 | } 44 | 45 | list = append(list, path) 46 | return nil 47 | }) 48 | 49 | if err != nil { 50 | log.Errorf("Failed to list files: %s", err) 51 | return list, nil 52 | } 53 | 54 | return list, nil 55 | } 56 | 57 | // ReadFile read a file content 58 | func ReadFile(srcFile string) ([]byte, error) { 59 | file, err := os.Open(srcFile) 60 | if err != nil { 61 | return []byte{}, err 62 | } 63 | defer file.Close() 64 | 65 | b, err := ioutil.ReadAll(file) 66 | if err != nil { 67 | return []byte{}, err 68 | } 69 | 70 | return b, nil 71 | } 72 | 73 | // Exists reports whether the named file or directory exists. 74 | func Exists(name string) bool { 75 | if _, err := os.Stat(name); err != nil { 76 | if os.IsNotExist(err) { 77 | return false 78 | } 79 | } 80 | return true 81 | } 82 | 83 | // GetGitVersion return the docs git version 84 | func GetGitVersion(docsDir string) (string, error) { 85 | cmd := exec.Command("git", "describe") 86 | cmd.Dir = docsDir 87 | res, err := cmd.CombinedOutput() 88 | return strings.Trim(string(res), " \n\r"), err 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muka/go-bluetooth 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/fatih/structs v1.1.0 7 | github.com/godbus/dbus/v5 v5.0.3 8 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 9 | github.com/pkg/errors v0.9.1 10 | github.com/sirupsen/logrus v1.6.0 11 | github.com/stretchr/testify v1.6.1 12 | github.com/suapapa/go_eddystone v1.3.1 13 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 14 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346 15 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muka/go-bluetooth/04c4f09c514e6847ae186ed39befba782816b3dd/gopher.png -------------------------------------------------------------------------------- /hw/hw.go: -------------------------------------------------------------------------------- 1 | package hw 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/hw/linux" 5 | "github.com/muka/go-bluetooth/hw/linux/btmgmt" 6 | "github.com/muka/go-bluetooth/hw/linux/hciconfig" 7 | ) 8 | 9 | func GetAdapter(adapterID string) (a linux.AdapterInfo, err error) { 10 | return linux.GetAdapter(adapterID) 11 | } 12 | 13 | func GetAdapters() ([]linux.AdapterInfo, error) { 14 | return linux.GetAdapters() 15 | } 16 | 17 | func Up(adapterID string) error { 18 | return linux.Up(adapterID) 19 | } 20 | 21 | func Down(adapterID string) error { 22 | return linux.Down(adapterID) 23 | } 24 | 25 | func Reset(adapterID string) error { 26 | return linux.Reset(adapterID) 27 | } 28 | 29 | func NewBtMgmt(adapterID string) *btmgmt.BtMgmt { 30 | return btmgmt.NewBtMgmt(adapterID) 31 | } 32 | 33 | func NewHCIConfig(adapterID string) *hciconfig.HCIConfig { 34 | return hciconfig.NewHCIConfig(adapterID) 35 | } 36 | -------------------------------------------------------------------------------- /hw/linux/btmgmt/btmgmt_test.go: -------------------------------------------------------------------------------- 1 | package btmgmt 2 | 3 | import "testing" 4 | import log "github.com/sirupsen/logrus" 5 | 6 | func TestGetAdapters(t *testing.T) { 7 | 8 | log.SetLevel(log.TraceLevel) 9 | 10 | list, err := GetAdapters() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | if len(list) == 0 { 16 | t.Fatal("At least an adapter should be available") 17 | } 18 | 19 | } 20 | 21 | func TestGetAdapter(t *testing.T) { 22 | 23 | _, err := GetAdapter("0") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /hw/linux/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os/exec" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // Exec Execute a command and collect the output 10 | func Exec(args ...string) (string, error) { 11 | 12 | baseCmd := args[0] 13 | cmdArgs := args[1:] 14 | 15 | log.Tracef("Exec: %s %s", baseCmd, cmdArgs) 16 | 17 | cmd := exec.Command(baseCmd, cmdArgs...) 18 | res, err := cmd.CombinedOutput() 19 | 20 | return string(res), err 21 | } 22 | -------------------------------------------------------------------------------- /hw/linux/hci/hci_test.go: -------------------------------------------------------------------------------- 1 | package hci 2 | 3 | import ( 4 | "testing" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func TestHciList(t *testing.T) { 10 | 11 | log.SetLevel(log.DebugLevel) 12 | 13 | list, err := List() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | if len(list) == 0 { 19 | t.Fatal("At least an adapter should be available") 20 | } 21 | 22 | } 23 | 24 | func TestHciUp(t *testing.T) { 25 | 26 | log.SetLevel(log.DebugLevel) 27 | 28 | list, err := List() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if len(list) == 0 { 34 | t.Fatal("At least an adapter should be available") 35 | } 36 | 37 | err = Up(list[0]) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | err = Down(list[0]) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /hw/linux/hciconfig/hciconfig.go: -------------------------------------------------------------------------------- 1 | package hciconfig 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/muka/go-bluetooth/hw/linux/cmd" 8 | ) 9 | 10 | // GetAdapters return the list of available adapters 11 | func GetAdapters() ([]HCIConfigResult, error) { 12 | 13 | out, err := cmd.Exec("hciconfig") 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | if len(out) == 0 { 19 | return nil, errors.New("hciconfig provided no response") 20 | } 21 | 22 | list := []HCIConfigResult{} 23 | parts := strings.Split(out, "\nhci") 24 | 25 | for i, el := range parts { 26 | if i > 0 { 27 | el = "hci" + el 28 | } 29 | cfg := parseControllerInfo(el) 30 | list = append(list, cfg) 31 | } 32 | 33 | // log.Debugf("%++v", list) 34 | 35 | return list, nil 36 | } 37 | 38 | // GetAdapter return an adapter 39 | func GetAdapter(adapterID string) (*HCIConfigResult, error) { 40 | h := NewHCIConfig(adapterID) 41 | return h.Status() 42 | } 43 | 44 | // NewHCIConfig initialize a new HCIConfig 45 | func NewHCIConfig(adapterID string) *HCIConfig { 46 | return &HCIConfig{adapterID} 47 | } 48 | 49 | //HCIConfigResult contains details for an adapter 50 | type HCIConfigResult struct { 51 | AdapterID string 52 | Enabled bool 53 | Address string 54 | Type string 55 | Bus string 56 | } 57 | 58 | // HCIConfig an hciconfig command wrapper 59 | type HCIConfig struct { 60 | adapterID string 61 | } 62 | 63 | func parseControllerInfo(out string) HCIConfigResult { 64 | cfg := HCIConfigResult{} 65 | 66 | cfg.AdapterID = strings.Trim(out[:6], " \t:") 67 | 68 | s := strings.Replace(out[6:], "\t", "", -1) 69 | lines := strings.Split(s, "\n") 70 | // var parts []string 71 | for i, line := range lines { 72 | if i > 2 { 73 | break 74 | } 75 | if i == 2 { 76 | pp := strings.Split(line, " ") 77 | cfg.Enabled = (pp[0] == "UP") 78 | continue 79 | } 80 | 81 | subparts := strings.Split(line, " ") 82 | for _, subpart := range subparts { 83 | pp := strings.Split(subpart, ": ") 84 | switch pp[0] { 85 | case "Type": 86 | cfg.Type = pp[1] 87 | continue 88 | case "Bus": 89 | cfg.Bus = pp[1] 90 | continue 91 | case "BD Address": 92 | cfg.Address = pp[1] 93 | continue 94 | } 95 | } 96 | } 97 | 98 | return cfg 99 | } 100 | 101 | //Status return status information for a hci device 102 | func (h *HCIConfig) Status() (*HCIConfigResult, error) { 103 | 104 | out, err := cmd.Exec("hciconfig", h.adapterID) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | cfg := parseControllerInfo(out) 110 | 111 | return &cfg, nil 112 | } 113 | 114 | // Up Turn on an HCI device 115 | func (h *HCIConfig) Up() (*HCIConfigResult, error) { 116 | _, err := cmd.Exec("hciconfig", h.adapterID, "up") 117 | if err != nil { 118 | return nil, err 119 | } 120 | return h.Status() 121 | } 122 | 123 | // Down Turn down an HCI device 124 | func (h *HCIConfig) Down() (*HCIConfigResult, error) { 125 | _, err := cmd.Exec("hciconfig", h.adapterID, "down") 126 | if err != nil { 127 | return nil, err 128 | } 129 | return h.Status() 130 | } 131 | -------------------------------------------------------------------------------- /hw/linux/hciconfig/hciconfig_test.go: -------------------------------------------------------------------------------- 1 | package hciconfig 2 | 3 | import ( 4 | "testing" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func TestStatus(t *testing.T) { 10 | 11 | h := NewHCIConfig("hci0") 12 | _, err := h.Status() 13 | if err != nil { 14 | t.Fatal() 15 | } 16 | 17 | } 18 | 19 | func TestUpDown(t *testing.T) { 20 | 21 | h := NewHCIConfig("hci0") 22 | _, err := h.Status() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | _, err = h.Down() 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | _, err = h.Up() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | } 38 | 39 | func TestGetAdapters(t *testing.T) { 40 | log.SetLevel(log.DebugLevel) 41 | _, err := GetAdapters() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /hw/linux/hcitool/hcitool.go: -------------------------------------------------------------------------------- 1 | package hcitool 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/muka/go-bluetooth/hw/linux/cmd" 8 | ) 9 | 10 | type HcitoolDev struct { 11 | ID string 12 | Address string 13 | } 14 | 15 | // GetAdapter Return an adapter using hcitool as backend 16 | func GetAdapter(adapterID string) (*HcitoolDev, error) { 17 | 18 | list, err := GetAdapters() 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | for _, a := range list { 24 | if a.ID == adapterID { 25 | return a, nil 26 | } 27 | } 28 | 29 | return nil, nil 30 | } 31 | 32 | // GetAdapters Return a list of adapters using hcitool as backend 33 | func GetAdapters() ([]*HcitoolDev, error) { 34 | 35 | list := make([]*HcitoolDev, 0) 36 | raw, err := cmd.Exec("hcitool", "dev") 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // raw: 42 | // Devices: 43 | // hci1 70:C9:4E:58:AA:7E 44 | 45 | lines := strings.Split(raw, "\n") 46 | lines = lines[1:] 47 | 48 | // hci1 70:C9:4E:58:AA:7E 49 | re1 := regexp.MustCompile("^[ \t]*([a-zA-Z0-9]+)[ \t]*([a-zA-Z0-9:]+)$") 50 | 51 | for i := 0; i < len(lines); i++ { 52 | 53 | if !re1.MatchString(lines[i]) { 54 | continue 55 | } 56 | 57 | el := new(HcitoolDev) 58 | 59 | res := re1.FindStringSubmatch(lines[i]) 60 | if len(res) > 1 { 61 | el.ID = res[1] 62 | el.Address = res[2] 63 | list = append(list, el) 64 | } 65 | 66 | } 67 | 68 | return list, nil 69 | } 70 | -------------------------------------------------------------------------------- /hw/linux/hcitool/hcitool_test.go: -------------------------------------------------------------------------------- 1 | package hcitool 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestHcitoolGetAdapters(t *testing.T) { 12 | 13 | log.SetLevel(log.DebugLevel) 14 | 15 | list, err := GetAdapters() 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | assert.NotEmpty(t, list) 21 | 22 | } 23 | 24 | func TestHcitoolGetAdapter(t *testing.T) { 25 | 26 | log.SetLevel(log.DebugLevel) 27 | 28 | a, err := GetAdapter("hci0") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if a == nil { 34 | t.Fatal("An adapter should be available") 35 | } 36 | 37 | } 38 | 39 | func TestHcitoolGetAdapterNotfound(t *testing.T) { 40 | log.SetLevel(log.DebugLevel) 41 | list, err := GetAdapters() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | size := len(list) 47 | devID := fmt.Sprintf("hci%d", (size + 1)) 48 | a, err := GetAdapter(devID) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | if a != nil { 54 | t.Fatal(fmt.Sprintf("%s should not be avail", devID)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /hw/linux/linux.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/muka/go-bluetooth/hw/linux/btmgmt" 8 | "github.com/muka/go-bluetooth/hw/linux/hci" 9 | "github.com/muka/go-bluetooth/hw/linux/hciconfig" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type BackendType string 14 | 15 | const ( 16 | BackendBtmgmt BackendType = "btmgmt" 17 | BackendHCI BackendType = "hci" 18 | BackendHCIConfig BackendType = "hciconfig" 19 | ) 20 | 21 | var Backend BackendType = BackendHCIConfig 22 | 23 | type AdapterInfo struct { 24 | AdapterID string 25 | Address string 26 | Type string 27 | Enabled bool 28 | } 29 | 30 | // GetAdapter return status information for a controller 31 | func GetAdapter(adapterID string) (a AdapterInfo, err error) { 32 | 33 | list, err := GetAdapters() 34 | if err != nil { 35 | return a, err 36 | } 37 | 38 | for _, a := range list { 39 | if a.AdapterID == adapterID { 40 | return a, err 41 | } 42 | } 43 | 44 | return a, fmt.Errorf("Adapter %s not found", adapterID) 45 | } 46 | 47 | // GetAdapters return a list of status information of available controllers 48 | func GetAdapters() ([]AdapterInfo, error) { 49 | 50 | list, err := hciconfig.GetAdapters() 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | list1 := []AdapterInfo{} 56 | for _, info := range list { 57 | list1 = append(list1, AdapterInfo{ 58 | AdapterID: info.AdapterID, 59 | Enabled: info.Enabled, 60 | Type: info.Type, 61 | Address: info.Address, 62 | }) 63 | } 64 | 65 | return list1, err 66 | } 67 | 68 | func Up(adapterID string) error { 69 | 70 | status, err := GetAdapter(adapterID) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | if status.Enabled { 76 | return nil 77 | } 78 | 79 | if Backend == BackendHCIConfig { 80 | _, err := hciconfig.NewHCIConfig(adapterID).Up() 81 | return err 82 | } 83 | 84 | if Backend == BackendBtmgmt { 85 | return btmgmt.NewBtMgmt(adapterID).SetPowered(true) 86 | } 87 | 88 | if Backend == BackendHCI { 89 | 90 | id, err := strconv.Atoi(adapterID[3:]) 91 | if err != nil { 92 | return err 93 | } 94 | return hci.Up(id) 95 | } 96 | 97 | return fmt.Errorf("Unsupported backend type: %s", Backend) 98 | } 99 | 100 | func Down(adapterID string) error { 101 | 102 | status, err := GetAdapter(adapterID) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | if !status.Enabled { 108 | return nil 109 | } 110 | 111 | if Backend == BackendHCIConfig { 112 | _, err := hciconfig.NewHCIConfig(adapterID).Down() 113 | return err 114 | } 115 | 116 | if Backend == BackendBtmgmt { 117 | return btmgmt.NewBtMgmt(adapterID).SetPowered(false) 118 | } 119 | 120 | if Backend == BackendHCI { 121 | id, err := strconv.Atoi(adapterID[3:]) 122 | if err != nil { 123 | return err 124 | } 125 | return hci.Down(id) 126 | } 127 | 128 | return fmt.Errorf("Unsupported backend type: %s", Backend) 129 | } 130 | 131 | func Reset(adapterID string) error { 132 | err := Down(adapterID) 133 | if err != nil { 134 | log.Warnf("Down failed: %s", err) 135 | } 136 | return Up(adapterID) 137 | } 138 | -------------------------------------------------------------------------------- /hw/linux/linux_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "testing" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetAdapters(t *testing.T) { 11 | 12 | log.SetLevel(log.DebugLevel) 13 | 14 | list, err := GetAdapters() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | assert.NotEmpty(t, list) 19 | } 20 | 21 | func TestGetAdapter(t *testing.T) { 22 | log.SetLevel(log.DebugLevel) 23 | _, err := GetAdapter("hci0") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | 29 | func TestGetAdapterNotFound(t *testing.T) { 30 | _, err := GetAdapter("hci999") 31 | if err == nil { 32 | t.Fatal("adapter should not exists") 33 | } 34 | } 35 | 36 | func TestUp(t *testing.T) { 37 | err := Up("hci0") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | 43 | func TestDown(t *testing.T) { 44 | err := Down("hci0") 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | } 49 | 50 | func TestReset(t *testing.T) { 51 | err := Reset("hci0") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /hw/linux/rfkill/rfkill_test.go: -------------------------------------------------------------------------------- 1 | package rfkill 2 | 3 | import "testing" 4 | 5 | var testAdapterID = "hci0" 6 | 7 | func TestGetAdapterStatus(t *testing.T) { 8 | _, err := GetAdapterStatus(testAdapterID) 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | } 13 | 14 | func TestToggleAdapter(t *testing.T) { 15 | err := ToggleAdapter(testAdapterID) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | } 20 | 21 | func TestTurnOnAdapter(t *testing.T) { 22 | err := TurnOnAdapter(testAdapterID) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | } 27 | 28 | func TestTurnOffAdapter(t *testing.T) { 29 | err := TurnOffAdapter(testAdapterID) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | 35 | func TestTurnOnBluetooth(t *testing.T) { 36 | err := TurnOnBluetooth() 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | 42 | func TestTurnOffBluetooth(t *testing.T) { 43 | err := TurnOffBluetooth() 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | 49 | func TestToggleBluetooth(t *testing.T) { 50 | err := ToggleBluetooth() 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hw/linux/rfkill/switch.go: -------------------------------------------------------------------------------- 1 | package rfkill 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | 7 | "github.com/muka/go-bluetooth/hw/linux/hciconfig" 8 | ) 9 | 10 | var rfclass = [...]string{ 11 | "bluetooth", 12 | "wifi", 13 | } 14 | 15 | var rfkillHandler = NewRFKill() 16 | 17 | // GetHCIConfig return an HCIConfig struct 18 | func GetHCIConfig(adapterID string) *hciconfig.HCIConfig { 19 | return hciconfig.NewHCIConfig(adapterID) 20 | } 21 | 22 | // GetAdapterStatus return the status of an adapter 23 | func GetAdapterStatus(adapterID string) (*RFKillResult, error) { 24 | 25 | if !rfkillHandler.IsInstalled() { 26 | return nil, errors.New("rfkill is not available") 27 | } 28 | 29 | list, err := rfkillHandler.ListAll() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | for _, adapter := range list { 35 | if adapter.Description == adapterID { 36 | // dbgSwitch("Got adapter index %d desc: %s type: %s hard-block: %t soft-block: %t", 37 | // adapter.Index, 38 | // adapter.Description, 39 | // adapter.IdentifierType, 40 | // adapter.HardBlocked, 41 | // adapter.SoftBlocked, 42 | // ) 43 | 44 | return &adapter, nil 45 | } 46 | } 47 | 48 | return nil, errors.New("Adapter not found") 49 | } 50 | 51 | // ToggleAdapter Swap Off/On a device 52 | func ToggleAdapter(adapterID string) error { 53 | err := TurnOffAdapter(adapterID) 54 | if err != nil { 55 | return err 56 | } 57 | return TurnOnAdapter(adapterID) 58 | } 59 | 60 | // TurnOnAdapter Enable a rfkill managed device 61 | func TurnOnAdapter(adapterID string) error { 62 | 63 | var identifier string 64 | if isRFClass(adapterID) { 65 | identifier = adapterID 66 | } else { 67 | adapter, err := GetAdapterStatus(adapterID) 68 | if err != nil { 69 | return err 70 | } 71 | identifier = strconv.Itoa(adapter.Index) 72 | } 73 | 74 | if rfkillHandler.IsSoftBlocked(adapterID) { 75 | err := rfkillHandler.SoftUnblock(identifier) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | if rfkillHandler.IsHardBlocked(adapterID) { 81 | return errors.New("Adapter is hard locked, check for a physical switch to enable it") 82 | } 83 | return nil 84 | } 85 | 86 | // TurnOffAdapter Enable a rfkill managed device 87 | func TurnOffAdapter(adapterID string) error { 88 | 89 | var identifier string 90 | if isRFClass(adapterID) { 91 | identifier = adapterID 92 | } else { 93 | adapter, err := GetAdapterStatus(adapterID) 94 | if err != nil { 95 | return err 96 | } 97 | identifier = strconv.Itoa(adapter.Index) 98 | } 99 | 100 | if !rfkillHandler.IsSoftBlocked(adapterID) { 101 | err := rfkillHandler.SoftBlock(identifier) 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | func isRFClass(id string) bool { 110 | for _, class := range rfclass { 111 | if class == id { 112 | return true 113 | } 114 | } 115 | return false 116 | } 117 | 118 | // TurnOnBluetooth turn on bluetooth support 119 | func TurnOnBluetooth() error { 120 | return TurnOnAdapter("bluetooth") 121 | } 122 | 123 | // TurnOffBluetooth turn on bluetooth support 124 | func TurnOffBluetooth() error { 125 | return TurnOffAdapter("bluetooth") 126 | } 127 | 128 | // ToggleBluetooth toggle off/on the bluetooth support 129 | func ToggleBluetooth() error { 130 | err := TurnOffBluetooth() 131 | if err != nil { 132 | return err 133 | } 134 | return TurnOnBluetooth() 135 | } 136 | -------------------------------------------------------------------------------- /props/props.go: -------------------------------------------------------------------------------- 1 | package props 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/fatih/structs" 8 | "github.com/godbus/dbus/v5" 9 | "github.com/godbus/dbus/v5/prop" 10 | "github.com/muka/go-bluetooth/bluez" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type PropInfo struct { 15 | prop.Prop 16 | Skip bool 17 | } 18 | 19 | func ParseProperties(propertyVal bluez.Properties) map[string]*PropInfo { 20 | 21 | t := structs.New(propertyVal) 22 | 23 | res := map[string]*PropInfo{} 24 | 25 | for _, field := range t.Fields() { 26 | 27 | if !field.IsExported() { 28 | continue 29 | } 30 | 31 | // if _, ok := field.Value().(dbus.ObjectPath); ok && field.IsZero() { 32 | // log.Debugf("parseProperties: skip empty ObjectPath %s", field.Name()) 33 | // continue 34 | // } 35 | 36 | propInfo := new(PropInfo) 37 | propInfo.Value = field.Value() 38 | 39 | res[field.Name()] = propInfo 40 | 41 | tag := field.Tag("dbus") 42 | if tag == "" { 43 | continue 44 | } 45 | 46 | parts := strings.Split(tag, ",") 47 | for i := 0; i < len(parts); i++ { 48 | 49 | tagKey := parts[i] 50 | tagValue := "" 51 | if strings.Contains(parts[i], "=") { 52 | subpts := strings.Split(parts[i], "=") 53 | tagKey = subpts[0] 54 | tagValue = strings.Join(subpts[1:], "=") 55 | } 56 | 57 | if tagKey == "ignore" { 58 | if tagValue == "" { 59 | propInfo.Skip = true 60 | } else { 61 | 62 | checkField, ok := t.FieldOk(tagValue) 63 | if !ok { 64 | log.Warnf("%s: field not found, is it avaialable?", tagValue) 65 | continue 66 | } 67 | if !checkField.IsExported() { 68 | log.Warnf("%s: field must be exported. (add a tag `ignore` to avoid exposing it as property)", tagValue) 69 | continue 70 | } 71 | 72 | varKind := checkField.Kind() 73 | if varKind != reflect.Bool { 74 | log.Warnf("%s: ignore tag expect a bool property to check, %s given", tagValue, varKind) 75 | continue 76 | } 77 | 78 | if checkField.Value().(bool) { 79 | propInfo.Skip = true 80 | } 81 | } 82 | } 83 | 84 | // check if empty 85 | if tagKey == "omitEmpty" { 86 | if field.IsZero() { 87 | propInfo.Skip = true 88 | } 89 | } 90 | 91 | switch tagKey { 92 | case "emit": 93 | propInfo.Emit = prop.EmitTrue 94 | propInfo.Writable = true 95 | case "invalidates": 96 | propInfo.Emit = prop.EmitInvalidates 97 | propInfo.Writable = true 98 | case "writable": 99 | propInfo.Writable = true 100 | default: 101 | t := reflect.TypeOf(propertyVal) 102 | m, ok := t.MethodByName(tagKey) 103 | if ok { 104 | propInfo.Writable = true 105 | propInfo.Callback = m.Func.Interface().(func(*prop.Change) *dbus.Error) 106 | } 107 | } 108 | } 109 | 110 | } 111 | 112 | return res 113 | } 114 | -------------------------------------------------------------------------------- /props/to_map.go: -------------------------------------------------------------------------------- 1 | package props 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/bluez" 5 | ) 6 | 7 | // Convert a struct to map applying options from struct tag 8 | func ToMap(a bluez.Properties) map[string]interface{} { 9 | 10 | propsInfo := ParseProperties(a) 11 | 12 | res := make(map[string]interface{}) 13 | for name, info := range propsInfo { 14 | if info.Skip { 15 | continue 16 | } 17 | res[name] = info.Value 18 | } 19 | 20 | return res 21 | } 22 | -------------------------------------------------------------------------------- /tests/watch_properties_routines_num.go: -------------------------------------------------------------------------------- 1 | /** 2 | * This script demonstrates the increase of go routines depending on the 3 | * UnwatchProperties change. The code is related to 4 | * https://github.com/muka/go-bluetooth/issues/113 and just documents 5 | * the issue to see if the fix brings any value. 6 | * 7 | * Functionally this script doesn't do anything beyond connecting to a 8 | * BLE device and repeatedly subscribing and unsubscribing to receive 9 | * notifications as fast as possible. Given the "as fast as possible" 10 | * part also caught a panic when the channel was close in inappropriate 11 | * moment of routine started in the WatchProperties which resulted in 12 | * an attempt to write to a closed channel 13 | */ 14 | package main 15 | 16 | import ( 17 | "errors" 18 | "flag" 19 | "fmt" 20 | "runtime" 21 | "time" 22 | 23 | "github.com/muka/go-bluetooth/api" 24 | "github.com/muka/go-bluetooth/bluez/profile/gatt" 25 | ) 26 | 27 | func runWatchPropertiesTestIteration(char *gatt.GattCharacteristic1) (int, error) { 28 | ch, err := char.WatchProperties() 29 | if err != nil { 30 | return 0, err 31 | } 32 | 33 | err = char.StartNotify() 34 | if err != nil { 35 | fmt.Printf("Error: %s", err) 36 | return 0, err 37 | } 38 | 39 | go func() { 40 | for e := range ch { 41 | if e == nil { 42 | return 43 | } 44 | } 45 | }() 46 | 47 | err = char.UnwatchProperties(ch) 48 | if err != nil { 49 | return 0, err 50 | } 51 | err = char.StopNotify() 52 | if err != nil { 53 | fmt.Printf("Error: %s", err) 54 | return 0, err 55 | } 56 | 57 | // Optionally wait a little for the anonumous go routine to finish 58 | // If you don't wait here then the results in go routines count diff 59 | // will sometimes show +1 or -1 to average around 0 due to delay 60 | // in reception of nil on WatchProperties() created channel 61 | // time.Sleep(10 * time.Millisecond) 62 | return runtime.NumGoroutine(), nil 63 | } 64 | 65 | func main() { 66 | hciName := flag.String("hci", "hci1", "Name of your HCI device") 67 | devMac := flag.String("mac", "CA:AF:FE:00:BE:EF", "MAC of the device to connect for test") 68 | charUUID := flag.String("uuid", "76494b4a-305c-4368-aa02-923b1b709333", "UUID characteristic which notifies") 69 | flag.Parse() 70 | 71 | a, err := api.GetAdapter(*hciName) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | fmt.Println("Starting scan") 77 | err = a.StartDiscovery() 78 | if err != nil { 79 | panic(err) 80 | } 81 | time.Sleep(2 * time.Second) 82 | err = a.StopDiscovery() 83 | if err != nil { 84 | panic(err) 85 | } 86 | fmt.Println("Scan finished") 87 | 88 | dev, err := a.GetDeviceByAddress(*devMac) 89 | if err != nil { 90 | panic(err) 91 | } 92 | if dev == nil { 93 | msg := fmt.Sprintf("Device %s not found. Cannot connect\n", *devMac) 94 | panic(errors.New(msg)) 95 | } 96 | 97 | fmt.Printf("Connecting to %s\n", dev.Properties.Address) 98 | err = dev.Connect() 99 | if err != nil { 100 | panic(err) 101 | } 102 | 103 | char, err := dev.GetCharByUUID(*charUUID) 104 | if err != nil { 105 | panic("search device failed: " + err.Error()) 106 | } 107 | 108 | // Run test iterations 109 | iterCount := 100 110 | prev := runtime.NumGoroutine() 111 | startGoRoutineCount := prev 112 | for i := 1; i < iterCount; i++ { 113 | result, err := runWatchPropertiesTestIteration(char) 114 | if err != nil { 115 | fmt.Printf("Error: %s\n", err) 116 | } 117 | diff := result - prev 118 | fmt.Printf("[Iteration %04d] Result: %d, diff: %d\n", i, result, diff) 119 | prev = result 120 | } 121 | 122 | endGoRoutineCount := runtime.NumGoroutine() 123 | diff := endGoRoutineCount - startGoRoutineCount 124 | fmt.Printf("Number of go routines changed by %d. Started with %d,"+ 125 | "finished with %d\n", diff, startGoRoutineCount, endGoRoutineCount) 126 | fmt.Printf("Done\n") 127 | } 128 | -------------------------------------------------------------------------------- /util/map_struct_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/godbus/dbus/v5" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestStructToMap(t *testing.T) { 12 | 13 | log.SetLevel(log.DebugLevel) 14 | 15 | struct1 := struct { 16 | ManufacturerData map[uint16]interface{} 17 | }{} 18 | 19 | val1 := map[uint16][]byte{ 20 | 0x00: {0x01, 0x02, 0x03}, 21 | } 22 | 23 | map1 := map[string]dbus.Variant{ 24 | "ManufacturerData": dbus.MakeVariant(val1), 25 | "Foo": dbus.MakeVariant(val1), 26 | } 27 | 28 | err := MapToStruct(&struct1, map1) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | val2, ok := struct1.ManufacturerData[0x00] 34 | assert.True(t, ok) 35 | assert.Equal(t, val2.([]byte), val1[0x00]) 36 | 37 | } 38 | 39 | func TestNestedStructToMap(t *testing.T) { 40 | 41 | log.SetLevel(log.DebugLevel) 42 | 43 | type fooStruct struct { 44 | Field1 string 45 | Field2 string 46 | } 47 | type barStruct struct { 48 | Field uint32 49 | Nested *fooStruct 50 | } 51 | 52 | struct1 := struct { 53 | BarData barStruct 54 | }{BarData: barStruct{Nested: &fooStruct{}}} 55 | 56 | nestedVal := map[string]dbus.Variant{ 57 | "Field1": dbus.MakeVariant("val3-1"), 58 | "Field2": dbus.MakeVariant("val3-2"), 59 | } 60 | val1 := map[string]dbus.Variant{ 61 | "Field": dbus.MakeVariant(uint32(9)), 62 | "Nested": dbus.MakeVariant(nestedVal), 63 | } 64 | 65 | map1 := map[string]dbus.Variant{ 66 | "BarData": dbus.MakeVariant(val1), 67 | } 68 | 69 | err := MapToStruct(&struct1, map1) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | assert.Equal(t, struct1.BarData.Field, val1["Field"].Value().(uint32)) 75 | assert.Equal(t, struct1.BarData.Nested.Field2, nestedVal["Field2"].Value().(string)) 76 | } 77 | --------------------------------------------------------------------------------