From f8b39d90de94817a9b718569733b01b63611d173 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Mon, 10 Nov 2025 14:18:22 +0100 Subject: [PATCH 1/3] corrected events --- .gitignore | 2 + app.go | 1 - frontend/src/App.svelte | 128 ++++----- .../General/RoundDropdownList.svelte | 153 +++++++++++ .../src/components/Settings/Settings.svelte | 93 ------- frontend/src/main.js | 3 +- frontend/src/runtime-events.js | 137 ++++++++++ frontend/src/stores.js | 9 +- hardware/FTDIFinder.go | 10 +- project.go | 248 ++++++++++++++++++ 10 files changed, 620 insertions(+), 164 deletions(-) create mode 100644 frontend/src/components/General/RoundDropdownList.svelte create mode 100644 frontend/src/runtime-events.js create mode 100644 project.go diff --git a/.gitignore b/.gitignore index a83910e..8195664 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ frontend/wailsjs *.exe *.o *.rc +*.dll +*.dll.a frontend/public \ No newline at end of file diff --git a/app.go b/app.go index 8708e62..67b9289 100644 --- a/app.go +++ b/app.go @@ -24,7 +24,6 @@ type App struct { 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 diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index b7d7ba7..c17324c 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -7,66 +7,50 @@ import Settings from './components/Settings/Settings.svelte'; import Devices from './components/Devices/Devices.svelte'; import Show from './components/Show/Show.svelte'; + import DropdownList from "./components/General/DropdownList.svelte"; + import RoundDropdownList from "./components/General/RoundDropdownList.svelte"; import GeneralConsole from './components/Console/GeneralConsole.svelte'; import RoundIconButton from './components/General/RoundIconButton.svelte'; - import { generateToast, showInformation, needProjectSave, peripherals } from './stores'; - import { SaveProject } from '../wailsjs/go/main/App.js'; - import { construct_svelte_component } from 'svelte/internal'; - import { EventsOn } from '../wailsjs/runtime' - import { CreateProject } from "../wailsjs/go/main/App"; + import { generateToast, showInformation, needProjectSave, projectsList } from './stores.js'; + import { GetProjects, CreateProject, OpenProjectFromDisk, SaveProject } from '../wailsjs/go/main/App.js'; import { WindowSetTitle } from "../wailsjs/runtime/runtime" - import { get } from "svelte/store" import ToastNotification from './components/General/ToastNotification.svelte'; + import { onMount, onDestroy } from 'svelte' + import { destroyRuntimeEvents, initRuntimeEvents } from './runtime-events.js' - // Handle the event when a new peripheral is detected - EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){ - // When a new peripheral is detected, add it to the map and: - // - Pass the isDetected key to true - // - Set the isSaved key to the last value - let peripheralsList = get(peripherals) - let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved - peripheralInfo.isDetected = true - peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false - peripherals.update((peripherals) => { - peripherals[peripheralInfo.SerialNumber] = peripheralInfo - return {...peripherals} - }) - console.log("Hardware has been added to the system"); - generateToast('info', 'bxs-hdd', $_("peripheralArrivalToast") + ' ' + peripheralInfo.Name + '') - }) - - // Handle the event when a peripheral is removed from the system - EventsOn('PERIPHERAL_REMOVAL', function(peripheralInfo){ - console.log("Hardware has been removed from the system"); - // When a peripheral is disconnected, pass its isDetected key to false - // If the isSaved key is set to false, we can completely remove the peripheral from the list - let peripheralsList = get(peripherals) - let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved - let needToDelete = (lastSavedProperty !== true) ? true : false - peripherals.update((storedPeripherals) => { - if (needToDelete){ - delete storedPeripherals[peripheralInfo.SerialNumber]; - return { ...storedPeripherals }; - } - storedPeripherals[peripheralInfo.SerialNumber].isDetected = false - return {...storedPeripherals} + function initializeNewProject(){ + // Instanciate a new project + 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")) }) - generateToast('warning', 'bxs-hdd', $_("peripheralRemovalToast") + ' ' + peripheralInfo.Name + '') + } + + // Initialize runtime events at startup + onMount(() => { + initRuntimeEvents() + + // Handle window shortcuts + document.addEventListener('keydown', function(event) { + // Check the CTRL+S keys + if ((event.ctrlKey || event.metaKey) && event.key === 's') { + // Avoid the natural behaviour + event.preventDefault(); + // Save the current project + saveProject() + } + }); + + // Initialize a new project + initializeNewProject() }) - // Handle the event when a peripheral status is updated - EventsOn('PERIPHERAL_STATUS', function(peripheral, status){ - console.log("Hardware status has been updated to " + status); - // When a peripheral status is updated, update it in the store - peripherals.update((storedPeripherals) => { - return { - ...storedPeripherals, - [peripheral.SerialNumber]: { - ...storedPeripherals[peripheral.SerialNumber], - isSaved: true, - Status: status, - }, - }}) + // Destroy runtime events at shutdown + onDestroy(() => { + destroyRuntimeEvents() }) // Set the window title @@ -92,23 +76,41 @@ }) } - // Handle window shortcuts - document.addEventListener('keydown', function(event) { - // Check the CTRL+S keys - if ((event.ctrlKey || event.metaKey) && event.key === 's') { - // Avoid the natural behaviour - event.preventDefault(); - // Save the current project - saveProject() - } - }); + // Open the selected project + function openSelectedProject(event){ + let selectedOption = event.detail.key + // Open the selected project + OpenProjectFromDisk(selectedOption).then(() => { + // Project opened, we set the needSave flag to false (already saved) + needProjectSave.set(false) + }).catch((error) => { + console.error(`Unable to open the project: ${error}`) + generateToast('danger', 'bx-error', $_("projectOpenErrorToast")) + }) + } + + // Refresh the projects list + let choices = new Map() + function loadProjectsList(){ + GetProjects().then((projects) => { + choices = new Map(projects.map(item => [item.Save, item.Name])); + $projectsList = projects + }).catch((error) => { + console.error(`Unable to get the projects list: ${error}`) + generateToast('danger', 'bx-error', $_("projectsLoadErrorToast")) + }) + }
+ + + + {#if $needProjectSave} - + {/if}
diff --git a/frontend/src/components/General/RoundDropdownList.svelte b/frontend/src/components/General/RoundDropdownList.svelte new file mode 100644 index 0000000..e538148 --- /dev/null +++ b/frontend/src/components/General/RoundDropdownList.svelte @@ -0,0 +1,153 @@ + + + + +
+ + + {#if (operationalStatus !== undefined)} +
+
+ {/if} + +
+ {#each Array.from(choices) as [key, value]} +
handleclick({key})}>{value}
+ {/each} +
+
+ + \ No newline at end of file diff --git a/frontend/src/components/Settings/Settings.svelte b/frontend/src/components/Settings/Settings.svelte index a2e7b48..9625ecb 100644 --- a/frontend/src/components/Settings/Settings.svelte +++ b/frontend/src/components/Settings/Settings.svelte @@ -1,109 +1,16 @@ - - - diff --git a/frontend/src/main.js b/frontend/src/main.js index 034c3f3..70e0647 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,8 +1,9 @@ import App from './App.svelte'; import { WindowSetTitle } from "../wailsjs/runtime/runtime" +import { _ } from 'svelte-i18n' -import {showInformation, needProjectSave} from './stores.js'; +import {messages, showInformation, needProjectSave} from './stores.js'; // Load dictionaries import { addMessages, init } from 'svelte-i18n'; diff --git a/frontend/src/runtime-events.js b/frontend/src/runtime-events.js new file mode 100644 index 0000000..961acb9 --- /dev/null +++ b/frontend/src/runtime-events.js @@ -0,0 +1,137 @@ +import { EventsOn, EventsOff } from "../wailsjs/runtime/runtime.js" +import { peripherals, generateToast, needProjectSave, showInformation } from './stores' +import { get } from "svelte/store" +import { _ } from 'svelte-i18n' + +function addPeripheral (peripheralInfo){ + // When a new peripheral is detected, add it to the map and: + // - Pass the isDetected key to true + // - Set the isSaved key to the last value + let peripheralsList = get(peripherals) + let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved + peripheralInfo.isDetected = true + peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false + peripherals.update((peripherals) => { + peripherals[peripheralInfo.SerialNumber] = peripheralInfo + return {...peripherals} + }) + console.log("Hardware has been added to the system"); + generateToast('info', 'bxs-hdd', get(_)("peripheralArrivalToast") + ' ' + peripheralInfo.Name + '') +} + +function removePeripheral (peripheralInfo){ + console.log("Hardware has been removed from the system"); + // When a peripheral is disconnected, pass its isDetected key to false + // If the isSaved key is set to false, we can completely remove the peripheral from the list + let peripheralsList = get(peripherals) + let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved + let needToDelete = (lastSavedProperty !== true) ? true : false + peripherals.update((storedPeripherals) => { + if (needToDelete){ + delete storedPeripherals[peripheralInfo.SerialNumber]; + return { ...storedPeripherals }; + } + storedPeripherals[peripheralInfo.SerialNumber].isDetected = false + return {...storedPeripherals} + }) + generateToast('warning', 'bxs-hdd', get(_)("peripheralRemovalToast") + ' ' + peripheralInfo.Name + '') +} + +function updatePeripheral(peripheral, status){ + console.log("Hardware status has been updated to " + status); + // When a peripheral status is updated, update it in the store + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [peripheral.SerialNumber]: { + ...storedPeripherals[peripheral.SerialNumber], + isSaved: true, + Status: status, + }, + }}) +} + +function loadPeripheral (peripheralInfo) { + peripherals.update((storedPeripherals) => { + // Add the saved peripherals of the project + // If already exists pass the isSaved key to true, if not create the peripheral and set it to disconnected + // Add the peripheral to the list of peripherals, with the last isDetected key and the isSaved key to true + let lastDetectedKey = storedPeripherals[peripheralInfo.SerialNumber]?.isDetected + storedPeripherals[peripheralInfo.SerialNumber] = peripheralInfo + storedPeripherals[peripheralInfo.SerialNumber].isDetected = (lastDetectedKey === true) ? true : false + storedPeripherals[peripheralInfo.SerialNumber].isSaved = true + return {...storedPeripherals} + }) + //TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder + needProjectSave.set(true) +} + +function loadProject (showInfo){ + // Store project information + showInformation.set(showInfo) + + console.log("Project has been opened"); + generateToast('info', 'bx-folder-open', get(_)("projectOpenedToast") + ' ' + showInfo.Name + '') +} + +function unloadPeripheral (peripheralInfo) { + peripherals.update((storedPeripherals) => { + // Set all the isSaved keys to false and delete the disconnected peripherals + storedPeripherals[peripheralInfo.SerialNumber].isSaved = false + if (!storedPeripherals[peripheralInfo.SerialNumber].isDetected) { + delete storedPeripherals[peripheralInfo.SerialNumber] + } + return {...storedPeripherals} + }) + + //TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder + needProjectSave.set(true) + } + +let initialized = false + +export function initRuntimeEvents(){ + if (initialized) return + initialized = true + + // Handle the event when a new peripheral is detected + EventsOn('PERIPHERAL_ARRIVAL', addPeripheral) + + // Handle the event when a peripheral is removed from the system + EventsOn('PERIPHERAL_REMOVAL', removePeripheral) + + // Handle the event when a peripheral status is updated + EventsOn('PERIPHERAL_STATUS', updatePeripheral) + + // Handle the event when a new project need to be loaded + EventsOn('LOAD_PROJECT', loadProject) + + // Handle a peripheral loaded in the project + EventsOn('LOAD_PERIPHERAL', loadPeripheral) + + // Handle a peripheral unloaded from the project + EventsOn('UNLOAD_PERIPHERAL', unloadPeripheral) +} + +export function destroyRuntimeEvents(){ + if (!initialized) return + initialized = false + + // Handle the event when a new peripheral is detected + EventsOff('PERIPHERAL_ARRIVAL') + + // Handle the event when a peripheral is removed from the system + EventsOff('PERIPHERAL_REMOVAL') + + // Handle the event when a peripheral status is updated + EventsOff('PERIPHERAL_STATUS') + + // Handle the event when a new project need to be loaded + EventsOff('LOAD_PROJECT') + + // Handle a peripheral loaded in the project + EventsOff('LOAD_PERIPHERAL') + + // Handle a peripheral unloaded from the project + EventsOff('UNLOAD_PERIPHERAL') +} \ No newline at end of file diff --git a/frontend/src/stores.js b/frontend/src/stores.js index a060938..16d093e 100644 --- a/frontend/src/stores.js +++ b/frontend/src/stores.js @@ -10,13 +10,12 @@ export let showInformation = writable({}) // Toasts notifications export let messages = writable([]) export function generateToast(type, icon, text){ - messages.update((value) => { - value.push( { id: Date.now(), type: type, icon: icon, text: text } ) - return value.slice(-5) - }) + messages.update((value) => { + value.push( { id: Date.now(), type: type, icon: icon, text: text } ) + return value.slice(-5) + }) } - // Application colors export const colors = writable({ first: "#1B262C", diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go index 3a748e2..f24812f 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -11,6 +11,7 @@ import ( "unsafe" "github.com/rs/zerolog/log" + "github.com/wailsapp/wails/v2/pkg/runtime" ) /* @@ -52,6 +53,9 @@ func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData Peri f.registeredPeripherals[peripheralData.SerialNumber] = ftdiPeripheral log.Trace().Any("periph", &ftdiPeripheral).Str("file", "FTDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created") + // Emit the event to the front + runtime.EventsEmit(ctx, "LOAD_PERIPHERAL", peripheralData) + // Peripheral created, connect it err = ftdiPeripheral.Connect(ctx) if err != nil { @@ -80,8 +84,12 @@ func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralID stri if err != nil { return err } + + delete(f.registeredPeripherals, peripheralID) + + // Emit the event to the front + runtime.EventsEmit(ctx, "UNLOAD_PERIPHERAL", peripheral.info) } - delete(f.registeredPeripherals, peripheralID) return nil } diff --git a/project.go b/project.go new file mode 100644 index 0000000..0a249ab --- /dev/null +++ b/project.go @@ -0,0 +1,248 @@ +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() error { + + // Create new project information + date := time.Now() + projectInfo := ProjectInfo{ + 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", + }, + make(map[string]hardware.PeripheralInfo), + } + + // The project isn't saved for now + a.projectSave = "" + + return a.OpenProject(projectInfo) +} + +// OpenProjectFromDisk opens a project based on its filename +func (a *App) OpenProjectFromDisk(projectFile string) 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 fmt.Errorf("unable to read the project file: %v", err) + } + log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project file read") + + // Import project data structure + projectInfo := ProjectInfo{} + err = yaml.Unmarshal(content, &projectInfo) + if err != nil { + log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to get the project information") + return fmt.Errorf("unable to get the project information: %v", err) + } + log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project information got") + + // The project is saved + a.projectSave = projectFile + + return a.OpenProject(projectInfo) +} + +// OpenProject opens a project based on its information +func (a *App) OpenProject(projectInfo ProjectInfo) error { + // Close the current project + err := a.CloseCurrentProject() + if err != nil { + return fmt.Errorf("unable to close project: %w", err) + } + + // Open the project + a.projectInfo = projectInfo + + // Send an event with the project data + runtime.EventsEmit(a.ctx, "LOAD_PROJECT", projectInfo.ShowInfo) + + // Load all peripherals of the project + projectPeripherals := a.projectInfo.PeripheralsInfo + for key, value := range projectPeripherals { + hostFinder, err := a.hardwareManager.GetFinder(value.ProtocolName) + if err != nil { + return fmt.Errorf("unable to find the finder '%s': %w", value.ProtocolName, err) + } + _, err = hostFinder.RegisterPeripheral(a.ctx, value) + if err != nil { + return fmt.Errorf("unable to register the peripheral S/N '%s'", key) + } + } + return nil +} + +// CloseCurrentProject closes the current project +func (a *App) CloseCurrentProject() error { + // Unregistrer all peripherals of the project + projectPeripherals := a.projectInfo.PeripheralsInfo + for key, value := range projectPeripherals { + hostFinder, err := a.hardwareManager.GetFinder(value.ProtocolName) + if err != nil { + return fmt.Errorf("unable to find the finder '%s': %w", value.ProtocolName, err) + } + err = hostFinder.UnregisterPeripheral(a.ctx, key) + if err != nil { + return fmt.Errorf("unable to unregister the peripheral S/N '%s': %w", key, err) + } + } + + // Unload project info in the front + return 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 +} -- 2.49.1 From 621c1922caaaf64ad61796e387de6bebe8e1dfe5 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Mon, 10 Nov 2025 15:55:32 +0100 Subject: [PATCH 2/3] resolve peripheral status --- frontend/src/runtime-events.js | 11 ++++++----- hardware/FTDIFinder.go | 4 +++- hardware/FTDIPeripheral.go | 3 +++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/runtime-events.js b/frontend/src/runtime-events.js index 961acb9..1902a40 100644 --- a/frontend/src/runtime-events.js +++ b/frontend/src/runtime-events.js @@ -27,11 +27,12 @@ function removePeripheral (peripheralInfo){ let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved let needToDelete = (lastSavedProperty !== true) ? true : false peripherals.update((storedPeripherals) => { - if (needToDelete){ - delete storedPeripherals[peripheralInfo.SerialNumber]; - return { ...storedPeripherals }; - } - storedPeripherals[peripheralInfo.SerialNumber].isDetected = false + if (needToDelete){ + delete storedPeripherals[peripheralInfo.SerialNumber]; + return { ...storedPeripherals }; + } + storedPeripherals[peripheralInfo.SerialNumber].isDetected = false + storedPeripherals[peripheralInfo.SerialNumber].Status = "disconnected" return {...storedPeripherals} }) generateToast('warning', 'bxs-hdd', get(_)("peripheralRemovalToast") + ' ' + peripheralInfo.Name + '') diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go index f24812f..df00560 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -60,11 +60,13 @@ func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData Peri err = ftdiPeripheral.Connect(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") + return peripheralData.SerialNumber, nil } // Peripheral connected, activate it err = ftdiPeripheral.Activate(ctx) if err != nil { log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the peripheral") + return peripheralData.SerialNumber, nil } // Peripheral activated return peripheralData.SerialNumber, nil @@ -131,7 +133,7 @@ func (f *FTDIFinder) Start(ctx context.Context) error { return nil } -// ForceScan explicily asks for scanning peripherals +// ForceScan explicitly asks for scanning peripherals func (f *FTDIFinder) ForceScan() { select { case f.scanChannel <- struct{}{}: diff --git a/hardware/FTDIPeripheral.go b/hardware/FTDIPeripheral.go index 1c22062..c8ed185 100644 --- a/hardware/FTDIPeripheral.go +++ b/hardware/FTDIPeripheral.go @@ -51,6 +51,7 @@ func (p *FTDIPeripheral) Connect(ctx context.Context) error { serialNumber := C.CString(p.info.SerialNumber) defer C.free(unsafe.Pointer(serialNumber)) if C.dmx_connect(p.dmxSender, serialNumber) != C.DMX_OK { + runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "disconnected") 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'") } @@ -92,9 +93,11 @@ func (p *FTDIPeripheral) Activate(ctx context.Context) error { err := C.dmx_activate(p.dmxSender) if err != C.DMX_OK { + runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "deactivated") return errors.Errorf("unable to activate the DMX sender!") } + // Test only C.dmx_setValue(p.dmxSender, C.uint16_t(1), C.uint8_t(255)) C.dmx_setValue(p.dmxSender, C.uint16_t(5), C.uint8_t(255)) -- 2.49.1 From b094019ed9f6832880e9f2c2420756066cdb5fe6 Mon Sep 17 00:00:00 2001 From: Valentin Boulanger Date: Tue, 11 Nov 2025 20:13:27 +0100 Subject: [PATCH 3/3] fix project life and plug-and-replay on peripherals --- .../General/RoundDropdownList.svelte | 3 +- .../src/components/Settings/DeviceCard.svelte | 15 +- .../Settings/InputsOutputsContent.svelte | 45 +--- frontend/src/runtime-events.js | 157 ++++++----- frontend/src/stores.js | 13 +- hardware/FTDIFinder.go | 250 +++++++++++------- hardware/FTDIPeripheral.go | 21 +- hardware/OS2LPeripheral.go | 4 +- hardware/hardware.go | 46 ++-- hardware/interfaces.go | 18 +- peripherals.go | 14 +- project.go | 2 +- 12 files changed, 326 insertions(+), 262 deletions(-) diff --git a/frontend/src/components/General/RoundDropdownList.svelte b/frontend/src/components/General/RoundDropdownList.svelte index e538148..80f618f 100644 --- a/frontend/src/components/General/RoundDropdownList.svelte +++ b/frontend/src/components/General/RoundDropdownList.svelte @@ -99,7 +99,7 @@ {/if} -
{#each Array.from(choices) as [key, value]}
handleclick({key})}>{value}
@@ -125,7 +125,6 @@ .list { z-index: 200; padding: 0.2em; - backdrop-filter: blur(20px); margin-top: 0.2em; position: absolute; width: auto; diff --git a/frontend/src/components/Settings/DeviceCard.svelte b/frontend/src/components/Settings/DeviceCard.svelte index 8910208..8c72cb1 100644 --- a/frontend/src/components/Settings/DeviceCard.svelte +++ b/frontend/src/components/Settings/DeviceCard.svelte @@ -15,13 +15,14 @@ export let signalizable = false; export let signalized = false; export let selected = false; - export let status = "disconnected"; + export let status = "PERIPHERAL_DISCONNECTED"; // Emit a delete event when the device is being removed const dispatch = createEventDispatcher(); function remove(event){ dispatch('delete') } + function add(event){ dispatch('add') } @@ -37,11 +38,11 @@
-
+
-

{#if status == "disconnected" } {/if}{title}

+

{#if status == "PERIPHERAL_DISCONNECTED" } {/if}{title}

{type} {location != '' ? "- " : ""}{location}
- {#if status == "disconnected"} + {#if status == "PERIPHERAL_DISCONNECTED"}
Disconnected
{:else}
{line1}
@@ -50,9 +51,9 @@
- - - + + +
diff --git a/frontend/src/components/Settings/InputsOutputsContent.svelte b/frontend/src/components/Settings/InputsOutputsContent.svelte index 0f5b2c8..a8373a4 100644 --- a/frontend/src/components/Settings/InputsOutputsContent.svelte +++ b/frontend/src/components/Settings/InputsOutputsContent.svelte @@ -11,20 +11,8 @@ // Add the peripheral to the project function addPeripheral(peripheral){ // Add the peripheral to the project (backend) - AddPeripheral(peripheral).then((serialNumber) => { - peripherals.update((storedPeripherals) => { - return { - ...storedPeripherals, - [serialNumber]: { - ...storedPeripherals[serialNumber], - Name: peripheral.Name, - ProtocolName: peripheral.ProtocolName, - SerialNumber: serialNumber, - isSaved: true, - }, - }}) - $needProjectSave = true - }).catch((error) => { + AddPeripheral(peripheral) + .catch((error) => { console.log("Unable to add the peripheral to the project: " + error) generateToast('danger', 'bx-error', $_("addPeripheralErrorToast")) }) @@ -33,27 +21,8 @@ // Remove the peripheral from the project function removePeripheral(peripheral) { // Delete the peripheral from the project (backend) - RemovePeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => { - // If the peripheral is not detected, we can delete it form the store - // If not, we only pass the isSaved key to false - let peripheralsList = get(peripherals) - let lastDetectedProperty = peripheralsList[peripheral.SerialNumber]?.isDetected - let needToDelete = (lastDetectedProperty !== true) ? true : false - peripherals.update((storedPeripherals) => { - if (needToDelete){ - delete storedPeripherals[peripheral.SerialNumber]; - return { ...storedPeripherals }; - } - storedPeripherals[peripheral.SerialNumber].isSaved = false - return { ...storedPeripherals }; - }) - $needProjectSave = true - // If the peripheral is currently selected, unselect it - if (selectedPeripheralSN == peripheral.SerialNumber) { - selectedPeripheralSN = null - selectedPeripheralSettings = {} - } - }).catch((error) => { + RemovePeripheral(peripheral) + .catch((error) => { console.log("Unable to remove the peripheral from the project: " + error) generateToast('danger', 'bx-error', $_("removePeripheralErrorToast")) }) @@ -76,7 +45,7 @@ } } - // Unselect the peripheral if it is disconnect + // Unselect the peripheral if it is disconnected $: { Object.entries($peripherals).filter(([serialNumber, peripheral]) => { if (!peripheral.isDetected && peripheral.isSaved && selectedPeripheralSN == serialNumber) { @@ -120,7 +89,7 @@ if(!peripheral.isSaved) addPeripheral(peripheral) }} - status="connected" title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/> + status="PERIPHERAL_CONNECTED" title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/> {/if} {/each}

{$_("projectHardwareOthersLabel")}

@@ -134,7 +103,7 @@ {#if savedPeripheralNumber > 0} {#each Object.entries($peripherals) as [serialNumber, peripheral]} {#if peripheral.isSaved} - removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)} on:click={() => selectPeripheral(peripheral)} + removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)} on:click={() => selectPeripheral(peripheral)} title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} selected={serialNumber == selectedPeripheralSN} removable signalizable/> {/if} {/each} diff --git a/frontend/src/runtime-events.js b/frontend/src/runtime-events.js index 1902a40..59a290b 100644 --- a/frontend/src/runtime-events.js +++ b/frontend/src/runtime-events.js @@ -3,68 +3,91 @@ import { peripherals, generateToast, needProjectSave, showInformation } from './ import { get } from "svelte/store" import { _ } from 'svelte-i18n' -function addPeripheral (peripheralInfo){ - // When a new peripheral is detected, add it to the map and: - // - Pass the isDetected key to true - // - Set the isSaved key to the last value - let peripheralsList = get(peripherals) - let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved - peripheralInfo.isDetected = true - peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false - peripherals.update((peripherals) => { - peripherals[peripheralInfo.SerialNumber] = peripheralInfo - return {...peripherals} - }) +// New peripheral has been added to the system +function peripheralArrival (peripheralInfo){ + // If not exists, add it to the map + // isDetected key to true + + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [peripheralInfo.SerialNumber]: { + ...storedPeripherals[peripheralInfo.SerialNumber], + Name: peripheralInfo.Name, + ProtocolName: peripheralInfo.ProtocolName, + SerialNumber: peripheralInfo.SerialNumber, + Settings: peripheralInfo.Settings, + isDetected: true, + }, + }}) console.log("Hardware has been added to the system"); generateToast('info', 'bxs-hdd', get(_)("peripheralArrivalToast") + ' ' + peripheralInfo.Name + '') } -function removePeripheral (peripheralInfo){ - console.log("Hardware has been removed from the system"); - // When a peripheral is disconnected, pass its isDetected key to false - // If the isSaved key is set to false, we can completely remove the peripheral from the list - let peripheralsList = get(peripherals) - let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved - let needToDelete = (lastSavedProperty !== true) ? true : false - peripherals.update((storedPeripherals) => { - if (needToDelete){ - delete storedPeripherals[peripheralInfo.SerialNumber]; - return { ...storedPeripherals }; - } - storedPeripherals[peripheralInfo.SerialNumber].isDetected = false - storedPeripherals[peripheralInfo.SerialNumber].Status = "disconnected" - return {...storedPeripherals} - }) - generateToast('warning', 'bxs-hdd', get(_)("peripheralRemovalToast") + ' ' + peripheralInfo.Name + '') -} +// Peripheral is removed from the system +function peripheralRemoval (peripheralInfo){ + // If not exists, add it to the map + // isDetected key to false -function updatePeripheral(peripheral, status){ - console.log("Hardware status has been updated to " + status); - // When a peripheral status is updated, update it in the store peripherals.update((storedPeripherals) => { return { ...storedPeripherals, - [peripheral.SerialNumber]: { - ...storedPeripherals[peripheral.SerialNumber], - isSaved: true, - Status: status, + [peripheralInfo.SerialNumber]: { + ...storedPeripherals[peripheralInfo.SerialNumber], + Name: peripheralInfo.Name, + ProtocolName: peripheralInfo.ProtocolName, + SerialNumber: peripheralInfo.SerialNumber, + Settings: peripheralInfo.Settings, + isDetected: false, + status: "PERIPHERAL_DISCONNECTED", }, }}) + console.log("Hardware has been removed from the system"); + generateToast('warning', 'bxs-hdd', get(_)("peripheralRemovalToast") + ' ' + peripheralInfo.Name + '') } +// Update peripheral status +function peripheralUpdateStatus(peripheralInfo, status){ + // If not exists, add it to the map + // change status key + + peripherals.update((storedPeripherals) => { + console.log(status) + return { + ...storedPeripherals, + [peripheralInfo.SerialNumber]: { + ...storedPeripherals[peripheralInfo.SerialNumber], + Name: peripheralInfo.Name, + ProtocolName: peripheralInfo.ProtocolName, + SerialNumber: peripheralInfo.SerialNumber, + Settings: peripheralInfo.Settings, + status: status, + }, + }}) + + console.log("Hardware status has been updated to " + status); +} + +// Load the peripheral in the project function loadPeripheral (peripheralInfo) { - peripherals.update((storedPeripherals) => { - // Add the saved peripherals of the project - // If already exists pass the isSaved key to true, if not create the peripheral and set it to disconnected - // Add the peripheral to the list of peripherals, with the last isDetected key and the isSaved key to true - let lastDetectedKey = storedPeripherals[peripheralInfo.SerialNumber]?.isDetected - storedPeripherals[peripheralInfo.SerialNumber] = peripheralInfo - storedPeripherals[peripheralInfo.SerialNumber].isDetected = (lastDetectedKey === true) ? true : false - storedPeripherals[peripheralInfo.SerialNumber].isSaved = true - return {...storedPeripherals} - }) - //TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder - needProjectSave.set(true) + // If not exists, add it to the map + // isSaved key to true + + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [peripheralInfo.SerialNumber]: { + ...storedPeripherals[peripheralInfo.SerialNumber], + Name: peripheralInfo.Name, + ProtocolName: peripheralInfo.ProtocolName, + SerialNumber: peripheralInfo.SerialNumber, + Settings: peripheralInfo.Settings, + isSaved: true, + }, + }}) + console.log("Hardware has been added to the project"); + //TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder + needProjectSave.set(true) } function loadProject (showInfo){ @@ -75,19 +98,27 @@ function loadProject (showInfo){ generateToast('info', 'bx-folder-open', get(_)("projectOpenedToast") + ' ' + showInfo.Name + '') } +// Unload the hardware from the project function unloadPeripheral (peripheralInfo) { - peripherals.update((storedPeripherals) => { - // Set all the isSaved keys to false and delete the disconnected peripherals - storedPeripherals[peripheralInfo.SerialNumber].isSaved = false - if (!storedPeripherals[peripheralInfo.SerialNumber].isDetected) { - delete storedPeripherals[peripheralInfo.SerialNumber] - } - return {...storedPeripherals} - }) + // If not exists, add it to the map + // isSaved key to false - //TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder - needProjectSave.set(true) - } + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [peripheralInfo.SerialNumber]: { + ...storedPeripherals[peripheralInfo.SerialNumber], + Name: peripheralInfo.Name, + ProtocolName: peripheralInfo.ProtocolName, + SerialNumber: peripheralInfo.SerialNumber, + Settings: peripheralInfo.Settings, + isSaved: false, + }, + }}) + console.log("Hardware has been removed from the project"); + //TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder + needProjectSave.set(true) +} let initialized = false @@ -96,13 +127,13 @@ export function initRuntimeEvents(){ initialized = true // Handle the event when a new peripheral is detected - EventsOn('PERIPHERAL_ARRIVAL', addPeripheral) + EventsOn('PERIPHERAL_ARRIVAL', peripheralArrival) // Handle the event when a peripheral is removed from the system - EventsOn('PERIPHERAL_REMOVAL', removePeripheral) + EventsOn('PERIPHERAL_REMOVAL', peripheralRemoval) // Handle the event when a peripheral status is updated - EventsOn('PERIPHERAL_STATUS', updatePeripheral) + EventsOn('PERIPHERAL_STATUS', peripheralUpdateStatus) // Handle the event when a new project need to be loaded EventsOn('LOAD_PROJECT', loadProject) diff --git a/frontend/src/stores.js b/frontend/src/stores.js index 16d093e..02aeb53 100644 --- a/frontend/src/stores.js +++ b/frontend/src/stores.js @@ -34,4 +34,15 @@ export const secondSize = writable("14px") export const thirdSize = writable("20px") // List of current hardware -export let peripherals = writable({}) \ No newline at end of file +export let peripherals = writable({}) + +// Peripheral structure : + +// Name string `yaml:"name"` // Name of the peripheral +// SerialNumber string `yaml:"sn"` // S/N of the peripheral +// ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral +// Settings map[string]interface{} `yaml:"settings"` // Peripheral settings + +// isSaved // if the peripheral is saved in the project +// isDetected // if the peripheral is detected by the system +// status // the status of connection \ No newline at end of file diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go index df00560..2fc2e5b 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -2,8 +2,6 @@ package hardware import ( "context" - _ "embed" - "errors" "fmt" goRuntime "runtime" "sync" @@ -24,74 +22,93 @@ import "C" // FTDIFinder manages all the FTDI peripherals type FTDIFinder struct { wg sync.WaitGroup + mu sync.Mutex - 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 + saved map[string]PeripheralInfo // Peripherals saved in the project + detected map[string]*FTDIPeripheral // Detected peripherals + + scanEvery time.Duration // Scans peripherals periodically + + onArrival func(p PeripheralInfo) // When a peripheral arrives + onRemoval func(p PeripheralInfo) // When a peripheral goes away } // NewFTDIFinder creates a new FTDI finder -func NewFTDIFinder(findPeriod time.Duration) *FTDIFinder { +func NewFTDIFinder(scanEvery time.Duration) *FTDIFinder { log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder created") return &FTDIFinder{ - findTicker: time.NewTicker(findPeriod), - foundPeripherals: make(map[string]PeripheralInfo), - registeredPeripherals: make(map[string]*FTDIPeripheral), - scanChannel: make(chan struct{}), + scanEvery: scanEvery, + saved: make(map[string]PeripheralInfo), + detected: make(map[string]*FTDIPeripheral), } } +// OnArrival is the callback function when a new peripheral arrives +func (f *FTDIFinder) OnArrival(cb func(p PeripheralInfo)) { + f.onArrival = cb +} + +// OnRemoval i the callback when a peripheral goes away +func (f *FTDIFinder) OnRemoval(cb func(p PeripheralInfo)) { + f.onRemoval = cb +} + // 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") + f.mu.Lock() + defer f.mu.Unlock() - // Emit the event to the front + f.saved[peripheralData.SerialNumber] = peripheralData + + // If already detected, connect it + if peripheral, ok := f.detected[peripheralData.SerialNumber]; ok { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusConnecting) + err := peripheral.Connect(ctx) + if err != nil { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) + log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") + return peripheralData.SerialNumber, nil + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) + // Peripheral connected, activate it + err = peripheral.Activate(ctx) + if err != nil { + log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the FTDI peripheral") + return peripheralData.SerialNumber, nil + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusActivated) + } + + // Emits the event in the hardware runtime.EventsEmit(ctx, "LOAD_PERIPHERAL", peripheralData) - // Peripheral created, connect it - err = ftdiPeripheral.Connect(ctx) - if err != nil { - log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") - return peripheralData.SerialNumber, nil - } - // Peripheral connected, activate it - err = ftdiPeripheral.Activate(ctx) - if err != nil { - log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the peripheral") - return peripheralData.SerialNumber, nil - } - // Peripheral activated return peripheralData.SerialNumber, nil } // UnregisterPeripheral unregisters an existing peripheral -func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralID string) error { - peripheral, registered := f.registeredPeripherals[peripheralID] - if registered { +func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) error { + f.mu.Lock() + defer f.mu.Unlock() + + if peripheral, detected := f.detected[peripheralData.SerialNumber]; detected { // Deactivating peripheral err := peripheral.Deactivate(ctx) if err != nil { - return err + log.Err(err).Str("sn", peripheralData.SerialNumber).Msg("unable to deactivate the peripheral") + return nil } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) // Disconnecting peripheral err = peripheral.Disconnect() if err != nil { - return err + log.Err(err).Str("sn", peripheralData.SerialNumber).Msg("unable to disconnect the peripheral") + return nil } - - delete(f.registeredPeripherals, peripheralID) - - // Emit the event to the front - runtime.EventsEmit(ctx, "UNLOAD_PERIPHERAL", peripheral.info) + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) } + delete(f.saved, peripheralData.SerialNumber) + runtime.EventsEmit(ctx, "UNLOAD_PERIPHERAL", peripheralData) + return nil } @@ -110,18 +127,15 @@ func (f *FTDIFinder) Initialize() error { func (f *FTDIFinder) Start(ctx context.Context) error { f.wg.Add(1) go func() { + ticker := time.NewTicker(f.scanEvery) + defer ticker.Stop() defer f.wg.Done() + for { select { case <-ctx.Done(): return - case <-f.findTicker.C: - // Scan the peripherals - err := f.scanPeripherals(ctx) - if err != nil { - log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals") - } - case <-f.scanChannel: + case <-ticker.C: // Scan the peripherals err := f.scanPeripherals(ctx) if err != nil { @@ -135,11 +149,11 @@ func (f *FTDIFinder) Start(ctx context.Context) error { // ForceScan explicitly asks for scanning peripherals func (f *FTDIFinder) ForceScan() { - select { - case f.scanChannel <- struct{}{}: - default: - // Ignore if the channel is full or if it is closed - } + // select { + // case f.scanChannel <- struct{}{}: + // default: + // // Ignore if the channel is full or if it is closed + // } } // GetName returns the name of the driver @@ -150,25 +164,27 @@ func (f *FTDIFinder) GetName() string { // GetPeripheralSettings gets the peripheral settings func (f *FTDIFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) { // Return the specified peripheral - peripheral, found := f.registeredPeripherals[peripheralID] - if !found { - log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") - return nil, fmt.Errorf("unable to found the peripheral") - } - log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") - return peripheral.GetSettings(), nil + // peripheral, found := f.registeredPeripherals[peripheralID] + // if !found { + // log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") + // return nil, fmt.Errorf("unable to found the peripheral") + // } + // log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") + // return peripheral.GetSettings(), nil + return make(map[string]interface{}), nil } // SetPeripheralSettings sets the peripheral settings func (f *FTDIFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error { // Return the specified peripheral - peripheral, found := f.registeredPeripherals[peripheralID] - if !found { - log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") - return fmt.Errorf("unable to found the peripheral") - } - log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") - return peripheral.SetSettings(settings) + // peripheral, found := f.registeredPeripherals[peripheralID] + // if !found { + // log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") + // return fmt.Errorf("unable to found the peripheral") + // } + // log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") + // return peripheral.SetSettings(settings) + return nil } // scanPeripherals scans the FTDI peripherals @@ -188,19 +204,19 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { C.get_ftdi_devices((*C.FTDIPeripheralC)(devicesPtr), C.int(count)) - temporaryPeripherals := make(map[string]PeripheralInfo) + currentMap := make(map[string]PeripheralInfo) for i := 0; i < count; i++ { d := devices[i] sn := C.GoString(d.serialNumber) desc := C.GoString(d.description) - isOpen := d.isOpen != 0 + // isOpen := d.isOpen != 0 - temporaryPeripherals[sn] = PeripheralInfo{ + currentMap[sn] = PeripheralInfo{ SerialNumber: sn, Name: desc, - IsOpen: isOpen, + // IsOpen: isOpen, ProtocolName: "FTDI", } @@ -208,12 +224,63 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { C.free_ftdi_device(&d) } - log.Info().Any("peripherals", temporaryPeripherals).Msg("available FTDI peripherals") + log.Info().Any("peripherals", currentMap).Msg("available FTDI peripherals") - // Emit the peripherals changes to the front - emitPeripheralsChanges(ctx, f.foundPeripherals, temporaryPeripherals) - // Store the new peripherals list - f.foundPeripherals = temporaryPeripherals + // Detect arrivals + for sn, peripheralData := range currentMap { + if _, known := f.detected[sn]; !known { + peripheral := NewFTDIPeripheral(peripheralData) + f.detected[sn] = peripheral + + if f.onArrival != nil { + go f.onArrival(peripheralData) + } + log.Info().Str("sn", sn).Str("name", peripheralData.Name).Msg("[FTDI] New peripheral detected") + + // Disconnected by default + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) + + // If the peripheral is saved in the project => connect + if _, saved := f.saved[sn]; saved { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusConnecting) + err := peripheral.Connect(ctx) + if err != nil { + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) + log.Err(err).Str("sn", sn).Msg("unable to connect the FTDI peripheral") + return nil + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDeactivated) + err = peripheral.Activate(ctx) + if err != nil { + log.Err(err).Str("sn", sn).Msg("unable to activate the FTDI peripheral") + return nil + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusActivated) + } + } + } + + // Detect removals + for sn, oldPeripheral := range f.detected { + if _, still := currentMap[sn]; !still { + + // Properly clean the DMX device + err := oldPeripheral.Disconnect() + if err != nil { + log.Err(err).Str("sn", sn).Msg("unable to clean the FTDI peripheral after disconnection") + } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), oldPeripheral.GetInfo(), PeripheralStatusDisconnected) + + // Delete it from the detected list + delete(f.detected, sn) + log.Info().Str("sn", sn).Str("name", oldPeripheral.GetInfo().Name).Msg("[FTDI] peripheral removed") + + // Execute the removal callback + if f.onRemoval != nil { + go f.onRemoval(oldPeripheral.GetInfo()) + } + } + } return nil } @@ -221,29 +288,26 @@ func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { 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) + // 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)) - } - } + // 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...) - } + // if len(errs) > 0 { + // return errors.Join(errs...) + // } log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped") return nil } diff --git a/hardware/FTDIPeripheral.go b/hardware/FTDIPeripheral.go index c8ed185..8debace 100644 --- a/hardware/FTDIPeripheral.go +++ b/hardware/FTDIPeripheral.go @@ -2,14 +2,12 @@ package hardware import ( "context" - _ "embed" "sync" "unsafe" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/wailsapp/wails/v2/pkg/runtime" ) /* @@ -28,12 +26,12 @@ type FTDIPeripheral struct { } // NewFTDIPeripheral creates a new FTDI peripheral -func NewFTDIPeripheral(info PeripheralInfo) (*FTDIPeripheral, error) { +func NewFTDIPeripheral(info PeripheralInfo) *FTDIPeripheral { log.Info().Str("file", "FTDIPeripheral").Str("name", info.Name).Str("s/n", info.SerialNumber).Msg("FTDI peripheral created") return &FTDIPeripheral{ info: info, dmxSender: nil, - }, nil + } } // Connect connects the FTDI peripheral @@ -47,13 +45,11 @@ func (p *FTDIPeripheral) Connect(ctx context.Context) error { p.dmxSender = C.dmx_create() // 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 { - runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "disconnected") 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'") + return errors.Errorf("unable to connect '%s'", p.info.SerialNumber) } p.wg.Add(1) @@ -63,10 +59,7 @@ func (p *FTDIPeripheral) Connect(ctx context.Context) error { _ = p.Disconnect() }() - // 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 } @@ -79,6 +72,9 @@ func (p *FTDIPeripheral) Disconnect() error { // Destroy the dmx sender C.dmx_destroy(p.dmxSender) + + // Reset the pointer to the peripheral + p.dmxSender = nil return nil } @@ -93,7 +89,6 @@ func (p *FTDIPeripheral) Activate(ctx context.Context) error { err := C.dmx_activate(p.dmxSender) if err != C.DMX_OK { - runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "deactivated") return errors.Errorf("unable to activate the DMX sender!") } @@ -101,10 +96,7 @@ func (p *FTDIPeripheral) Activate(ctx context.Context) error { C.dmx_setValue(p.dmxSender, C.uint16_t(1), C.uint8_t(255)) C.dmx_setValue(p.dmxSender, C.uint16_t(5), C.uint8_t(255)) - runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "activated") - log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device activated successfully") - return nil } @@ -123,7 +115,6 @@ func (p *FTDIPeripheral) Deactivate(ctx context.Context) error { } log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device deactivated successfully") - return nil } diff --git a/hardware/OS2LPeripheral.go b/hardware/OS2LPeripheral.go index 07e69a0..4bc9703 100644 --- a/hardware/OS2LPeripheral.go +++ b/hardware/OS2LPeripheral.go @@ -30,9 +30,9 @@ func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) { func (p *OS2LPeripheral) Connect(ctx context.Context) error { log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected") go func() { - runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "connecting") + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.info, "connecting") time.Sleep(5 * time.Second) - runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "disconnected") + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), p.info, "disconnected") }() return nil } diff --git a/hardware/hardware.go b/hardware/hardware.go index cbc841d..466f4b4 100644 --- a/hardware/hardware.go +++ b/hardware/hardware.go @@ -7,21 +7,30 @@ import ( "sync" "github.com/rs/zerolog/log" - "github.com/wailsapp/wails/v2/pkg/runtime" ) // PeripheralEvent is trigger by the finders when the scan is complete type PeripheralEvent string +// PeripheralStatus is the peripheral status (DISCONNECTED => CONNECTING => DEACTIVATED => ACTIVATED) +type PeripheralStatus string + const ( // PeripheralArrival is triggerd when a peripheral has been connected to the system PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL" // PeripheralRemoval is triggered when a peripheral has been disconnected from the system PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL" - // PeripheralStatus is triggered when a peripheral status has been updated (disconnected - connecting - connected) - PeripheralStatus PeripheralEvent = "PERIPHERAL_STATUS" - // debounceDuration = 500 * time.Millisecond + // PeripheralStatusUpdated is triggered when a peripheral status has been updated (disconnected - connecting - connected) + PeripheralStatusUpdated PeripheralEvent = "PERIPHERAL_STATUS" + // PeripheralStatusDisconnected : peripheral is now disconnected + PeripheralStatusDisconnected PeripheralStatus = "PERIPHERAL_DISCONNECTED" + // PeripheralStatusConnecting : peripheral is now connecting + PeripheralStatusConnecting PeripheralStatus = "PERIPHERAL_CONNECTING" + // PeripheralStatusDeactivated : peripheral is now deactivated + PeripheralStatusDeactivated PeripheralStatus = "PERIPHERAL_DEACTIVATED" + // PeripheralStatusActivated : peripheral is now activated + PeripheralStatusActivated PeripheralStatus = "PERIPHERAL_ACTIVATED" ) // HardwareManager is the class who manages the hardware @@ -45,13 +54,19 @@ func NewHardwareManager() *HardwareManager { // Start starts to find new peripheral events func (h *HardwareManager) Start(ctx context.Context) error { - // Initialize all the finders + // Initialize all the finders and their callback functions for finderName, finder := range h.finders { err := finder.Initialize() if err != nil { log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to initialize finder") return err } + finder.OnArrival(func(p PeripheralInfo) { + runtime.EventsEmit(ctx, string(PeripheralArrival), p) + }) + finder.OnRemoval(func(p PeripheralInfo) { + runtime.EventsEmit(ctx, string(PeripheralRemoval), p) + }) err = finder.Start(ctx) if err != nil { log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to start finder") @@ -105,27 +120,6 @@ func (h *HardwareManager) Scan() error { } } -// emitPeripheralsChanges compares the old and new peripherals to determine which ones have been added or removed. -func emitPeripheralsChanges(ctx context.Context, oldPeripherals map[string]PeripheralInfo, newPeripherals map[string]PeripheralInfo) { - log.Trace().Any("oldList", oldPeripherals).Any("newList", newPeripherals).Msg("emitting peripherals changes to the front") - - // Identify removed peripherals: present in the old list but not in the new list - for oldPeriphName := range oldPeripherals { - if _, exists := newPeripherals[oldPeriphName]; !exists { - runtime.EventsEmit(ctx, string(PeripheralRemoval), oldPeripherals[oldPeriphName]) - log.Trace().Str("file", "hardware").Str("event", string(PeripheralRemoval)).Msg("emit peripheral removal event") - } - } - - // Identify added peripherals: present in the new list but not in the old list - for newPeriphName := range newPeripherals { - if _, exists := oldPeripherals[newPeriphName]; !exists { - runtime.EventsEmit(ctx, string(PeripheralArrival), newPeripherals[newPeriphName]) - log.Trace().Str("file", "hardware").Str("event", string(PeripheralArrival)).Msg("emit peripheral arrival event") - } - } -} - // WaitStop stops the hardware manager func (h *HardwareManager) WaitStop() error { log.Trace().Str("file", "hardware").Msg("closing the hardware manager") diff --git a/hardware/interfaces.go b/hardware/interfaces.go index 039cf13..d48b3fe 100644 --- a/hardware/interfaces.go +++ b/hardware/interfaces.go @@ -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() 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 @@ -18,21 +18,25 @@ type Peripheral interface { // PeripheralInfo represents a peripheral information type PeripheralInfo struct { - Name string `yaml:"name"` // Name of the peripheral - SerialNumber string `yaml:"sn"` // S/N of the peripheral - ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral - IsOpen bool // Open flag for peripheral connection - Settings map[string]interface{} `yaml:"settings"` // Peripheral settings + Name string `yaml:"name"` // Name of the peripheral + SerialNumber string `yaml:"sn"` // S/N of the peripheral + ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral + // IsConnected bool // If the peripheral is connected to the system + // IsActivated bool // If the peripheral is activated in the project + // IsDetected bool // If the peripheral is detected by the system + Settings map[string]interface{} `yaml:"settings"` // Peripheral settings } // PeripheralFinder represents how compatible peripheral drivers are implemented type PeripheralFinder interface { Initialize() error // Initializes the protocol + OnArrival(cb func(p PeripheralInfo)) // Callback function when a peripheral arrives + OnRemoval(cb func(p PeripheralInfo)) // Callback function when a peripheral goes away Start(context.Context) error // Start 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 + UnregisterPeripheral(context.Context, PeripheralInfo) error // Unregisters an existing peripheral GetPeripheralSettings(string) (map[string]interface{}, error) // Gets the peripheral settings SetPeripheralSettings(string, map[string]interface{}) error // Sets the peripheral settings GetName() string // Get the name of the finder diff --git a/peripherals.go b/peripherals.go index e128c1d..91ae570 100644 --- a/peripherals.go +++ b/peripherals.go @@ -65,21 +65,21 @@ func (a *App) UpdatePeripheralSettings(protocolName, peripheralID string, settin } // RemovePeripheral removes a peripheral from the project -func (a *App) RemovePeripheral(protocolName string, peripheralID string) error { +func (a *App) RemovePeripheral(peripheralData hardware.PeripheralInfo) error { // Unregister the peripheral from the finder - f, err := a.hardwareManager.GetFinder(protocolName) + f, err := a.hardwareManager.GetFinder(peripheralData.ProtocolName) if err != nil { - log.Err(err).Str("file", "peripherals").Str("protocolName", protocolName).Msg("unable to find the finder") + log.Err(err).Str("file", "peripherals").Str("protocolName", peripheralData.ProtocolName).Msg("unable to find the finder") return fmt.Errorf("unable to find the finder") } - err = f.UnregisterPeripheral(a.ctx, peripheralID) + err = f.UnregisterPeripheral(a.ctx, peripheralData) if err != nil { - log.Err(err).Str("file", "peripherals").Str("peripheralID", peripheralID).Msg("unable to unregister this peripheral") + log.Err(err).Str("file", "peripherals").Str("peripheralID", peripheralData.SerialNumber).Msg("unable to unregister this peripheral") return fmt.Errorf("unable to unregister this peripheral") } // Remove the peripheral ID from the project - delete(a.projectInfo.PeripheralsInfo, peripheralID) - log.Info().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("peripheral removed from project") + delete(a.projectInfo.PeripheralsInfo, peripheralData.SerialNumber) + log.Info().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Str("periphID", peripheralData.SerialNumber).Msg("peripheral removed from project") return nil } diff --git a/project.go b/project.go index 0a249ab..af7a157 100644 --- a/project.go +++ b/project.go @@ -151,7 +151,7 @@ func (a *App) CloseCurrentProject() error { if err != nil { return fmt.Errorf("unable to find the finder '%s': %w", value.ProtocolName, err) } - err = hostFinder.UnregisterPeripheral(a.ctx, key) + err = hostFinder.UnregisterPeripheral(a.ctx, value) if err != nil { return fmt.Errorf("unable to unregister the peripheral S/N '%s': %w", key, err) } -- 2.49.1