278 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|