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