package main import ( "encoding/binary" "fmt" "image" "image/color" "io/ioutil" "log" "os" "path" "time" "git.wow.st/gmp/ble" "gopkg.in/yaml.v2" "gioui.org/app" "gioui.org/io/system" "gioui.org/layout" "gioui.org/f32" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/text" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" "gioui.org/font/gofont" ) type ( D = layout.Dimensions C = layout.Context ) 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]) } } type Stopwatch struct { lasttime time.Time elapsed time.Time running bool startstopBtn *widget.Clickable resetBtn *widget.Clickable h, m, s int } func NewStopwatch() Stopwatch { return Stopwatch{ startstopBtn: &widget.Clickable{}, resetBtn: &widget.Clickable{}, } } func eventloop() { w := app.NewWindow( app.Size(unit.Dp(350), unit.Dp(600)), app.Title("HRM"), ) th := material.NewTheme(gofont.Collection()) th.TextSize = unit.Sp(fontSize) var ops op.Ops 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() state := "starting" var hr int var periph ble.Peripheral var wide bool periphs := make([]ble.Peripheral, 0) btns := make([]*widget.Clickable, 0) backBtn := &widget.Clickable{} var page, offpage, scanpage, connpage, hrpage func(gtx C) D f := &layout.Flex{Axis: layout.Vertical} offpage = func(gtx C) D { return f.Layout(gtx, layout.Rigid(func(gtx C) D { return material.Body1(th, "Heart Rate Monitor").Layout(gtx) }), layout.Rigid(func(gtx C) D { return material.Body1(th, "Bluetooth is Powered Off").Layout(gtx) }), ) } appname := func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { return material.Body1(th, "Heart Rate Monitor").Layout(gtx) }) } appstate := func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { return material.Body1(th, state).Layout(gtx) }) } periphname := func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { return material.Body1(th, periph.Name).Layout(gtx) }) } leftbar := func(gtx C) D { f := &layout.Flex{Axis: layout.Vertical} return f.Layout(gtx, layout.Rigid(appname), layout.Rigid(func(gtx C) D { return material.Body1(th, "").Layout(gtx) }), layout.Rigid(appstate), ) } scanlist := func(gtx C) D { lst := &layout.List{Axis: layout.Vertical} return lst.Layout(gtx, len(periphs), func(gtx C, i int) D { gtx.Constraints.Min.X = gtx.Constraints.Max.X ret := layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx C) D { return material.Button(th, btns[i], periphs[i].Name).Layout(gtx) }) if btns[i].Clicked() { b.StopScan() periph = periphs[i] b.Connect(periph) state = "connecting" page = connpage w.Invalidate() } return ret }) } scanpage = func(gtx C) D { if wide { f2 := &layout.Flex{Axis: layout.Horizontal} return f2.Layout(gtx, layout.Rigid(leftbar), layout.Flexed(1, scanlist), ) } else { return f.Layout(gtx, layout.Rigid(appname), layout.Rigid(appstate), layout.Rigid(scanlist), ) } } connpage = func(gtx C) D { return f.Layout(gtx, layout.Rigid(appname), layout.Rigid(appstate), layout.Rigid(periphname), layout.Rigid(func(gtx C) D { ret := material.Button(th, backBtn, "Cancel").Layout(gtx) if backBtn.Clicked() { ble.CancelConnection(periph) periphs = periphs[:0] Config.Autoconnect = "" saveConfig() b.Scan() state = "scanning" page = scanpage } return ret }), ) } hrcircle := func(gtx C) (int, D) { var w, h1 float32 ret := layout.Center.Layout(gtx, func(gtx C) D { blue := color.NRGBA{0x3f, 0x51, 0xb5, 255} white := color.NRGBA{255, 255, 255, 255} w, h1 = float32(gtx.Constraints.Max.X), float32(gtx.Constraints.Max.Y) if w < h1 { h1 = w } s2 := h1 * 0.9 p2a := f32.Point{(h1-s2)/2, (h1-s2)/2} circle := clip.Circle{ Center: f32.Point{X: h1/2, Y: h1/2}, Radius: h1 / 2}.Op(gtx.Ops) paint.FillShape(gtx.Ops, blue, circle) circle = clip.Circle{ Center: f32.Point{X: h1/2, Y: h1/2}, Radius: h1 * 0.9 / 2}.Op(gtx.Ops) paint.FillShape(gtx.Ops, white, circle) op.Offset(p2a).Add(gtx.Ops) gtx.Constraints.Max.X = int(s2) gtx.Constraints.Max.Y = int(s2) gtx.Constraints.Min.X = int(s2) gtx.Constraints.Min.Y = int(s2) l := material.H1(th, fmt.Sprintf("%d", hr)) l.Alignment = text.Middle layout.Center.Layout(gtx, func(gtx C) D { return l.Layout(gtx) }) var ret D ret.Size = image.Point{int(h1), int(h1)} return ret }) return int(h1), ret } sw := NewStopwatch() stopwatch := func(gtx C) D { startstoptxt := "start" if sw.running { sw.elapsed = sw.elapsed.Add(time.Since(sw.lasttime)) startstoptxt = "stop" } sw.lasttime = time.Now() f := layout.Flex{Axis: layout.Vertical} return f.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { gtx.Constraints.Max.X = 1e6 return material.H4(th, sw.elapsed.Format("15:04:05.00")).Layout(gtx) }) }), layout.Rigid(func(gtx C) D { f2 := layout.Flex{Axis: layout.Horizontal} gtx.Constraints.Min.Y = 100 ret := f2.Layout(gtx, layout.Flexed(0.5, func(gtx C) D { return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx C) D { return material.Button(th, sw.startstopBtn, startstoptxt).Layout(gtx) }) }), layout.Flexed(0.5, func(gtx C) D { return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx C) D { return material.Button(th, sw.resetBtn, "reset").Layout(gtx) }) }), ) if sw.startstopBtn.Clicked() { if sw.running { sw.running = false } else { sw.running = true } } if sw.resetBtn.Clicked() { sw.elapsed = time.Time{} sw.running = false } return ret }), ) } swidth := new(int) hrpage = func(gtx C) D { var ret, ret2 D stopbtn := func(gtx C) D { gtx.Constraints.Min.Y = 100 return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx C) D { return material.Button(th, backBtn, "Disconnect").Layout(gtx) }) } if wide { f2 := &layout.Flex{Axis: layout.Horizontal} ret = f2.Layout(gtx, layout.Flexed(0.4, func(gtx C) D { f3 := &layout.Flex{Axis: layout.Vertical} c1 := layout.Rigid(func(gtx C) D { ret2 = appname(gtx) *swidth = ret2.Size.X return ret2 }) c2 := layout.Rigid(func(gtx C) D { ret2 = periphname(gtx) s2 := ret2.Size.X if s2 > *swidth { *swidth = s2 } return ret2 }) var c3 layout.FlexChild if gtx.Constraints.Max.X > 370 { c3 = layout.Rigid(stopwatch) } else { c3 = layout.Rigid(func(gtx C) D { return D{} }) } c4 := layout.Rigid(stopbtn) return f3.Layout(gtx, c1, layout.Rigid(appstate), c2, layout.Flexed(1.0, func(gtx C) D { return material.Body1(th, "").Layout(gtx) }), c3, c4, ) }), layout.Flexed(0.6, func(gtx C) D { _, ret := hrcircle(gtx) return ret }), ) } else { // !wide c1 := layout.Rigid(func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { gtx.Constraints.Min.X = *swidth gtx.Constraints.Max.X = *swidth return stopwatch(gtx) }) }) //swheight := gtx.Dimensions.Size.Y c2 := layout.Rigid(func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { gtx.Constraints.Min.X = *swidth gtx.Constraints.Max.X = *swidth return stopbtn(gtx) }) }) if gtx.Constraints.Max.Y < 775 { //gtx.Constraints.Max.Y += swheight ret = f.Layout(gtx, layout.Rigid(appname), layout.Rigid(appstate), layout.Rigid(periphname), layout.Flexed(1.0, func(gtx C) D { *swidth, ret2 = hrcircle(gtx) return ret2 }), c2, ) } else { ret = f.Layout(gtx, layout.Rigid(appname), layout.Rigid(appstate), layout.Rigid(periphname), layout.Flexed(1.0, func(gtx C) D { *swidth, ret2 = hrcircle(gtx) return ret2 }), c1, c2, ) } } if backBtn.Clicked() { ble.Disconnect(periph) periphs = periphs[:0] Config.Autoconnect = "" saveConfig() b.Scan() state = "scanning" page = scanpage } return ret } page = offpage t := time.NewTicker(time.Second/30) for { select { case <-t.C: w.Invalidate() case e := <-b.Events(): log.Print("<-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.Clickable{}) 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: os.Exit(0) case system.FrameEvent: gtx := layout.NewContext(&ops, e) if e.Size.X > e.Size.Y { wide = true } else { wide = false } resetSysinset(e.Insets) sysinset.Layout(gtx, func(gtx C) D { return margin.Layout(gtx, page) }) e.Frame(gtx.Ops) default: handleEvent(e, b) } } } }