24-project-life #28

Merged
thinkode merged 3 commits from 24-project-life into develop 2025-11-11 19:14:45 +00:00
17 changed files with 876 additions and 350 deletions

2
.gitignore vendored
View File

@@ -9,4 +9,6 @@ frontend/wailsjs
*.exe
*.o
*.rc
*.dll
*.dll.a
frontend/public

1
app.go
View File

@@ -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

View File

@@ -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") + ' <b>' + peripheralInfo.Name + '</b>')
})
// 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") + ' <b>' + peripheralInfo.Name + '</b>')
}
// 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"))
})
}
</script>
<header>
<!-- Project buttons -->
<RoundIconButton on:mouseup={initializeNewProject} icon='bxs-plus-square' width="2.5em" tooltip={$_("newProjectTooltip")}/>
<RoundDropdownList on:click={loadProjectsList} on:selected={openSelectedProject} choices={choices} icon='bxs-folder-open' width="2.5em" tooltip={$_("openProjectTooltip")}/>
<NavigationBar on:navigationChanged="{onNavigationChanged}"/>
{#if $needProjectSave}
<RoundIconButton on:mouseup={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}></RoundIconButton>
<RoundIconButton on:mouseup={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}/>
{/if}
<Clock/>
</header>

View File

@@ -0,0 +1,152 @@
<!-- Create a round icon button -->
<script>
import { createEventDispatcher, onDestroy } from 'svelte';
import {colors} from '../../stores.js';
import Tooltip from './Tooltip.svelte';
import { _ } from 'svelte-i18n'
export let icon = "bxs-heart" // The icon wanted
export let width = "10em" // The button width
export let active = false // If the button is active or not
export let tooltip = "Default tooltip" // The description shown in the tooltip
export let operationalStatus = undefined// The optional button status
export let okStatusLabel = "" // The label shown when the button is OK
export let nokStatusLabel = "" // The label shown when the button is NOK
export let choices = new Map()
export let style = '';
let tooltipMessage = tooltip
// Default values for background and foreground
$: background = $colors.first
$: foreground = $colors.first
// Change the background when the selected prop changed
$: {
if (active === true) {
background = $colors.third
foreground = $colors.fourth
} else {
background = $colors.fourth
foreground = $colors.second
}
}
// Show the operational status if specified
// undefined => no status displayed
// operationalStatus = true => OK color displayed
// operationalStatus = false => NOK color displayed
$: statusColor = $colors.nok
$: {
if (operationalStatus === true){
statusColor = $colors.ok
tooltipMessage = tooltip + " " + okStatusLabel
} else {
statusColor = $colors.nok
tooltipMessage = tooltip + " " + nokStatusLabel
}
}
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function handleclick(key){
// Deactivate the list visibility
hideList()
dispatch('selected', key)
}
// Show the option list
let listShowing = false
function toggleList(){
if (!listShowing) {
dispatch('click')
}
listShowing = !listShowing
}
function hideList(){
listShowing = false
}
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
let buttonRef
function toggleTooltip(active){
const rect = buttonRef.getBoundingClientRect();
tooltipPosition = {
top: rect.bottom + 5, // Ajouter une marge en bas
left: rect.left, // Centrer horizontalement
};
tooltipShowing = active
}
</script>
<div class="container">
<button bind:this={buttonRef}
style="width:{width}; height:{width}; border-radius:{width}; background-color:{background}; color:{foreground};"
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
on:click={toggleList}>
<i class='bx {icon}' style="font-size:100%;"></i>
</button>
<!-- Showing the badge status if the button has an operational status -->
{#if (operationalStatus !== undefined)}
<div class="badge"
style="width: calc({width} / 3); height: calc({width} / 3); border-radius: calc({width}); background-color:{statusColor}; display:block;">
</div>
{/if}
<Tooltip message={tooltipMessage} show={tooltipShowing} position={tooltipPosition}></Tooltip>
<div class="list" style="color: {$colors.white}; display: {listShowing ? "block" : "none"}; border: 2px solid {$colors.second}; background-color: {$colors.first};"
on:mouseleave={hideList}>
{#each Array.from(choices) as [key, value]}
<div class="item" on:click={() => handleclick({key})}>{value}</div>
{/each}
</div>
</div>
<style>
.item{
border-radius: 0.3em;
padding: 0.3em;
}
.item:hover {
background-color: var(--second-color);
color: var(--white-color);
}
.container {
position: relative;
display: inline-block;
}
.list {
z-index: 200;
padding: 0.2em;
margin-top: 0.2em;
position: absolute;
width: auto;
cursor: pointer;
border-radius: 0.5em;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 30em;
max-height: 40vh;
overflow-y: scroll;
scrollbar-width: none;
}
button{
display: inline-block;
margin: 0;
border:none;
cursor: pointer;
}
button:hover{
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.25) inset;
}
</style>

View File

@@ -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 @@
</script>
<div class="card" on:dblclick={dblclick}>
<div class="{selected ? "selected" : "unselected"} {status == "connecting" ? "waiting" : ""}" on:mousedown={click} style="color: {(status == "disconnected") ? $colors.first : $colors.white};">
<div class="{selected ? "selected" : "unselected"} {status == "PERIPHERAL_CONNECTING" ? "waiting" : ""}" on:mousedown={click} style="color: {(status == "PERIPHERAL_DISCONNECTED") ? $colors.first : $colors.white};">
<div style="z-index: 1;">
<p>{#if status == "disconnected" }<i class='bx bx-no-signal' style="font-size:100%; color: var(--nok-color);"></i> {/if}{title}</p>
<p>{#if status == "PERIPHERAL_DISCONNECTED" }<i class='bx bx-no-signal' style="font-size:100%; color: var(--nok-color);"></i> {/if}{title}</p>
<h6 class="subtitle">{type} {location != '' ? "- " : ""}<i>{location}</i></h6>
{#if status == "disconnected"}
{#if status == "PERIPHERAL_DISCONNECTED"}
<h6><b>Disconnected</b></h6>
{:else}
<h6>{line1}</h6>
@@ -50,9 +51,9 @@
</div>
<div class="actions">
<InfoButton on:click={add} color="{(status == "disconnected") ? $colors.first : $colors.white}" style="margin: 0.2em; display: { addable ? 'flex' : 'none' }" icon='bxs-message-square-add' interactive message={$_("projectHardwareAddTooltip")}/>
<InfoButton on:click={remove} color="{(status == "disconnected") ? $colors.first : $colors.white}" style="margin: 0.2em; display: { removable ? 'flex' : 'none' }" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")}/>
<InfoButton style="margin: 0.2em; display: { (status == "activated" || status == "deactivated") ? 'flex' : 'none' }" background={ (status == "activated") ? $colors.ok : (status == "deactivated") ? $colors.nok : null} icon='bx-pulse' hide={!signalizable}/>
<InfoButton on:click={add} color="{(status == "PERIPHERAL_DISCONNECTED") ? $colors.first : $colors.white}" style="margin: 0.2em; display: { addable ? 'flex' : 'none' }" icon='bxs-message-square-add' interactive message={$_("projectHardwareAddTooltip")}/>
<InfoButton on:click={remove} color="{(status == "PERIPHERAL_DISCONNECTED") ? $colors.first : $colors.white}" style="margin: 0.2em; display: { removable ? 'flex' : 'none' }" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")}/>
<InfoButton style="margin: 0.2em; display: { (status == "PERIPHERAL_ACTIVATED" || status == "PERIPHERAL_DEACTIVATED") ? 'flex' : 'none' }" background={ (status == "PERIPHERAL_ACTIVATED") ? $colors.ok : (status == "PERIPHERAL_DEACTIVATED") ? $colors.nok : null} icon='bx-pulse' hide={!signalizable}/>
</div>
</div>
</div>

View File

@@ -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}
<p style="color: var(--first-color);"><i class='bx bxs-network-chart' ></i> {$_("projectHardwareOthersLabel")}</p>
@@ -134,7 +103,7 @@
{#if savedPeripheralNumber > 0}
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
{#if peripheral.isSaved}
<DeviceCard status="{peripheral.Status}" on:delete={() => removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)} on:click={() => selectPeripheral(peripheral)}
<DeviceCard status="{peripheral.status}" on:delete={() => 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}

View File

@@ -1,109 +1,16 @@
<script lang=ts>
import { generateToast, projectsList, showInformation, needProjectSave, peripherals } from '../../stores.js';
import RoundedButton from "../General/RoundedButton.svelte";
import ProjectPropertiesContent from "./ProjectPropertiesContent.svelte";
import DropdownList from "../General/DropdownList.svelte";
import InputsOutputsContent from "./InputsOutputsContent.svelte";
import Tab from "../General/Tab.svelte";
import { CreateProject, GetProjects, OpenProjectFromDisk } from "../../../wailsjs/go/main/App";
import { _ } from 'svelte-i18n'
import {colors} from '../../stores.js';
import { get } from "svelte/store"
import { EventsOn } from '../../../wailsjs/runtime'
const tabs = [
{ title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent },
{ title: $_("projectInputOutputTab"), icon: 'bxs-plug', tooltip: $_("projectInputOutputTooltip"), component: InputsOutputsContent },
];
// 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"))
})
}
// Unsave peripherals from the store and remove the disconnected peripherals
function unsavePeripherals(){
peripherals.update((storedPeripherals) => {
// Set all the isSaved keys to false and delete the disconnected peripherals
for (let peripheralID in storedPeripherals) {
storedPeripherals[peripheralID].isSaved = false
if (!storedPeripherals[peripheralID].isDetected) {
delete storedPeripherals[peripheralID]
}
}
return {...storedPeripherals}
})
}
// Load the saved peripherals into the store
function loadPeripherals(peripheralsInfo){
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
for (let peripheralID in peripheralsInfo){
// Add the peripheral to the list of peripherals, with the last isDetected key and the isSaved key to true
let lastDetectedKey = storedPeripherals[peripheralID]?.isDetected
storedPeripherals[peripheralID] = peripheralsInfo[peripheralID]
storedPeripherals[peripheralID].isDetected = (lastDetectedKey === true) ? true : false
storedPeripherals[peripheralID].isSaved = true
}
return {...storedPeripherals}
})
}
// Handle the event when a new project need to be loaded
EventsOn('LOAD_PROJECT', function(projectInfo){
// Store project information
showInformation.set(projectInfo.ShowInfo)
// Remove the saved peripherals of the current project
unsavePeripherals()
// Load new project peripherals
loadPeripherals(projectInfo.PeripheralsInfo)
console.log("Project has been opened");
generateToast('info', 'bx-folder-open', $_("projectOpenedToast") + ' <b>' + projectInfo.ShowInfo.Name + '</b>')
})
// Open the selected project
function openSelectedProject(event){
let selectedOption = event.detail.key
// Open the selected project
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"))
})
}
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"))
})
}
// Instantiate a new project
initializeNewProject()
</script>
<!-- Project buttons -->
<RoundedButton on:click={initializeNewProject} text={$_("newProjectString")} icon='bxs-plus-square' tooltip={$_("newProjectTooltip")}/>
<DropdownList icon='bxs-folder-open' text={$_("openProjectString")} choices={choices} tooltip={$_("openProjectTooltip")} on:click={loadProjectsList} on:selected={openSelectedProject}/>
<!-- Project tabcontrol -->
<Tab { tabs } maxHeight='73vh'/>

View File

@@ -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';

View File

@@ -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") + ' <b>' + peripheralInfo.Name + '</b>')
}
// 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") + ' <b>' + peripheralInfo.Name + '</b>')
}
// 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") + ' <b>' + showInfo.Name + '</b>')
}
// 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')
}

View File

@@ -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({})
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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")

View File

@@ -6,7 +6,7 @@ import "context"
type Peripheral interface {
Connect(context.Context) error // Connect the peripheral
IsConnected() bool // Return if the peripheral is connected or not
Disconnect() 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

View File

@@ -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
}

248
project.go Normal file
View File

@@ -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
}