hrm/hrm/main.go

509 lines
12 KiB
Go
Raw Normal View History

2019-10-23 18:29:13 -04:00
package main
import (
"encoding/binary"
2019-10-23 18:29:13 -04:00
"fmt"
"image"
"image/color"
2019-10-23 18:29:13 -04:00
"io/ioutil"
"log"
"os"
"path"
"time"
2019-10-23 18:29:13 -04:00
"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"
2019-10-23 18:29:13 -04:00
"gioui.org/text"
"gioui.org/unit"
2019-10-23 18:29:13 -04:00
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/font/gofont"
2019-10-23 18:29:13 -04:00
)
2020-06-12 16:13:33 -04:00
type (
D = layout.Dimensions
C = layout.Context
)
2019-10-23 18:29:13 -04:00
type conf struct {
Autoconnect string
2019-10-23 18:29:13 -04:00
}
var Config conf
var conffile string
2019-10-23 18:29:13 -04:00
func main() {
2019-10-28 12:46:57 -04:00
conffile = path.Join(getConfDir(), "config.yml")
2019-10-23 18:29:13 -04:00
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 {
2019-11-27 10:48:13 -05:00
if len(x) < 4 {
return 0
}
flags := x[0]
if flags&0x80 != 0 { // uint16 format
2019-11-27 10:48:13 -05:00
return int(binary.BigEndian.Uint16(x[1:3]))
} else {
return int(x[1])
}
}
type Stopwatch struct {
lasttime time.Time
elapsed time.Time
running bool
2020-06-12 16:13:33 -04:00
startstopBtn *widget.Clickable
resetBtn *widget.Clickable
h, m, s int
}
func NewStopwatch() Stopwatch {
return Stopwatch{
2020-06-12 16:13:33 -04:00
startstopBtn: &widget.Clickable{},
resetBtn: &widget.Clickable{},
}
}
2019-10-23 18:29:13 -04:00
func eventloop() {
w := app.NewWindow(
app.Size(unit.Dp(350), unit.Dp(600)),
2019-10-23 18:29:13 -04:00
app.Title("HRM"),
)
2020-06-12 16:13:33 -04:00
th := material.NewTheme(gofont.Collection())
2019-11-22 16:44:26 -05:00
th.TextSize = unit.Sp(fontSize)
2020-06-12 16:13:33 -04:00
var ops op.Ops
2019-10-23 18:29:13 -04:00
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()
2019-11-27 10:48:13 -05:00
b.Enable(w)
2019-10-23 18:29:13 -04:00
state := "starting"
var hr int
2019-10-23 18:29:13 -04:00
var periph ble.Peripheral
var wide bool
periphs := make([]ble.Peripheral, 0)
2020-06-12 16:13:33 -04:00
btns := make([]*widget.Clickable, 0)
backBtn := &widget.Clickable{}
2019-10-23 18:29:13 -04:00
2020-06-12 16:13:33 -04:00
var page, offpage, scanpage, connpage, hrpage func(gtx C) D
2019-10-23 18:29:13 -04:00
f := &layout.Flex{Axis: layout.Vertical}
2020-06-12 16:13:33 -04:00
offpage = func(gtx C) D {
return f.Layout(gtx,
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Heart Rate Monitor").Layout(gtx)
}),
2020-06-12 16:13:33 -04:00
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Bluetooth is Powered Off").Layout(gtx)
}),
)
}
2020-06-12 16:13:33 -04:00
appname := func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return material.Body1(th, "Heart Rate Monitor").Layout(gtx)
})
}
2020-06-12 16:13:33 -04:00
appstate := func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return material.Body1(th, state).Layout(gtx)
})
}
2020-06-12 16:13:33 -04:00
periphname := func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return material.Body1(th, periph.Name).Layout(gtx)
})
}
2020-06-12 16:13:33 -04:00
leftbar := func(gtx C) D {
f := &layout.Flex{Axis: layout.Vertical}
2020-06-12 16:13:33 -04:00
return f.Layout(gtx,
layout.Rigid(appname),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "").Layout(gtx)
}),
layout.Rigid(appstate),
)
}
2020-06-12 16:13:33 -04:00
scanlist := func(gtx C) D {
lst := &layout.List{Axis: layout.Vertical}
2020-06-12 16:13:33 -04:00
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)
2019-10-23 18:29:13 -04:00
})
2020-06-12 16:13:33 -04:00
if btns[i].Clicked() {
b.StopScan()
periph = periphs[i]
b.Connect(periph)
state = "connecting"
page = connpage
w.Invalidate()
}
2020-06-12 16:13:33 -04:00
return ret
2019-10-23 18:29:13 -04:00
})
}
2020-06-12 16:13:33 -04:00
scanpage = func(gtx C) D {
if wide {
f2 := &layout.Flex{Axis: layout.Horizontal}
2020-06-12 16:13:33 -04:00
return f2.Layout(gtx,
layout.Rigid(leftbar),
layout.Flexed(1, scanlist),
)
} else {
2020-06-12 16:13:33 -04:00
return f.Layout(gtx,
layout.Rigid(appname),
layout.Rigid(appstate),
layout.Rigid(scanlist),
)
}
2019-10-23 18:29:13 -04:00
}
2020-06-12 16:13:33 -04:00
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
}
2020-06-12 16:13:33 -04:00
return ret
}),
)
}
2020-06-12 16:13:33 -04:00
hrcircle := func(gtx C) (int, D) {
var w, h1 float32
2020-06-12 16:13:33 -04:00
ret := layout.Center.Layout(gtx, func(gtx C) D {
blue := color.RGBA{0x3f, 0x51, 0xb5, 255}
white := color.RGBA{255, 255, 255, 255}
2020-06-12 16:13:33 -04:00
w, h1 = float32(gtx.Constraints.Max.X), float32(gtx.Constraints.Max.Y)
if w < h1 {
h1 = w
}
s2 := h1 * 0.9
r1 := f32.Rectangle{f32.Point{0, 0}, f32.Point{h1, h1}}
p2a := f32.Point{(h1-s2)/2, (h1-s2)/2}
p2b := f32.Point{s2 + (h1-s2)/2, s2 + (h1-s2)/2}
r2 := f32.Rectangle{p2a, p2b}
clip.Rect{ Rect: r1, NE: h1/2, NW: h1/2, SE: h1/2, SW: h1/2}.Op(gtx.Ops).Add(gtx.Ops)
paint.ColorOp{Color: blue}.Add(gtx.Ops)
paint.PaintOp{Rect: r1}.Add(gtx.Ops)
clip.Rect{ Rect: r2, NE: s2/2, NW: s2/2, SE: s2/2, SW: s2/2}.Op(gtx.Ops).Add(gtx.Ops)
paint.ColorOp{Color: white}.Add(gtx.Ops)
paint.PaintOp{Rect: r2}.Add(gtx.Ops)
op.TransformOp{}.Offset(p2a).Add(gtx.Ops)
2020-06-12 16:13:33 -04:00
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
2020-06-12 16:13:33 -04:00
layout.Center.Layout(gtx, func(gtx C) D {
return l.Layout(gtx)
})
2020-06-12 16:13:33 -04:00
var ret D
ret.Size = image.Point{int(h1), int(h1)}
return ret
})
2020-06-12 16:13:33 -04:00
return int(h1), ret
2019-10-23 18:29:13 -04:00
}
sw := NewStopwatch()
2020-06-12 16:13:33 -04:00
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}
2020-06-12 16:13:33 -04:00
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)
})
}),
2020-06-12 16:13:33 -04:00
layout.Rigid(func(gtx C) D {
f2 := layout.Flex{Axis: layout.Horizontal}
2020-06-12 16:13:33 -04:00
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)
})
}),
2020-06-12 16:13:33 -04:00
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)
})
}),
)
2020-06-12 16:13:33 -04:00
if sw.startstopBtn.Clicked() {
if sw.running {
sw.running = false
} else {
sw.running = true
}
}
2020-06-12 16:13:33 -04:00
if sw.resetBtn.Clicked() {
sw.elapsed = time.Time{}
sw.running = false
}
2020-06-12 16:13:33 -04:00
return ret
}),
)
2019-10-23 18:29:13 -04:00
}
swidth := new(int)
2020-06-12 16:13:33 -04:00
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}
2020-06-12 16:13:33 -04:00
ret = f2.Layout(gtx,
layout.Flexed(0.4, func(gtx C) D {
f3 := &layout.Flex{Axis: layout.Vertical}
2020-06-12 16:13:33 -04:00
c1 := layout.Rigid(func(gtx C) D {
ret2 = appname(gtx)
*swidth = ret2.Size.X
return ret2
})
2020-06-12 16:13:33 -04:00
c2 := layout.Rigid(func(gtx C) D {
ret2 = periphname(gtx)
s2 := ret2.Size.X
if s2 > *swidth {
*swidth = s2
}
2020-06-12 16:13:33 -04:00
return ret2
})
var c3 layout.FlexChild
2020-06-12 16:13:33 -04:00
if gtx.Constraints.Max.X > 370 {
c3 = layout.Rigid(stopwatch)
} else {
c3 = layout.Rigid(func(gtx C) D {
return D{}
})
}
2020-06-12 16:13:33 -04:00
c4 := layout.Rigid(stopbtn)
return f3.Layout(gtx,
c1,
2020-06-12 16:13:33 -04:00
layout.Rigid(appstate),
c2,
2020-06-12 16:13:33 -04:00
layout.Flexed(1.0, func(gtx C) D {
return material.Body1(th, "").Layout(gtx)
}),
c3,
c4,
)
}),
2020-06-12 16:13:33 -04:00
layout.Flexed(0.6, func(gtx C) D {
_, ret := hrcircle(gtx)
return ret
}),
)
} else { // !wide
2020-06-12 16:13:33 -04:00
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)
})
})
2020-06-12 16:13:33 -04:00
//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)
})
})
2020-06-12 16:13:33 -04:00
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 {
2020-06-12 16:13:33 -04:00
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,
)
}
}
2020-06-12 16:13:33 -04:00
if backBtn.Clicked() {
ble.Disconnect(periph)
periphs = periphs[:0]
Config.Autoconnect = ""
saveConfig()
b.Scan()
state = "scanning"
page = scanpage
}
2020-06-12 16:13:33 -04:00
return ret
}
page = offpage
2019-10-23 18:29:13 -04:00
t := time.NewTicker(time.Second/30)
2019-10-23 18:29:13 -04:00
for {
select {
case <-t.C:
w.Invalidate()
2019-11-27 11:50:36 -05:00
case e := <-b.Events():
2019-10-23 18:29:13 -04:00
switch e := e.(type) {
case ble.UpdateStateEvent:
2019-10-25 09:08:14 -04:00
fmt.Printf("UpdateState: %s\n", e.State)
2019-10-23 18:29:13 -04:00
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:
2019-10-25 09:08:14 -04:00
fmt.Printf("found %s (%s)\n", e.Peripheral.Name, e.Peripheral.Identifier)
2019-10-23 18:29:13 -04:00
periphs = append(periphs, e.Peripheral)
2020-06-12 16:13:33 -04:00
btns = append(btns, &widget.Clickable{})
if e.Peripheral.Identifier == Config.Autoconnect && b.Connect(e.Peripheral) {
state = "connecting"
page = connpage
2019-10-23 18:29:13 -04:00
}
case ble.ConnectEvent:
2019-10-23 19:29:41 -04:00
fmt.Printf("Connect event\n")
b.StopScan()
2019-10-23 18:29:13 -04:00
state = "connected"
periph = e.Peripheral
Config.Autoconnect = periph.Identifier
saveConfig()
e.Peripheral.DiscoverServices()
2019-10-23 18:29:13 -04:00
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)
2019-11-27 10:48:13 -05:00
//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)
2019-10-23 18:29:13 -04:00
}
2019-10-25 09:08:14 -04:00
w.Invalidate() // refresh on any Bluetooth event
2019-10-23 18:29:13 -04:00
case e := <-w.Events():
switch e := e.(type) {
case system.DestroyEvent:
return
case system.FrameEvent:
2020-06-12 16:13:33 -04:00
gtx := layout.NewContext(&ops, e)
if e.Size.X > e.Size.Y {
wide = true
} else {
wide = false
}
2019-10-23 18:29:13 -04:00
resetSysinset(e.Insets)
2020-06-12 16:13:33 -04:00
sysinset.Layout(gtx, func(gtx C) D {
return margin.Layout(gtx, page)
2019-10-23 18:29:13 -04:00
})
e.Frame(gtx.Ops)
}
}
}
}