package main import ( "encoding/binary" "fmt" "io/ioutil" "log" "os" "path" "git.wow.st/gmp/ble" "gopkg.in/yaml.v2" "gioui.org/app" "gioui.org/io/system" "gioui.org/layout" "gioui.org/text" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "gioui.org/font/gofont" ) type conf struct { Autoconnect string } var Config conf var conffile string func main() { conffile = path.Join(getConfDir(), "config.yml") if _, err := os.Stat(conffile); os.IsNotExist(err) { fd, err := os.Create(conffile) if err != nil { log.Fatal("Cannot create configuration file: ", err) } fd.Close() } confbytes, err := ioutil.ReadFile(conffile) if err != nil { log.Fatal("Cannot read configuration file: ", err) } if err = yaml.UnmarshalStrict(confbytes, &Config); err != nil { log.Fatal("Cannot parse configuration file: ", err) } go eventloop() app.Main() } func saveConfig() { confbytes, err := yaml.Marshal(&Config) if err != nil { log.Fatal("Cannot encode configuration: ", err) } err = ioutil.WriteFile(conffile, confbytes, 0700) } func hrDecode(x []byte) int { if len(x) < 4 { return 0 } flags := x[0] if flags&0x80 != 0 { // uint16 format return int(binary.BigEndian.Uint16(x[1:3])) } else { return int(x[1]) } } func eventloop() { w := app.NewWindow( app.Size(unit.Dp(400), unit.Dp(400)), app.Title("HRM"), ) gofont.Register() th := material.NewTheme() th.TextSize = unit.Sp(fontSize) gtx := &layout.Context{Queue: w.Queue()} sysinset := &layout.Inset{} resetSysinset := func(x system.Insets) { sysinset.Top = x.Top sysinset.Bottom = x.Bottom sysinset.Left = x.Left sysinset.Right = x.Right } margin := layout.UniformInset(unit.Dp(10)) b := ble.NewBLE() b.Enable(w) state := "starting" var hr int var periph ble.Peripheral periphs := make([]ble.Peripheral, 0) btns := make([]*widget.Button, 0) backBtn := &widget.Button{} var page, offpage, scanpage, connpage, hrpage func() f := &layout.Flex{Axis: layout.Vertical} offpage = func() { c1 := f.Rigid(gtx, func() { th.Body1("Heart Rate Monitor").Layout(gtx) }) c2 := f.Rigid(gtx, func() { th.Body1("Bluetooth is Powered Off").Layout(gtx) }) f.Layout(gtx, c1, c2) } scanpage = func() { c1 := f.Rigid(gtx, func() { th.Body1("Heart Rate Monitor").Layout(gtx) }) c2 := f.Rigid(gtx, func() { th.Body1(state).Layout(gtx) }) c3 := f.Rigid(gtx, func() { lst := &layout.List{Axis: layout.Vertical} lst.Layout(gtx, len(periphs), func(i int) { gtx.Constraints.Width.Min = gtx.Constraints.Width.Max layout.UniformInset(unit.Dp(2)).Layout(gtx, func() { th.Button(periphs[i].Name).Layout(gtx, btns[i]) }) if btns[i].Clicked(gtx) { b.StopScan() periph = periphs[i] b.Connect(periph) state = "connecting" page = connpage w.Invalidate() } }) }) f.Layout(gtx, c1, c2, c3) } connpage = func() { c1 := f.Rigid(gtx, func() { th.Body1("Heart Rate Monitor").Layout(gtx) }) c2 := f.Rigid(gtx, func() { th.Body1("Connecting").Layout(gtx) }) c3 := f.Rigid(gtx, func() { th.Body1(periph.Name).Layout(gtx) }) c4 := f.Rigid(gtx, func() { th.Button("Cancel").Layout(gtx, backBtn) if backBtn.Clicked(gtx) { ble.CancelConnection(periph) periphs = periphs[:0] Config.Autoconnect = "" saveConfig() b.Scan() state = "scanning" page = scanpage } }) f.Layout(gtx, c1, c2, c3, c4) } hrpage = func() { c1 := f.Rigid(gtx, func() { th.Body1("Heart Rate Monitor").Layout(gtx) }) c2 := f.Rigid(gtx, func() { th.Body1(periph.Name).Layout(gtx) }) c3 := f.Rigid(gtx, func() { l := th.H1(fmt.Sprintf("%d", hr)) l.Alignment = text.Middle l.Layout(gtx) }) c4 := f.Rigid(gtx, func() { th.Button("Stop").Layout(gtx, backBtn) if backBtn.Clicked(gtx) { ble.Disconnect(periph) periphs = periphs[:0] Config.Autoconnect = "" saveConfig() b.Scan() state = "scanning" page = scanpage } }) f.Layout(gtx, c1, c2, c3, c4) } page = offpage for { select { case e := <-b.Events(): switch e := e.(type) { case ble.UpdateStateEvent: fmt.Printf("UpdateState: %s\n", e.State) state = e.State if state != "powered on" { periphs = periphs[:0] page = offpage } else { if Config.Autoconnect != "" && b.Connect(ble.Peripheral{Identifier: Config.Autoconnect}) { state = "connecting" page = connpage } else { periphs = periphs[:0] state = "scanning" page = scanpage b.Scan() } } case ble.DiscoverPeripheralEvent: fmt.Printf("found %s (%s)\n", e.Peripheral.Name, e.Peripheral.Identifier) periphs = append(periphs, e.Peripheral) btns = append(btns, &widget.Button{}) if e.Peripheral.Identifier == Config.Autoconnect && b.Connect(e.Peripheral) { state = "connecting" page = connpage } case ble.ConnectEvent: fmt.Printf("Connect event\n") b.StopScan() state = "connected" periph = e.Peripheral Config.Autoconnect = periph.Identifier saveConfig() e.Peripheral.DiscoverServices() page = hrpage case ble.ConnectTimeoutEvent: fmt.Printf("Connect timeout\n") state = "timeout" periphs = periphs[:0] page = scanpage Config.Autoconnect = "" saveConfig() b.Scan() case ble.DiscoverServiceEvent: fmt.Printf("DiscoverService %s\n", e.Gatt.UUID) //if e.Gatt.UUID == "180D" { if e.Gatt.IsHRM() { fmt.Printf("Found HRM Service\n") e.Peripheral.DiscoverCharacteristics(e.Service) } case ble.DiscoverCharacteristicEvent: fmt.Printf("DiscoverCharacteristic %s\n", e.Gatt.UUID) //if e.Gatt.UUID == "2A37" { if e.Gatt.IsHRV() { fmt.Printf("Found heart rate value\n") e.Peripheral.SetNotifyValue(e.Characteristic) } case ble.UpdateValueEvent: hr = hrDecode(e.Data) } w.Invalidate() // refresh on any Bluetooth event case e := <-w.Events(): switch e := e.(type) { case system.DestroyEvent: return case system.FrameEvent: gtx.Reset(e.Config, e.Size) resetSysinset(e.Insets) sysinset.Layout(gtx, func() { margin.Layout(gtx, page) }) e.Frame(gtx.Ops) } } } }