Add GUI frontend

This commit is contained in:
2021-08-25 21:18:24 -07:00
parent cbcefb149e
commit b7bd385c43
11 changed files with 536 additions and 8 deletions

30
cmd/itgui/error.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
func guiErr(err error, msg string, parent fyne.Window) {
msgLbl := widget.NewLabel(msg)
msgLbl.Wrapping = fyne.TextWrapWord
msgLbl.Alignment = fyne.TextAlignCenter
rect := canvas.NewRectangle(color.Transparent)
rect.SetMinSize(fyne.NewSize(350, 0))
content := container.NewVBox(
msgLbl,
rect,
)
if err != nil {
errLbl := widget.NewLabel(err.Error())
content.Add(widget.NewAccordion(
widget.NewAccordionItem("More Details", errLbl),
))
}
dialog.NewCustom("Error", "Ok", content, parent).Show()
}

141
cmd/itgui/info.go Normal file
View File

@@ -0,0 +1,141 @@
package main
import (
"bufio"
"errors"
"fmt"
"image/color"
"net"
"encoding/json"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"go.arsenm.dev/itd/internal/types"
)
func infoTab(parent fyne.Window) *fyne.Container {
infoLayout := container.NewVBox(
// Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent),
)
heartRateLbl := newText("0 BPM", 24)
heartRate := container.NewVBox(
newText("Heart Rate", 12),
heartRateLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(heartRate)
go watch(types.ReqTypeWatchHeartRate, func(data interface{}) {
heartRateLbl.Text = fmt.Sprintf("%d BPM", int(data.(float64)))
heartRateLbl.Refresh()
}, parent)
battLevelLbl := newText("0%", 24)
battLevel := container.NewVBox(
newText("Battery Level", 12),
battLevelLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(battLevel)
go watch(types.ReqTypeWatchBattLevel, func(data interface{}) {
battLevelLbl.Text = fmt.Sprintf("%d%%", int(data.(float64)))
battLevelLbl.Refresh()
}, parent)
fwVerString, err := get(types.ReqTypeFwVersion)
if err != nil {
panic(err)
}
fwVer := container.NewVBox(
newText("Firmware Version", 12),
newText(fwVerString.(string), 24),
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(fwVer)
btAddrString, err := get(types.ReqTypeBtAddress)
if err != nil {
panic(err)
}
btAddr := container.NewVBox(
newText("Bluetooth Address", 12),
newText(btAddrString.(string), 24),
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(btAddr)
return infoLayout
}
func watch(req int, onRecv func(data interface{}), parent fyne.Window) error {
conn, err := net.Dial("unix", SockPath)
if err != nil {
return err
}
defer conn.Close()
err = json.NewEncoder(conn).Encode(types.Request{
Type: req,
})
if err != nil {
return err
}
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
res, err := getResp(scanner.Bytes())
if err != nil {
guiErr(err, "Error getting response from connection", parent)
continue
}
onRecv(res.Value)
}
return nil
}
func get(req int) (interface{}, error) {
conn, err := net.Dial("unix", SockPath)
if err != nil {
return nil, err
}
defer conn.Close()
err = json.NewEncoder(conn).Encode(types.Request{
Type: req,
})
if err != nil {
return nil, err
}
line, _, err := bufio.NewReader(conn).ReadLine()
if err != nil {
return nil, err
}
res, err := getResp(line)
if err != nil {
return nil, err
}
return res.Value, nil
}
func getResp(line []byte) (*types.Response, error) {
var res types.Response
err := json.Unmarshal(line, &res)
if err != nil {
return nil, err
}
if res.Error {
return nil, errors.New(res.Message)
}
return &res, nil
}
func newText(t string, size float32) *canvas.Text {
text := canvas.NewText(t, theme.ForegroundColor())
text.TextSize = size
return text
}

23
cmd/itgui/main.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
)
var SockPath = "/tmp/itd/socket"
func main() {
a := app.New()
window := a.NewWindow("itgui")
tabs := container.NewAppTabs(
container.NewTabItem("Info", infoTab(window)),
container.NewTabItem("Notify", notifyTab(window)),
container.NewTabItem("Set Time", timeTab(window)),
container.NewTabItem("Upgrade", upgradeTab(window)),
)
window.SetContent(tabs)
window.ShowAndRun()
}

43
cmd/itgui/notify.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"encoding/json"
"net"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/internal/types"
)
func notifyTab(parent fyne.Window) *fyne.Container {
titleEntry := widget.NewEntry()
titleEntry.SetPlaceHolder("Title")
bodyEntry := widget.NewMultiLineEntry()
bodyEntry.SetPlaceHolder("Body")
sendBtn := widget.NewButton("Send", func() {
conn, err := net.Dial("unix", SockPath)
if err != nil {
guiErr(err, "Error dialing socket", parent)
return
}
json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeNotify,
Data: types.ReqDataNotify{
Title: titleEntry.Text,
Body: bodyEntry.Text,
},
})
})
return container.NewVBox(
layout.NewSpacer(),
titleEntry,
bodyEntry,
sendBtn,
layout.NewSpacer(),
)
}

