Created a devices service and moved the CDashDisplay service to the sahdow realm

This commit is contained in:
2026-08-25 14:46:20 +01:00
parent fe86993c6d
commit f4bde52f40
19 changed files with 444 additions and 184 deletions
+96
View File
@@ -0,0 +1,96 @@
package cdashdisplay
import (
"esdi/peripheral/communication"
"esdi/peripheral/communication/packets"
"fmt"
"time"
"github.com/tarm/serial"
portp "go.bug.st/serial"
)
func listPorts() ([]string, error) {
ports, err := portp.GetPortsList()
if err != nil {
return []string{}, err
}
return ports, nil
}
func probe(WT *communication.WalkieTalkie) error {
err := WT.TurnOn()
if err != nil {
return err
}
// Send the identification command
cmd := communication.CmdRequestID
var response packets.IdentificationPacket
err = WT.SendCommand(cmd, []byte{0x06, 0x07, 0x08, 0x09}, &response)
if err != nil {
return err
}
if response.DeviceID != 0x01 {
return fmt.Errorf("wrong ID")
}
return nil
}
func findDisplayPort() (*communication.WalkieTalkie, error) {
ports, err := listPorts()
if err != nil {
return nil, err
}
pLogger.Info(fmt.Sprintf("Looking into %v", ports))
var wt *communication.WalkieTalkie
for _, port := range ports {
pLogger.Info(fmt.Sprintf("Trying port %s", port))
wt = &communication.WalkieTalkie{
Cfg: &serial.Config{
Name: port,
Baud: 115200,
ReadTimeout: 500 * time.Millisecond,
},
}
pLogger.Info(fmt.Sprintf("Started probing port %s", port))
probeResult := make(chan error, 1)
go func() {
probeResult <- probe(wt)
}()
select {
case err = <-probeResult:
// Probe completed normally (could be success or error)
case <-time.After(2 * time.Second):
// Hard timeout reached
err = fmt.Errorf("probe completely hung/timed out: %s", port)
}
pLogger.Info(fmt.Sprintf("Finished probing port %s", port))
if err == nil {
pLogger.Info(fmt.Sprintf("Success probing port %s: %+v", port, err))
break
}
pLogger.Info(fmt.Sprintf("wasn't port %s", port))
wt = nil
}
if wt == nil {
return nil, fmt.Errorf("couldn't find cdashdisplay")
}
pLogger.Info(fmt.Sprintf("found cdashdisplay on port: %s", wt.Cfg.Name))
return wt, nil
}
+391
View File
@@ -0,0 +1,391 @@
// Package cdashdisplay will handle the USB connections
package cdashdisplay
import (
"bytes"
"fmt"
"io"
"log/slog"
"os"
"path"
"time"
helper "esdi/helpers"
"esdi/peripheral/communication"
"esdi/peripheral/communication/packets"
"esdi/peripheral/types"
"esdi/telemetry"
"gopkg.in/yaml.v3"
)
var pLogger *slog.Logger
func SetLogger(l *slog.Logger) {
pLogger = l
}
// I have to move this to some kind of configuration place
const (
layoutsDir = "./layouts/"
)
const (
newWindowCMDID types.Command = 3
destroyWindowCMDID types.Command = 4
updateWindowDimsCMDID types.Command = 5
updateWindowCMDID types.Command = 6 // Change this to a move cmd instead
sendDataCMDID types.Command = 7
newLayoutCMDID types.Command = 8
)
const (
MoveLeft = iota
MoveDown
MoveUp
MoveRight
)
var DefaultDecorations = UIDecorations{
HasBorder: 1,
BGColour: 0x1041, // Look in the eslabsCurses library for these colours
FGColour: 0xffff,
TitleColour: 0xffff,
BorderColour: 0xf800,
TitleSize: 2,
TextSize: 4,
Padding: 0x00,
}
type UpdateDimsPacket struct {
ID int16
Dims UIDimensions
}
type FString32 [32]byte
func (s FString32) String() string {
n := bytes.IndexByte(s[:], 0)
if n == -1 {
n = len(s)
}
return string(s[:n])
}
func (s FString32) MarshalYAML() (any, error) {
// trim trailing zero bytes
n := bytes.IndexByte(s[:], 0)
if n == -1 {
n = len(s)
}
return string(s[:n]), nil
}
func (s *FString32) UnmarshalYAML(value *yaml.Node) error {
if value.Kind != yaml.ScalarNode {
return fmt.Errorf("expected YAML scalar for FixedString32")
}
b := []byte(value.Value)
if len(b) > len(s) {
return fmt.Errorf("string too long (max %d bytes)", len(s))
}
// zero-fill first
for i := range s {
s[i] = 0
}
copy(s[:], b)
return nil
}
type CDashState struct {
Layout *LayoutTree
}
func NewCDashState() *CDashState {
return &CDashState{
Layout: NewLayoutTree(),
}
}
type CDashDisplay struct {
WT *communication.WalkieTalkie
State *CDashState
}
// Connect will try to find and connect to the CDashDisplay
func Discover() (*CDashDisplay, error) {
// Look for the port
p, err := findDisplayPort()
if err != nil {
pLogger.Info("failed to find cdashdisplay port: %s", err.Error())
return nil, err
}
return &CDashDisplay{
WT: p,
State: NewCDashState(),
}, nil
}
func (d *CDashDisplay) SendCommand() {
}
func (d *CDashDisplay) CreateWindow(win *DesktopUIWindow) (*DesktopUIWindow, error) {
bytes, err := helper.StructToBytes(win.UIWindow)
if err != nil {
return nil, err
}
// Send the command
var wID packets.NewWindowID
err = d.WT.SendCommand(newWindowCMDID, bytes, &wID)
if err != nil {
return nil, err
}
win.UIData.IDX = wID.ID
pLogger.Info(fmt.Sprintf("Recived ID message: %v", wID))
d.State.Layout.AddWindow(win)
return win, nil
}
func (d *CDashDisplay) UpdateWindow(win *DesktopUIWindow) error {
data := UIWindowUpdatePacket{
WinID: win.UIData.IDX,
Window: win.UIWindow,
}
bytes, err := helper.StructToBytes(data)
if err != nil {
return err
}
err = d.WT.SendCommand(updateWindowCMDID, bytes, nil)
if err != nil {
return err
}
// NOTE: need to check the flow of this because:
// windows has a pointer to a UIWindow stored
// I get it and update it in the controller
// I send the pointer here
// -> it should be the same pointer then right?
pLogger.Debug(fmt.Sprintf("PreUpdate ID: %p", win))
d.State.Layout.Windows[win.UIData.IDX] = win
pLogger.Debug(fmt.Sprintf("PostUpdate ID: %p", win))
// Yeah, same address as suspected
// I can't think about it right now. I'll think about that tomorrow
return nil
}
func (d *CDashDisplay) DestroyWindow(wID int16) error {
type UIWindowDestructPacket struct {
WinID int16
}
packet := UIWindowDestructPacket{
WinID: wID,
}
bytes, err := helper.StructToBytes(packet)
if err != nil {
return err
}
err = d.WT.SendCommand(destroyWindowCMDID, bytes, nil)
if err != nil {
return err
}
// NODE: add this
d.State.Layout.RemoveWindow(wID)
return nil
}
func (d *CDashDisplay) updateWindowDimensions(win *UIWindow, packet UpdateDimsPacket) error {
pLogger.Debug(fmt.Sprintf("UPDATE: %v", packet))
bytes, err := helper.StructToBytes(packet)
if err != nil {
return err
}
// var ack packets.AckPacket
err = d.WT.SendCommand(updateWindowDimsCMDID, bytes, nil)
if err != nil && err != io.EOF {
return err
}
// Nothing bad happened afaik
pLogger.Debug(fmt.Sprintf("cur dims: %v", win.Dims))
win.Dims = packet.Dims
pLogger.Debug(fmt.Sprintf("new dims: %v", win.Dims))
return nil
}
func (d *CDashDisplay) MoveWindow(wID int16, delta *helper.Vector) error {
// Get the window from the layout
window, ok := d.State.Layout.Windows[wID]
if !ok {
return fmt.Errorf("window with ID '%d' doesn't exist", wID)
}
// Now we will create new dimensions
newDimensions := window.Dims
newDimensions.X0 += delta.DX
newDimensions.Y0 += delta.DY
packet := UpdateDimsPacket{
ID: wID,
Dims: newDimensions,
}
err := d.updateWindowDimensions(&window.UIWindow, packet)
if err != nil {
return err
}
window.Dims = newDimensions
return nil
}
func (d *CDashDisplay) ResizeWindow(wID int16, delta *helper.Vector) error {
// Get the window from the layout
window, ok := d.State.Layout.Windows[wID]
if !ok {
return fmt.Errorf("window with ID '%d' doesn't exist", wID)
}
// Now we will create new dimensions
newDimensions := window.Dims
if delta.DX > 0 {
newDimensions.Width += delta.DX
} else {
newDimensions.Width -= delta.DX
newDimensions.X0 -= 2 * delta.DX
}
if delta.DY > 0 {
newDimensions.Height += delta.DY
} else {
newDimensions.Height -= delta.DY
newDimensions.Y0 -= 2 * delta.DY
}
packet := UpdateDimsPacket{
ID: wID,
Dims: newDimensions,
}
err := d.updateWindowDimensions(&window.UIWindow, packet)
if err != nil {
return err
}
window.Dims = newDimensions
return nil
}
func (d *CDashDisplay) SaveLayout(outputPath string) error {
file, err := os.OpenFile(path.Join(layoutsDir, outputPath),
os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
if err != nil {
return err
}
data, err := yaml.Marshal(d.State.Layout)
if err != nil {
return err
}
_, err = file.Write(data)
if err != nil {
return err
}
return nil
}
func (d *CDashDisplay) LoadLayout(layoutName string) error {
data, err := os.ReadFile(path.Join(layoutsDir, layoutName))
if err != nil {
return err
}
layout := NewLayoutTree()
err = yaml.Unmarshal(data, layout)
if err != nil {
return err
}
for _, w := range layout.Windows {
_, err = d.CreateWindow(w)
if err != nil {
// NOTE: Add a way to handle multiple errors ?
return err
}
}
return nil
}
func (d *CDashDisplay) UnloadLayout() error {
var err error
for _, w := range d.State.Layout.Windows {
pLogger.Debug(fmt.Sprintf("= Removing %d ==============================================",
w.UIData.IDX))
err = d.DestroyWindow(w.UIData.IDX)
time.Sleep(75 * time.Millisecond)
if err != nil {
pLogger.Error(fmt.Sprintf("failed to destroy window: %+v", err))
// NOTE: Add a way to handle multiple errors ?
return err
}
pLogger.Debug(fmt.Sprintf("= Removing %d ==============================================",
w.UIData.IDX))
}
return nil
}
func (d *CDashDisplay) SendData(data *telemetry.TelemetryData) {
packet := data.Pack()
bytes, err := helper.StructToBytes(packet)
if err != nil {
return
}
curStr := ""
byteCount := 0
for _, byte := range bytes {
byteCount++
curStr += fmt.Sprintf("%02x ", byte)
if byteCount == 8 {
pLogger.Debug(curStr)
curStr = ""
byteCount = 0
}
}
// var ack packets.AckPacket
err = d.WT.SendCommand(sendDataCMDID, bytes, nil)
if err != nil && err != io.EOF {
return
}
}
+9
View File
@@ -0,0 +1,9 @@
package cdashdisplay
import "esdi/peripheral/devices"
// TODO: I believe we don't need the #esdi/peripheral/devices thing anymore
const (
ID = devices.CDashDisplayDevID
Name = devices.CDashDisplayDevName
)
+25
View File
@@ -0,0 +1,25 @@
package cdashdisplay
import "fmt"
type LayoutTree struct {
Windows map[int16]*DesktopUIWindow `yaml:"Windows"`
}
func NewLayoutTree() *LayoutTree {
return &LayoutTree{
Windows: make(map[int16]*DesktopUIWindow),
}
}
func (l *LayoutTree) AddWindow(w *DesktopUIWindow) {
pLogger.Debug(fmt.Sprintf("adding window '%d' - %v", w.UIData.IDX))
l.Windows[w.UIData.IDX] = w
pLogger.Debug(fmt.Sprintf("new map - %v", l.Windows))
}
func (l *LayoutTree) RemoveWindow(idx int16) {
pLogger.Debug(fmt.Sprintf("removing window '%d'", idx))
delete(l.Windows, idx)
pLogger.Debug(fmt.Sprintf("new map - %v", l.Windows))
}
+70
View File
@@ -0,0 +1,70 @@
package cdashdisplay
// In this file we will place all structs that are 1:1 representation of the
// types in the device transport layer ->
const (
ShowIDFalse uint8 = 0
ShowIDTrue uint8 = 1
)
const (
WinTypeBASE uint8 = iota
WinTypeBAR
WinTypeSTRING
WinTypeTABLE
)
var WinTYPES = []string{"BASE", "BAR", "STRING", "TABLE"}
type UIDimensions struct {
X0 uint16 `yaml:"X0"`
Y0 uint16 `yaml:"Y0"`
Width uint16 `yaml:"Width"`
Height uint16 `yaml:"Height"`
}
type UIDecorations struct {
BGColour uint16 `yaml:"BGColour"`
FGColour uint16 `yaml:"FGColour"`
TitleColour uint16 `yaml:"TitleColour"`
BorderColour uint16 `yaml:"BorderColour"`
TitleSize uint8 `yaml:"TitleSize"`
TextSize uint8 `yaml:"TextSize"`
HasBorder uint8 `yaml:"HasBorder"`
Padding uint8 `yaml:"Padding"`
}
// NOTE: we can't use FString32 for this - too many bytes
// NOTE: use a bit flags for this options instead
type UIWindowOpts struct {
ShowID uint8 `yaml:"ShowID"`
WinType uint8 `yaml:"WinType"`
PreviewValue FString32 `yaml:"PreviewValue"`
}
type UIWindow struct {
Dims UIDimensions `yaml:"Dims"`
Decor UIDecorations `yaml:"Decor"`
Opts UIWindowOpts `yaml:"Opts"`
Title FString32 `yaml:"Title"`
}
type DesktopUIWindow struct {
UIWindow
UIData DesktopUIData
}
type DesktopUIData struct {
IDX int16 `yaml:"WID"`
TelemetryField string `yaml:"TelemetryField"`
}
type UIWindowUpdatePacket struct {
WinID int16
Window UIWindow
}
// func getUIWindowDTO(w *DesktopWindowData) *UIWindow {
//
// }