22 Commits

Author SHA1 Message Date
4fbb75ad19 34-midi (#35)
Closes #34 Implement MIDI peripherals
Implement device concept
Cleaning project

Reviewed-on: #35
2025-12-02 18:02:17 +00:00
932c288a9c 23-os2l (#32)
Implements the OS2L feature (tested with Virtual DJ).
Graphics improvements.

Reviewed-on: #32
2025-11-14 10:46:24 +00:00
121a14ac61 resolved: CPU usage for FTDI peripherals (#31)
Resolved CPU usage for FTDI peripherals.
2025-11-12 11:10:43 +00:00
6dd555265c 24-project-life (#28)
Fix peripherals states and implements plug and reload feature.
Graphics improvements.
Stability improvements.

Reviewed-on: #28
2025-11-11 19:14:44 +00:00
9e8cbed73f 24-activate-peripherals (#26)
Reviewed-on: #26
2025-11-02 10:00:12 +00:00
15d0f8b61b clean up arch 2025-11-02 10:57:53 +01:00
abcc3e0b5e resolved: activating/deactivating peripherals 2025-11-01 12:23:22 +01:00
cb5c5b688e Resolve CGO configuration + version in EXE 2025-10-25 12:25:09 +02:00
65e2def501 added open flag indicator for FTDI 2025-10-19 19:56:05 +02:00
bc15407cad resolved disconnected hardware when it is detected 2025-08-31 11:15:38 +02:00
0db468bfef add a waiting status on device cards 2025-01-26 15:11:05 +01:00
4e0829e821 rework on the peripherals and finders 2025-01-26 12:01:31 +01:00
c7fe171cb4 21-peripherals-settings (#22)
Added the concept of peripheral settings.

Reviewed-on: #22
2025-01-25 17:43:45 +00:00
0e3f57f5fb 11-hardware-definition (#17)
- Detection of FTDI and MIDI peripherals
- Creation od OS2L peripherals
- Optimization

Reviewed-on: #17
2025-01-18 14:53:29 +00:00
1052dcc8d5 added the toast notifications center (#19) 2024-12-24 13:36:11 +00:00
a231263825 project management feature (#15)
Reviewed-on: #15
2024-11-01 20:10:28 +00:00
364dabee69 10-project-settings (#13)
Added the project properties page.
2024-07-08 11:19:35 +00:00
b33df4b447 Added the application logo (#12)
Added the application logo.
2024-07-07 16:34:44 +00:00
d6dc8405dd added final navigation bar (#9)
Reviewed-on: #9
2024-06-24 07:02:39 +00:00
d9a01d440b feat: restored the project 2023-08-26 09:33:05 +00:00
8fe9c0a4e8 feat: added the build status 2023-08-25 20:37:16 +00:00
7ac2d71b4d feat: initialize the wails project 2023-08-25 20:33:11 +00:00
72 changed files with 6813 additions and 2 deletions

15
.gitignore vendored
View File

@@ -0,0 +1,15 @@
build/bin
projects
mapping
node_modules
frontend/.vscode
frontend/dist
frontend/wailsjs
*/package-lock.json
*/package.json.md5
*.exe
*.o
*.rc
*.dll
*.dll.a
frontend/public

66
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,66 @@
{
"files.associations": {
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"*.tcc": "cpp",
"cctype": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"compare": "cpp",
"concepts": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"format": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"new": "cpp",
"numbers": "cpp",
"ostream": "cpp",
"semaphore": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"thread": "cpp",
"typeinfo": "cpp",
"variant": "cpp",
"queue": "cpp",
"ranges": "cpp",
"text_encoding": "cpp"
}
}

View File

@@ -1,3 +1,75 @@
# modelRepository
[![Build Status](https://drone.vbprojects.fr/api/badges/DMXStage/dmxconnect/status.svg?ref=refs/heads/develop)](https://drone.vbprojects.fr/DMXStage/dmxconnect)
Dépôt de modèle.
# DMXConnect
## Introduction
Ce logiciel permet d'animer l'atmoshpère des soirées en permettant de piloter de manière manuelle et automatique des d'appareils DMX.
DMXConnect vous accompagne de la création de vos appareils DMX dans une bibliothèque jusqu'à leur pilotage automatique avec une intégration Spotify, en passant par la configuration de votre scène et de votre matériel.
REMARQUE : il n'est pas un logiciel de mixage audio.
## Fonctionnalités
Ce logiciel dispose des 3 grandes fonctionnalités suivantes :
- Paramétrage universel des appareils DMX : propose d'enregistrer ses appareils DMX dans une bibliothèque. L'utilisateur crée depuis l'interface les modes de canaux, les différents canaux de son matériel, et leur assigne des valeurs et des noms. Cette configuration est sauvegardée automatiquement.
- Configuration d'une scène : placement des appareils DMX depuis la bibliothèque sur une scène, choix du mode de canal des appareils et attribution d'une fonction personnalisée par l'utilisateur (permet de grouper les appareils suivant une caractéristique (basses, aigus, etc.) qui peut être reprise lors du pilotage d'une atmoshpère).
- Pilotage d'une atmosphère : permet de piloter les appareils DMX de la scène individuellement ou en groupe selon leur fonction (basses, aigus, etc.). Possibilité de réaliser des séquences d'animation pour représenter des atmosphères personnalisées (feu, hélicoptère, etc.).
Lancement des séquences grâce aux touches du clavier.
Lors de sa première installation, le logiciel vient avec un set de plusieurs appareils et plusieurs ambiances pré-configurés.
Pour aider au pilotage de la soirée, d'autres fonctionnalités peuvent être ajoutées comme :
- La gestion du volume sonore suivant l'heure (configurable sous forme de jauge ou même de courbe !), sans casser l'ambiance (ex : atteinte d'un niveau sonore bas vers 22h)
- La gestion du type de musique suivant l'ambiance observée ou voulue par le DJ. Configurable avec une panoplie de jauges suivant les niveaux de dançabilité, de joie, de tristesse, d'énervement, de sensibilité, de basses, d'aigus, etc.
- Une intégration Spotify permettant de contrôler le flux de la musique (play/pause, musique précédente, musique suivante, etc.). Visualisation de la musique en cours et de ses métadonnées (tempo, artiste, analyse appronfondie sur la dançabilité, les basses, etc.). Mode de pilotage automatique dans lequel le logiciel choisit les animations en fonction des métadonnées de la musique.
Obtention de suggestions pour les musiques suivantes selon l'ambiance configurée par le DJ.
## Téléchargement
Pour télécharger DMXConnect, choisissez une version depuis notre [zone de téléchargement](https://factory.vbprojects.fr/DMXStage/dmxconnect/releases).
Plusieurs solutions s'offrent à vous.
### Depuis l'exécutable (recommandé)
Il vous suffit de cliquer sur le bouton **Télécharger**.
Une fois l'archive téléchargée dans votre navigateur, il vous faudra l'extraire à l'emplacement de votre choix, puis ouvrir le dossier généré.
Vous pouvez lancer l'application en double-cliquant sur l'exécutable selon votre plateforme.
Vous êtes maintenant prêt à utiliser DMXConnect !
### Depuis les sources
REMARQUE : Afin de compiler le projet, vous devez avoir **Go (v1.21.x)**, **NodeJS (v18.x)** et **NPM (v9.x)** d'installé sur votre machine.
Vous avez aussi la possibilité de télécharger les sources du projet et de compiler le projet sur votre ordinateur. Pour ce faire, vous devez cliquer sur **Code source (ZIP)** ou **Code source (TAR.GZ)** selon l'extension que vous préférez.
Vous devez télécharger l'archive, puis l'extraire dans un dossier `src` à la racine de votre ``GOPATH`` (souvent `<utilisateur>/go/src`) vous pouvez ouvrir un terminal dans ce dossier et lancer les commandes suivantes.
*Installation de wails :*
```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```
*Détermination des dépendances requises pour wails :*
```bash
wails doctor
```
L'utilitaire vous permet de rechercher les dépendances à installer en fonction de votre système.
*Lancement de l'application :*
```bash
wails dev
```
Le logiciel devrait s'ouvrir sur la page d'accueil. Vous êtes maintenant prêt à utiliser DMXConnect !

125
app.go Normal file
View File

@@ -0,0 +1,125 @@
package main
import (
"context"
"dmxconnect/hardware"
genericmidi "dmxconnect/hardware/genericMIDI"
"dmxconnect/hardware/genericftdi"
"dmxconnect/hardware/os2l"
"fmt"
"io"
"time"
"github.com/rs/zerolog/log"
"os"
"strings"
"sync"
)
// App struct
type App struct {
ctx context.Context
cancelFunc context.CancelFunc
wait sync.WaitGroup
hardwareManager *hardware.Manager // For managing all the hardware
wmiMutex sync.Mutex // Avoid some WMI operations at the same time
projectInfo ProjectInfo // The project information structure
projectSave string // The file name of the project
}
// NewApp creates a new App application struct
func NewApp() *App {
// Create a new hadware manager
hardwareManager := hardware.NewManager()
// Register all the providers to use as hardware scanners
hardwareManager.RegisterProvider(genericftdi.NewProvider(3 * time.Second))
hardwareManager.RegisterProvider(os2l.NewProvider())
hardwareManager.RegisterProvider(genericmidi.NewProvider(3 * time.Second))
return &App{
hardwareManager: hardwareManager,
projectSave: "",
projectInfo: ProjectInfo{
EndpointsInfo: make(map[string]hardware.EndpointInfo),
},
}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) onStartup(ctx context.Context) {
a.ctx, a.cancelFunc = context.WithCancel(ctx)
// Starting the hardware manager
a.wait.Add(1)
go func() {
defer a.wait.Done()
err := a.hardwareManager.Start(a.ctx)
if err != nil {
log.Err(err).Str("file", "app").Msg("unable to start the hardware manager")
return
}
}()
}
// onReady is called when the DOM is ready
// We get the current endpoints connected
func (a *App) onReady(ctx context.Context) {
// log.Debug().Str("file", "endpoints").Msg("getting endpoints...")
// err := a.hardwareManager.Scan()
// if err != nil {
// log.Err(err).Str("file", "app").Msg("unable to get the endpoints")
// }
return
}
// onShutdown is called when the app is closing
// We stop all the pending processes
func (a *App) onShutdown(ctx context.Context) {
// Close the application properly
log.Trace().Str("file", "app").Msg("app is closing")
// Explicitly close the context
a.cancelFunc()
// Wait for application to close properly
a.hardwareManager.WaitStop()
// a.cancelFunc()
// err := a.hardwareManager.Stop()
// if err != nil {
// log.Err(err).Str("file", "app").Msg("unable to stop the hardware manager")
// }
return
}
func formatString(input string) string {
// Convertir en minuscules
lowerCaseString := strings.ToLower(input)
// Remplacer les espaces par des underscores
formattedString := strings.ReplaceAll(lowerCaseString, " ", "_")
return formattedString
}
func copy(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
return 0, err
}
if !sourceFileStat.Mode().IsRegular() {
return 0, fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return 0, err
}
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return 0, err
}
defer destination.Close()
nBytes, err := io.Copy(destination, source)
return nBytes, err
}

55
build.bat Normal file
View File

@@ -0,0 +1,55 @@
@echo off
setlocal
echo ============================================
echo [INFO] Starting Wails build script
echo ============================================
rem Détection du mode (par défaut : build)
set "MODE=build"
if /i "%~1"=="-dev" set "MODE=dev"
rem 1⃣ Essayer de récupérer le dernier tag
for /f "tokens=*" %%i in ('git describe --tags --abbrev=0 2^>nul') do set "GIT_TAG=%%i"
rem 2⃣ Si pas de tag, utiliser le hash du commit
if "%GIT_TAG%"=="" (
for /f "tokens=*" %%i in ('git rev-parse --short HEAD 2^>nul') do set "GIT_TAG=%%i"
)
rem 3⃣ Si Git nest pas dispo, mettre "unknown"
if "%GIT_TAG%"=="" set "GIT_TAG=unknown"
echo [INFO] Git version detected: %GIT_TAG%
echo [INFO] Mode selectionne : %MODE%
echo [INFO] Moving to the C++ folder...
cd /d "%~dp0hardware\genericftdi\cpp" || (
echo [ERROR] Impossible d'accéder à hardware\genericftdi\cpp
exit /b 1
)
echo [INFO] Compiling C++ libraries...
call generate.bat || (
echo [ERROR] Échec de la compilation C++
exit /b 1
)
echo [INFO] Returning to project root...
cd /d "%~dp0" || exit /b 1
if /i "%MODE%"=="dev" (
echo [INFO] Launching Wails in DEV mode...
wails dev
) else (
echo [INFO] Building Wails application...
wails build -o "dmxconnect-%GIT_TAG%.exe"
)
echo ============================================
echo [SUCCESS] Done!
echo ============================================
endlocal

BIN
build/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

15
build/windows/info.json Normal file
View File

@@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

61
endpoints.go Normal file
View File

@@ -0,0 +1,61 @@
package main
import (
"dmxconnect/hardware"
"fmt"
"github.com/rs/zerolog/log"
)
// AddEndpoint adds a endpoint to the project
func (a *App) AddEndpoint(endpointData hardware.EndpointInfo) (string, error) {
// Register this new endpoint
serialNumber, err := a.hardwareManager.RegisterEndpoint(a.ctx, endpointData)
if err != nil {
return "", fmt.Errorf("unable to register the endpoint '%s': %w", serialNumber, err)
}
log.Trace().Str("file", "endpoint").Str("protocolName", endpointData.ProtocolName).Str("periphID", serialNumber).Msg("device registered to the provider")
// Rewrite the serialnumber for virtual devices
endpointData.SerialNumber = serialNumber
// Add the endpoint ID to the project
if a.projectInfo.EndpointsInfo == nil {
a.projectInfo.EndpointsInfo = make(map[string]hardware.EndpointInfo)
}
a.projectInfo.EndpointsInfo[endpointData.SerialNumber] = endpointData
log.Info().Str("file", "endpoint").Str("protocolName", endpointData.ProtocolName).Str("periphID", endpointData.SerialNumber).Msg("endpoint added to project")
return endpointData.SerialNumber, nil
}
// GetEndpointSettings gets the endpoint settings
func (a *App) GetEndpointSettings(protocolName, endpointSN string) (map[string]any, error) {
return a.hardwareManager.GetEndpointSettings(endpointSN)
}
// UpdateEndpointSettings updates a specific setting of a endpoint
func (a *App) UpdateEndpointSettings(protocolName, endpointID string, settings map[string]any) error {
// Save the settings in the application
if a.projectInfo.EndpointsInfo == nil {
a.projectInfo.EndpointsInfo = make(map[string]hardware.EndpointInfo)
}
pInfo := a.projectInfo.EndpointsInfo[endpointID]
pInfo.Settings = settings
a.projectInfo.EndpointsInfo[endpointID] = pInfo
// Apply changes in the endpoint
return a.hardwareManager.SetEndpointSettings(a.ctx, endpointID, pInfo.Settings)
}
// RemoveEndpoint removes a endpoint from the project
func (a *App) RemoveEndpoint(endpointData hardware.EndpointInfo) error {
// Unregister the endpoint from the provider
err := a.hardwareManager.UnregisterEndpoint(a.ctx, endpointData)
if err != nil {
return fmt.Errorf("unable to unregister this endpoint: %w", err)
}
// Remove the endpoint ID from the project
delete(a.projectInfo.EndpointsInfo, endpointData.SerialNumber)
log.Info().Str("file", "endpoint").Str("protocolName", endpointData.ProtocolName).Str("periphID", endpointData.SerialNumber).Msg("endpoint removed from project")
return nil
}

63
frontend/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Svelte + Vite
This template should help get you started developing with Svelte in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/)
+ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its
serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less,
and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer
experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite`
templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been
structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash
references keeps the default TypeScript setting of accepting type information from the entire workspace, while also
adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to
install the recommended extension upon opening the project.
**Why enable `checkJs` in the JS template?**
It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate.
This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of
JavaScript, it is trivial to change the configuration.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr`
and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the
details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be
replaced by HMR.
```js
// store.js
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<link rel='stylesheet' href='./src/style.css'>
<title>DMXConnect</title>
</head>
<body style="background-color:#1B262C;">
<script src="./src/main.js" type="module"></script>
</body>
</html>

19
frontend/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"svelte": "^3.49.0",
"vite": "^3.0.7"
},
"dependencies": {
"svelte-i18n": "^3.7.0"
}
}

BIN
frontend/public/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

145
frontend/src/App.svelte Normal file
View File

@@ -0,0 +1,145 @@
<script>
import { _ } from 'svelte-i18n'
import NavigationBar from './components/General/NavigationBar.svelte';
import Clock from './components/General/Clock.svelte'
import Preparation from './components/Preparation/Preparation.svelte';
import Animation from './components/Animation/Animation.svelte';
import Settings from './components/Settings/Settings.svelte';
import Devices from './components/Devices/Devices.svelte';
import Show from './components/Show/Show.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, projectsList } from './stores.js';
import { GetProjects, CreateProject, OpenProjectFromDisk, SaveProject } from '../wailsjs/go/main/App.js';
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
import ToastNotification from './components/General/ToastNotification.svelte';
import { onMount, onDestroy } from 'svelte'
import { destroyRuntimeEvents, initRuntimeEvents } from './runtime-events.js'
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"))
})
}
// 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()
})
// Destroy runtime events at shutdown
onDestroy(() => {
destroyRuntimeEvents()
})
// Set the window title
$: {
WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (" + $_("unsavedProjectFlag") + ")" : ""))
}
let selectedMenu = "settings"
// When the navigation menu changed, update the selected menu
function onNavigationChanged(event){
selectedMenu = event.detail.menu;
}
// Save the project
function saveProject(){
SaveProject().then((filePath) => {
needProjectSave.set(false)
console.log("Project has been saved to " + filePath)
generateToast('info', 'bxs-save', $_("projectSavedToast"))
}).catch((error) => {
console.error(`Unable to save the project: ${error}`)
generateToast('danger', 'bx-error', $_("projectSaveErrorToast") + ' ' + error)
})
}
// 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")}/>
{/if}
<Clock/>
</header>
<main>
{#if selectedMenu === "settings"}
<Settings />
{:else if selectedMenu === "devices"}
<Devices />
{:else if selectedMenu === "preparation"}
<Preparation />
{:else if selectedMenu === "animation"}
<Animation />
{:else if selectedMenu === "show"}
<Show />
{:else if selectedMenu === "console"}
<GeneralConsole />
{/if}
<ToastNotification/>
</main>
<style>
main {
text-align: left;
max-width: 240px;
margin: 0 auto;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

View File

@@ -0,0 +1 @@
<h1>Animation creator</h1>

View File

@@ -0,0 +1 @@
<h1>General console</h1>

View File

@@ -0,0 +1 @@
<h1>Devices configuration</h1>

View File

@@ -0,0 +1,32 @@
<script>
import { onDestroy } from 'svelte';
import {colors} from '../../stores.js';
let time = new Date()
$: hours = time.getHours().toLocaleString('en-US', { minimumIntegerDigits: 2, useGrouping: false })
$: minutes = time.getMinutes().toLocaleString('en-US', { minimumIntegerDigits: 2, useGrouping: false })
$: seconds = time.getSeconds().toLocaleString('en-US', { minimumIntegerDigits: 2, useGrouping: false })
const interval = setInterval(() => {
time = new Date()
}, 1000);
</script>
<div style='color:{$colors.fourth}'>
<span class="bold">{hours}:{minutes}</span><span>{seconds}</span>
</div>
<style>
div{
float:right;
}
.bold {
font-weight: bold;
font-size: 2em;
}
</style>

View File

@@ -0,0 +1,103 @@
<script lang=ts>
import { createEventDispatcher, onDestroy } from 'svelte';
import {colors} from '../../stores.js';
import Tooltip from './Tooltip.svelte';
export let text = 'Default button';
export let icon = ''
export let tooltip = "Default tooltip"
export let choices = new Map()
export let active = false;
export let style = '';
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
}
// 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
}
</script>
<!-- <Tooltip message={tooltip} show={tooltipShowing}></Tooltip> -->
<div class="container">
<button bind:this={buttonRef}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
on:click={toggleList}
style='color: {$colors.white}; background-color: { active ? $colors.second : $colors.third }; border:none; {style}'><i class='bx { icon}'></i> { text }
</button>
<Tooltip message={tooltip} show={tooltipShowing} position={tooltipPosition}></Tooltip>
<div class="list" style="color: {$colors.white}; display: {listShowing ? "block" : "none"};"
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;
backdrop-filter: blur(20px);
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{
cursor: pointer;
border-radius: 0.5em;
margin: 0;
}
button:hover{
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.25) inset;
}
</style>

View File

@@ -0,0 +1,63 @@
<script>
import { stop_propagation } from 'svelte/internal';
import Tooltip from '../General/Tooltip.svelte';
import { createEventDispatcher } from 'svelte';
export let width = '20px';
export let background = '';
export let icon = '';
export let color = 'white';
export let style = '';
export let interactive = false;
export let message = '';
export let hide = 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
}
// Emit a click event when the button is being clicked
const dispatch = createEventDispatcher();
function click(event){
event.stopPropagation()
dispatch('click')
}
</script>
<div class="container">
<div class="badge" bind:this={buttonRef}
style="opacity: {hide ? 0 : 1}; pointer-events: {hide ? 'none' : 'all'}; width: {width}; height: {width}; color: {color}; background-color: {background}; border-radius: calc({width} / 2); cursor: {interactive ? 'pointer' : ''}; {style}"
on:mousedown={click}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}>
<i class='bx {icon}' style="font-size:100%;"></i>
</div>
{#if message}
<Tooltip message={message} show={tooltipShowing} position={tooltipPosition}></Tooltip>
{/if}
</div>
<style>
.container{
position: relative;
}
.badge {
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
</style>

View File

@@ -0,0 +1,90 @@
<script lang=ts>
import { createEventDispatcher } from 'svelte';
import {colors} from '../../stores.js';
export let label = '';
export let type = 'text';
export let min = undefined;
export let max = undefined;
export let src = undefined;
export let alt = undefined;
export let width = undefined;
export let height = undefined;
export let value = '';
export let placeholder = undefined;
const dispatch = createEventDispatcher();
function handleInput(){
dispatch('input')
}
function handleBlur(event){
dispatch('blur', event)
}
function handleDblClick(){
dispatch('dblclick')
}
</script>
<div style="width: {width}; height: {height};">
<p style="color: {$colors.white};">{label}</p>
<!-- Handle the textarea input -->
{#if type === 'large'}
<textarea style="background-color: {$colors.second}; color: {$colors.white};" placeholder={placeholder} value={value} on:dblclick={handleDblClick} on:input={handleInput} on:blur={handleBlur}/>
<!-- Handle the simple inputs -->
{:else}
<input style="background-color: {$colors.second}; color: {$colors.white};" type={type} min={min} max={max} src={src} alt={alt} value={value} placeholder={placeholder} on:dblclick={handleDblClick} on:input={handleInput} on:blur={handleBlur}/>
{/if}
</div>
<style>
div{
display:inline-block;
}
p{
margin:0;
}
input{
border:none;
border-radius: 0.5em;
text-align: center;
width: 100%;
}
input::selection {
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
/* Pour Firefox */
input::-moz-selection {
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
input:focus {
outline: 1px solid #BBE1FA;
}
textarea{
border:none;
border-radius: 0.5em;
resize: none;
width: 100%;
}
textarea::selection {
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
/* Pour Firefox */
textarea::-moz-selection {
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
textarea:focus {
outline: 1px solid #BBE1FA;
}
</style>

View File

@@ -0,0 +1,62 @@
<script>
import RoundIconButton from './RoundIconButton.svelte';
import Toggle from './Toggle.svelte';
import { createEventDispatcher, onDestroy } from 'svelte';
import {colors} from '../../stores.js';
import { _ } from 'svelte-i18n'
//---Navigation System---//
let menuStates = {
settings: true,
devices: false,
preparation: false,
animation: false,
show: false,
console:false
};
// Handle the click on a navigation button
function handleNavigation(menu) {
emitNavigationEvent(menu);
deselectMenus();
menuStates[menu] = true;
}
// Deselect all menus from the navigation bar
function deselectMenus(){
for (const menu in menuStates) {
menuStates[menu] = false;
}
}
// Emit navigation events
const dispatch = createEventDispatcher();
function emitNavigationEvent(menu) {
dispatch('navigationChanged', {
menu: menu
});
}
</script>
<div style="background-color: {$colors.second};">
<RoundIconButton on:mousedown="{() => handleNavigation("settings")}" icon="bx-cog" width="2.5em" tooltip={$_("settingsMenuTooltip")} active={menuStates.settings}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("devices")}" icon="bx-video-plus" width="2.5em" tooltip={$_("devicesMenuTooltip")} active={menuStates.devices}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("preparation")}" icon="bx-layer" width="2.5em" tooltip="{$_("preparationMenuTooltip")}" active={menuStates.preparation}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("animation")}" icon="bx-film" width="2.5em" tooltip="{$_("animationMenuTooltip")}" active={menuStates.animation}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("show")}" icon="bxs-grid" width="2.5em" tooltip="{$_("showMenuTooltip")}" active={menuStates.show}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("console")}" icon="bx-slider" width="2.5em" tooltip="{$_("consoleMenuTooltip")}" active={menuStates.console}></RoundIconButton>
<Toggle icon="bx-shape-square" width="2.5em" height="1.3em" tooltip="{$_("stageRenderingToggleTooltip")}"></Toggle>
<Toggle icon="bx-play" width="2.5em" height="1.3em" tooltip="{$_("showActivationToggleTooltip")}"></Toggle>
</div>
<style>
div {
display: inline-flex;
align-items: center;
padding: 4px;
border-radius: 40px;
gap: 0.3em;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,156 @@
<!-- 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}>
{#if choices.size != 0}
{#each Array.from(choices) as [key, value]}
<div class="item" on:click={() => handleclick({key})}>{value}</div>
{/each}
{:else}
<div class="item"><i>{$_("openProjectEmpty")}</i></div>
{/if}
</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

@@ -0,0 +1,115 @@
<!-- 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
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 emitMouseDown() {
dispatch('mousedown');
}
function emitMouseUp() {
dispatch('mouseup');
}
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:mousedown={emitMouseDown}
on:mouseup={emitMouseUp}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}>
<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>
<style>
.container {
position: relative;
display: inline-block;
}
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;
}
.badge{
position: absolute;
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
transform: translate(200%, -100%);
}
div{
display:inline-block;
}
</style>

View File

@@ -0,0 +1,62 @@
<script lang=ts>
import { createEventDispatcher, onDestroy } from 'svelte';
import {colors} from '../../stores.js';
import Tooltip from './Tooltip.svelte';
export let text = 'Default button';
export let icon = ''
export let tooltip = "Default tooltip"
export let active = false;
export let style = '';
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
}
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function emitClick() {
dispatch('click');
}
function handleBlur(){
dispatch('blur')
}
</script>
<div class="container">
<button bind:this={buttonRef}
on:blur={handleBlur}
on:mousedown={emitClick}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
style='color: {$colors.white}; background-color: { active ? $colors.second : $colors.third }; {style}'><i class='bx { icon}'></i> { text }
</button>
<Tooltip message={tooltip} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>
.container{
position: relative;
display: inline-block;
}
button{
cursor: pointer;
border-radius: 0.5em;
margin: 0;
border:none;
}
button:hover{
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.25) inset;
}
</style>

View File

@@ -0,0 +1,50 @@
<script lang=ts>
import RoundedButton from "./RoundedButton.svelte";
import { onDestroy } from 'svelte';
import {colors} from '../../stores.js';
export let tabs = [];
export let maxWidth = undefined;
export let maxHeight = undefined;
let activeTab = 0;
function setActiveTab(index) {
activeTab = index;
}
</script>
<div class="tabContainer" style="color: {$colors.white};">
<div class="headerContainer">
{#each tabs as tab, index}
<RoundedButton style="margin: 0.1em;" text={tab.title} icon={tab.icon} tooltip={tab.tooltip} active={ (activeTab !== index) ? true : false } on:click={() => setActiveTab(index)}/>
{/each}
</div>
<div class="bodyContainer"
style='background-color: {$colors.first}; max-width: {maxWidth};'>
{#if tabs[activeTab]}
<svelte:component this={tabs[activeTab].component} />
{/if}
</div>
</div>
<style>
.headerContainer{
margin:0;
border-radius: 0.5em;
}
.tabContainer{
margin-top: 1em;
color: white;
}
.headerContainer{
padding: 0.1em;
border-radius: 0.5em;
}
.bodyContainer{
padding: 0.5em;
background-color: red;
border-radius: 0.5em;
}
</style>

View File

@@ -0,0 +1,94 @@
<script lang=ts>
import { time } from 'svelte-i18n';
import {messages, colors} from '../../stores.js';
let timers = {};
function removeToast(id) {
// Supprime un toast par son ID
$messages = $messages.filter(message => message.id !== id);
clearTimeout(timers[id]);
}
$: {
// Ajoute un timer pour supprimer automatiquement les toasts
for (const message of $messages) {
if (!timers[message.id]) {
timers[message.id] = setTimeout(() => removeToast(message.id), 5000);
}
}
}
</script>
<div class="notificationsPanel">
{#each $messages as message}
<div class="toastMessage" style="background-color: {$colors.second}; color: {$colors.white}" on:mouseup={() => removeToast(message.id)}>
<div class="toastIndicator" style="background-color:{(message.type == 'danger') ? $colors.nok : (message.type == 'warning') ? $colors.orange : $colors.third};"></div>
<p><i class='bx {message.icon}'></i> {@html message.text}</p>
</div>
{/each}
</div>
<style>
p{
margin: 0.5em;
}
.toastIndicator{
width: 10px;
border-top-left-radius: 0.5em;
border-bottom-left-radius: 0.5em;
}
.toastMessage:hover {
opacity: 1;
}
.toastMessage{
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
z-index: 150;
display: flex;
opacity: 0.8;
border-radius: 0.5em;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1), /* Ombre principale douce */
0px 1px 3px rgba(0, 0, 0, 0.06); /* Ombre plus subtile */
animation: fade-in 0.3s ease-out, fade-out 0.3s ease-in 4.7s;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 0.9;
transform: translateY(0);
}
}
@keyframes fade-out {
from {
opacity: 0.9;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(20px);
}
}
.notificationsPanel{
max-width: 50em;
align-items: flex-end;
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 1000;
}
</style>

View File

@@ -0,0 +1,113 @@
<!-- Create a toggle button -->
<script lang=ts>
import { createEventDispatcher, onDestroy } from 'svelte';
import {colors} from '../../stores.js';
import Tooltip from './Tooltip.svelte';
import { _ } from 'svelte-i18n'
export let icon = "" // The icon wanted
export let width = "10em" // The button width
export let height = "5em" // The button height
export let tooltip = "Default tooltip" // The description shown in the tooltip
export let checked = false
let tooltipMessage = tooltip
$: cssVarStyles = `--thumb-background:${$colors.second};--thumb-background-selected:${$colors.third};--thumb-color:${$colors.fourth}`;
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function emitClick(event) {
event.preventDefault();
event.target.blur();
dispatch('click', event);
}
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" style="{cssVarStyles}">
<label class="customToggle" bind:this={buttonRef}
on:mousedown={emitClick}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
style="width:{width}; height:{height}; border-radius:{width}; background-color:{$colors.fourth};">
<input type="checkbox" {checked}>
<span class="checkmark" style="width: {height}; height: 100%; border-radius:{height};">
<i class='bx {icon}' style="font-size:{height};"/>
</span>
</label>
<Tooltip message={tooltipMessage} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>
.container {
position: relative;
display: inline-block;
}
div{
display:inline-block;
}
.customToggle {
cursor: pointer;
overflow: hidden;
padding: 0.1em;
}
.customToggle:hover{
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.25) inset;
}
.customToggle input[type="checkbox"] {
opacity: 0;
position: absolute; /* Position absolue pour garder l'élément dans le flux */
cursor: pointer;
}
.customToggle input[type="checkbox"]:checked + .checkmark {
background-color: var(--thumb-background-selected); /* Couleur lorsque la case est cochée */
float: right;
animation: checkmark-slide-in 0.2s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
}
@keyframes checkmark-slide-in {
0% {
transform: translateX(-50px) scale(1);
opacity: 1;
}
50% {
transform: translateX(0) scale(1);
opacity: 1;
}
70% {
transform: translateX(-5px) scale(1);
opacity: 1;
}
100% {
transform: translateX(0) scale(1);
opacity: 1;
}
}
.checkmark {
text-align:center;
float: left;
background-color: var(--thumb-background);
color: var(--thumb-color);
transition: opacity 0.3s, transform 0.3s;
}
</style>

View File

@@ -0,0 +1,46 @@
<script>
export let message = "Default tooltip"
export let show = false
export let position = { top: 0, left: 0 }
export let duration = 3000
import {colors} from '../../stores.js';
import { onDestroy } from 'svelte';
let tooltipTimeout
$:{
if (show) {
tooltipTimeout = setTimeout(() => {
show = false
}, duration)
} else {
clearTimeout(tooltipTimeout)
}
}
</script>
<div class="tooltip {show ? 'visible' : ''}" style="background-color:{$colors.fourth}; top: {position.top}px; left: {position.left}px;">
<p style="color:{$colors.first};">{message}</p>
</div>
<style>
.tooltip {
position: fixed;
border-radius: 0.5em;
white-space: nowrap;
z-index: 100;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
}
.tooltip.visible {
visibility: visible;
opacity: 1;
}
p{
margin:5px;
font-size: 13px;
}
</style>

View File

@@ -0,0 +1 @@
<h1>Show preparation</h1>

View File

@@ -0,0 +1,134 @@
<script lang=ts>
import {colors} from '../../stores.js';
import InfoButton from "../General/InfoButton.svelte";
import { _ } from 'svelte-i18n'
import { createEventDispatcher } from 'svelte';
import Input from "../General/Input.svelte";
export let title = 'Default card';
export let type = '';
export let location = '';
export let line1 = '';
export let line2 = '';
export let removable = false;
export let addable = false;
export let signalizable = false;
export let signalized = false;
export let selected = false;
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')
}
function click(){
dispatch('click')
}
function dblclick(){
dispatch('dblclick')
}
</script>
<div class="card" on:dblclick={dblclick}>
<div class="{selected ? "selected" : "unselected"} {status == "PERIPHERAL_CONNECTING" ? "waiting" : ""}" on:mousedown={click} style="color: {$colors.white};">
<div style="z-index: 1;">
<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 == "PERIPHERAL_DISCONNECTED"}
<h6><b>Disconnected</b></h6>
{:else}
<h6>{line1}</h6>
<h6>{line2}</h6>
{/if}
</div>
<div class="actions">
<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="{$colors.white}" style="margin: 0.2em; display: { removable ? 'flex' : 'none' }" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")}/>
<InfoButton style="transition: background-color 0.3s ease; margin: 0.2em; display: { (status == "PERIPHERAL_ACTIVATED" || status == "PERIPHERAL_DEACTIVATED") ? 'flex' : 'none' }" background={ (signalizable && signalized) ? $colors.orange : (status == "PERIPHERAL_ACTIVATED") ? $colors.ok : (status == "PERIPHERAL_DEACTIVATED") ? $colors.nok : null} icon='bx-pulse' hide={!signalizable}/>
</div>
</div>
</div>
<style>
.unselected:hover{
background: linear-gradient(to bottom right, var(--second-color), var(--third-color));
}
.card{
position: relative;
}
.selected {
background-color: var(--third-color);
position: relative;
margin: 0.2em;
padding: 0.2em 0.3em 0.5em 0.5em;
border-radius: 0.5em;
display: flex;
justify-content: space-between;
text-align: left;
cursor: pointer;
overflow: hidden;
}
.unselected{
background-color: var(--second-color);
position: relative;
margin: 0.2em;
padding: 0.2em 0.3em 0.5em 0.5em;
border-radius: 0.5em;
display: flex;
justify-content: space-between;
text-align: left;
cursor: pointer;
overflow: hidden;
}
.subtitle{
margin-bottom: 0.5em;
}
.actions {
margin-left: 0.2em;
z-index: 2;
}
p{
margin: 0;
}
h6 {
margin: 0;
font-weight: 1;
}
.waiting::before{
content: '';
position: absolute;
background: linear-gradient(var(--first-color), var(--second-color));
width: 100%;
height: 60%;
animation: rotate 3s linear infinite;
}
.waiting::after{
content: '';
position: absolute;
background: var(--second-color);
inset: 2px;
border-radius: 0.2em;
}
/* Définition de l'animation */
@keyframes rotate {
from {
transform: rotate(0deg); /* Début de la rotation */
}
to {
transform: rotate(360deg); /* Fin de la rotation */
}
}
</style>

View File

@@ -0,0 +1,181 @@
<script lang=ts>
import DeviceCard from "./DeviceCard.svelte";
import Input from "../General/Input.svelte";
import { t, _ } from 'svelte-i18n'
import { generateToast, needProjectSave, endpoints, colors } from "../../stores";
import { get } from "svelte/store"
import { UpdateEndpointSettings, GetEndpointSettings, RemoveEndpoint, AddEndpoint } from "../../../wailsjs/go/main/App";
import RoundedButton from "../General/RoundedButton.svelte";
// Create the endpoint to the project
function createEndpoint(endpoint){
// Create the endpoint to the project (backend)
AddEndpoint(endpoint)
.catch((error) => {
console.log("Unable to create the endpoint: " + error)
generateToast('danger', 'bx-error', $_("addEndpointErrorToast"))
})
}
// Add the endpoint to the project
function addEndpoint(endpoint){
// Add the endpoint to the project (backend)
AddEndpoint(endpoint)
.catch((error) => {
console.log("Unable to add the endpoint to the project: " + error)
generateToast('danger', 'bx-error', $_("addEndpointErrorToast"))
})
}
// Remove the endpoint from the project
function removeEndpoint(endpoint) {
// Delete the endpoint from the project (backend)
RemoveEndpoint(endpoint)
.catch((error) => {
console.log("Unable to remove the endpoint from the project: " + error)
generateToast('danger', 'bx-error', $_("removeEndpointErrorToast"))
})
}
// Select the endpoint to edit its settings
let selectedEndpointSN = null
let selectedEndpointSettings = {}
function selectEndpoint(endpoint){
// Load the settings array if the endpoint is detected
if (endpoint.isSaved){
GetEndpointSettings(endpoint.ProtocolName, endpoint.SerialNumber).then((endpointSettings) => {
selectedEndpointSettings = endpointSettings
// Select the current endpoint
selectedEndpointSN = endpoint.SerialNumber
}).catch((error) => {
console.log("Unable to get the endpoint settings: " + error)
generateToast('danger', 'bx-error', $_("getEndpointSettingsErrorToast"))
})
}
}
// Unselect the endpoint if it is disconnected
$: {
Object.entries($endpoints).filter(([serialNumber, endpoint]) => {
if (!endpoint.isDetected && endpoint.isSaved && selectedEndpointSN == serialNumber) {
selectedEndpointSN = null
selectedEndpointSettings = {}
}
});
}
// Get the number of saved endpoints
$: savedEndpointNumber = Object.values($endpoints).filter(endpoint => endpoint.isSaved).length;
// Validate the endpoint settings
function validate(settingName, settingValue){
console.log("Endpoint setting '" + settingName + "' set to '" + settingValue + "'")
// Get the old setting type and convert the new setting to this type
const convert = {
number: Number,
string: String,
boolean: Boolean,
}[typeof(selectedEndpointSettings[settingName])] || (x => x)
selectedEndpointSettings[settingName] = convert(settingValue)
let endpointProtocolName = get(endpoints)[selectedEndpointSN].ProtocolName
UpdateEndpointSettings(endpointProtocolName, selectedEndpointSN, selectedEndpointSettings).then(()=> {
$needProjectSave = true
}).catch((error) => {
console.log("Unable to save the endpoint setting: " + error)
generateToast('danger', 'bx-error', $_("endpointSettingSaveErrorToast"))
})
}
</script>
<div class="hardware">
<div class="libraryPanel">
<div class="availableHardware">
<p style="color: var(--white-color);"><i class='bx bxs-plug'></i> {$_("projectHardwareDetectedLabel")}</p>
</div>
{#each Object.entries($endpoints) as [serialNumber, endpoint]}
{#if endpoint.isDetected}
<DeviceCard on:add={() => addEndpoint(endpoint)} on:dblclick={() => {
if(!endpoint.isSaved)
addEndpoint(endpoint)
}}
status="PERIPHERAL_CONNECTED" title={endpoint.Name} type={endpoint.ProtocolName} location={endpoint.Location ? endpoint.Location : ""} line1={"S/N: " + endpoint.SerialNumber} addable={!endpoint.isSaved}/>
{/if}
{/each}
<div class="availableHardware">
<p style="color: var(--white-color);"><i class='bx bxs-network-chart' ></i> {$_("projectHardwareOthersLabel")}</p>
</div>
<DeviceCard on:add={() => createEndpoint({Name: "OS2L virtual device", ProtocolName: "OS2L", SerialNumber: ""})}
on:dblclick={() => createEndpoint({Name: "OS2L virtual device", ProtocolName: "OS2L", SerialNumber: ""})}
status="PERIPHERAL_CONNECTED" title={"OS2L virtual device"} type={"OS2L"} location={""} addable={true}/>
</div>
<div style="padding: 0.5em; flex:2; width:100%;">
<p style="margin-bottom: 1em;">{$_("projectHardwareSavedLabel")}</p>
<div class="configuredHardware">
{#if savedEndpointNumber > 0}
{#each Object.entries($endpoints) as [serialNumber, endpoint]}
{#if endpoint.isSaved}
<DeviceCard status="{endpoint.status}" on:delete={() => removeEndpoint(endpoint)} on:dblclick={() => removeEndpoint(endpoint)} on:click={() => selectEndpoint(endpoint)}
title={endpoint.Name} type={endpoint.ProtocolName} location={endpoint.Location ? endpoint.Location : ""} line1={endpoint.SerialNumber ? "S/N: " + endpoint.SerialNumber : ""} selected={serialNumber == selectedEndpointSN} removable signalizable signalized={endpoint.eventEmitted}/>
{/if}
{/each}
{:else}
<i>{$_("projectHardwareEmptyLabel")}</i>
{/if}
</div>
<div class='flexSettings'>
{#if Object.keys(selectedEndpointSettings).length > 0}
{#each Object.entries(selectedEndpointSettings) as [settingName, settingValue]}
<div class="endpointSetting">
<Input on:blur={(event) => validate(settingName, event.detail.target.value)} label={$t(settingName)} type="{typeof(settingValue)}" width='100%' value="{settingValue}"/>
</div>
{/each}
{/if}
</div>
</div>
</div>
<style>
.endpointSetting{
margin: 0.5em;
}
.flexSettings{
display: flex;
flex-wrap: wrap;
width: 100%;
overflow-y: auto;
}
p {
margin: 0;
}
.hardware {
display: flex;
width: 100%;
}
.availableHardware {
background-color: var(--second-color);
border-radius: 0.5em;
padding: 0.2em;
max-height: calc(100vh - 300px);
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.libraryPanel {
padding: 0.5em;
/* width: 13em; */
height: calc(100vh - 2*8px - 2*4px - 40px - 1em - 2*0.1em - 2*0.1em - 2*0.4em - 21.6px - 2*0.5em);
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.libraryPanel::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}
.configuredHardware {
display: flex;
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,51 @@
<script lang=ts>
import { set_data_contenteditable } from 'svelte/internal';
import { ChooseAvatarPath, UpdateShowInfo } from '../../../wailsjs/go/main/App.js';
import { showInformation, needProjectSave } from '../../stores.js';
import Input from "../General/Input.svelte";
import RoundedButton from '../General/RoundedButton.svelte';
import { _ } from 'svelte-i18n'
// Choose the avatar path
function chooseAvatar(){
ChooseAvatarPath().then((avatarPath) => {
$showInformation["Avatar"] = avatarPath
UpdateShowInfo($showInformation).then(()=> {
$needProjectSave = true
})
}).catch((error) => {
console.error(`An error occured: ${error}`)
})
}
// Validate the project information
function validate(field, value){
$showInformation[field] = value
console.log($showInformation)
UpdateShowInfo($showInformation).then(()=> {
$needProjectSave = true
})
}
</script>
<div class='flexSettings'>
<div>
<Input on:blur={(event) => validate("Name", event.detail.target.value)} label={$_("projectShowNameLabel")} type='text' value={$showInformation.Name}/>
<Input on:blur={(event) => validate("Date", event.detail.target.value)} label={$_("projectShowDateLabel")} type='datetime-local' value={$showInformation.Date}/>
</div>
<div>
<Input on:dblclick={chooseAvatar} label={$_("projectAvatarLabel")} type='image' alt={$_("projectAvatarLabel")} width='11em' src={$showInformation.Avatar}/>
</div>
</div>
<div>
<Input on:blur={(event) => validate("Comments", event.detail.target.value)} label={$_("projectCommentsLabel")} type='large' width='100%' value={$showInformation.Comments}/>
</div>
<style>
.flexSettings{
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,18 @@
<script lang=ts>
import ProjectPropertiesContent from "./ProjectPropertiesContent.svelte";
import InputsOutputsContent from "./InputsOutputsContent.svelte";
import Tab from "../General/Tab.svelte";
import { _ } from 'svelte-i18n'
const tabs = [
{ title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent },
{ title: $_("projectInputOutputTab"), icon: 'bxs-plug', tooltip: $_("projectInputOutputTooltip"), component: InputsOutputsContent },
];
</script>
<!-- Project tabcontrol -->
<Tab { tabs }/>
<style>
</style>

View File

@@ -0,0 +1 @@
<h1>Show mapping</h1>

66
frontend/src/lang/en.json Normal file
View File

@@ -0,0 +1,66 @@
{
"settingsMenuTooltip": "Project settings",
"devicesMenuTooltip": "Devices configuration",
"preparationMenuTooltip": "Show preparation",
"animationMenuTooltip": "Animation creator",
"showMenuTooltip": "Show mapping",
"consoleMenuTooltip": "General console",
"stageRenderingToggleTooltip": "Show/hide the rendering view",
"showActivationToggleTooltip": "Activate/Deactivate the play mode",
"saveButtonTooltip": "Save the project",
"newProjectString": "New",
"newProjectTooltip": "Create a new project",
"openProjectString": "Open",
"openProjectTooltip": "Open an existing project",
"openProjectEmpty": "No project found",
"unsavedProjectFlag": "unsaved",
"projectPropertiesTab": "Project properties",
"projectPropertiesTooltip": "The project properties",
"projectInputOutputTab": "Hardware",
"projectInputOutputTooltip": "The input/output hardware definition",
"projectShowNameLabel": "Show name",
"projectShowDateLabel": "Show date",
"projectSaveLabel": "Save name",
"projectRenameButton": "Rename",
"projectRenameTooltip": "Rename the project file",
"projectUniversesLabel": "Number of DMX universes",
"projectAvatarLabel": "Show avatar",
"projectAvatarTooltip": "Load a new show avatar",
"projectCommentsLabel": "Comments",
"projectCommentsPlaceholder": "Leave your comments here",
"projectLoadAvatarButton": "Load a new avatar",
"projectHardwareShowLabel" : "My Show",
"projectHardwareInputsLabel": "INPUTS",
"projectHardwareOutputsLabel": "OUTPUTS",
"projectHardwareDeleteTooltip": "Delete",
"projectHardwareAddTooltip": "Add to project",
"projectHardwareNoSelection": "Empty",
"projectHardwareSavedLabel": "Saved in project",
"projectHardwareDetectedLabel": "Detected",
"projectHardwareOthersLabel": "Others",
"projectHardwareEmptyLabel": "No hardware saved for this project",
"endpointArrivalToast": "Endpoint inserted:",
"endpointRemovalToast": "Endpoint removed:",
"projectSavedToast": "The project has been saved",
"projectSaveErrorToast": "Unable to save the project:",
"addEndpointErrorToast": "Unable to add this endpoint to project",
"createEndpointErrorToast": "Unable to create this endpoint",
"removeEndpointErrorToast": "Unable to remove this endpoint from project",
"os2lEndpointCreatedToast": "Your OS2L endpoint has been created",
"os2lEndpointCreateErrorToast": "Unable to create the OS2L endpoint",
"getEndpointSettingsErrorToast": "Unable to get the endpoint settings",
"projectsLoadErrorToast": "Unable to get the projects list",
"projectOpenedToast": "The project was opened:",
"projectOpenErrorToast": "Unable to open the project",
"projectCreatedToast": "The project was created",
"projectCreateErrorToast": "Unable to create the project",
"endpointSettingSaveErrorToast": "Unable to save the endpoint settings",
"os2lIp": "OS2L server IP",
"os2lPort": "OS2L server port"
}

28
frontend/src/main.js Normal file
View File

@@ -0,0 +1,28 @@
import App from './App.svelte';
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
import { _ } from 'svelte-i18n'
import {messages, showInformation, needProjectSave} from './stores.js';
// Load dictionaries
import { addMessages, init } from 'svelte-i18n';
// Import dictionaries
import en from './lang/en.json';
// Add dictionaries to svelte-i18n
addMessages('en', en);
// Initialize svelte-i18n dictionaries
init({
fallbackLocale: 'en',
initialLocale: 'en',
});
// Create the main app
const app = new App({
target: document.body,
});
export default app;

View File

@@ -0,0 +1,225 @@
import { EventsOn, EventsOff } from "../wailsjs/runtime/runtime.js"
import { endpoints, generateToast, needProjectSave, showInformation } from './stores'
import { get } from "svelte/store"
import { _ } from 'svelte-i18n'
// New endpoint has been added to the system
function endpointArrival (endpointInfo){
// If not exists, add it to the map
// isDetected key to true
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
isDetected: true,
},
}})
console.log("Hardware has been added to the system");
generateToast('info', 'bxs-hdd', get(_)("endpointArrivalToast") + ' <b>' + endpointInfo.Name + '</b>')
}
// Endpoint is removed from the system
function endpointRemoval (endpointInfo){
// If not exists, add it to the map
// isDetected key to false
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
isDetected: false,
status: "PERIPHERAL_DISCONNECTED",
},
}})
console.log("Hardware has been removed from the system");
generateToast('warning', 'bxs-hdd', get(_)("endpointRemovalToast") + ' <b>' + endpointInfo.Name + '</b>')
}
// Update endpoint status
function endpointUpdateStatus(endpointInfo, status){
// If not exists, add it to the map
// change status key
endpoints.update((storedEndpoints) => {
console.log(status)
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
status: status,
},
}})
console.log("Hardware status has been updated to " + status);
}
// Load the endpoint in the project
function loadEndpoint (endpointInfo) {
// If not exists, add it to the map
// isSaved key to true
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.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 unloadEndpoint (endpointInfo) {
// If not exists, add it to the map
// isSaved key to false
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.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)
}
// A endpoint event has been emitted
function onEndpointEvent(sn, event) {
// If not exists, add it to the map
// eventEmitted key to true for 0.2 sec
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[sn]: {
...storedEndpoints[sn],
eventEmitted: true
},
}})
setTimeout(() => {
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[sn]: {
...storedEndpoints[sn],
eventEmitted: false
},
}})
}, 200);
}
// When a new device is added
function onDeviceArrival(deviceInfo) {
console.log("New device arrival")
console.log(deviceInfo)
}
// When a new device is removed
function onDeviceRemoval(sn){
console.log("New device removal")
console.log(sn)
}
let initialized = false
export function initRuntimeEvents(){
if (initialized) return
initialized = true
// Handle the event when a new endpoint is detected
EventsOn('PERIPHERAL_ARRIVAL', endpointArrival)
// Handle the event when a endpoint is removed from the system
EventsOn('PERIPHERAL_REMOVAL', endpointRemoval)
// Handle the event when a endpoint status is updated
EventsOn('PERIPHERAL_STATUS', endpointUpdateStatus)
// Handle the event when a new project need to be loaded
EventsOn('LOAD_PROJECT', loadProject)
// Handle a endpoint loaded in the project
EventsOn('PERIPHERAL_LOAD', loadEndpoint)
// Handle a endpoint unloaded from the project
EventsOn('PERIPHERAL_UNLOAD', unloadEndpoint)
// Handle a endpoint event
EventsOn('PERIPHERAL_EVENT_EMITTED', onEndpointEvent)
// Handle a device arrival
EventsOn('DEVICE_ARRIVAL', onDeviceArrival)
// Handle a device removal
EventsOn('DEVICE_REMOVAL', onDeviceRemoval)
}
export function destroyRuntimeEvents(){
if (!initialized) return
initialized = false
// Handle the event when a new endpoint is detected
EventsOff('PERIPHERAL_ARRIVAL')
// Handle the event when a endpoint is removed from the system
EventsOff('PERIPHERAL_REMOVAL')
// Handle the event when a endpoint status is updated
EventsOff('PERIPHERAL_STATUS')
// Handle the event when a new project need to be loaded
EventsOff('LOAD_PROJECT')
// Handle a endpoint loaded in the project
EventsOff('PERIPHERAL_LOAD')
// Handle a endpoint unloaded from the project
EventsOff('PERIPHERAL_UNLOAD')
// Handle a endpoint event
EventsOff('PERIPHERAL_EVENT_EMITTED')
// Handle a device arrival
EventsOff('DEVICE_ARRIVAL')
// Handle a device removal
EventsOff('DEVICE_REMOVAL')
}

49
frontend/src/stores.js Normal file
View File

@@ -0,0 +1,49 @@
import { writable } from 'svelte/store';
// Projects management
export let projectsList = writable([])
export let needProjectSave = writable(true)
// Show settings
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)
})
}
// Application colors
export const colors = writable({
first: "#1B262C",
second: "#0F4C75",
third: "#3282B8",
fourth: "#BBE1FA",
ok: "#2BA646",
nok: "#A6322B",
white: "#FFFFFF",
orange: "#BC9714"
})
// Font sizes defined in the software
export const firstSize = writable("10px")
export const secondSize = writable("14px")
export const thirdSize = writable("20px")
// List of current hardware
export let endpoints = writable({})
// Endpoint structure :
// Name string `yaml:"name"` // Name of the endpoint
// SerialNumber string `yaml:"sn"` // S/N of the endpoint
// ProtocolName string `yaml:"protocol"` // Protocol name of the endpoint
// Settings map[string]interface{} `yaml:"settings"` // Endpoint settings
// isSaved // if the endpoint is saved in the project
// isDetected // if the endpoint is detected by the system
// status // the status of connection
// eventEmitted // if an event has been emitted for this endpoint (disappear after a delay)

79
frontend/src/style.css Normal file
View File

@@ -0,0 +1,79 @@
:root{
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
--first-color: #1B262C;
--second-color: #0F4C75;
--third-color: #3282B8;
--fourth-color: #BBE1FA;
--ok-color: #2BA646;
--nok-color: #A6322B;
--white-color: #FFFFFF;
--orange-color: #BC9714;
}
html, body {
position: relative;
width: 100%;
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
button {
color: #333;
background-color: #f4f4f4;
outline: none;
}
button:disabled {
color: #999;
}
button:not(:disabled):active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}

2
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

7
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,7 @@
import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()]
})

45
go.mod Normal file
View File

@@ -0,0 +1,45 @@
module dmxconnect
go 1.21
toolchain go1.21.3
require (
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.33.0
github.com/wailsapp/wails/v2 v2.9.1
gitlab.com/gomidi/midi v1.23.7
gitlab.com/gomidi/rtmididrv v0.15.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.10 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.5.1 => /home/dev/go/pkg/mod

108
go.sum Normal file
View File

@@ -0,0 +1,108 @@
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlEdc=
github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
gitlab.com/gomidi/midi v1.21.0/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c=
gitlab.com/gomidi/midi v1.23.7 h1:I6qKoIk9s9dcX+pNf0jC+tziCzJFn82bMpuntRkLeik=
gitlab.com/gomidi/midi v1.23.7/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c=
gitlab.com/gomidi/rtmididrv v0.15.0 h1:52Heco8Y3Jjcl4t0yDUVikOxfI8FMF1Zq+qsG++TUeo=
gitlab.com/gomidi/rtmididrv v0.15.0/go.mod h1:p/6IL1LGgj7utcv3wXudsDWiD9spgAdn0O8LDsGIPG0=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,72 @@
package hardware
import (
"context"
"fmt"
"math/rand"
"strings"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// DeviceEvent is triggered when a device event changes
type DeviceEvent string
const (
// DeviceArrival is triggered when a device arrival
DeviceArrival DeviceEvent = "DEVICE_ARRIVAL"
// DeviceRemoval is triggered when a device removal
DeviceRemoval EndpointEvent = "DEVICE_REMOVAL"
)
// MappingInfo is the configuration for each device
type MappingInfo struct {
DeviceInfo struct {
Name string `yaml:"name"`
Manufacturer string `yaml:"manufacturer"`
Type string `yaml:"type"`
} `yaml:"device"`
Features map[string]any `yaml:"features"`
}
// DeviceInfo represents the device data
type DeviceInfo struct {
SerialNumber string // The device s/n
Name string // The device name
Manufacturer string // The device manufacturer
Version string // The device version
}
// Device represents the logical to be controlled
type Device struct {
DeviceInfo DeviceInfo // The device base information
Endpoint Endpoint // The device endpoint which control this device
}
// AddDevice adds a new device to the manager
func (h *Manager) AddDevice(ctx context.Context, device DeviceInfo, endpoint Endpoint) error {
// If the SerialNumber is empty, generate another one
if device.SerialNumber == "" {
device.SerialNumber = strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32)))
}
// Add or replace the device to the manager
h.devices[device.SerialNumber] = &Device{device, endpoint}
// Send the event to the front
runtime.EventsEmit(ctx, string(DeviceArrival), device)
return nil
}
// RemoveDevice removes a device from the manager
func (h *Manager) RemoveDevice(ctx context.Context, serialNumber string) error {
// Delete the device from the manager
if serialNumber == "" {
return fmt.Errorf("the device s/n is empty")
}
delete(h.devices, serialNumber)
// Send the event to the front
runtime.EventsEmit(ctx, string(DeviceRemoval), serialNumber)
return nil
}

View File

@@ -0,0 +1,221 @@
package hardware
import (
"context"
"fmt"
"github.com/rs/zerolog/log"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// EndpointEvent is trigger by the providers when the scan is complete
type EndpointEvent string
// EndpointStatus is the endpoint status (DISCONNECTED => CONNECTING => DEACTIVATED => ACTIVATED)
type EndpointStatus string
const (
// EndpointArrival is triggerd when a endpoint has been connected to the system
EndpointArrival EndpointEvent = "PERIPHERAL_ARRIVAL"
// EndpointRemoval is triggered when a endpoint has been disconnected from the system
EndpointRemoval EndpointEvent = "PERIPHERAL_REMOVAL"
// EndpointLoad is triggered when a endpoint is added to the project
EndpointLoad EndpointEvent = "PERIPHERAL_LOAD"
// EndpointUnload is triggered when a endpoint is removed from the project
EndpointUnload EndpointEvent = "PERIPHERAL_UNLOAD"
// EndpointStatusUpdated is triggered when a endpoint status has been updated (disconnected - connecting - deactivated - activated)
EndpointStatusUpdated EndpointEvent = "PERIPHERAL_STATUS"
// EndpointEventEmitted is triggered when a endpoint event is emitted
EndpointEventEmitted EndpointEvent = "PERIPHERAL_EVENT_EMITTED"
// EndpointStatusDisconnected : endpoint is now disconnected
EndpointStatusDisconnected EndpointStatus = "PERIPHERAL_DISCONNECTED"
// EndpointStatusConnecting : endpoint is now connecting
EndpointStatusConnecting EndpointStatus = "PERIPHERAL_CONNECTING"
// EndpointStatusDeactivated : endpoint is now deactivated
EndpointStatusDeactivated EndpointStatus = "PERIPHERAL_DEACTIVATED"
// EndpointStatusActivated : endpoint is now activated
EndpointStatusActivated EndpointStatus = "PERIPHERAL_ACTIVATED"
)
// Endpoint represents the methods used to manage a endpoint (input or output hardware)
type Endpoint interface {
Connect(context.Context) error // Connect the endpoint
// SetEventCallback(func(any)) // Callback is called when an event is emitted from the endpoint
Disconnect(context.Context) error // Disconnect the endpoint
Activate(context.Context) error // Activate the endpoint
Deactivate(context.Context) error // Deactivate the endpoint
SetDeviceArrivalCallback(func(context.Context, DeviceInfo, Endpoint) error) // Set the callback function when a new device is detected by the endpoint
SetDeviceRemovalCallback(func(context.Context, string) error) // Set the callback function when a device is not detected anymore by the endpoint
GetSettings() map[string]any // Get the endpoint settings
SetSettings(context.Context, map[string]any) error // Set a endpoint setting
SetDeviceProperty(context.Context, uint32, byte) error // Update a device property
WaitStop() error // Properly close the endpoint
GetInfo() EndpointInfo // Get the endpoint information
}
// EndpointInfo represents a endpoint information
type EndpointInfo struct {
Name string `yaml:"name"` // Name of the endpoint
SerialNumber string `yaml:"sn"` // S/N of the endpoint
ProtocolName string `yaml:"protocol"` // Protocol name of the endpoint
Settings map[string]any `yaml:"settings"` // Endpoint settings
}
// RegisterEndpoint registers a new endpoint
func (h *Manager) RegisterEndpoint(ctx context.Context, endpointInfo EndpointInfo) (string, error) {
h.mu.Lock()
defer h.mu.Unlock()
// Create the endpoint from its provider (if needed)
if provider, found := h.providers[endpointInfo.ProtocolName]; found {
var err error
endpointInfo, err = provider.Create(ctx, endpointInfo)
if err != nil {
return "", err
}
}
// Do not save if the endpoint doesn't have a S/N
if endpointInfo.SerialNumber == "" {
return "", fmt.Errorf("serial number is empty for this endpoint")
}
h.SavedEndpoints[endpointInfo.SerialNumber] = endpointInfo
runtime.EventsEmit(ctx, string(EndpointStatusUpdated), endpointInfo, EndpointStatusDisconnected)
// If already detected, connect it
if endpoint, ok := h.DetectedEndpoints[endpointInfo.SerialNumber]; ok {
h.wg.Add(1)
go func() {
defer h.wg.Done()
err := endpoint.Connect(ctx)
if err != nil {
log.Err(err).Str("file", "FTDIProvider").Str("endpointSN", endpointInfo.SerialNumber).Msg("unable to connect the endpoint")
return
}
// Endpoint connected, activate it
err = endpoint.Activate(ctx)
if err != nil {
log.Err(err).Str("file", "FTDIProvider").Str("endpointSN", endpointInfo.SerialNumber).Msg("unable to activate the FTDI endpoint")
return
}
}()
}
// Emits the event in the hardware
runtime.EventsEmit(ctx, string(EndpointLoad), endpointInfo)
return endpointInfo.SerialNumber, nil
}
// UnregisterEndpoint unregisters an existing endpoint
func (h *Manager) UnregisterEndpoint(ctx context.Context, endpointInfo EndpointInfo) error {
h.mu.Lock()
defer h.mu.Unlock()
if endpoint, detected := h.DetectedEndpoints[endpointInfo.SerialNumber]; detected {
// Deactivating endpoint
err := endpoint.Deactivate(ctx)
if err != nil {
log.Err(err).Str("sn", endpointInfo.SerialNumber).Msg("unable to deactivate the endpoint")
return nil
}
// Disconnecting endpoint
err = endpoint.Disconnect(ctx)
if err != nil {
log.Err(err).Str("sn", endpointInfo.SerialNumber).Msg("unable to disconnect the endpoint")
return nil
}
// Remove the endpoint from its provider (if needed)
if provider, found := h.providers[endpointInfo.ProtocolName]; found {
err = provider.Remove(ctx, endpoint)
if err != nil {
return err
}
}
}
delete(h.SavedEndpoints, endpointInfo.SerialNumber)
runtime.EventsEmit(ctx, string(EndpointUnload), endpointInfo)
return nil
}
// GetEndpointSettings gets the endpoint settings
func (h *Manager) GetEndpointSettings(endpointSN string) (map[string]any, error) {
// Return the specified endpoint
endpoint, found := h.DetectedEndpoints[endpointSN]
if !found {
// Endpoint not detected, return the last settings saved
if savedEndpoint, isFound := h.SavedEndpoints[endpointSN]; isFound {
return savedEndpoint.Settings, nil
}
return nil, fmt.Errorf("unable to found the endpoint")
}
return endpoint.GetSettings(), nil
}
// SetEndpointSettings sets the endpoint settings
func (h *Manager) SetEndpointSettings(ctx context.Context, endpointSN string, settings map[string]any) error {
endpoint, found := h.DetectedEndpoints[endpointSN]
if !found {
return fmt.Errorf("unable to found the FTDI endpoint")
}
return endpoint.SetSettings(ctx, settings)
}
// OnEndpointArrival is called when a endpoint arrives in the system
func (h *Manager) OnEndpointArrival(ctx context.Context, endpoint Endpoint) {
// Add the endpoint to the detected hardware
h.DetectedEndpoints[endpoint.GetInfo().SerialNumber] = endpoint
// Specify the callback functions to manages devices
endpoint.SetDeviceArrivalCallback(h.AddDevice)
endpoint.SetDeviceRemovalCallback(h.RemoveDevice)
// If the endpoint is saved in the project, connect it
if _, saved := h.SavedEndpoints[endpoint.GetInfo().SerialNumber]; saved {
h.wg.Add(1)
go func(p Endpoint) {
defer h.wg.Done()
err := p.Connect(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to connect the FTDI endpoint")
return
}
err = p.Activate(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to activate the FTDI endpoint")
return
}
}(endpoint)
}
// TODO: Update the Endpoint reference in the corresponding devices
runtime.EventsEmit(ctx, string(EndpointArrival), endpoint.GetInfo())
}
// OnEndpointRemoval is called when a endpoint exits the system
func (h *Manager) OnEndpointRemoval(ctx context.Context, endpoint Endpoint) {
// Properly deactivating and disconnecting the endpoint
h.wg.Add(1)
go func(p Endpoint) {
defer h.wg.Done()
err := p.Deactivate(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to deactivate endpoint after disconnection")
}
err = p.Disconnect(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to disconnect the endpoint after disconnection")
}
}(endpoint)
// Remove the endpoint from the hardware
delete(h.DetectedEndpoints, endpoint.GetInfo().SerialNumber)
// TODO: Update the Endpoint reference in the corresponding devices
runtime.EventsEmit(ctx, string(EndpointRemoval), endpoint.GetInfo())
}

View File

@@ -0,0 +1,180 @@
package genericftdi
import (
"context"
"dmxconnect/hardware"
"sync"
"unsafe"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
/*
#include <stdlib.h>
#cgo LDFLAGS: -L${SRCDIR}/../../build/bin -ldmxSender
#include "cpp/include/dmxSenderBridge.h"
*/
import "C"
// Endpoint contains the data of an FTDI endpoint
type Endpoint struct {
wg sync.WaitGroup
info hardware.EndpointInfo // The endpoint basic data
dmxSender unsafe.Pointer // The command object for piloting the DMX ouptut
}
// NewEndpoint creates a new FTDI endpoint
func NewEndpoint(info hardware.EndpointInfo) *Endpoint {
log.Info().Str("file", "FTDIEndpoint").Str("name", info.Name).Str("s/n", info.SerialNumber).Msg("FTDI endpoint created")
return &Endpoint{
info: info,
dmxSender: nil,
}
}
// SetDeviceArrivalCallback is called when we need to add a new device to the hardware
func (p *Endpoint) SetDeviceArrivalCallback(adc func(context.Context, hardware.DeviceInfo, hardware.Endpoint) error) {
}
// SetDeviceRemovalCallback is called when we need to remove a device from the hardware
func (p *Endpoint) SetDeviceRemovalCallback(rdc func(context.Context, string) error) {
}
// Connect connects the FTDI endpoint
func (p *Endpoint) Connect(ctx context.Context) error {
// Check if the device has already been created
if p.dmxSender != nil {
return errors.Errorf("the DMX device has already been created!")
}
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusConnecting)
// Create the DMX sender
p.dmxSender = C.dmx_create()
// Connect the FTDI
serialNumber := C.CString(p.info.SerialNumber)
defer C.free(unsafe.Pointer(serialNumber))
if C.dmx_connect(p.dmxSender, serialNumber) != C.DMX_OK {
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDisconnected)
log.Error().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("unable to connect the DMX device")
return errors.Errorf("unable to connect '%s'", p.info.SerialNumber)
}
p.wg.Add(1)
go func() {
defer p.wg.Done()
<-ctx.Done()
_ = p.Disconnect(ctx)
}()
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDeactivated)
log.Trace().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("DMX device connected successfully")
return nil
}
// Disconnect disconnects the FTDI endpoint
func (p *Endpoint) Disconnect(ctx context.Context) error {
// Check if the device has already been created
if p.dmxSender == nil {
return errors.Errorf("the DMX device has not been connected!")
}
// Destroy the dmx sender
C.dmx_destroy(p.dmxSender)
// Reset the pointer to the endpoint
p.dmxSender = nil
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDisconnected)
return nil
}
// Activate activates the FTDI endpoint
func (p *Endpoint) Activate(ctx context.Context) error {
// Check if the device has already been created
if p.dmxSender == nil {
return errors.Errorf("the DMX sender has not been created!")
}
log.Trace().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("activating FTDI endpoint...")
err := C.dmx_activate(p.dmxSender)
if err != C.DMX_OK {
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(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusActivated)
log.Trace().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("DMX device activated successfully")
return nil
}
// Deactivate deactivates the FTDI endpoint
func (p *Endpoint) Deactivate(ctx context.Context) error {
// Check if the device has already been created
if p.dmxSender == nil {
return errors.Errorf("the DMX device has not been created!")
}
log.Trace().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("deactivating FTDI endpoint...")
err := C.dmx_deactivate(p.dmxSender)
if err != C.DMX_OK {
return errors.Errorf("unable to deactivate the DMX sender!")
}
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDeactivated)
log.Trace().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("DMX device deactivated successfully")
return nil
}
// SetSettings sets a specific setting for this endpoint
func (p *Endpoint) SetSettings(ctx context.Context, settings map[string]any) error {
return errors.Errorf("unable to set the settings: not implemented")
}
// SetDeviceProperty sends a command to the specified device
func (p *Endpoint) SetDeviceProperty(ctx context.Context, channelNumber uint32, channelValue byte) error {
// Check if the device has already been created
if p.dmxSender == nil {
return errors.Errorf("the DMX device has not been created!")
}
log.Trace().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("setting device property on FTDI endpoint...")
err := C.dmx_setValue(p.dmxSender, C.uint16_t(channelNumber), C.uint8_t(channelValue))
if err != C.DMX_OK {
return errors.Errorf("unable to update the channel value!")
}
log.Trace().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("device property set on FTDI endpoint successfully")
return nil
}
// GetSettings gets the endpoint settings
func (p *Endpoint) GetSettings() map[string]interface{} {
return map[string]interface{}{}
}
// GetInfo gets all the endpoint information
func (p *Endpoint) GetInfo() hardware.EndpointInfo {
return p.info
}
// WaitStop wait about the endpoint to close
func (p *Endpoint) WaitStop() error {
log.Info().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("waiting for FTDI endpoint to close...")
p.wg.Wait()
log.Info().Str("file", "FTDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("FTDI endpoint closed!")
return nil
}

View File

@@ -0,0 +1,181 @@
package genericftdi
import (
"context"
"dmxconnect/hardware"
"fmt"
goRuntime "runtime"
"sync"
"time"
"unsafe"
"github.com/rs/zerolog/log"
)
/*
#include <stdlib.h>
#cgo LDFLAGS: -L${SRCDIR}/../../build/bin -ldetectFTDI
#include "cpp/include/detectFTDIBridge.h"
*/
import "C"
// Provider manages all the FTDI endpoints
type Provider struct {
wg sync.WaitGroup
mu sync.Mutex
detected map[string]*Endpoint // Detected endpoints
scanEvery time.Duration // Scans endpoints periodically
onArrival func(context.Context, hardware.Endpoint) // When a endpoint arrives
onRemoval func(context.Context, hardware.Endpoint) // When a endpoint goes away
}
// NewProvider creates a new FTDI provider
func NewProvider(scanEvery time.Duration) *Provider {
log.Trace().Str("file", "FTDIProvider").Msg("FTDI provider created")
return &Provider{
scanEvery: scanEvery,
detected: make(map[string]*Endpoint),
}
}
// OnArrival is the callback function when a new endpoint arrives
func (f *Provider) OnArrival(cb func(context.Context, hardware.Endpoint)) {
f.onArrival = cb
}
// OnRemoval i the callback when a endpoint goes away
func (f *Provider) OnRemoval(cb func(context.Context, hardware.Endpoint)) {
f.onRemoval = cb
}
// Initialize initializes the FTDI provider
func (f *Provider) Initialize() error {
// Check platform
if goRuntime.GOOS != "windows" {
log.Error().Str("file", "FTDIProvider").Str("platform", goRuntime.GOOS).Msg("FTDI provider not compatible with your platform")
return fmt.Errorf("the FTDI provider is not compatible with your platform yet (%s)", goRuntime.GOOS)
}
log.Trace().Str("file", "FTDIProvider").Msg("FTDI provider initialized")
return nil
}
// Create creates a new endpoint, based on the endpoint information (manually created)
func (f *Provider) Create(ctx context.Context, endpointInfo hardware.EndpointInfo) (hardware.EndpointInfo, error) {
return hardware.EndpointInfo{}, nil
}
// Remove removes an existing endpoint (manually created)
func (f *Provider) Remove(ctx context.Context, endpoint hardware.Endpoint) error {
return nil
}
// Start starts the provider and search for endpoints
func (f *Provider) 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 <-ticker.C:
// Scan the endpoints
err := f.scanEndpoints(ctx)
if err != nil {
log.Err(err).Str("file", "FTDIProvider").Msg("unable to scan FTDI endpoints")
}
}
}
}()
return nil
}
// GetName returns the name of the driver
func (f *Provider) GetName() string {
return "FTDI"
}
// scanEndpoints scans the FTDI endpoints
func (f *Provider) scanEndpoints(ctx context.Context) error {
log.Trace().Str("file", "FTDIProvider").Msg("FTDI scan triggered")
count := int(C.get_endpoints_number())
log.Info().Int("number", count).Msg("number of FTDI devices connected")
// Allocating C array
size := C.size_t(count) * C.size_t(unsafe.Sizeof(C.FTDIEndpointC{}))
devicesPtr := C.malloc(size)
defer C.free(devicesPtr)
devices := (*[1 << 20]C.FTDIEndpointC)(devicesPtr)[:count:count]
C.get_ftdi_devices((*C.FTDIEndpointC)(devicesPtr), C.int(count))
currentMap := make(map[string]hardware.EndpointInfo)
for i := 0; i < count; i++ {
d := devices[i]
sn := C.GoString(d.serialNumber)
desc := C.GoString(d.description)
// isOpen := d.isOpen != 0
currentMap[sn] = hardware.EndpointInfo{
SerialNumber: sn,
Name: desc,
// IsOpen: isOpen,
ProtocolName: "FTDI",
}
// Free C memory
C.free_ftdi_device(&d)
}
log.Info().Any("endpoints", currentMap).Msg("available FTDI endpoints")
// Detect arrivals
for sn, endpointData := range currentMap {
// If the scanned endpoint isn't in the detected list, create it
if _, known := f.detected[sn]; !known {
endpoint := NewEndpoint(endpointData)
if f.onArrival != nil {
f.onArrival(ctx, endpoint)
}
}
}
// Detect removals
for detectedSN, detectedEndpoint := range f.detected {
if _, still := currentMap[detectedSN]; !still {
// Delete it from the detected list
delete(f.detected, detectedSN)
// Execute the removal callback
if f.onRemoval != nil {
f.onRemoval(ctx, detectedEndpoint)
}
}
}
return nil
}
// WaitStop stops the provider
func (f *Provider) WaitStop() error {
log.Trace().Str("file", "FTDIProvider").Msg("stopping the FTDI provider...")
// Wait for goroutines to stop
f.wg.Wait()
log.Trace().Str("file", "FTDIProvider").Msg("FTDI provider stopped")
return nil
}

View File

@@ -0,0 +1,7 @@
@REM Compiling DETECTFTDI library
g++ -shared -o ../../../build/bin/libdetectFTDI.dll src/detectFTDI.cpp -fPIC -Wl,--out-implib,../../../build/bin/libdetectFTDI.dll.a -L"lib" -lftd2xx -mwindows
@REM Compiling DMXSENDER library
g++ -shared -o ../../../build/bin/libdmxSender.dll src/dmxSender.cpp -fPIC -Wl,--out-implib,../../../build/bin/libdmxSender.dll.a -L"lib" -lftd2xx -mwindows
@REM g++ -shared -o libdmxSender.so dmxSender.cpp -fPIC -I"include" -L"lib" -lftd2xx -mwindows

View File

@@ -0,0 +1,19 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char* serialNumber;
char* description;
int isOpen;
} FTDIEndpointC;
int get_endpoints_number();
void get_ftdi_devices(FTDIEndpointC* devices, int count);
void free_ftdi_device(FTDIEndpointC* device);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,30 @@
// Declare the C++ function from the shared library
#include <stdint.h>
typedef enum {
DMX_OK,
DMX_CHANNEL_TOO_LOW_ERROR,
DMX_CHANNEL_TOO_HIGH_ERROR,
DMX_VALUE_TOO_LOW_ERROR,
DMX_VALUE_TOO_HIGH_ERROR,
DMX_OPEN_ERROR,
DMX_SET_BAUDRATE_ERROR,
DMX_SET_DATA_CHARACTERISTICS_ERROR,
DMX_SET_FLOW_ERROR,
DMX_UNKNOWN_ERROR
} DMXError;
typedef void DMXDevice;
extern DMXDevice* dmx_create();
extern void* dmx_destroy(DMXDevice* dev);
extern DMXError dmx_connect(DMXDevice* dev, char* serialNumber);
extern DMXError dmx_activate(DMXDevice* dev);
extern DMXError dmx_deactivate(DMXDevice* dev);
extern DMXError dmx_setValue(DMXDevice* dev, uint16_t channel, uint8_t value);

Binary file not shown.

View File

@@ -0,0 +1,91 @@
#include "../include/detectFTDIBridge.h"
#include "detectFTDI.h"
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <iostream>
int getFTDIEndpointsNumber() {
DWORD numDevs = 0;
if (FT_CreateDeviceInfoList(&numDevs) != FT_OK) {
std::cerr << "Unable to get FTDI devices: create list error\n";
}
return numDevs;
}
std::vector<FTDIEndpoint> scanFTDIEndpoints() {
DWORD numDevs = 0;
if (FT_CreateDeviceInfoList(&numDevs) != FT_OK) {
std::cerr << "Unable to get FTDI devices: create list error\n";
return {};
}
if (numDevs == 0) {
return {};
}
std::vector<FT_DEVICE_LIST_INFO_NODE> devInfo(numDevs);
if (FT_GetDeviceInfoList(devInfo.data(), &numDevs) != FT_OK) {
std::cerr << "Unable to get FTDI devices: get list error\n";
return {};
}
std::vector<FTDIEndpoint> endpoints;
endpoints.reserve(numDevs);
for (const auto& info : devInfo) {
if (info.SerialNumber[0] != '\0') {
endpoints.push_back({
info.SerialNumber,
info.Description,
static_cast<bool>(info.Flags & FT_FLAGS_OPENED)
});
}
}
return endpoints;
}
extern "C" {
int get_endpoints_number() {
return getFTDIEndpointsNumber();
}
void get_ftdi_devices(FTDIEndpointC* devices, int count) {
if (!devices || count <= 0) {
return;
}
auto list = scanFTDIEndpoints();
int n = std::min(count, static_cast<int>(list.size()));
for (int i = 0; i < n; ++i) {
const auto& src = list[i];
auto& dst = devices[i];
dst.serialNumber = static_cast<char*>(std::malloc(src.serialNumber.size() + 1));
std::strcpy(dst.serialNumber, src.serialNumber.c_str());
dst.description = static_cast<char*>(std::malloc(src.description.size() + 1));
std::strcpy(dst.description, src.description.c_str());
dst.isOpen = src.isOpen ? 1 : 0;
}
}
void free_ftdi_device(FTDIEndpointC* device) {
if (!device) return;
if (device->serialNumber) {
std::free(device->serialNumber);
device->serialNumber = nullptr;
}
if (device->description) {
std::free(device->description);
device->description = nullptr;
}
}
} // extern "C"

View File

@@ -0,0 +1,14 @@
#pragma once
#include "ftd2xx.h"
#include <string>
#include <vector>
struct FTDIEndpoint {
std::string serialNumber;
std::string description;
bool isOpen;
};
int getFTDIEndpointsNumber();
std::vector<FTDIEndpoint> scanFTDIEndpoints();

View File

@@ -0,0 +1,225 @@
//dmxSender.cpp
#include "dmxSender.h"
#include <iostream>
#define DMX_START_CODE 0x00
#define BREAK_DURATION_US 110
#define MAB_DURATION_US 16
#define DMX_CHANNELS 512
#define FREQUENCY 44
#define INTERVAL (1000000 / FREQUENCY)
// Initialize default values for starting the DMX device
DMXDevice::DMXDevice(){
std::cout << " [DMXSENDER] " << "Creating a new DMXDevice..." << std::endl;
ftHandle = nullptr;
isOutputActivated = false;
resetChannels();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << " [DMXSENDER] " << "DMXDevice created!" << std::endl;
}
// Properly close the DMX device
DMXDevice::~DMXDevice(){
std::cout << " [DMXSENDER] " << "Removing the DMXDevice..." << std::endl;
std::cout << " [DMXSENDER] " << "Deactivating the DMXDevice..." << std::endl;
deactivate();
std::cout << " [DMXSENDER] " << "DMXDevice deactivated!" << std::endl;
if (ftHandle != nullptr){
std::cout << " [DMXSENDER] " << "ftHandle not null, closing it..." << std::endl;
FT_Close(ftHandle);
std::cout << " [DMXSENDER] " << "FT_HANDLE closed!" << std::endl;
ftHandle = nullptr;
}
std::cout << " [DMXSENDER] " << "DMXDevice removed!" << std::endl;
}
// Connect the device on a specific port
DMXError DMXDevice::connect(char* serialNumber){
std::cout << " [DMXSENDER] " << "Connecting the DMXDevice..." << std::endl;
ftStatus = FT_OpenEx((PVOID)serialNumber, FT_OPEN_BY_SERIAL_NUMBER, &ftHandle);
if (ftStatus != FT_OK) {
std::cout << " [DMXSENDER] " << "Error when connecting the DMXDevice..." << std::endl;
return DMX_OPEN_ERROR;
}
std::cout << " [DMXSENDER] " << "DMXDevice connected, setting up..." << std::endl;
ftStatus = FT_SetBaudRate(ftHandle, 250000);
if (ftStatus != FT_OK) {
std::cout << " [DMXSENDER] " << "Error when setting the baudrate..." << std::endl;
FT_Close(ftHandle);
return DMX_SET_BAUDRATE_ERROR;
}
ftStatus |= FT_SetDataCharacteristics(ftHandle, 8, FT_STOP_BITS_2, FT_PARITY_NONE); // 8 bits, no parity, 1 stop bit
if (ftStatus != FT_OK) {
std::cout << " [DMXSENDER] " << "Error when setting the data characteristics..." << std::endl;
FT_Close(ftHandle);
return DMX_SET_DATA_CHARACTERISTICS_ERROR;
}
ftStatus |= FT_SetFlowControl(ftHandle, FT_FLOW_NONE, 0, 0);
if (ftStatus != FT_OK) {
std::cout << " [DMXSENDER] " << "Error when trying to set up the flow control..." << std::endl;
FT_Close(ftHandle);
return DMX_SET_FLOW_ERROR;
}
std::cout << " [DMXSENDER] " << "DMXDevice connected!" << std::endl;
return DMX_OK;
}
// Activate the DMX flow
DMXError DMXDevice::activate(){
std::cout << " [DMXSENDER] " << "Activating the DMXDevice..." << std::endl;
isOutputActivated.store(true);
// Send the DMX frames
std::thread updateThread([this]() {
this->sendDMX(ftHandle);
});
updateThread.detach();
std::cout << " [DMXSENDER] " << "DMXDevice activated!" << std::endl;
return DMX_OK;
}
// Deactivate the DMX flow
DMXError DMXDevice::deactivate(){
std::cout << " [DMXSENDER] " << "Deactivating the DMXDevice..." << std::endl;
std::cout << " [DMXSENDER] " << "Resetting channels..." << std::endl;
resetChannels();
std::cout << " [DMXSENDER] " << "Channels resetted!" << std::endl;
isOutputActivated.store(false);
std::cout << " [DMXSENDER] " << "DMXDevice deactivated!" << std::endl;
return DMX_OK;
}
// Set the value of a DMX channel
DMXError DMXDevice::setValue(uint16_t channel, uint8_t value){
std::cout << " [DMXSENDER] " << "Setting a channel value..." << std::endl;
if (channel < 1) {
std::cout << " [DMXSENDER] " << "Unable to set channel value: channel number too low!" << std::endl;
return DMX_CHANNEL_TOO_LOW_ERROR;
}
if (channel > 512) {
std::cout << " [DMXSENDER] " << "Unable to set channel value: channel number too high!" << std::endl;
return DMX_CHANNEL_TOO_HIGH_ERROR;
}
if(value < 0) {
std::cout << " [DMXSENDER] " << "Unable to set channel value: channel value too low!" << std::endl;
return DMX_VALUE_TOO_LOW_ERROR;
}
if(value > 255) {
std::cout << " [DMXSENDER] " << "Unable to set channel value: channel value too high!" << std::endl;
return DMX_VALUE_TOO_HIGH_ERROR;
}
dmxData[channel].store(value);
std::cout << " [DMXSENDER] " << "Channel value set!" << std::endl;
return DMX_OK;
}
// Send a break line
FT_STATUS DMXDevice::sendBreak(FT_HANDLE ftHandle) {
ftStatus = FT_SetBreakOn(ftHandle); // Set BREAK ON
if (ftStatus != FT_OK) {
std::cout << " [DMXSENDER] " << "Unable to put break signal ON!" << std::endl;
return ftStatus;
}
std::this_thread::sleep_for(std::chrono::microseconds(BREAK_DURATION_US));
ftStatus = FT_SetBreakOff(ftHandle); // Set BREAK OFF
if (ftStatus != FT_OK) {
std::cout << " [DMXSENDER] " << "Unable to put break signal OFF!" << std::endl;
return ftStatus;
}
return ftStatus;
}
// Continuously send the DMX frame
void DMXDevice::sendDMX(FT_HANDLE ftHandle) {
while (isOutputActivated) {
// Send the BREAK
ftStatus = sendBreak(ftHandle);
if (ftStatus != FT_OK) {
std::cout << " [DMXSENDER] " << "Unable to send break signal! Deactivating output..." << std::endl;
deactivate();
continue;
}
// Send the MAB
std::this_thread::sleep_for(std::chrono::microseconds(MAB_DURATION_US));
DWORD bytesWritten = 0;
// Send the DMX frame
ftStatus = FT_Write(ftHandle, dmxData, DMX_CHANNELS, &bytesWritten);
if (ftStatus != FT_OK || bytesWritten != DMX_CHANNELS) { // Error detected when trying to send the frame. Deactivate the line.
std::cout << " [DMXSENDER] " << "Error when trying to send the DMX frame! Deactivating output..." << std::endl;
deactivate();
continue;
}
// Wait before sending the next frame
std::this_thread::sleep_for(std::chrono::microseconds(INTERVAL - BREAK_DURATION_US - MAB_DURATION_US));
}
}
// Resetting the DMX channels
void DMXDevice::resetChannels(){
for (auto &v : dmxData) {
v.store(0);
}
dmxData[0].store(DMX_START_CODE);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Linkable functions from Golang
extern "C" {
// Create a new DMX device
DMXDevice* dmx_create() {
return new DMXDevice();
}
// Destroy a DMX device
void dmx_destroy(DMXDevice* dev) {
dev->~DMXDevice();
}
// Connect a DMX device
DMXError dmx_connect(DMXDevice* dev, char* serialNumber) {
try{
return dev->connect(serialNumber);
} catch (...) {
return DMX_UNKNOWN_ERROR;
}
}
// Activate a DMX device
DMXError dmx_activate(DMXDevice* dev) {
try{
return dev->activate();
} catch (...) {
return DMX_UNKNOWN_ERROR;
}
}
// Deactivate a DMX device
DMXError dmx_deactivate(DMXDevice* dev) {
try{
return dev->activate();
} catch (...) {
return DMX_UNKNOWN_ERROR;
}
}
// Set the channel value of a DMX device
DMXError dmx_setValue(DMXDevice* dev, int channel, int value) {
try {
return dev->setValue(channel, value);
} catch (...) {
return DMX_UNKNOWN_ERROR;
}
}
}

View File

@@ -0,0 +1,65 @@
// dmxSender.h
#pragma once
#include <vector>
#include <cstdint>
#include <atomic>
#include <thread>
#include "ftd2xx.h"
#define DMX_START_CODE 0x00
#define BREAK_DURATION_US 110
#define MAB_DURATION_US 16
#define DMX_CHANNELS 512
#define FREQUENCY 44
#define INTERVAL (1000000 / FREQUENCY)
typedef enum {
DMX_OK,
DMX_CHANNEL_TOO_LOW_ERROR,
DMX_CHANNEL_TOO_HIGH_ERROR,
DMX_VALUE_TOO_LOW_ERROR,
DMX_VALUE_TOO_HIGH_ERROR,
DMX_OPEN_ERROR,
DMX_SET_BAUDRATE_ERROR,
DMX_SET_DATA_CHARACTERISTICS_ERROR,
DMX_SET_FLOW_ERROR,
DMX_UNKNOWN_ERROR
} DMXError;
class DMXDevice {
public:
// Initialize default values for starting the DMX device
DMXDevice();
// Properly close the DMX device
~DMXDevice();
// Connect the device on a specific port
DMXError connect(char* serialNumber);
// Activate the DMX flow
DMXError activate();
// Deactivate the DMX flow
DMXError deactivate();
// Set the value of a DMX channel
DMXError setValue(uint16_t channel, uint8_t value);
// Resetting the DMX channels
void resetChannels();
private:
FT_STATUS ftStatus; // FTDI endpoint status
FT_HANDLE ftHandle = nullptr; // FTDI object
std::atomic<uint8_t> dmxData[DMX_CHANNELS + 1]; // For storing dynamically the DMX data
std::atomic<bool> isOutputActivated = false; // Boolean to start/stop the DMX flow
// Send a break line
FT_STATUS sendBreak(FT_HANDLE ftHandle);
// Continuously send the DMX frame
void sendDMX(FT_HANDLE ftHandle);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
#include "../src/detectFTDI.h"
#include <iostream>
int main(){
int endpointsNumber = getFTDIEndpointsNumber();
std::vector<FTDIEndpoint> endpoints = scanFTDIEndpoints();
// for (const auto& endpoint : endpoints) {
// std::cout << endpoint.serialNumber << " (" << endpoint.description << ") -> IS OPEN: " << endpoint.isOpen << std::endl;
// }
}

View File

@@ -0,0 +1,122 @@
#include "../src/dmxSender.h"
#include <iostream>
#include <thread>
#include <chrono>
int main(){
std::cout << "Debugging application DMXSENDER" << std::endl;
DMXDevice* dev = nullptr;
try {
dev = new DMXDevice();
}
catch(const std::exception &e){
std::cout << "Unable to create a DMX device: " << e.what() << std::endl;
}
if (!dev) {
std::cout << "Device not created, aborting." << std::endl;
return 1;
}
try {
bool err = dev->connect(0);
if (err == true) {
delete dev;
return 1;
}
}
catch (const std::exception &e){
std::cout << "Unable to connect" << e.what() << std::endl;
delete dev;
return 1;
}
try{
dev->activate();
}
catch(const std::exception &e){
std::cout << "Unable to activate" << e.what() << std::endl;
delete dev;
return 1;
}
try{
dev->setValue(1, 100);
dev->setValue(2, 255);
}
catch(const std::exception &e){
std::cout << "Unable to activate" << e.what() << std::endl;
delete dev;
return 1;
}
Sleep(500);
try{
dev->setValue(2, 127);
dev->setValue(3, 255);
}
catch(const std::exception &e){
std::cout << "Unable to activate" << e.what() << std::endl;
delete dev;
return 1;
}
Sleep(500);
try{
dev->setValue(3, 127);
dev->setValue(4, 255);
}
catch(const std::exception &e){
std::cout << "Unable to activate" << e.what() << std::endl;
delete dev;
return 1;
}
Sleep(500);
try{
dev->setValue(2, 0);
dev->setValue(3, 0);
dev->setValue(4, 0);
dev->setValue(5, 255);
}
catch(const std::exception &e){
std::cout << "Unable to activate" << e.what() << std::endl;
delete dev;
return 1;
}
Sleep(5000);
// try{
// dev->setValue(3, 255);
// }
// catch(const std::exception &e){
// std::cout << "Unable to activate" << e.what() << std::endl;
// delete dev;
// return 1;
// }
// Sleep(5000);
// try{
// dev->setValue(4, 255);
// }
// catch(const std::exception &e){
// std::cout << "Unable to activate" << e.what() << std::endl;
// delete dev;
// return 1;
// }
// Sleep(5000);
delete dev;
return 0;
}

View File

@@ -0,0 +1,140 @@
package genericmidi
import (
"context"
"dmxconnect/hardware"
"fmt"
"sync"
"github.com/rs/zerolog/log"
"github.com/wailsapp/wails/v2/pkg/runtime"
"gitlab.com/gomidi/midi"
)
// Device represents the device to control
type Device struct {
ID string // Device ID
Mapping hardware.MappingInfo // Device mapping configuration
}
// ------------------- //
// Endpoint contains the data of a MIDI endpoint
type Endpoint struct {
wg sync.WaitGroup
inputPorts []midi.In
outputsPorts []midi.Out
info hardware.EndpointInfo // The endpoint info
settings map[string]interface{} // The settings of the endpoint
devices []Device // All the MIDI devices that the endpoint can handle
}
// NewEndpoint creates a new MIDI endpoint
func NewEndpoint(endpointData hardware.EndpointInfo, inputs []midi.In, outputs []midi.Out) *Endpoint {
log.Trace().Str("file", "MIDIEndpoint").Str("name", endpointData.Name).Str("s/n", endpointData.SerialNumber).Msg("MIDI endpoint created")
return &Endpoint{
info: endpointData,
inputPorts: inputs,
outputsPorts: outputs,
settings: endpointData.Settings,
}
}
// SetDeviceArrivalCallback is called when we need to add a new device to the hardware
func (p *Endpoint) SetDeviceArrivalCallback(adc func(context.Context, hardware.DeviceInfo, hardware.Endpoint) error) {
}
// SetDeviceRemovalCallback is called when we need to remove a device from the hardware
func (p *Endpoint) SetDeviceRemovalCallback(rdc func(context.Context, string) error) {
}
// Connect connects the MIDI endpoint
func (p *Endpoint) Connect(ctx context.Context) error {
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusConnecting)
// Open input ports
for _, port := range p.inputPorts {
err := port.Open()
if err != nil {
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDisconnected)
return fmt.Errorf("unable to open the MIDI IN port: %w", err)
}
port.SetListener(func(msg []byte, delta int64) {
// Emit the event to the front
runtime.EventsEmit(ctx, string(hardware.EndpointEventEmitted), p.info.SerialNumber, msg)
log.Debug().Str("message", string(msg)).Int64("delta", delta).Msg("message received")
})
log.Info().Str("name", port.String()).Msg("port open successfully")
}
p.wg.Add(1)
go func() {
defer p.wg.Done()
<-ctx.Done()
_ = p.Disconnect(ctx)
}()
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDeactivated)
return nil
}
// Disconnect disconnects the MIDI endpoint
func (p *Endpoint) Disconnect(ctx context.Context) error {
// Close all inputs ports
for _, port := range p.inputPorts {
err := port.Close()
if err != nil {
return fmt.Errorf("unable to close the MIDI IN port: %w", err)
}
}
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDisconnected)
return nil
}
// Activate activates the MIDI endpoint
func (p *Endpoint) Activate(ctx context.Context) error {
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusActivated)
return nil
}
// Deactivate deactivates the MIDI endpoint
func (p *Endpoint) Deactivate(ctx context.Context) error {
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDeactivated)
return nil
}
// SetSettings sets a specific setting for this endpoint
func (p *Endpoint) SetSettings(ctx context.Context, settings map[string]any) error {
p.settings = settings
return nil
}
// SetDeviceProperty - not implemented for this kind of endpoint
func (p *Endpoint) SetDeviceProperty(context.Context, uint32, byte) error {
return nil
}
// GetSettings gets the endpoint settings
func (p *Endpoint) GetSettings() map[string]interface{} {
return p.settings
}
// GetInfo gets the endpoint information
func (p *Endpoint) GetInfo() hardware.EndpointInfo {
return p.info
}
// WaitStop wait about the endpoint to close
func (p *Endpoint) WaitStop() error {
log.Info().Str("file", "MIDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("waiting for MIDI endpoint to close...")
p.wg.Wait()
log.Info().Str("file", "MIDIEndpoint").Str("s/n", p.info.SerialNumber).Msg("MIDI endpoint closed!")
return nil
}

View File

@@ -0,0 +1,248 @@
package genericmidi
import (
"context"
"dmxconnect/hardware"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/rs/zerolog/log"
"gitlab.com/gomidi/rtmididrv"
)
// Provider represents how the protocol is defined
type Provider struct {
wg sync.WaitGroup
mu sync.Mutex
detected map[string]*Endpoint // Detected endpoints
scanEvery time.Duration // Scans endpoints periodically
onArrival func(context.Context, hardware.Endpoint) // When a endpoint arrives
onRemoval func(context.Context, hardware.Endpoint) // When a endpoint goes away
}
// NewProvider creates a new MIDI provider
func NewProvider(scanEvery time.Duration) *Provider {
log.Trace().Str("file", "MIDIProvider").Msg("MIDI provider created")
return &Provider{
scanEvery: scanEvery,
detected: make(map[string]*Endpoint),
}
}
// OnArrival is the callback function when a new endpoint arrives
func (f *Provider) OnArrival(cb func(context.Context, hardware.Endpoint)) {
f.onArrival = cb
}
// OnRemoval i the callback when a endpoint goes away
func (f *Provider) OnRemoval(cb func(context.Context, hardware.Endpoint)) {
f.onRemoval = cb
}
// Initialize initializes the MIDI driver
func (f *Provider) Initialize() error {
log.Trace().Str("file", "MIDIProvider").Msg("MIDI provider initialized")
return nil
}
// Create creates a new endpoint, based on the endpoint information (manually created)
func (f *Provider) Create(ctx context.Context, endpointInfo hardware.EndpointInfo) (hardware.EndpointInfo, error) {
return hardware.EndpointInfo{}, nil
}
// Remove removes an existing endpoint (manually created)
func (f *Provider) Remove(ctx context.Context, endpoint hardware.Endpoint) error {
return nil
}
// Start starts the provider and search for endpoints
func (f *Provider) 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 <-ticker.C:
// Scan the endpoints
err := f.scanEndpoints(ctx)
if err != nil {
log.Err(err).Str("file", "MIDIProvider").Msg("unable to scan MIDI endpoints")
}
}
}
}()
return nil
}
// WaitStop stops the provider
func (f *Provider) WaitStop() error {
log.Trace().Str("file", "MIDIProvider").Msg("stopping the MIDI provider...")
// Wait for goroutines to stop
f.wg.Wait()
log.Trace().Str("file", "MIDIProvider").Msg("MIDI provider stopped")
return nil
}
// GetName returns the name of the driver
func (f *Provider) GetName() string {
return "MIDI"
}
func splitStringAndNumber(input string) (string, int, error) {
// Regular expression to match the text part and the number at the end
re := regexp.MustCompile(`^(.*?)(\d+)$`)
matches := re.FindStringSubmatch(input)
// Check if the regex found both a text part and a number
if len(matches) == 3 {
// matches[1]: text part (might contain trailing spaces)
// matches[2]: numeric part as a string
textPart := strings.TrimSpace(matches[1]) // Remove any trailing spaces from the text
numberPart, err := strconv.Atoi(matches[2])
if err != nil {
return "", 0, err // Return error if the number conversion fails
}
return textPart, numberPart, nil
}
// Return an error if no trailing number is found
return "", 0, fmt.Errorf("no number found at the end of the string")
}
// scanEndpoints scans the MIDI endpoints
func (f *Provider) scanEndpoints(ctx context.Context) error {
currentMap := make(map[string]*Endpoint)
drv, err := rtmididrv.New()
if err != nil {
return fmt.Errorf("unable to open the MIDI driver")
}
defer drv.Close()
// Get MIDI INPUT ports
ins, err := drv.Ins()
if err != nil {
return fmt.Errorf("unable to scan MIDI IN ports: %s", err)
}
for _, port := range ins {
// Exclude microsoft wavetable from the list
if strings.Contains(port.String(), "GS Wavetable") {
continue
}
baseName := normalizeName(port.String())
sn := strings.ReplaceAll(strings.ToLower(baseName), " ", "_")
if _, ok := currentMap[sn]; !ok {
currentMap[sn] = &Endpoint{
info: hardware.EndpointInfo{
Name: baseName,
SerialNumber: sn,
ProtocolName: "MIDI",
},
}
}
currentMap[sn].inputPorts = append(currentMap[sn].inputPorts, port)
log.Info().Any("endpoints", currentMap).Msg("available MIDI IN ports")
}
// Get MIDI OUTPUT ports
outs, err := drv.Outs()
if err != nil {
return fmt.Errorf("unable to scan MIDI OUT ports: %s", err)
}
for _, port := range outs {
// Exclude microsoft wavetable from the list
if strings.Contains(port.String(), "GS Wavetable") {
continue
}
baseName := normalizeName(port.String())
sn := strings.ReplaceAll(strings.ToLower(baseName), " ", "_")
if _, ok := currentMap[sn]; !ok {
currentMap[sn] = &Endpoint{
info: hardware.EndpointInfo{
Name: baseName,
SerialNumber: sn,
ProtocolName: "MIDI",
},
}
}
currentMap[sn].outputsPorts = append(currentMap[sn].outputsPorts, port)
log.Info().Any("endpoints", currentMap).Msg("available MIDI OUT ports")
}
log.Debug().Any("value", currentMap).Msg("MIDI endpoints map")
// Detect arrivals
for sn, discovery := range currentMap {
if _, known := f.detected[sn]; !known {
endpoint := NewEndpoint(discovery.info, discovery.inputPorts, discovery.outputsPorts)
f.detected[sn] = endpoint
if f.onArrival != nil {
f.onArrival(ctx, discovery)
}
}
}
// Detect removals
for sn, oldEndpoint := range f.detected {
if _, still := currentMap[sn]; !still {
log.Info().Str("sn", sn).Str("name", oldEndpoint.GetInfo().Name).Msg("[MIDI] endpoint removed")
// Execute the removal callback
if f.onRemoval != nil {
f.onRemoval(ctx, oldEndpoint)
}
// Delete it from the detected list
delete(f.detected, sn)
}
}
return nil
}
func normalizeName(raw string) string {
name := strings.TrimSpace(raw)
// Si parenthèses, prendre le texte à l'intérieur
start := strings.Index(name, "(")
end := strings.LastIndex(name, ")")
if start != -1 && end != -1 && start < end {
name = name[start+1 : end]
return strings.TrimSpace(name)
}
// Sinon, supprimer le dernier mot s'il est un entier
parts := strings.Fields(name) // découpe en mots
if len(parts) > 1 {
if _, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
parts = parts[:len(parts)-1] // retirer le dernier mot
}
}
return strings.Join(parts, " ")
}

90
hardware/hardware.go Normal file
View File

@@ -0,0 +1,90 @@
package hardware
import (
"context"
"errors"
"fmt"
"sync"
"github.com/rs/zerolog/log"
)
// Manager is the class who manages the hardware
type Manager struct {
mu sync.Mutex
wg sync.WaitGroup
providers map[string]Provider // The map of endpoints providers
devices map[string]*Device // The map of devices
DetectedEndpoints map[string]Endpoint // The current list of endpoints
SavedEndpoints map[string]EndpointInfo // The list of stored endpoints
}
// NewManager creates a new hardware manager
func NewManager() *Manager {
log.Trace().Str("package", "hardware").Msg("Hardware instance created")
return &Manager{
providers: make(map[string]Provider, 0),
devices: make(map[string]*Device, 0),
DetectedEndpoints: make(map[string]Endpoint, 0),
SavedEndpoints: make(map[string]EndpointInfo, 0),
}
}
// Start starts to find new endpoint events
func (h *Manager) Start(ctx context.Context) error {
for providerName, provider := range h.providers {
// Initialize the provider
err := provider.Initialize()
if err != nil {
log.Err(err).Str("file", "hardware").Str("providerName", providerName).Msg("unable to initialize provider")
return err
}
// Set callback functions
provider.OnArrival(h.OnEndpointArrival)
provider.OnRemoval(h.OnEndpointRemoval)
// Start the provider
err = provider.Start(ctx)
if err != nil {
log.Err(err).Str("file", "hardware").Str("providerName", providerName).Msg("unable to start provider")
return err
}
}
return nil
}
// WaitStop stops the hardware manager
func (h *Manager) WaitStop() error {
log.Trace().Str("file", "hardware").Msg("closing the hardware manager")
// Stop each provider
var errs []error
for name, f := range h.providers {
if err := f.WaitStop(); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", name, err))
}
}
// Wait for all the endpoints to close
log.Trace().Str("file", "MIDIProvider").Msg("closing all MIDI endpoints")
for registeredEndpointSN, registeredEndpoint := range h.DetectedEndpoints {
err := registeredEndpoint.WaitStop()
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", registeredEndpointSN, err))
}
}
// Wait for goroutines to finish
h.wg.Wait()
// Returning errors
if len(errs) > 0 {
return errors.Join(errs...)
}
log.Info().Str("file", "hardware").Msg("hardware manager stopped")
return nil
}

View File

@@ -0,0 +1,309 @@
package os2l
import (
"context"
"dmxconnect/hardware"
"encoding/json"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/rs/zerolog/log"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// Message represents an OS2L message
type Message struct {
Event string `json:"evt"`
Name string `json:"name"`
State string `json:"state"`
ID int64 `json:"id"`
Param float64 `json:"param"`
}
// Endpoint contains the data of an OS2L endpoint
type Endpoint struct {
wg sync.WaitGroup
info hardware.EndpointInfo // The basic info for this endpoint
serverIP string // OS2L server IP
serverPort int // OS2L server port
listener net.Listener // Net listener (TCP)
listenerCancel context.CancelFunc // Call this function to cancel the endpoint activation
eventCallback func(any) // This callback is called for returning events
addDeviceCallback func(context.Context, hardware.DeviceInfo, hardware.Endpoint) error // Add a device to the hardware
removeDeviceCallback func(context.Context, string) error // Remove a device from the hardware
}
// NewOS2LEndpoint creates a new OS2L endpoint
func NewOS2LEndpoint(endpointData hardware.EndpointInfo) (*Endpoint, error) {
endpoint := &Endpoint{
info: endpointData,
listener: nil,
eventCallback: nil,
}
log.Trace().Str("file", "OS2LEndpoint").Str("name", endpointData.Name).Str("s/n", endpointData.SerialNumber).Msg("OS2L endpoint created")
return endpoint, endpoint.loadSettings(endpointData.Settings)
}
// SetEventCallback sets the callback for returning events
func (p *Endpoint) SetEventCallback(eventCallback func(any)) {
p.eventCallback = eventCallback
}
// SetDeviceArrivalCallback is called when we need to add a new device to the hardware
func (p *Endpoint) SetDeviceArrivalCallback(adc func(context.Context, hardware.DeviceInfo, hardware.Endpoint) error) {
p.addDeviceCallback = adc
}
// SetDeviceRemovalCallback is called when we need to remove a device from the hardware
func (p *Endpoint) SetDeviceRemovalCallback(rdc func(context.Context, string) error) {
p.removeDeviceCallback = rdc
}
// Connect connects the OS2L endpoint
func (p *Endpoint) Connect(ctx context.Context) error {
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusConnecting)
var err error
addr := net.TCPAddr{Port: p.serverPort, IP: net.ParseIP(p.serverIP)}
log.Debug().Any("addr", addr).Msg("parametres de connexion à la connexion")
p.listener, err = net.ListenTCP("tcp", &addr)
if err != nil {
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDisconnected)
return fmt.Errorf("unable to set the OS2L TCP listener")
}
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDeactivated)
log.Info().Str("file", "OS2LEndpoint").Msg("OS2L endpoint connected")
// TODO: To remove : simulate a device arrival/removal
p.addDeviceCallback(ctx, hardware.DeviceInfo{
SerialNumber: "0DE3FF",
Name: "OS2L test device",
Manufacturer: "BlueSig",
Version: "0.1.0-dev",
}, p)
return nil
}
// handleMessage handles an OS2L message
func (p *Endpoint) handleMessage(raw []byte) error {
message := Message{}
err := json.Unmarshal(raw, &message)
if err != nil {
return fmt.Errorf("Unable to parse the OS2L message: %w", err)
}
log.Debug().Str("event", message.Event).Str("name", message.Name).Str("state", message.State).Int("ID", int(message.ID)).Float64("param", message.Param).Msg("OS2L event received")
// Return the event to the provider
if p.eventCallback != nil {
go p.eventCallback(message)
}
return nil
}
// loadSettings check and load the settings in the endpoint
func (p *Endpoint) loadSettings(settings map[string]any) error {
// Check if the IP exists
serverIP, found := settings["os2lIp"]
if !found {
// Set default IP address
serverIP = "127.0.0.1"
}
// Check if it is a string
ipSetting, ok := serverIP.(string)
if ok {
p.serverIP = ipSetting
} else {
return fmt.Errorf("The specified IP is not a string")
}
// Check if the port exists
serverPort, found := settings["os2lPort"]
if !found {
// Set default port
serverPort = 9995
}
switch v := serverPort.(type) {
case int:
p.serverPort = v
case float64:
p.serverPort = int(v) // JSON numbers are float64
default:
return fmt.Errorf("The specified port is not a number, got %T", serverPort)
}
return nil
}
// Disconnect disconnects the MIDI endpoint
func (p *Endpoint) Disconnect(ctx context.Context) error {
// Close the TCP listener if not null
if p.listener != nil {
p.listener.Close()
}
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDisconnected)
log.Info().Str("file", "OS2LEndpoint").Msg("OS2L endpoint disconnected")
// TODO: To remove : simulate a device arrival/removal
p.removeDeviceCallback(ctx, "0DE3FF")
return nil
}
// Activate activates the OS2L endpoint
func (p *Endpoint) Activate(ctx context.Context) error {
// Create a derived context to handle deactivation
var listenerCtx context.Context
listenerCtx, p.listenerCancel = context.WithCancel(ctx)
if p.listener == nil {
return fmt.Errorf("the listener isn't defined")
}
p.wg.Add(1)
go func() {
defer p.wg.Done()
for {
select {
case <-listenerCtx.Done():
return
default:
p.listener.(*net.TCPListener).SetDeadline(time.Now().Add(1 * time.Second))
conn, err := p.listener.Accept()
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
continue
}
if strings.Contains(err.Error(), "use of closed network connection") || strings.Contains(err.Error(), "invalid argument") {
return
}
log.Err(err).Str("file", "OS2LEndpoint").Msg("unable to accept the connection")
continue
}
// Every client is handled in a dedicated goroutine
p.wg.Add(1)
go func(c net.Conn) {
defer p.wg.Done()
defer c.Close()
buffer := make([]byte, 1024)
for {
select {
case <-listenerCtx.Done():
return
default:
c.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
n, err := c.Read(buffer)
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
// Lecture a expiré → vérifier si le contexte est annulé
select {
case <-listenerCtx.Done():
return
default:
continue // pas annulé → relancer Read
}
}
return // autre erreur ou EOF
}
p.handleMessage(buffer[:n])
}
}
}(conn)
}
}
}()
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusActivated)
log.Info().Str("file", "OS2LEndpoint").Msg("OS2L endpoint activated")
return nil
}
// Deactivate deactivates the OS2L endpoint
func (p *Endpoint) Deactivate(ctx context.Context) error {
if p.listener == nil {
return fmt.Errorf("the listener isn't defined")
}
// Cancel listener by context
if p.listenerCancel != nil {
p.listenerCancel()
}
runtime.EventsEmit(ctx, string(hardware.EndpointStatusUpdated), p.GetInfo(), hardware.EndpointStatusDeactivated)
log.Info().Str("file", "OS2LEndpoint").Msg("OS2L endpoint deactivated")
return nil
}
// SetSettings sets a specific setting for this endpoint
func (p *Endpoint) SetSettings(ctx context.Context, settings map[string]any) error {
err := p.loadSettings(settings)
if err != nil {
return fmt.Errorf("unable to load settings: %w", err)
}
// Reconnect the endpoint
p.wg.Add(1)
go func() {
defer p.wg.Done()
err := p.Deactivate(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to deactivate")
return
}
err = p.Disconnect(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to disconnect")
return
}
// Add a sleep to view changes
time.Sleep(500 * time.Millisecond)
err = p.Connect(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to connect")
return
}
err = p.Activate(ctx)
if err != nil {
log.Err(err).Str("sn", p.GetInfo().SerialNumber).Msg("unable to activate")
return
}
}()
log.Info().Str("sn", p.GetInfo().SerialNumber).Msg("endpoint settings set")
return nil
}
// SetDeviceProperty - not implemented for this kind of endpoint
func (p *Endpoint) SetDeviceProperty(context.Context, uint32, byte) error {
return nil
}
// GetSettings gets the endpoint settings
func (p *Endpoint) GetSettings() map[string]any {
return map[string]any{
"os2lIp": p.serverIP,
"os2lPort": p.serverPort,
}
}
// GetInfo gets the endpoint information
func (p *Endpoint) GetInfo() hardware.EndpointInfo {
return p.info
}
// WaitStop stops the endpoint
func (p *Endpoint) WaitStop() error {
log.Info().Str("file", "OS2LEndpoint").Str("s/n", p.info.SerialNumber).Msg("waiting for OS2L endpoint to close...")
p.wg.Wait()
log.Info().Str("file", "OS2LEndpoint").Str("s/n", p.info.SerialNumber).Msg("OS2L endpoint closed!")
return nil
}

View File

@@ -0,0 +1,106 @@
package os2l
import (
"context"
"dmxconnect/hardware"
"fmt"
"math/rand"
"strings"
"sync"
"github.com/rs/zerolog/log"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// Provider represents how the protocol is defined
type Provider struct {
wg sync.WaitGroup
mu sync.Mutex
detected map[string]*Endpoint // The list of saved endpoints
onArrival func(context.Context, hardware.Endpoint) // When a endpoint arrives
onRemoval func(context.Context, hardware.Endpoint) // When a endpoint goes away
}
// NewProvider creates a new OS2L provider
func NewProvider() *Provider {
log.Trace().Str("file", "OS2LProvider").Msg("OS2L provider created")
return &Provider{
detected: make(map[string]*Endpoint),
}
}
// Initialize initializes the provider
func (f *Provider) Initialize() error {
log.Trace().Str("file", "OS2LProvider").Msg("OS2L provider initialized")
return nil
}
// OnArrival is the callback function when a new endpoint arrives
func (f *Provider) OnArrival(cb func(context.Context, hardware.Endpoint)) {
f.onArrival = cb
}
// OnRemoval if the callback when a endpoint goes away
func (f *Provider) OnRemoval(cb func(context.Context, hardware.Endpoint)) {
f.onRemoval = cb
}
// Create creates a new endpoint, based on the endpoint information (manually created)
func (f *Provider) Create(ctx context.Context, endpointInfo hardware.EndpointInfo) (hardware.EndpointInfo, error) {
// If the SerialNumber is empty, generate another one
// TODO: Move the serialnumber generator to the endpoint itself
if endpointInfo.SerialNumber == "" {
endpointInfo.SerialNumber = strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32)))
}
// Create a new OS2L endpoint
endpoint, err := NewOS2LEndpoint(endpointInfo)
if err != nil {
return hardware.EndpointInfo{}, fmt.Errorf("unable to create the OS2L endpoint: %w", err)
}
// Set the event callback
endpoint.SetEventCallback(func(event any) {
runtime.EventsEmit(ctx, string(hardware.EndpointEventEmitted), endpointInfo.SerialNumber, event)
})
f.detected[endpointInfo.SerialNumber] = endpoint
if f.onArrival != nil {
f.onArrival(ctx, endpoint) // Ask to register the endpoint in the project
}
return endpointInfo, err
}
// Remove removes an existing endpoint (manually created)
func (f *Provider) Remove(ctx context.Context, endpoint hardware.Endpoint) error {
if f.onRemoval != nil {
f.onRemoval(ctx, endpoint)
}
delete(f.detected, endpoint.GetInfo().SerialNumber)
return nil
}
// GetName returns the name of the driver
func (f *Provider) GetName() string {
return "OS2L"
}
// Start starts the provider
func (f *Provider) Start(ctx context.Context) error {
// No endpoints to scan here
return nil
}
// WaitStop stops the provider
func (f *Provider) WaitStop() error {
log.Trace().Str("file", "OS2LProvider").Msg("stopping the OS2L provider...")
// Waiting internal tasks
f.wg.Wait()
log.Trace().Str("file", "OS2LProvider").Msg("OS2L provider stopped")
return nil
}

View File

@@ -0,0 +1,37 @@
package hardware
import (
"context"
"fmt"
"github.com/rs/zerolog/log"
)
// Provider represents how compatible endpoint drivers are implemented
type Provider interface {
Initialize() error // Initializes the protocol
Create(ctx context.Context, endpointInfo EndpointInfo) (EndpointInfo, error) // Manually create a endpoint
Remove(ctx context.Context, endpoint Endpoint) error // Manually remove a endpoint
OnArrival(cb func(context.Context, Endpoint)) // Callback function when a endpoint arrives
OnRemoval(cb func(context.Context, Endpoint)) // Callback function when a endpoint goes away
Start(context.Context) error // Start the detection
WaitStop() error // Waiting for provider to close
GetName() string // Get the name of the provider
}
// GetProvider returns a register provider
func (h *Manager) GetProvider(providerName string) (Provider, error) {
provider, exists := h.providers[providerName]
if !exists {
log.Error().Str("file", "hardware").Str("providerName", providerName).Msg("unable to get the provider")
return nil, fmt.Errorf("unable to locate the '%s' provider", providerName)
}
log.Debug().Str("file", "hardware").Str("providerName", providerName).Msg("got provider")
return provider, nil
}
// RegisterProvider registers a new endpoints provider
func (h *Manager) RegisterProvider(provider Provider) {
h.providers[provider.GetName()] = provider
log.Info().Str("file", "hardware").Str("providerName", provider.GetName()).Msg("provider registered")
}

55
main.go Normal file
View File

@@ -0,0 +1,55 @@
package main
import (
"embed"
"time"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// Configure the logger
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: "2006-01-02 15:04:05",
})
zerolog.TimestampFunc = func() time.Time {
return time.Now().Local()
}
zerolog.SetGlobalLevel(zerolog.TraceLevel)
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
Title: "dmxconnect",
Width: 1024,
Height: 768,
WindowStartState: options.Maximised,
AssetServer: &assetserver.Options{
Assets: assets,
},
OnStartup: app.onStartup,
OnDomReady: app.onReady,
OnShutdown: app.onShutdown,
Bind: []interface{}{
app,
},
LogLevel: logger.ERROR,
})
if err != nil {
log.Err(err).Str("file", "main").Msg("unable to start the application")
}
}

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "dmxconnect",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

1
package.json Normal file
View File

@@ -0,0 +1 @@
{}

243
project.go Normal file
View File

@@ -0,0 +1,243 @@
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.EndpointInfo),
}
// 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 endpoints of the project
projectEndpoints := a.projectInfo.EndpointsInfo
for key, value := range projectEndpoints {
_, err = a.hardwareManager.RegisterEndpoint(a.ctx, value)
if err != nil {
return fmt.Errorf("unable to register the endpoint S/N '%s': %w", key, err)
}
}
return nil
}
// CloseCurrentProject closes the current project
func (a *App) CloseCurrentProject() error {
// Unregister all endpoints of the project
projectEndpoints := a.hardwareManager.SavedEndpoints
for key, value := range projectEndpoints {
err := a.hardwareManager.UnregisterEndpoint(a.ctx, value)
if err != nil {
return fmt.Errorf("unable to unregister the endpoint 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")
}
// Get hardware info
a.projectInfo.EndpointsInfo = a.hardwareManager.SavedEndpoints
// Marshal the project
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
EndpointsInfo map[string]hardware.EndpointInfo `yaml:"endpoints"` // Endpoints information
}

13
wails.json Normal file
View File

@@ -0,0 +1,13 @@
{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "dmxconnect",
"outputfilename": "dmxconnect.exe",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "auto",
"author": {
"name": "",
"email": ""
}
}