65
cmd/itgui/time.go Normal file
View File

@@ -0,0 +1,65 @@
package main
import (
"encoding/json"
"net"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/internal/types"
)
func timeTab(parent fyne.Window) *fyne.Container {
timeEntry := widget.NewEntry()
timeEntry.SetText(time.Now().Format(time.RFC1123))
currentBtn := widget.NewButton("Set Current", func() {
timeEntry.SetText(time.Now().Format(time.RFC1123))
setTime(true)
})
timeBtn := widget.NewButton("Set", func() {
parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text)
if err != nil {
guiErr(err, "Error parsing time string", parent)
return
}
setTime(false, parsedTime)
})
return container.NewVBox(
layout.NewSpacer(),
timeEntry,
currentBtn,
timeBtn,
layout.NewSpacer(),
)
}
// setTime sets the first element in the variadic parameter
// if current is false, otherwise, it sets the current time.
func setTime(current bool, t ...time.Time) error {
conn, err := net.Dial("unix", SockPath)
if err != nil {
return err
}
var data string
if current {
data = "now"
} else {
data = t[0].Format(time.RFC3339)
}
defer conn.Close()
err = json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeSetTime,
Data: data,
})
if err != nil {
return err
}
return nil
}

163
cmd/itgui/upgrade.go Normal file
View File

@@ -0,0 +1,163 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/itd/internal/types"
)
func upgradeTab(parent fyne.Window) *fyne.Container {
var (
archivePath string
fiwmarePath string
initPktPath string
)
archiveDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
if e != nil || uc == nil {
return
}
uc.Close()
archivePath = uc.URI().Path()
}, parent)
archiveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
archiveBtn := widget.NewButton("Select archive (.zip)", archiveDialog.Show)
firmwareDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
if e != nil || uc == nil {
return
}
uc.Close()
fiwmarePath = uc.URI().Path()
}, parent)
firmwareDialog.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
firmwareBtn := widget.NewButton("Select init packet (.bin)", firmwareDialog.Show)
initPktDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
if e != nil || uc == nil {
return
}
uc.Close()
initPktPath = uc.URI().Path()
}, parent)
initPktDialog.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
initPktBtn := widget.NewButton("Select init packet (.dat)", initPktDialog.Show)
initPktBtn.Hide()
firmwareBtn.Hide()
upgradeTypeSelect := widget.NewSelect([]string{
"Archive",
"Files",
}, func(s string) {
archiveBtn.Hide()
initPktBtn.Hide()
firmwareBtn.Hide()
switch s {
case "Archive":
archiveBtn.Show()
case "Files":
initPktBtn.Show()
firmwareBtn.Show()
}
})
upgradeTypeSelect.SetSelectedIndex(0)
startBtn := widget.NewButton("Start", func() {
if archivePath == "" && (initPktPath == "" && fiwmarePath == "") {
guiErr(nil, "Upgrade requires archive or files selected", parent)
return
}
progressLbl := widget.NewLabelWithStyle("0 / 0 B", fyne.TextAlignCenter, fyne.TextStyle{})
progressBar := widget.NewProgressBar()
progressDlg := widget.NewModalPopUp(container.NewVBox(
layout.NewSpacer(),
progressLbl,
progressBar,
layout.NewSpacer(),
), parent.Canvas())
progressDlg.Resize(fyne.NewSize(300, 100))
var fwUpgType int
var files []string
switch upgradeTypeSelect.Selected {
case "Archive":
fwUpgType = types.UpgradeTypeArchive
files = append(files, archivePath)
case "Files":
fwUpgType = types.UpgradeTypeFiles
files = append(files, initPktPath, fiwmarePath)
}
conn, err := net.Dial("unix", SockPath)
if err != nil {
guiErr(err, "Error dialing socket", parent)
return
}
defer conn.Close()
json.NewEncoder(conn).Encode(types.Request{
Type: types.ReqTypeFwUpgrade,
Data: types.ReqDataFwUpgrade{
Type: fwUpgType,
Files: files,
},
})
progressDlg.Show()
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
var res types.Response
// Decode scanned line into response struct
err = json.Unmarshal(scanner.Bytes(), &res)
if err != nil {
guiErr(err, "Error decoding response", parent)
return
}
if res.Error {
guiErr(err, "Error returned in response", parent)
return
}
var event types.DFUProgress
// Decode response data into progress struct
err = mapstructure.Decode(res.Value, &event)
if err != nil {
guiErr(err, "Error decoding response value", parent)
return
}
// If transfer finished, break
if event.Received == event.Total {
break
}
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
progressBar.Max = float64(event.Total)
progressBar.Value = float64(event.Received)
progressBar.Refresh()
}
conn.Close()
progressDlg.Hide()
})
return container.NewVBox(
layout.NewSpacer(),
upgradeTypeSelect,
archiveBtn,
firmwareBtn,
initPktBtn,
startBtn,
layout.NewSpacer(),
)
}