2023-08-25 20:33:11 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2024-12-15 13:45:46 +01:00
|
|
|
"changeme/hardware"
|
2023-08-25 20:33:11 +00:00
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2024-11-01 20:10:28 +00:00
|
|
|
"io"
|
|
|
|
|
"log"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
2024-12-15 13:45:46 +01:00
|
|
|
"sync"
|
2024-12-20 17:18:57 +01:00
|
|
|
"time"
|
2024-11-01 20:10:28 +00:00
|
|
|
|
|
|
|
|
"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
|
2023-08-25 20:33:11 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// App struct
|
|
|
|
|
type App struct {
|
2024-12-15 13:45:46 +01:00
|
|
|
ctx context.Context
|
|
|
|
|
hardwareManager *hardware.HardwareManager // For managing all the hardware
|
|
|
|
|
wmiMutex sync.Mutex // Avoid some WMI operations at the same time
|
2024-12-20 17:18:57 +01:00
|
|
|
projectInfo ProjectInfo // The project information structure
|
|
|
|
|
projectSave string // The file name of the project
|
2024-12-15 13:45:46 +01:00
|
|
|
// FOR TESTING PURPOSE ONLY
|
|
|
|
|
ftdi *hardware.FTDIPeripheral
|
2023-08-25 20:33:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewApp creates a new App application struct
|
|
|
|
|
func NewApp() *App {
|
2024-12-15 13:45:46 +01:00
|
|
|
// Create a new hadware manager
|
|
|
|
|
hardwareManager := hardware.NewHardwareManager()
|
|
|
|
|
hardwareManager.RegisterFinder(hardware.NewMIDIFinder())
|
|
|
|
|
hardwareManager.RegisterFinder(hardware.NewFTDIFinder())
|
|
|
|
|
return &App{
|
|
|
|
|
hardwareManager: hardwareManager,
|
2024-12-20 17:18:57 +01:00
|
|
|
projectSave: "",
|
2024-12-21 13:24:00 +01:00
|
|
|
projectInfo: ProjectInfo{
|
|
|
|
|
PeripheralsInfo: make(map[string]hardware.PeripheralInfo),
|
|
|
|
|
},
|
2024-12-15 13:45:46 +01:00
|
|
|
}
|
2023-08-25 20:33:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// startup is called when the app starts. The context is saved
|
|
|
|
|
// so we can call the runtime methods
|
|
|
|
|
func (a *App) startup(ctx context.Context) {
|
|
|
|
|
a.ctx = ctx
|
2024-12-15 13:45:46 +01:00
|
|
|
err := a.hardwareManager.Start(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Unable to start the device manager: %s", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-20 17:18:57 +01:00
|
|
|
// 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",
|
|
|
|
|
}
|
|
|
|
|
return a.projectInfo.ShowInfo
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-15 13:45:46 +01:00
|
|
|
// GetPeripherals gets all the peripherals connected
|
2024-12-20 17:18:57 +01:00
|
|
|
func (a *App) GetPeripherals() error {
|
|
|
|
|
return a.hardwareManager.Scan(a.ctx)
|
2023-08-25 20:33:11 +00:00
|
|
|
}
|
|
|
|
|
|
2024-11-01 20:10:28 +00:00
|
|
|
// GetProjects gets all the projects in the projects directory
|
2024-12-20 17:18:57 +01:00
|
|
|
func (a *App) GetProjects() ([]ProjectMetaData, error) {
|
|
|
|
|
projects := []ProjectMetaData{}
|
2024-11-01 20:10:28 +00:00
|
|
|
|
|
|
|
|
f, err := os.Open(projectsDirectory)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Unable to open the projects directory: %v", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
files, err := f.Readdir(0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Unable to read the projects directory: %v", err)
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, fileInfo := range files {
|
|
|
|
|
// Open the file and get the show name
|
|
|
|
|
fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name()))
|
|
|
|
|
if err == nil {
|
|
|
|
|
projectObject := ProjectInfo{}
|
|
|
|
|
err = yaml.Unmarshal(fileData, &projectObject)
|
|
|
|
|
if err == nil {
|
|
|
|
|
// Add the SaveFile property
|
2024-12-20 17:18:57 +01:00
|
|
|
projects = append(projects, ProjectMetaData{
|
|
|
|
|
Name: projectObject.ShowInfo.Name,
|
|
|
|
|
Save: fileInfo.Name(),
|
|
|
|
|
})
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return projects, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetProjectInfo returns the information of the saved project
|
2024-12-20 17:18:57 +01:00
|
|
|
func (a *App) GetProjectInfo(projectFile string) (ProjectInfo, error) {
|
|
|
|
|
// Open the project file
|
2024-11-01 20:10:28 +00:00
|
|
|
projectPath := filepath.Join(projectsDirectory, projectFile)
|
|
|
|
|
content, err := os.ReadFile(projectPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Unable to read the project file: %v", err)
|
2024-12-20 17:18:57 +01:00
|
|
|
return ProjectInfo{}, err
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
2024-12-21 13:24:00 +01:00
|
|
|
a.projectInfo = ProjectInfo{}
|
2024-12-20 17:18:57 +01:00
|
|
|
err = yaml.Unmarshal(content, &a.projectInfo)
|
2024-11-01 20:10:28 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalf("Unable to get the project information: %v", err)
|
2024-12-20 17:18:57 +01:00
|
|
|
return ProjectInfo{}, err
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
2024-12-20 17:18:57 +01:00
|
|
|
// Load it into the app
|
|
|
|
|
a.projectSave = projectFile
|
|
|
|
|
// Return the show information
|
|
|
|
|
return a.projectInfo, nil
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
// Copy the avatar to the application avatars path
|
|
|
|
|
avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath))
|
|
|
|
|
_, err = copy(filePath, avatarPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return filepath.Base(filePath), nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-20 17:18:57 +01:00
|
|
|
// UpdateShowInfo updates the show information
|
|
|
|
|
func (a *App) UpdateShowInfo(showInfo ShowInfo) {
|
|
|
|
|
fmt.Printf("%s\n", showInfo)
|
|
|
|
|
a.projectInfo.ShowInfo = showInfo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddPeripheral adds a peripheral to the project
|
|
|
|
|
func (a *App) AddPeripheral(protocolName string, peripheralID string) error {
|
|
|
|
|
// Get the device from its finder
|
|
|
|
|
p, found := a.hardwareManager.GetPeripheral(protocolName, peripheralID)
|
|
|
|
|
if !found {
|
|
|
|
|
return fmt.Errorf("Unable to localize the peripheral %s", peripheralID)
|
|
|
|
|
}
|
|
|
|
|
// Add the peripheral ID to the project
|
2024-12-21 13:24:00 +01:00
|
|
|
a.projectInfo.PeripheralsInfo[peripheralID] = p.GetInfo()
|
2024-12-20 17:18:57 +01:00
|
|
|
// TODO: Connect the peripheral
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemovePeripheral adds a peripheral to the project
|
|
|
|
|
func (a *App) RemovePeripheral(protocolName string, peripheralID string) error {
|
|
|
|
|
// TODO: Disconnect the peripheral
|
|
|
|
|
// Remove the peripheral ID from the project
|
2024-12-21 13:24:00 +01:00
|
|
|
delete(a.projectInfo.PeripheralsInfo, peripheralID)
|
2024-12-20 17:18:57 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 20:10:28 +00:00
|
|
|
// SaveProject saves the project
|
2024-12-20 17:18:57 +01:00
|
|
|
func (a *App) SaveProject() (string, error) {
|
|
|
|
|
log.Printf("Saving the project %s to %s", a.projectInfo.ShowInfo.Name, a.projectSave)
|
2024-11-01 20:10:28 +00:00
|
|
|
// If there is no save file, create a new one with the show name
|
2024-12-20 17:18:57 +01:00
|
|
|
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)
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
2024-12-20 17:18:57 +01:00
|
|
|
data, err := yaml.Marshal(a.projectInfo)
|
2024-11-01 20:10:28 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2024-12-20 17:18:57 +01:00
|
|
|
// Create the project directory if not exists
|
|
|
|
|
err = os.MkdirAll(projectsDirectory, os.ModePerm)
|
2024-11-01 20:10:28 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2024-12-20 17:18:57 +01:00
|
|
|
err = os.WriteFile(filepath.Join(projectsDirectory, a.projectSave), data, os.ModePerm)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return a.projectSave, nil
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ShowInfo defines the information of the show
|
|
|
|
|
type ShowInfo struct {
|
2024-12-20 17:18:57 +01:00
|
|
|
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
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProjectInfo defines all the information for a lighting project
|
|
|
|
|
type ProjectInfo struct {
|
2024-12-21 13:24:00 +01:00
|
|
|
ShowInfo ShowInfo `yaml:"show"` // Show information
|
|
|
|
|
PeripheralsInfo map[string]hardware.PeripheralInfo `yaml:"peripherals"` // Peripherals information
|
2024-11-01 20:10:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func formatString(input string) string {
|
|
|
|
|
// Convertir en minuscules
|
|
|
|
|
lowerCaseString := strings.ToLower(input)
|
|
|
|
|
// Remplacer les espaces par des underscores
|
|
|
|
|
formattedString := strings.ReplaceAll(lowerCaseString, " ", "_")
|
|
|
|
|
return formattedString
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func copy(src, dst string) (int64, error) {
|
|
|
|
|
sourceFileStat, err := os.Stat(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !sourceFileStat.Mode().IsRegular() {
|
|
|
|
|
return 0, fmt.Errorf("%s is not a regular file", src)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
source, err := os.Open(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
defer source.Close()
|
|
|
|
|
|
|
|
|
|
destination, err := os.Create(dst)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
defer destination.Close()
|
|
|
|
|
nBytes, err := io.Copy(destination, source)
|
|
|
|
|
return nBytes, err
|
2023-08-25 20:33:11 +00:00
|
|
|
}
|
2024-12-15 13:45:46 +01:00
|
|
|
|
|
|
|
|
// FOR TESTING PURPOSE ONLY
|
|
|
|
|
|
|
|
|
|
func (a *App) ConnectFTDI() error {
|
|
|
|
|
// Create a new FTDI object
|
|
|
|
|
var err error
|
2024-12-20 17:18:57 +01:00
|
|
|
a.ftdi, err = hardware.NewFTDIPeripheral("FTDI TEST INTERFACE", "A50825I", 0, "Virtual FTDI finder")
|
2024-12-15 13:45:46 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return a.ftdi.Connect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) ActivateFTDI() error {
|
|
|
|
|
return a.ftdi.Activate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) SetDeviceFTDI(channelValue byte) error {
|
|
|
|
|
return a.ftdi.SetDeviceProperty(0, 0, channelValue)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) DeactivateFTDI() error {
|
|
|
|
|
return a.ftdi.Deactivate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) DisconnectFTDI() error {
|
|
|
|
|
return a.ftdi.Disconnect()
|
|
|
|
|
}
|