Rewrite itgui and add new screenshots
@@ -28,10 +28,15 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
|
||||
)
|
||||
if err != nil {
|
||||
// Create new label containing error text
|
||||
errLbl := widget.NewLabel(err.Error())
|
||||
errEntry := widget.NewEntry()
|
||||
errEntry.SetText(err.Error())
|
||||
// If text changed, change it back
|
||||
errEntry.OnChanged = func(string) {
|
||||
errEntry.SetText(err.Error())
|
||||
}
|
||||
// Create new dropdown containing error label
|
||||
content.Add(widget.NewAccordion(
|
||||
widget.NewAccordionItem("More Details", errLbl),
|
||||
widget.NewAccordionItem("More Details", errEntry),
|
||||
))
|
||||
}
|
||||
if fatal {
|
||||
@@ -49,5 +54,4 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
|
||||
// Show error dialog
|
||||
dialog.NewCustom("Error", "Ok", content, parent).Show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
163
cmd/itgui/firmware.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
// Create select to chose between archive and files upgrade
|
||||
typeSelect := widget.NewSelect([]string{"Archive", "Files"}, nil)
|
||||
typeSelect.PlaceHolder = "Upgrade Type"
|
||||
|
||||
// Create map to store files
|
||||
files := map[string]string{}
|
||||
|
||||
// Create and disable start button
|
||||
startBtn := widget.NewButton("Start", nil)
|
||||
startBtn.Disable()
|
||||
|
||||
// Create new file open dialog for archive
|
||||
archiveDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
// Set archive path in map
|
||||
files[".zip"] = uc.URI().Path()
|
||||
// Enable start button
|
||||
startBtn.Enable()
|
||||
}, w)
|
||||
// Only allow .zip files
|
||||
archiveDlg.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
|
||||
// Create button to show dialog
|
||||
archiveBtn := widget.NewButton("Select Archive (.zip)", archiveDlg.Show)
|
||||
|
||||
// Create new file open dialog for firmware image
|
||||
imageDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
|
||||
// Set firmware image path in map
|
||||
files[".bin"] = uc.URI().Path()
|
||||
|
||||
// If the init packet was already selected
|
||||
_, datOk := files[".dat"]
|
||||
if datOk {
|
||||
// Enable start button
|
||||
startBtn.Enable()
|
||||
}
|
||||
}, w)
|
||||
// Only allow .bin files
|
||||
imageDlg.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
|
||||
// Create button to show dialog
|
||||
imageBtn := widget.NewButton("Select Firmware (.bin)", imageDlg.Show)
|
||||
|
||||
// Create new file open dialog for init packet
|
||||
initDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
|
||||
// Set init packet path in map
|
||||
files[".dat"] = uc.URI().Path()
|
||||
|
||||
// If the firmware image was already selected
|
||||
_, binOk := files[".bin"]
|
||||
if binOk {
|
||||
// Enable start button
|
||||
startBtn.Enable()
|
||||
}
|
||||
}, w)
|
||||
// Only allow .dat files
|
||||
initDlg.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
|
||||
// Create button to show dialog
|
||||
initBtn := widget.NewButton("Select Init Packet (.dat)", initDlg.Show)
|
||||
|
||||
var upgType api.UpgradeType = 255
|
||||
// When upgrade type changes
|
||||
typeSelect.OnChanged = func(s string) {
|
||||
// Delete all files from map
|
||||
delete(files, ".bin")
|
||||
delete(files, ".dat")
|
||||
delete(files, ".zip")
|
||||
// Hide all dialog buttons
|
||||
imageBtn.Hide()
|
||||
initBtn.Hide()
|
||||
archiveBtn.Hide()
|
||||
// Disable start button
|
||||
startBtn.Disable()
|
||||
|
||||
switch s {
|
||||
case "Files":
|
||||
// Set file upgrade type
|
||||
upgType = api.UpgradeTypeFiles
|
||||
// Show firmware image and init packet buttons
|
||||
imageBtn.Show()
|
||||
initBtn.Show()
|
||||
case "Archive":
|
||||
// Set archive upgrade type
|
||||
upgType = api.UpgradeTypeArchive
|
||||
// Show archive button
|
||||
archiveBtn.Show()
|
||||
}
|
||||
}
|
||||
// Select archive by default
|
||||
typeSelect.SetSelectedIndex(0)
|
||||
|
||||
// When start button pressed
|
||||
startBtn.OnTapped = func() {
|
||||
var args []string
|
||||
// Append the appropriate files for upgrade type
|
||||
switch upgType {
|
||||
case api.UpgradeTypeArchive:
|
||||
args = append(args, files[".zip"])
|
||||
case api.UpgradeTypeFiles:
|
||||
args = append(args, files[".dat"], files[".bin"])
|
||||
}
|
||||
|
||||
// If args are nil (invalid upgrade type)
|
||||
if args == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create new progress dialog
|
||||
progress := newProgress(w)
|
||||
// Start firmware upgrade
|
||||
progressCh, err := client.FirmwareUpgrade(ctx, upgType, args...)
|
||||
if err != nil {
|
||||
guiErr(err, "Error performing firmware upgrade", false, w)
|
||||
return
|
||||
}
|
||||
// Show progress dialog
|
||||
progress.Show()
|
||||
// For every progress event
|
||||
for progressEvt := range progressCh {
|
||||
// Set progress bar values
|
||||
progress.SetTotal(float64(progressEvt.Total))
|
||||
progress.SetValue(float64(progressEvt.Sent))
|
||||
}
|
||||
// Hide progress dialog
|
||||
progress.Hide()
|
||||
}
|
||||
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
typeSelect,
|
||||
archiveBtn,
|
||||
imageBtn,
|
||||
initBtn,
|
||||
startBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
}
|
||||
360
cmd/itgui/fs.go
Normal file
@@ -0,0 +1,360 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/data/binding"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func fsTab(ctx context.Context, client *api.Client, w fyne.Window, opened chan struct{}) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
|
||||
// Create new binding to store current directory
|
||||
cwdData := binding.NewString()
|
||||
cwdData.Set("/")
|
||||
|
||||
// Create new list binding to store fs listing entries
|
||||
lsData := binding.NewUntypedList()
|
||||
|
||||
// This goroutine waits until the fs tab is opened to
|
||||
// request the listing from the watch
|
||||
go func() {
|
||||
// Wait for opened signal
|
||||
<-opened
|
||||
|
||||
// Show loading pop up
|
||||
loading := newLoadingPopUp(w)
|
||||
loading.Show()
|
||||
|
||||
// Read root directory
|
||||
ls, err := client.ReadDir(ctx, "/")
|
||||
if err != nil {
|
||||
guiErr(err, "Error reading directory", false, w)
|
||||
return
|
||||
}
|
||||
// Set ls binding
|
||||
lsData.Set(lsToAny(ls))
|
||||
|
||||
// Hide loading pop up
|
||||
loading.Hide()
|
||||
}()
|
||||
|
||||
toolbar := widget.NewToolbar(
|
||||
widget.NewToolbarAction(
|
||||
theme.ViewRefreshIcon(),
|
||||
func() {
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
},
|
||||
),
|
||||
widget.NewToolbarAction(
|
||||
theme.UploadIcon(),
|
||||
func() {
|
||||
// Create open dialog for file that will be uploaded
|
||||
dlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
// Get filepath and close
|
||||
localPath := uc.URI().Path()
|
||||
uc.Close()
|
||||
|
||||
// Create new entry to store filepath
|
||||
filenameEntry := widget.NewEntry()
|
||||
// Set entry text to the file name of the selected file
|
||||
filenameEntry.SetText(filepath.Base(localPath))
|
||||
// Create new dialog asking for the filename of the file to be stored on the watch
|
||||
uploadDlg := dialog.NewForm("Upload", "Upload", "Cancel", []*widget.FormItem{
|
||||
widget.NewFormItem("Filename", filenameEntry),
|
||||
}, func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Get remote path by joining current directory with filename
|
||||
remotePath := filepath.Join(cwd, filenameEntry.Text)
|
||||
|
||||
// Create new progress dialog
|
||||
progressDlg := newProgress(w)
|
||||
progressDlg.Show()
|
||||
|
||||
// Upload file
|
||||
progressCh, err := client.Upload(ctx, remotePath, localPath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error uploading file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
for progressEvt := range progressCh {
|
||||
progressDlg.SetTotal(float64(progressEvt.Total))
|
||||
progressDlg.SetValue(float64(progressEvt.Sent))
|
||||
if progressEvt.Sent == progressEvt.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Close progress dialog
|
||||
progressDlg.Hide()
|
||||
|
||||
// Add file to listing (avoids full refresh)
|
||||
lsData.Append(api.FileInfo{
|
||||
IsDir: false,
|
||||
Name: filepath.Base(remotePath),
|
||||
})
|
||||
}, w)
|
||||
uploadDlg.Show()
|
||||
}, w)
|
||||
dlg.Show()
|
||||
|
||||
},
|
||||
),
|
||||
widget.NewToolbarAction(
|
||||
theme.FolderNewIcon(),
|
||||
func() {
|
||||
// Create new entry for filename
|
||||
filenameEntry := widget.NewEntry()
|
||||
// Create new dialog to ask for the filename
|
||||
mkdirDialog := dialog.NewForm("Make Directory", "Create", "Cancel", []*widget.FormItem{
|
||||
widget.NewFormItem("Filename", filenameEntry),
|
||||
}, func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Get remote path by joining current directory and filename
|
||||
remotePath := filepath.Join(cwd, filenameEntry.Text)
|
||||
|
||||
// Make directory
|
||||
err := client.Mkdir(ctx, remotePath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error creating directory", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Add directory to listing (avoids full refresh)
|
||||
lsData.Append(api.FileInfo{
|
||||
IsDir: true,
|
||||
Name: filepath.Base(remotePath),
|
||||
})
|
||||
}, w)
|
||||
mkdirDialog.Show()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// Add listener to listing data to create the new items on the GUI
|
||||
// whenever the listing changes
|
||||
lsData.AddListener(binding.NewDataListener(func() {
|
||||
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
|
||||
c.Refresh()
|
||||
}))
|
||||
|
||||
return container.NewBorder(
|
||||
nil,
|
||||
toolbar,
|
||||
nil,
|
||||
nil,
|
||||
container.NewVScroll(c),
|
||||
)
|
||||
}
|
||||
|
||||
// makeItems creates GUI objects from listing data
|
||||
func makeItems(
|
||||
ctx context.Context,
|
||||
client *api.Client,
|
||||
lsData binding.UntypedList,
|
||||
cwdData binding.String,
|
||||
w fyne.Window,
|
||||
c *fyne.Container,
|
||||
) []fyne.CanvasObject {
|
||||
// Get listing data
|
||||
ls, _ := lsData.Get()
|
||||
|
||||
// Create output slice with dame length as listing
|
||||
out := make([]fyne.CanvasObject, len(ls))
|
||||
for index, val := range ls {
|
||||
// Assert value as file info
|
||||
item := val.(api.FileInfo)
|
||||
|
||||
var icon fyne.Resource
|
||||
// Decide which icon to use
|
||||
if item.IsDir {
|
||||
if item.Name == ".." {
|
||||
icon = theme.NavigateBackIcon()
|
||||
} else {
|
||||
icon = theme.FolderIcon()
|
||||
}
|
||||
} else {
|
||||
icon = theme.FileIcon()
|
||||
}
|
||||
|
||||
// Create new button with the decided icon and the item name
|
||||
btn := widget.NewButtonWithIcon(item.Name, icon, nil)
|
||||
// Align left
|
||||
btn.Alignment = widget.ButtonAlignLeading
|
||||
// Decide which callback function to use
|
||||
if item.IsDir {
|
||||
btn.OnTapped = func() {
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
cwd = filepath.Join(cwd, item.Name)
|
||||
// Set new current directory
|
||||
cwdData.Set(cwd)
|
||||
// Refresh GUI to display new directory
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
}
|
||||
} else {
|
||||
btn.OnTapped = func() {
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
remotePath := filepath.Join(cwd, item.Name)
|
||||
// Create new save dialog
|
||||
dlg := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
// Get path of selected file
|
||||
localPath := uc.URI().Path()
|
||||
// Close WriteCloser (it's not needed)
|
||||
uc.Close()
|
||||
|
||||
// Create new progress dialog
|
||||
progressDlg := newProgress(w)
|
||||
progressDlg.Show()
|
||||
|
||||
// Download file
|
||||
progressCh, err := client.Download(ctx, localPath, remotePath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error downloading file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// For every progress event
|
||||
for progressEvt := range progressCh {
|
||||
progressDlg.SetTotal(float64(progressEvt.Total))
|
||||
progressDlg.SetValue(float64(progressEvt.Sent))
|
||||
}
|
||||
|
||||
// Close progress dialog
|
||||
progressDlg.Hide()
|
||||
}, w)
|
||||
// Set filename to the item name
|
||||
dlg.SetFileName(item.Name)
|
||||
dlg.Show()
|
||||
}
|
||||
}
|
||||
|
||||
if item.Name == ".." {
|
||||
out[index] = btn
|
||||
continue
|
||||
}
|
||||
|
||||
moveBtn := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
|
||||
moveEntry := widget.NewEntry()
|
||||
dlg := dialog.NewForm("Move", "Move", "Cancel", []*widget.FormItem{
|
||||
widget.NewFormItem("New Path", moveEntry),
|
||||
}, func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
oldPath := filepath.Join(cwd, item.Name)
|
||||
|
||||
// Rename file
|
||||
err := client.Rename(ctx, oldPath, moveEntry.Text)
|
||||
if err != nil {
|
||||
guiErr(err, "Error renaming file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh GUI
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
}, w)
|
||||
dlg.Show()
|
||||
})
|
||||
|
||||
removeBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
path := filepath.Join(cwd, item.Name)
|
||||
|
||||
// Remove file
|
||||
err := client.Remove(ctx, path)
|
||||
if err != nil {
|
||||
guiErr(err, "Error removing file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh GUI
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
})
|
||||
|
||||
// Add button to GUI component list
|
||||
out[index] = container.NewBorder(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
container.NewHBox(moveBtn, removeBtn),
|
||||
btn,
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func refresh(
|
||||
ctx context.Context,
|
||||
cwdData binding.String,
|
||||
lsData binding.UntypedList,
|
||||
client *api.Client,
|
||||
w fyne.Window,
|
||||
c *fyne.Container,
|
||||
) {
|
||||
// Create and show new loading pop up
|
||||
loading := newLoadingPopUp(w)
|
||||
loading.Show()
|
||||
// Close pop up at the end of the function
|
||||
defer loading.Hide()
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Read directory
|
||||
ls, err := client.ReadDir(ctx, cwd)
|
||||
if err != nil {
|
||||
guiErr(err, "Error reading directory", false, w)
|
||||
return
|
||||
}
|
||||
// Set new listing data
|
||||
lsData.Set(lsToAny(ls))
|
||||
// Create new GUI objects
|
||||
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
|
||||
// Refresh GUI
|
||||
c.Refresh()
|
||||
}
|
||||
|
||||
func lsToAny(ls []api.FileInfo) []interface{} {
|
||||
out := make([]interface{}, len(ls)-1)
|
||||
for i, e := range ls {
|
||||
// Skip first element as it is always "."
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
out[i-1] = e
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -3,122 +3,84 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func infoTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
infoLayout := container.NewVBox(
|
||||
// Add rectangle for a bit of padding
|
||||
canvas.NewRectangle(color.Transparent),
|
||||
)
|
||||
|
||||
// Create label for heart rate
|
||||
heartRateLbl := newText("0 BPM", 24)
|
||||
// Creae container to store heart rate section
|
||||
heartRateSect := container.NewVBox(
|
||||
newText("Heart Rate", 12),
|
||||
heartRateLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(heartRateSect)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
onClose = append(onClose, cancel)
|
||||
func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
|
||||
// Create titled text for heart rate
|
||||
heartRateText := newTitledText("Heart Rate", "0 BPM")
|
||||
c.Add(heartRateText)
|
||||
// Watch heart rate
|
||||
heartRateCh, err := client.WatchHeartRate(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting heart rate channel", true, parent)
|
||||
guiErr(err, "Error watching heart rate", true, w)
|
||||
}
|
||||
go func() {
|
||||
// For every heart rate sample
|
||||
for heartRate := range heartRateCh {
|
||||
// Change text of heart rate label
|
||||
heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate)
|
||||
// Refresh label
|
||||
heartRateLbl.Refresh()
|
||||
// Set body of titled text
|
||||
heartRateText.SetBody(fmt.Sprintf("%d BPM", heartRate))
|
||||
}
|
||||
}()
|
||||
|
||||
// Create label for heart rate
|
||||
stepCountLbl := newText("0 Steps", 24)
|
||||
// Creae container to store heart rate section
|
||||
stepCountSect := container.NewVBox(
|
||||
newText("Step Count", 12),
|
||||
stepCountLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(stepCountSect)
|
||||
|
||||
stepCountCh, err := client.WatchStepCount(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting step count channel", true, parent)
|
||||
}
|
||||
go func() {
|
||||
for stepCount := range stepCountCh {
|
||||
// Change text of heart rate label
|
||||
stepCountLbl.Text = fmt.Sprintf("%d Steps", stepCount)
|
||||
// Refresh label
|
||||
stepCountLbl.Refresh()
|
||||
}
|
||||
}()
|
||||
|
||||
// Create label for battery level
|
||||
battLevelLbl := newText("0%", 24)
|
||||
// Create container to store battery level section
|
||||
battLevel := container.NewVBox(
|
||||
newText("Battery Level", 12),
|
||||
battLevelLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(battLevel)
|
||||
|
||||
// Create titled text for battery level
|
||||
battLevelText := newTitledText("Battery Level", "0%")
|
||||
c.Add(battLevelText)
|
||||
// Watch battery level
|
||||
battLevelCh, err := client.WatchBatteryLevel(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting battery level channel", true, parent)
|
||||
guiErr(err, "Error watching battery level", true, w)
|
||||
}
|
||||
go func() {
|
||||
// For every battery level sample
|
||||
for battLevel := range battLevelCh {
|
||||
// Change text of battery level label
|
||||
battLevelLbl.Text = fmt.Sprintf("%d%%", battLevel)
|
||||
// Refresh label
|
||||
battLevelLbl.Refresh()
|
||||
// Set body of titled text
|
||||
battLevelText.SetBody(fmt.Sprintf("%d%%", battLevel))
|
||||
}
|
||||
}()
|
||||
|
||||
fwVerString, err := client.Version(context.Background())
|
||||
// Create titled text for step count
|
||||
stepCountText := newTitledText("Step Count", "0 Steps")
|
||||
c.Add(stepCountText)
|
||||
// Watch step count
|
||||
stepCountCh, err := client.WatchStepCount(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting firmware string", true, parent)
|
||||
guiErr(err, "Error watching step count", true, w)
|
||||
}
|
||||
go func() {
|
||||
// For every step count sample
|
||||
for stepCount := range stepCountCh {
|
||||
// Set body of titled text
|
||||
stepCountText.SetBody(fmt.Sprintf("%d Steps", stepCount))
|
||||
}
|
||||
}()
|
||||
|
||||
fwVer := container.NewVBox(
|
||||
newText("Firmware Version", 12),
|
||||
newText(fwVerString, 24),
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(fwVer)
|
||||
|
||||
btAddrString, err := client.Address(context.Background())
|
||||
// Create new titled text for address
|
||||
addressText := newTitledText("Address", "")
|
||||
c.Add(addressText)
|
||||
// Get address
|
||||
address, err := client.Address(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
guiErr(err, "Error getting address", true, w)
|
||||
}
|
||||
// Set body of titled text
|
||||
addressText.SetBody(address)
|
||||
|
||||
btAddr := container.NewVBox(
|
||||
newText("Bluetooth Address", 12),
|
||||
newText(btAddrString, 24),
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(btAddr)
|
||||
// Create new titled text for version
|
||||
versionText := newTitledText("Version", "")
|
||||
c.Add(versionText)
|
||||
// Get version
|
||||
version, err := client.Version(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting version", true, w)
|
||||
}
|
||||
// Set body of titled text
|
||||
versionText.SetBody(version)
|
||||
|
||||
return infoLayout
|
||||
}
|
||||
|
||||
func newText(t string, size float32) *canvas.Text {
|
||||
text := canvas.NewText(t, theme.ForegroundColor())
|
||||
text.TextSize = size
|
||||
return text
|
||||
return container.NewVScroll(c)
|
||||
}
|
||||
|
||||
21
cmd/itgui/loading.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func newLoadingPopUp(w fyne.Window) *widget.PopUp {
|
||||
pb := widget.NewProgressBarInfinite()
|
||||
rect := canvas.NewRectangle(color.Transparent)
|
||||
rect.SetMinSize(fyne.NewSize(200, 0))
|
||||
|
||||
return widget.NewModalPopUp(
|
||||
container.NewMax(rect, pb),
|
||||
w.Canvas(),
|
||||
)
|
||||
}
|
||||
@@ -1,43 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
var onClose []func()
|
||||
|
||||
func main() {
|
||||
// Create new app
|
||||
a := app.New()
|
||||
// Create new window with title "itgui"
|
||||
window := a.NewWindow("itgui")
|
||||
window.SetOnClosed(func() {
|
||||
for _, closeFn := range onClose {
|
||||
closeFn()
|
||||
}
|
||||
})
|
||||
w := a.NewWindow("itgui")
|
||||
|
||||
// Create new context for use with the API client
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Connect to ITD API
|
||||
client, err := api.New(api.DefaultAddr)
|
||||
if err != nil {
|
||||
guiErr(err, "Error connecting to itd", true, window)
|
||||
guiErr(err, "Error connecting to ITD", true, w)
|
||||
}
|
||||
onClose = append(onClose, func() {
|
||||
client.Close()
|
||||
})
|
||||
|
||||
// Create new app tabs container
|
||||
// Create channel to signal that the fs tab has been opened
|
||||
fsOpened := make(chan struct{})
|
||||
fsOnce := &sync.Once{}
|
||||
|
||||
// Create app tabs
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Info", infoTab(window, client)),
|
||||
container.NewTabItem("Motion", motionTab(window, client)),
|
||||
container.NewTabItem("Notify", notifyTab(window, client)),
|
||||
container.NewTabItem("Set Time", timeTab(window, client)),
|
||||
container.NewTabItem("Upgrade", upgradeTab(window, client)),
|
||||
container.NewTabItem("Info", infoTab(ctx, client, w)),
|
||||
container.NewTabItem("Motion", motionTab(ctx, client, w)),
|
||||
container.NewTabItem("Notify", notifyTab(ctx, client, w)),
|
||||
container.NewTabItem("FS", fsTab(ctx, client, w, fsOpened)),
|
||||
container.NewTabItem("Time", timeTab(ctx, client, w)),
|
||||
container.NewTabItem("Firmware", firmwareTab(ctx, client, w)),
|
||||
)
|
||||
|
||||
// Set tabs as window content
|
||||
window.SetContent(tabs)
|
||||
// Show window and run app
|
||||
window.ShowAndRun()
|
||||
// When a tab is selected
|
||||
tabs.OnSelected = func(ti *container.TabItem) {
|
||||
// If the tab's name is FS
|
||||
if ti.Text == "FS" {
|
||||
// Signal fsOpened only once
|
||||
fsOnce.Do(func() {
|
||||
fsOpened <- struct{}{}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel context on close
|
||||
w.SetOnClosed(cancel)
|
||||
// Set content and show window
|
||||
w.SetContent(tabs)
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
||||
@@ -2,109 +2,61 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"fmt"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func motionTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
// Create label for heart rate
|
||||
xCoordLbl := newText("0", 24)
|
||||
// Creae container to store heart rate section
|
||||
xCoordSect := container.NewVBox(
|
||||
newText("X Coordinate", 12),
|
||||
xCoordLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
// Create titledText for each coordinate
|
||||
xText := newTitledText("X Coordinate", "0")
|
||||
yText := newTitledText("Y Coordinate", "0")
|
||||
zText := newTitledText("Z Coordinate", "0")
|
||||
|
||||
// Create label for heart rate
|
||||
yCoordLbl := newText("0", 24)
|
||||
// Creae container to store heart rate section
|
||||
yCoordSect := container.NewVBox(
|
||||
newText("Y Coordinate", 12),
|
||||
yCoordLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
// Create label for heart rate
|
||||
zCoordLbl := newText("0", 24)
|
||||
// Creae container to store heart rate section
|
||||
zCoordSect := container.NewVBox(
|
||||
newText("Z Coordinate", 12),
|
||||
zCoordLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
var ctxCancel func()
|
||||
|
||||
// Create variable to keep track of whether motion started
|
||||
started := false
|
||||
|
||||
// Create button to stop motion
|
||||
stopBtn := widget.NewButton("Stop", nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
onClose = append(onClose, cancel)
|
||||
|
||||
// Create button to start motion
|
||||
startBtn := widget.NewButton("Start", func() {
|
||||
// if motion is started
|
||||
if started {
|
||||
// Do nothing
|
||||
return
|
||||
}
|
||||
// Set motion started
|
||||
started = true
|
||||
// Watch motion values
|
||||
motionCh, err := client.WatchMotion(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting heart rate channel", true, parent)
|
||||
}
|
||||
// Create done channel
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case motion := <-motionCh:
|
||||
// Set labels to new values
|
||||
xCoordLbl.Text = strconv.Itoa(int(motion.X))
|
||||
yCoordLbl.Text = strconv.Itoa(int(motion.Y))
|
||||
zCoordLbl.Text = strconv.Itoa(int(motion.Z))
|
||||
// Refresh labels to display new values
|
||||
xCoordLbl.Refresh()
|
||||
yCoordLbl.Refresh()
|
||||
zCoordLbl.Refresh()
|
||||
}
|
||||
// Create start button
|
||||
toggleBtn := widget.NewButton("Start", nil)
|
||||
// Set button's on tapped callback
|
||||
toggleBtn.OnTapped = func() {
|
||||
switch toggleBtn.Text {
|
||||
case "Start":
|
||||
// Create new context for motion
|
||||
motionCtx, cancel := context.WithCancel(ctx)
|
||||
// Set ctxCancel to function so that stop button can run it
|
||||
ctxCancel = cancel
|
||||
// Watch motion
|
||||
motionCh, err := client.WatchMotion(motionCtx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error watching motion", false, w)
|
||||
return
|
||||
}
|
||||
}()
|
||||
// Create stop function
|
||||
stopBtn.OnTapped = func() {
|
||||
done <- struct{}{}
|
||||
started = false
|
||||
cancel()
|
||||
go func() {
|
||||
// For every motion event
|
||||
for motion := range motionCh {
|
||||
// Set coordinates
|
||||
xText.SetBody(fmt.Sprint(motion.X))
|
||||
yText.SetBody(fmt.Sprint(motion.Y))
|
||||
zText.SetBody(fmt.Sprint(motion.Z))
|
||||
}
|
||||
}()
|
||||
// Set button text to "Stop"
|
||||
toggleBtn.SetText("Stop")
|
||||
case "Stop":
|
||||
// Cancel motion context
|
||||
ctxCancel()
|
||||
// Set button text to "Start"
|
||||
toggleBtn.SetText("Start")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
// Run stop button function on close if possible
|
||||
onClose = append(onClose, func() {
|
||||
if stopBtn.OnTapped != nil {
|
||||
stopBtn.OnTapped()
|
||||
}
|
||||
})
|
||||
|
||||
// Return new container containing all elements
|
||||
return container.NewVBox(
|
||||
// Add rectangle for a bit of padding
|
||||
canvas.NewRectangle(color.Transparent),
|
||||
startBtn,
|
||||
stopBtn,
|
||||
xCoordSect,
|
||||
yCoordSect,
|
||||
zCoordSect,
|
||||
)
|
||||
return container.NewVScroll(container.NewVBox(
|
||||
toggleBtn,
|
||||
xText,
|
||||
yText,
|
||||
zText,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -10,30 +10,31 @@ import (
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func notifyTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
// Create new entry for notification title
|
||||
func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
c.Add(layout.NewSpacer())
|
||||
|
||||
// Create new entry for title
|
||||
titleEntry := widget.NewEntry()
|
||||
titleEntry.SetPlaceHolder("Title")
|
||||
c.Add(titleEntry)
|
||||
|
||||
// Create multiline entry for notification body
|
||||
// Create new multiline entry for body
|
||||
bodyEntry := widget.NewMultiLineEntry()
|
||||
bodyEntry.SetPlaceHolder("Body")
|
||||
c.Add(bodyEntry)
|
||||
|
||||
// Create new button to send notification
|
||||
// Create new send button
|
||||
sendBtn := widget.NewButton("Send", func() {
|
||||
err := client.Notify(context.Background(), titleEntry.Text, bodyEntry.Text)
|
||||
// Send notification
|
||||
err := client.Notify(ctx, titleEntry.Text, bodyEntry.Text)
|
||||
if err != nil {
|
||||
guiErr(err, "Error sending notification", false, parent)
|
||||
guiErr(err, "Error sending notification", false, w)
|
||||
return
|
||||
}
|
||||
})
|
||||
c.Add(sendBtn)
|
||||
|
||||
// Return new container containing all elements
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
titleEntry,
|
||||
bodyEntry,
|
||||
sendBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
c.Add(layout.NewSpacer())
|
||||
return container.NewVScroll(c)
|
||||
}
|
||||
|
||||
50
cmd/itgui/progress.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type progress struct {
|
||||
lbl *widget.Label
|
||||
pb *widget.ProgressBar
|
||||
*widget.PopUp
|
||||
}
|
||||
|
||||
func newProgress(w fyne.Window) progress {
|
||||
out := progress{}
|
||||
|
||||
// Create label to show how many bytes transfered and center it
|
||||
out.lbl = widget.NewLabel("0 / 0 B")
|
||||
out.lbl.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Create new progress bar
|
||||
out.pb = widget.NewProgressBar()
|
||||
|
||||
// Create new rectangle to set the size of the popup
|
||||
sizeRect := canvas.NewRectangle(color.Transparent)
|
||||
sizeRect.SetMinSize(fyne.NewSize(300, 50))
|
||||
|
||||
// Create vbox for label and progress bar
|
||||
l := container.NewVBox(out.lbl, out.pb)
|
||||
// Create popup
|
||||
out.PopUp = widget.NewModalPopUp(container.NewMax(l, sizeRect), w.Canvas())
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (p progress) SetTotal(v float64) {
|
||||
p.pb.Max = v
|
||||
p.pb.Refresh()
|
||||
p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", p.pb.Value, v))
|
||||
}
|
||||
|
||||
func (p progress) SetValue(v float64) {
|
||||
p.pb.SetValue(v)
|
||||
p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", v, p.pb.Max))
|
||||
}
|
||||
BIN
cmd/itgui/screenshots/firmware.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
cmd/itgui/screenshots/fs.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
cmd/itgui/screenshots/info.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
cmd/itgui/screenshots/mkdir.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
cmd/itgui/screenshots/motion.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
cmd/itgui/screenshots/notify.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
cmd/itgui/screenshots/progress.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
cmd/itgui/screenshots/time.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -11,51 +11,47 @@ import (
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func timeTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
// Create new entry for time string
|
||||
func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
c.Add(layout.NewSpacer())
|
||||
|
||||
// Create entry for time string
|
||||
timeEntry := widget.NewEntry()
|
||||
// Set text to current time formatter properly
|
||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||
timeEntry.SetPlaceHolder("RFC1123")
|
||||
|
||||
// Create button to set current time
|
||||
currentBtn := widget.NewButton("Set Current", func() {
|
||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||
setTime(client, true)
|
||||
})
|
||||
|
||||
// Create button to set time inside entry
|
||||
timeBtn := widget.NewButton("Set", func() {
|
||||
// Parse time as RFC1123 string
|
||||
parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text)
|
||||
setCurrentBtn := widget.NewButton("Set current time", func() {
|
||||
// Set current time
|
||||
err := client.SetTime(ctx, time.Now())
|
||||
if err != nil {
|
||||
guiErr(err, "Error parsing time string", false, parent)
|
||||
guiErr(err, "Error setting time", false, w)
|
||||
return
|
||||
}
|
||||
// Set time to parsed time
|
||||
setTime(client, false, parsedTime)
|
||||
// Set time entry to current time
|
||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||
})
|
||||
|
||||
// Return new container with all elements centered
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
timeEntry,
|
||||
currentBtn,
|
||||
timeBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
}
|
||||
// Create button to set time from entry
|
||||
setBtn := widget.NewButton("Set", func() {
|
||||
// Parse RFC1123 time string in entry
|
||||
newTime, err := time.Parse(time.RFC1123, timeEntry.Text)
|
||||
if err != nil {
|
||||
guiErr(err, "Error parsing time string", false, w)
|
||||
return
|
||||
}
|
||||
// Set time from parsed string
|
||||
err = client.SetTime(ctx, newTime)
|
||||
if err != nil {
|
||||
guiErr(err, "Error setting time", false, w)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// setTime sets the first element in the variadic parameter
|
||||
// if current is false, otherwise, it sets the current time.
|
||||
func setTime(client *api.Client, current bool, t ...time.Time) error {
|
||||
var err error
|
||||
if current {
|
||||
err = client.SetTime(context.Background(), time.Now())
|
||||
} else {
|
||||
err = client.SetTime(context.Background(), t[0])
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
c.Add(timeEntry)
|
||||
c.Add(setBtn)
|
||||
c.Add(setCurrentBtn)
|
||||
|
||||
c.Add(layout.NewSpacer())
|
||||
return c
|
||||
}
|
||||
|
||||
35
cmd/itgui/titledText.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import "fyne.io/fyne/v2/widget"
|
||||
|
||||
type titledText struct {
|
||||
*widget.RichText
|
||||
}
|
||||
|
||||
func newTitledText(title, text string) titledText {
|
||||
titleStyle := widget.RichTextStyleHeading
|
||||
titleStyle.TextStyle.Bold = false
|
||||
return titledText{
|
||||
widget.NewRichText(
|
||||
&widget.TextSegment{
|
||||
Style: widget.RichTextStyleParagraph,
|
||||
Text: title,
|
||||
},
|
||||
&widget.TextSegment{
|
||||
Style: titleStyle,
|
||||
Text: text,
|
||||
},
|
||||
&widget.SeparatorSegment{},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (t titledText) SetTitle(s string) {
|
||||
t.RichText.Segments[0].(*widget.TextSegment).Text = s
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
func (t titledText) SetBody(s string) {
|
||||
t.RichText.Segments[1].(*widget.TextSegment).Text = s
|
||||
t.Refresh()
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"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"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
var (
|
||||
archivePath string
|
||||
firmwarePath string
|
||||
initPktPath string
|
||||
)
|
||||
|
||||
var archiveBtn *widget.Button
|
||||
// Create archive selection dialog
|
||||
archiveDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
|
||||
if e != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
uc.Close()
|
||||
archivePath = uc.URI().Path()
|
||||
archiveBtn.SetText(fmt.Sprintf("Select archive (.zip) [%s]", filepath.Base(archivePath)))
|
||||
}, parent)
|
||||
// Limit dialog to .zip files
|
||||
archiveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
|
||||
// Create button to show dialog
|
||||
archiveBtn = widget.NewButton("Select archive (.zip)", archiveDialog.Show)
|
||||
|
||||
var firmwareBtn *widget.Button
|
||||
// Create firmware selection dialog
|
||||
firmwareDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
|
||||
if e != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
uc.Close()
|
||||
firmwarePath = uc.URI().Path()
|
||||
firmwareBtn.SetText(fmt.Sprintf("Select firmware (.bin) [%s]", filepath.Base(firmwarePath)))
|
||||
}, parent)
|
||||
// Limit dialog to .bin files
|
||||
firmwareDialog.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
|
||||
// Create button to show dialog
|
||||
firmwareBtn = widget.NewButton("Select firmware (.bin)", firmwareDialog.Show)
|
||||
|
||||
var initPktBtn *widget.Button
|
||||
// Create init packet selection dialog
|
||||
initPktDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
|
||||
if e != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
uc.Close()
|
||||
initPktPath = uc.URI().Path()
|
||||
initPktBtn.SetText(fmt.Sprintf("Select init packet (.dat) [%s]", filepath.Base(initPktPath)))
|
||||
}, parent)
|
||||
// Limit dialog to .dat files
|
||||
initPktDialog.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
|
||||
// Create button to show dialog
|
||||
initPktBtn = widget.NewButton("Select init packet (.dat)", initPktDialog.Show)
|
||||
|
||||
// Hide init packet and firmware buttons
|
||||
initPktBtn.Hide()
|
||||
firmwareBtn.Hide()
|
||||
|
||||
// Create dropdown to select upgrade type
|
||||
upgradeTypeSelect := widget.NewSelect([]string{
|
||||
"Archive",
|
||||
"Files",
|
||||
}, func(s string) {
|
||||
// Hide all buttons
|
||||
archiveBtn.Hide()
|
||||
initPktBtn.Hide()
|
||||
firmwareBtn.Hide()
|
||||
// Unhide appropriate button(s)
|
||||
switch s {
|
||||
case "Archive":
|
||||
archiveBtn.Show()
|
||||
case "Files":
|
||||
initPktBtn.Show()
|
||||
firmwareBtn.Show()
|
||||
}
|
||||
})
|
||||
// Select first elemetn
|
||||
upgradeTypeSelect.SetSelectedIndex(0)
|
||||
|
||||
// Create new button to start DFU
|
||||
startBtn := widget.NewButton("Start", func() {
|
||||
// If archive path does not exist and both init packet and firmware paths
|
||||
// also do not exist, return error
|
||||
if archivePath == "" && (initPktPath == "" && firmwarePath == "") {
|
||||
guiErr(nil, "Upgrade requires archive or files selected", false, parent)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new label for byte progress
|
||||
progressLbl := widget.NewLabelWithStyle("0 / 0 B", fyne.TextAlignCenter, fyne.TextStyle{})
|
||||
// Create new progress bar
|
||||
progressBar := widget.NewProgressBar()
|
||||
// Create modal dialog containing label and progress bar
|
||||
progressDlg := widget.NewModalPopUp(container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
progressLbl,
|
||||
progressBar,
|
||||
layout.NewSpacer(),
|
||||
), parent.Canvas())
|
||||
// Resize modal to 300x100
|
||||
progressDlg.Resize(fyne.NewSize(300, 100))
|
||||
|
||||
var fwUpgType api.UpgradeType
|
||||
var files []string
|
||||
// Get appropriate upgrade type and file paths
|
||||
switch upgradeTypeSelect.Selected {
|
||||
case "Archive":
|
||||
fwUpgType = api.UpgradeTypeArchive
|
||||
files = append(files, archivePath)
|
||||
case "Files":
|
||||
fwUpgType = api.UpgradeTypeFiles
|
||||
files = append(files, initPktPath, firmwarePath)
|
||||
}
|
||||
|
||||
progress, err := client.FirmwareUpgrade(context.Background(), fwUpgType, files...)
|
||||
if err != nil {
|
||||
guiErr(err, "Error initiating DFU", false, parent)
|
||||
return
|
||||
}
|
||||
|
||||
// Show progress dialog
|
||||
progressDlg.Show()
|
||||
|
||||
for event := range progress {
|
||||
// Set label text to received / total B
|
||||
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
|
||||
// Set progress bar values
|
||||
progressBar.Max = float64(event.Total)
|
||||
progressBar.Value = float64(event.Received)
|
||||
// Refresh progress bar
|
||||
progressBar.Refresh()
|
||||
// If transfer finished, break
|
||||
if int64(event.Sent) == event.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Hide progress dialog after completion
|
||||
progressDlg.Hide()
|
||||
|
||||
// Reset screen to default
|
||||
upgradeTypeSelect.SetSelectedIndex(0)
|
||||
firmwareBtn.SetText("Select firmware (.bin)")
|
||||
initPktBtn.SetText("Select init packet (.dat)")
|
||||
archiveBtn.SetText("Select archive (.zip)")
|
||||
firmwarePath = ""
|
||||
initPktPath = ""
|
||||
archivePath = ""
|
||||
|
||||
dialog.NewInformation(
|
||||
"Upgrade Complete",
|
||||
"The firmware was transferred successfully.\nRemember to validate the firmware in InfiniTime settings.",
|
||||
parent,
|
||||
).Show()
|
||||
})
|
||||
|
||||
// Return container containing all elements
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
upgradeTypeSelect,
|
||||
archiveBtn,
|
||||
firmwareBtn,
|
||||
initPktBtn,
|
||||
startBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
}
|
||||