24-activate-peripherals #26

Merged
thinkode merged 7 commits from 24-activate-peripherals into develop 2025-11-02 10:00:14 +00:00
11 changed files with 175 additions and 334 deletions
Showing only changes of commit 15d0f8b61b - Show all commits

34
app.go
View File

@@ -16,12 +16,15 @@ import (
// App struct
type App struct {
ctx context.Context
cancelFunc context.CancelFunc
ctx context.Context
cancelFunc context.CancelFunc
wait sync.WaitGroup
hardwareManager *hardware.HardwareManager // For managing all the hardware
wmiMutex sync.Mutex // Avoid some WMI operations at the same time
projectInfo ProjectInfo // The project information structure
projectSave string // The file name of the project
projectCancel context.CancelFunc // The project cancel function
}
// NewApp creates a new App application struct
@@ -44,11 +47,17 @@ func NewApp() *App {
// so we can call the runtime methods
func (a *App) onStartup(ctx context.Context) {
a.ctx, a.cancelFunc = context.WithCancel(ctx)
err := a.hardwareManager.Start(a.ctx)
if err != nil {
log.Err(err).Str("file", "app").Msg("unable to start the hardware manager")
return
}
// Starting the hardware manager
a.wait.Add(1)
go func() {
defer a.wait.Done()
err := a.hardwareManager.Start(a.ctx)
if err != nil {
log.Err(err).Str("file", "app").Msg("unable to start the hardware manager")
return
}
}()
}
// onReady is called when the DOM is ready
@@ -69,10 +78,13 @@ func (a *App) onShutdown(ctx context.Context) {
log.Trace().Str("file", "app").Msg("app is closing")
// Explicitly close the context
a.cancelFunc()
err := a.hardwareManager.Stop()
if err != nil {
log.Err(err).Str("file", "app").Msg("unable to stop the hardware manager")
}
// Wait for application to close properly
a.hardwareManager.WaitStop()
// a.cancelFunc()
// err := a.hardwareManager.Stop()
// if err != nil {
// log.Err(err).Str("file", "app").Msg("unable to stop the hardware manager")
// }
return
}

View File

@@ -92,12 +92,6 @@
})
}
// Instanciate a new project
CreateProject().then((showInfo) => {
showInformation.set(showInfo)
$needProjectSave = true
})
// Handle window shortcuts
document.addEventListener('keydown', function(event) {
// Check the CTRL+S keys

View File

@@ -57,8 +57,6 @@
</div>
</div>
<style>
.unselected:hover{
background: linear-gradient(to bottom right, var(--second-color), var(--third-color));

View File

@@ -153,14 +153,6 @@
{:else}
<i>{$_("projectHardwareNoSettingLabel")}</i>
{/if}
<!-- <button on:click={ftdiConnect}>Connect FTDI 0</button>
<button on:click={ftdiActivate}>Activate FTDI 0</button>
<div class="slidecontainer">
<input type="range" min="0" max="255" class="slider" bind:value={sliderValue} on:input={() => ftdiSetDevice(sliderValue)}>
</div>
<button on:click={ftdiDeactivate}>Deactivate FTDI 0</button>
<button on:click={ftdiDisconnect}>Disconnect FTDI 0</button> -->
</div>
</div>
</div>

View File

@@ -5,10 +5,11 @@
import DropdownList from "../General/DropdownList.svelte";
import InputsOutputsContent from "./InputsOutputsContent.svelte";
import Tab from "../General/Tab.svelte";
import { CreateProject, GetProjects, GetProjectInfo } from "../../../wailsjs/go/main/App";
import { CreateProject, GetProjects, OpenProjectFromDisk } from "../../../wailsjs/go/main/App";
import { _ } from 'svelte-i18n'
import {colors} from '../../stores.js';
import { get } from "svelte/store"
import { EventsOn } from '../../../wailsjs/runtime'
const tabs = [
{ title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent },
@@ -57,18 +58,28 @@
})
}
// Handle the event when a new project need to be loaded
EventsOn('LOAD_PROJECT', function(projectInfo){
// Store project information
showInformation.set(projectInfo.ShowInfo)
// Remove the saved peripherals of the current project
unsavePeripherals()
// Load new project peripherals
loadPeripherals(projectInfo.PeripheralsInfo)
console.log("Project has been opened");
generateToast('info', 'bx-folder-open', $_("projectOpenedToast") + ' <b>' + projectInfo.ShowInfo.Name + '</b>')
})
// Open the selected project
function openSelectedProject(event){
let selectedOption = event.detail.key
// Open the selected project
GetProjectInfo(selectedOption).then((projectInfo) => {
$showInformation = projectInfo.ShowInfo
// Remove the saved peripherals ofthe current project
unsavePeripherals()
// Load the new project peripherals
loadPeripherals(projectInfo.PeripheralsInfo)
OpenProjectFromDisk(selectedOption).then(() => {
// Project opened, we set the needSave flag to false (already saved)
needProjectSave.set(false)
generateToast('info', 'bx-folder-open', $_("projectOpenedToast") + ' <b>' + projectInfo.ShowInfo.Name + '</b>')
}).catch((error) => {
console.error(`Unable to open the project: ${error}`)
generateToast('danger', 'bx-error', $_("projectOpenErrorToast"))
@@ -77,14 +88,17 @@
function initializeNewProject(){
// Instanciate a new project
CreateProject().then((showInfo) => {
$showInformation = showInfo
// Remove the saved peripherals ofthe current project
unsavePeripherals()
$needProjectSave = true
generateToast('info', 'bxs-folder-plus', $_("projectCreatedToast"))
CreateProject().then(() => {
// Project created, we set the needSave flag to true (not already saved)
needProjectSave.set(true)
}).catch((error) => {
console.error(`Unable to create the project: ${error}`)
generateToast('danger', 'bx-error', $_("projectCreateErrorToast"))
})
}
// Instantiate a new project
initializeNewProject()
</script>
<!-- Project buttons -->

View File

@@ -58,6 +58,7 @@
"projectOpenedToast": "The project was opened:",
"projectOpenErrorToast": "Unable to open the project",
"projectCreatedToast": "The project was created",
"projectCreateErrorToast": "Unable to create the project",
"peripheralSettingSaveErrorToast": "Unable to save the peripheral settings",
"os2lIp": "OS2L server IP",

View File

@@ -3,6 +3,7 @@ package hardware
import (
"context"
_ "embed"
"errors"
"fmt"
goRuntime "runtime"
"sync"
@@ -19,20 +20,21 @@ import (
*/
import "C"
// FTDIFinder represents how the protocol is defined
// FTDIFinder manages all the FTDI peripherals
type FTDIFinder struct {
findTicker time.Ticker // Peripherals find ticker
wg sync.WaitGroup
findTicker *time.Ticker // Peripherals find ticker
foundPeripherals map[string]PeripheralInfo // The list of peripherals handled by this finder
registeredPeripherals map[string]*FTDIPeripheral // The list of found peripherals
scanChannel chan struct{} // The channel to trigger a scan event
goWait sync.WaitGroup // Check goroutines execution
}
// NewFTDIFinder creates a new FTDI finder
func NewFTDIFinder(findPeriod time.Duration) *FTDIFinder {
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder created")
return &FTDIFinder{
findTicker: *time.NewTicker(findPeriod),
findTicker: time.NewTicker(findPeriod),
foundPeripherals: make(map[string]PeripheralInfo),
registeredPeripherals: make(map[string]*FTDIPeripheral),
scanChannel: make(chan struct{}),
@@ -41,12 +43,15 @@ func NewFTDIFinder(findPeriod time.Duration) *FTDIFinder {
// RegisterPeripheral registers a new peripheral
func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) {
// Create a new FTDI peripheral
ftdiPeripheral, err := NewFTDIPeripheral(peripheralData)
if err != nil {
return "", fmt.Errorf("unable to create the FTDI peripheral: %v", err)
}
// Register it in the finder
f.registeredPeripherals[peripheralData.SerialNumber] = ftdiPeripheral
log.Trace().Any("periph", &ftdiPeripheral).Str("file", "FTDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created")
// Peripheral created, connect it
err = ftdiPeripheral.Connect(ctx)
if err != nil {
@@ -71,7 +76,7 @@ func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralID stri
return err
}
// Disconnecting peripheral
err = peripheral.Disconnect(ctx)
err = peripheral.Disconnect()
if err != nil {
return err
}
@@ -93,9 +98,9 @@ func (f *FTDIFinder) Initialize() error {
// Start starts the finder and search for peripherals
func (f *FTDIFinder) Start(ctx context.Context) error {
f.goWait.Add(1)
f.wg.Add(1)
go func() {
defer f.goWait.Done()
defer f.wg.Done()
for {
select {
case <-ctx.Done():
@@ -120,18 +125,11 @@ func (f *FTDIFinder) Start(ctx context.Context) error {
// ForceScan explicily asks for scanning peripherals
func (f *FTDIFinder) ForceScan() {
f.scanChannel <- struct{}{}
}
// Stop stops the finder
func (f *FTDIFinder) Stop() error {
log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...")
// Wait for goroutines to stop
f.goWait.Wait()
// Stop the ticker
f.findTicker.Stop()
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped")
return nil
select {
case f.scanChannel <- struct{}{}:
default:
// Ignore if the channel is full or if it is closed
}
}
// GetName returns the name of the driver
@@ -171,18 +169,12 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error {
log.Info().Int("number", count).Msg("number of FTDI devices connected")
// Alloue un tableau de structures côté C
// Allocating C array
size := C.size_t(count) * C.size_t(unsafe.Sizeof(C.FTDIPeripheralC{}))
devicesPtr := C.malloc(size)
defer C.free(devicesPtr)
devices := (*[1 << 30]C.FTDIPeripheralC)(devicesPtr)[:count:count]
type device struct {
SerialNumber string
Description string
IsOpen bool
}
devices := (*[1 << 20]C.FTDIPeripheralC)(devicesPtr)[:count:count]
C.get_ftdi_devices((*C.FTDIPeripheralC)(devicesPtr), C.int(count))
@@ -202,7 +194,7 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error {
ProtocolName: "FTDI",
}
// Libération mémoire allouée côté C
// Free C memory
C.free_ftdi_device(&d)
}
@@ -214,3 +206,34 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error {
f.foundPeripherals = temporaryPeripherals
return nil
}
// WaitStop stops the finder
func (f *FTDIFinder) WaitStop() error {
log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...")
// Stop the ticker
f.findTicker.Stop()
// Close the channel
close(f.scanChannel)
// Wait for all the peripherals to close
log.Trace().Str("file", "FTDIFinder").Msg("closing all FTDI peripherals")
var errs []error
for registeredPeripheralSN, registeredPeripheral := range f.registeredPeripherals {
err := registeredPeripheral.WaitStop()
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", registeredPeripheralSN, err))
}
}
// Wait for goroutines to stop
f.wg.Wait()
// Returning errors
if len(errs) > 0 {
return errors.Join(errs...)
}
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped")
return nil
}

View File

@@ -13,6 +13,7 @@ import (
)
/*
#include <stdlib.h>
#cgo LDFLAGS: -L${SRCDIR}/../build/bin -ldmxSender
#include "cpp/include/dmxSenderBridge.h"
*/
@@ -20,29 +21,23 @@ import "C"
// FTDIPeripheral contains the data of an FTDI peripheral
type FTDIPeripheral struct {
info PeripheralInfo // The peripheral basic data
settings map[string]interface{} // The settings of the peripheral
dmxSender unsafe.Pointer // The command object for piloting the DMX ouptut
waitGroup sync.WaitGroup // Waitgroup to wait goroutines
isConnected bool // If the FTDI is connected or not
wg sync.WaitGroup
info PeripheralInfo // The peripheral basic data
dmxSender unsafe.Pointer // The command object for piloting the DMX ouptut
}
// NewFTDIPeripheral creates a new FTDI peripheral
func NewFTDIPeripheral(info PeripheralInfo) (*FTDIPeripheral, error) {
log.Info().Str("file", "FTDIPeripheral").Str("name", info.Name).Str("s/n", info.SerialNumber).Msg("FTDI peripheral created")
settings := make(map[string]interface{})
return &FTDIPeripheral{
info: info,
dmxSender: nil,
settings: settings,
isConnected: false,
info: info,
dmxSender: nil,
}, nil
}
// Connect connects the FTDI peripheral
func (p *FTDIPeripheral) Connect(ctx context.Context) error {
runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "connecting")
// Check if the device has already been created
if p.dmxSender != nil {
return errors.Errorf("the DMX device has already been created!")
@@ -51,30 +46,31 @@ func (p *FTDIPeripheral) Connect(ctx context.Context) error {
// Create the DMX sender
p.dmxSender = C.dmx_create()
// Connect the peripheral
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("connecting FTDI peripheral...")
err := C.dmx_connect(p.dmxSender, C.CString(p.info.SerialNumber))
if err != C.DMX_OK {
log.Error().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Any("err", err).Msg("unable to connect the DMX device")
return errors.Errorf("Unable to connect the DMX Device on the specified port")
// Connect the FTDI
runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "connecting")
serialNumber := C.CString(p.info.SerialNumber)
defer C.free(unsafe.Pointer(serialNumber))
if C.dmx_connect(p.dmxSender, serialNumber) != C.DMX_OK {
log.Error().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("unable to connect the DMX device")
return errors.Errorf("unable to connect '%s'")
}
p.waitGroup.Add(1)
p.wg.Add(1)
go func() {
defer p.waitGroup.Done()
defer p.wg.Done()
<-ctx.Done()
p.Disconnect(ctx)
_ = p.Disconnect()
}()
p.isConnected = true
// Send deactivated state (connected but not activated for now)
runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "deactivated")
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device connected successfully")
return nil
}
// Disconnect disconnects the FTDI peripheral
func (p *FTDIPeripheral) Disconnect(ctx context.Context) error {
func (p *FTDIPeripheral) Disconnect() error {
// Check if the device has already been created
if p.dmxSender == nil {
return errors.Errorf("the DMX device has not been connected!")
@@ -82,15 +78,9 @@ func (p *FTDIPeripheral) Disconnect(ctx context.Context) error {
// Destroy the dmx sender
C.dmx_destroy(p.dmxSender)
p.isConnected = false
return nil
}
// IsConnected returns if the FTDI is connected or not
func (p *FTDIPeripheral) IsConnected() bool {
return p.isConnected
}
// Activate activates the FTDI peripheral
func (p *FTDIPeripheral) Activate(ctx context.Context) error {
// Check if the device has already been created
@@ -136,8 +126,7 @@ func (p *FTDIPeripheral) Deactivate(ctx context.Context) error {
// SetSettings sets a specific setting for this peripheral
func (p *FTDIPeripheral) SetSettings(settings map[string]interface{}) error {
p.settings = settings
return nil
return errors.Errorf("unable to set the settings: not implemented")
}
// SetDeviceProperty sends a command to the specified device
@@ -161,10 +150,18 @@ func (p *FTDIPeripheral) SetDeviceProperty(ctx context.Context, channelNumber ui
// GetSettings gets the peripheral settings
func (p *FTDIPeripheral) GetSettings() map[string]interface{} {
return p.settings
return map[string]interface{}{}
}
// GetInfo gets all the peripheral information
func (p *FTDIPeripheral) GetInfo() PeripheralInfo {
return p.info
}
// WaitStop wait about the peripheral to close
func (p *FTDIPeripheral) WaitStop() error {
log.Info().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("waiting for FTDI peripheral to close...")
p.wg.Wait()
log.Info().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("FTDI peripheral closed!")
return nil
}

View File

@@ -2,9 +2,9 @@ package hardware
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/rs/zerolog/log"
@@ -24,16 +24,13 @@ const (
// debounceDuration = 500 * time.Millisecond
)
var (
debounceTimer *time.Timer
)
// HardwareManager is the class who manages the hardware
type HardwareManager struct {
wg sync.WaitGroup
finders map[string]PeripheralFinder // The map of peripherals finders
peripherals []Peripheral // The current list of peripherals
peripheralsScanTrigger chan struct{} // Trigger the peripherals scans
goWait sync.WaitGroup // Wait for goroutines to terminate
}
// NewHardwareManager creates a new HardwareManager
@@ -48,6 +45,7 @@ func NewHardwareManager() *HardwareManager {
// Start starts to find new peripheral events
func (h *HardwareManager) Start(ctx context.Context) error {
// Initialize all the finders
for finderName, finder := range h.finders {
err := finder.Initialize()
if err != nil {
@@ -60,9 +58,11 @@ func (h *HardwareManager) Start(ctx context.Context) error {
return err
}
}
h.goWait.Add(1)
// Periodically scan all the finders
h.wg.Add(1)
go func() {
defer h.goWait.Done()
defer h.wg.Done()
for {
select {
case <-ctx.Done():
@@ -95,40 +95,14 @@ func (h *HardwareManager) RegisterFinder(finder PeripheralFinder) {
log.Info().Str("file", "hardware").Str("finderName", finder.GetName()).Msg("finder registered")
}
// // GetPeripheral gets the peripheral object from the parent finder
// func (h *HardwareManager) GetPeripheral(finderName string, peripheralID string) (Peripheral, bool) {
// // Get the finder
// parentFinder, found := h.finders[finderName]
// // If no finder found, return false
// if !found {
// log.Error().Str("file", "hardware").Str("finderName", finderName).Msg("unable to get the finder")
// return nil, false
// }
// log.Trace().Str("file", "hardware").Str("finderName", parentFinder.GetName()).Msg("finder got")
// // Contact the finder to get the peripheral
// return parentFinder.GetPeripheral(peripheralID)
// }
// Scan scans all the peripherals for the registered finders
func (h *HardwareManager) Scan() error {
h.peripheralsScanTrigger <- struct{}{}
return nil
}
// Stop stops the hardware manager
func (h *HardwareManager) Stop() error {
log.Trace().Str("file", "hardware").Msg("closing the hardware manager")
// Stop each finder
for finderName, finder := range h.finders {
err := finder.Stop()
if err != nil {
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to stop the finder")
}
select {
case h.peripheralsScanTrigger <- struct{}{}:
return nil
default:
return fmt.Errorf("scan trigger not available (manager stopped?)")
}
// Wait for goroutines to finish
h.goWait.Wait()
log.Info().Str("file", "hardware").Msg("hardware manager stopped")
return nil
}
// emitPeripheralsChanges compares the old and new peripherals to determine which ones have been added or removed.
@@ -151,3 +125,30 @@ func emitPeripheralsChanges(ctx context.Context, oldPeripherals map[string]Perip
}
}
}
// WaitStop stops the hardware manager
func (h *HardwareManager) WaitStop() error {
log.Trace().Str("file", "hardware").Msg("closing the hardware manager")
// Closing trigger channel
close(h.peripheralsScanTrigger)
// Stop each finder
var errs []error
for name, f := range h.finders {
if err := f.WaitStop(); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", name, err))
}
}
// Wait for goroutines to finish
h.wg.Wait()
// Returning errors
if len(errs) > 0 {
return errors.Join(errs...)
}
log.Info().Str("file", "hardware").Msg("hardware manager stopped")
return nil
}

View File

@@ -6,7 +6,7 @@ import "context"
type Peripheral interface {
Connect(context.Context) error // Connect the peripheral
IsConnected() bool // Return if the peripheral is connected or not
Disconnect(context.Context) error // Disconnect the peripheral
Disconnect() error // Disconnect the peripheral
Activate(context.Context) error // Activate the peripheral
Deactivate(context.Context) error // Deactivate the peripheral
SetSettings(map[string]interface{}) error // Set a peripheral setting
@@ -29,7 +29,7 @@ type PeripheralInfo struct {
type PeripheralFinder interface {
Initialize() error // Initializes the protocol
Start(context.Context) error // Start the detection
Stop() error // Stop the detection
WaitStop() error // Waiting for finder to close
ForceScan() // Explicitly scans for peripherals
RegisterPeripheral(context.Context, PeripheralInfo) (string, error) // Registers a new peripheral data
UnregisterPeripheral(context.Context, string) error // Unregisters an existing peripheral

View File

@@ -1,191 +0,0 @@
package main
import (
"dmxconnect/hardware"
"fmt"
"github.com/rs/zerolog/log"
"os"
"path/filepath"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
"gopkg.in/yaml.v2"
)
const (
projectsDirectory = "projects" // The directory were are stored all the projects
avatarsDirectory = "frontend/public" // The directory were are stored all the avatars
projectExtension = ".dmxproj" // The extension of a DMX Connect project
)
// GetProjects gets all the projects in the projects directory
func (a *App) GetProjects() ([]ProjectMetaData, error) {
projects := []ProjectMetaData{}
f, err := os.Open(projectsDirectory)
if err != nil {
log.Err(err).Str("file", "project").Msg("unable to open the projects directory")
return nil, fmt.Errorf("unable to open the projects directory: %v", err)
}
log.Trace().Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("projects directory opened")
files, err := f.Readdir(0)
if err != nil {
log.Err(err).Str("file", "project").Msg("unable to read the projects directory")
return nil, fmt.Errorf("unable to read the projects directory: %v", err)
}
log.Trace().Str("file", "project").Any("projectsFiles", files).Msg("project files got")
for _, fileInfo := range files {
// Open the file and get the show name
fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name()))
if err != nil {
log.Warn().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("unable to open the project file")
continue
}
log.Trace().Str("file", "project").Str("projectFile", fileInfo.Name()).Any("fileData", fileData).Msg("project file read")
projectObject := ProjectInfo{}
err = yaml.Unmarshal(fileData, &projectObject)
if err != nil {
log.Warn().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("project has invalid format")
continue
}
log.Trace().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("project file unmarshalled")
// Add the SaveFile property
projects = append(projects, ProjectMetaData{
Name: projectObject.ShowInfo.Name,
Save: fileInfo.Name(),
})
}
log.Info().Str("file", "project").Any("projectsList", projects).Msg("got the projects list")
return projects, nil
}
// CreateProject creates a new blank project
func (a *App) CreateProject() ShowInfo {
date := time.Now()
a.projectSave = ""
a.projectInfo.ShowInfo = ShowInfo{
Name: "My new show",
Date: fmt.Sprintf("%04d-%02d-%02dT%02d:%02d", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute()),
Avatar: "appicon.png",
Comments: "Write your comments here",
}
log.Info().Str("file", "project").Any("showInfo", a.projectInfo.ShowInfo).Msg("project has been created")
return a.projectInfo.ShowInfo
}
// GetProjectInfo returns the information of the saved project
func (a *App) GetProjectInfo(projectFile string) (ProjectInfo, error) {
// Open the project file
projectPath := filepath.Join(projectsDirectory, projectFile)
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project path is created")
content, err := os.ReadFile(projectPath)
if err != nil {
log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to read the project file")
return ProjectInfo{}, fmt.Errorf("unable to read the project file: %v", err)
}
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project file read")
a.projectInfo = ProjectInfo{}
err = yaml.Unmarshal(content, &a.projectInfo)
if err != nil {
log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to get the project information")
return ProjectInfo{}, fmt.Errorf("unable to get the project information: %v", err)
}
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project information got")
// Load it into the app
a.projectSave = projectFile
// Return the show information
log.Info().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("got the project information")
return a.projectInfo, nil
}
// ChooseAvatarPath opens a filedialog to choose the show avatar
func (a *App) ChooseAvatarPath() (string, error) {
// Open the file dialog box
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Choose your show avatar",
Filters: []runtime.FileFilter{
{
DisplayName: "Images",
Pattern: "*.png;*.jpg;*.jpeg",
},
},
})
if err != nil {
log.Err(err).Str("file", "project").Msg("unable to open the avatar dialog")
return "", err
}
log.Debug().Str("file", "project").Msg("avatar dialog is opened")
// Copy the avatar to the application avatars path
avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath))
log.Trace().Str("file", "project").Str("avatarPath", avatarPath).Msg("avatar path is created")
_, err = copy(filePath, avatarPath)
if err != nil {
log.Err(err).Str("file", "project").Str("avatarsDirectory", avatarsDirectory).Str("fileBase", filepath.Base(filePath)).Msg("unable to copy the avatar file")
return "", err
}
log.Info().Str("file", "project").Str("avatarFileName", filepath.Base(filePath)).Msg("got the new avatar file")
return filepath.Base(filePath), nil
}
// UpdateShowInfo updates the show information
func (a *App) UpdateShowInfo(showInfo ShowInfo) {
a.projectInfo.ShowInfo = showInfo
log.Info().Str("file", "project").Any("showInfo", showInfo).Msg("show information was updated")
}
// SaveProject saves the project
func (a *App) SaveProject() (string, error) {
// If there is no save file, create a new one with the show name
if a.projectSave == "" {
date := time.Now()
a.projectSave = fmt.Sprintf("%04d%02d%02d%02d%02d%02d%s", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), projectExtension)
log.Debug().Str("file", "project").Str("newProjectSave", a.projectSave).Msg("projectSave is null, getting a new one")
}
data, err := yaml.Marshal(a.projectInfo)
if err != nil {
log.Err(err).Str("file", "project").Any("projectInfo", a.projectInfo).Msg("unable to format the project information")
return "", err
}
log.Trace().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("projectInfo has been marshalled")
// Create the project directory if not exists
err = os.MkdirAll(projectsDirectory, os.ModePerm)
if err != nil {
log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("unable to create the projects directory")
return "", err
}
log.Trace().Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("projects directory has been created")
err = os.WriteFile(filepath.Join(projectsDirectory, a.projectSave), data, os.ModePerm)
if err != nil {
log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Str("projectSave", a.projectSave).Msg("unable to save the project")
return "", err
}
log.Info().Str("file", "project").Str("projectFileName", a.projectSave).Msg("project has been saved")
return a.projectSave, nil
}
// ShowInfo defines the information of the show
type ShowInfo struct {
Name string `yaml:"name"`
Date string `yaml:"date"`
Avatar string `yaml:"avatar"`
Comments string `yaml:"comments"`
}
// ProjectMetaData defines all the minimum information for a lighting project
type ProjectMetaData struct {
Name string // Show name
Save string // The save file of the project
}
// ProjectInfo defines all the information for a lighting project
type ProjectInfo struct {
ShowInfo ShowInfo `yaml:"show"` // Show information
PeripheralsInfo map[string]hardware.PeripheralInfo `yaml:"peripherals"` // Peripherals information
}