hrm/main.go
2019-11-27 10:48:13 -05:00

278 lines
6.1 KiB
Go

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
events := b.Events()
for {
select {
case e := <-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)
}
}
}
}