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..80f618f --- /dev/null +++ b/frontend/src/components/General/RoundDropdownList.svelte @@ -0,0 +1,152 @@ + + + + +
+ + + {#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/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/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..59a290b --- /dev/null +++ b/frontend/src/runtime-events.js @@ -0,0 +1,169 @@ +import { EventsOn, EventsOff } from "../wailsjs/runtime/runtime.js" +import { peripherals, generateToast, needProjectSave, showInformation } from './stores' +import { get } from "svelte/store" +import { _ } from 'svelte-i18n' + +// 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 + '') +} + +// Peripheral is removed from the system +function peripheralRemoval (peripheralInfo){ + // If not exists, add it to the map + // isDetected key to false + + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [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) { + // 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){ + // Store project information + showInformation.set(showInfo) + + console.log("Project has been opened"); + generateToast('info', 'bx-folder-open', get(_)("projectOpenedToast") + ' ' + showInfo.Name + '') +} + +// Unload the hardware from the project +function unloadPeripheral (peripheralInfo) { + // If not exists, add it to the map + // isSaved key to false + + 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 + +export function initRuntimeEvents(){ + if (initialized) return + initialized = true + + // Handle the event when a new peripheral is detected + EventsOn('PERIPHERAL_ARRIVAL', peripheralArrival) + + // Handle the event when a peripheral is removed from the system + EventsOn('PERIPHERAL_REMOVAL', peripheralRemoval) + + // Handle the event when a peripheral status is updated + EventsOn('PERIPHERAL_STATUS', peripheralUpdateStatus) + + // 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..02aeb53 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", @@ -35,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 3a748e2..2fc2e5b 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -2,8 +2,6 @@ package hardware import ( "context" - _ "embed" - "errors" "fmt" goRuntime "runtime" "sync" @@ -11,6 +9,7 @@ import ( "unsafe" "github.com/rs/zerolog/log" + "github.com/wailsapp/wails/v2/pkg/runtime" ) /* @@ -23,65 +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() - // 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") + 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) } - // 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") - } - // Peripheral activated + + // Emits the event in the hardware + runtime.EventsEmit(ctx, "LOAD_PERIPHERAL", peripheralData) + 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 } + runtime.EventsEmit(ctx, string(PeripheralStatusUpdated), peripheralData, PeripheralStatusDisconnected) } - delete(f.registeredPeripherals, peripheralID) + delete(f.saved, peripheralData.SerialNumber) + runtime.EventsEmit(ctx, "UNLOAD_PERIPHERAL", peripheralData) + return nil } @@ -100,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 { @@ -123,13 +147,13 @@ 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{}{}: - 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 @@ -140,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 @@ -178,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", } @@ -198,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 } @@ -211,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 1c22062..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,12 +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 { 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) @@ -62,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 } @@ -78,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 } @@ -95,13 +92,11 @@ func (p *FTDIPeripheral) Activate(ctx context.Context) error { 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)) - 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 } @@ -120,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 new file mode 100644 index 0000000..af7a157 --- /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, value) + 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 +}