forked from Elara6331/itd
Add GUI frontend
This commit is contained in:
30
cmd/itgui/error.go
Normal file
30
cmd/itgui/error.go
Normal 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
141
cmd/itgui/info.go
Normal 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
23
cmd/itgui/main.go
Normal 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
43
cmd/itgui/notify.go
Normal 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
65
cmd/itgui/time.go
Normal 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
163
cmd/itgui/upgrade.go
Normal 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(),
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user