generated from thinkode/modelRepository
Compare commits
15 Commits
main
...
cb5c5b688e
| Author | SHA1 | Date | |
|---|---|---|---|
| cb5c5b688e | |||
| 65e2def501 | |||
| bc15407cad | |||
| 0db468bfef | |||
| 4e0829e821 | |||
| c7fe171cb4 | |||
| 0e3f57f5fb | |||
| 1052dcc8d5 | |||
| a231263825 | |||
| 364dabee69 | |||
| b33df4b447 | |||
| d6dc8405dd | |||
| d9a01d440b | |||
| 8fe9c0a4e8 | |||
| 7ac2d71b4d |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -0,0 +1,12 @@
|
||||
build/bin
|
||||
projects
|
||||
node_modules
|
||||
frontend/.vscode
|
||||
frontend/dist
|
||||
frontend/wailsjs
|
||||
*/package-lock.json
|
||||
*/package.json.md5
|
||||
*.exe
|
||||
*.o
|
||||
*.rc
|
||||
frontend/public
|
||||
66
.vscode/settings.json
vendored
Normal file
66
.vscode/settings.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
76
README.md
76
README.md
@@ -1,3 +1,75 @@
|
||||
# modelRepository
|
||||
[](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 !
|
||||
110
app.go
Normal file
110
app.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dmxconnect/hardware"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
hardwareManager *hardware.HardwareManager // 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.NewHardwareManager()
|
||||
// hardwareManager.RegisterFinder(hardware.NewMIDIFinder(5 * time.Second))
|
||||
hardwareManager.RegisterFinder(hardware.NewFTDIFinder(5 * time.Second))
|
||||
// hardwareManager.RegisterFinder(hardware.NewOS2LFinder())
|
||||
return &App{
|
||||
hardwareManager: hardwareManager,
|
||||
projectSave: "",
|
||||
projectInfo: ProjectInfo{
|
||||
PeripheralsInfo: make(map[string]hardware.PeripheralInfo),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 peripherals connected
|
||||
func (a *App) onReady(ctx context.Context) {
|
||||
// log.Debug().Str("file", "peripherals").Msg("getting peripherals...")
|
||||
// err := a.hardwareManager.Scan()
|
||||
// if err != nil {
|
||||
// log.Err(err).Str("file", "app").Msg("unable to get the peripherals")
|
||||
// }
|
||||
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()
|
||||
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
55
build.bat
Normal 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 n’est 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\cpp" || (
|
||||
echo [ERROR] Impossible d'accéder à hardware\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
BIN
build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
BIN
build/windows/icon.ico
Normal file
BIN
build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
15
build/windows/info.json
Normal file
15
build/windows/info.json
Normal 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}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
22
build/windows/wails.exe.manifest
Normal file
22
build/windows/wails.exe.manifest
Normal 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>
|
||||
63
frontend/README.md
Normal file
63
frontend/README.md
Normal 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
13
frontend/index.html
Normal 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>
|
||||
38
frontend/jsconfig.json
Normal file
38
frontend/jsconfig.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
/**
|
||||
* svelte-preprocess cannot figure out whether you have
|
||||
* a value or a type, so tell TypeScript to enforce using
|
||||
* `import type` instead of `import` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* To have warnings / errors of the Svelte compiler at the
|
||||
* correct position, enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable this if you'd like to use dynamic types.
|
||||
*/
|
||||
"checkJs": true
|
||||
},
|
||||
/**
|
||||
* Use global.d.ts instead of compilerOptions.types
|
||||
* to avoid limiting type declarations.
|
||||
*/
|
||||
"include": [
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.js",
|
||||
"src/**/*.svelte"
|
||||
]
|
||||
}
|
||||
19
frontend/package.json
Normal file
19
frontend/package.json
Normal 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
BIN
frontend/public/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
151
frontend/src/App.svelte
Normal file
151
frontend/src/App.svelte
Normal file
@@ -0,0 +1,151 @@
|
||||
<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 GeneralConsole from './components/Console/GeneralConsole.svelte';
|
||||
import RoundIconButton from './components/General/RoundIconButton.svelte';
|
||||
import { generateToast, showInformation, needProjectSave, peripherals } from './stores';
|
||||
import { SaveProject } from '../wailsjs/go/main/App.js';
|
||||
import { construct_svelte_component } from 'svelte/internal';
|
||||
import { EventsOn } from '../wailsjs/runtime'
|
||||
import { CreateProject } from "../wailsjs/go/main/App";
|
||||
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
|
||||
import { get } from "svelte/store"
|
||||
import ToastNotification from './components/General/ToastNotification.svelte';
|
||||
|
||||
// Handle the event when a new peripheral is detected
|
||||
EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){
|
||||
// When a new peripheral is detected, add it to the map and:
|
||||
// - Pass the isDetected key to true
|
||||
// - Set the isSaved key to the last value
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
|
||||
peripheralInfo.isDetected = true
|
||||
peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false
|
||||
peripherals.update((peripherals) => {
|
||||
peripherals[peripheralInfo.SerialNumber] = peripheralInfo
|
||||
return {...peripherals}
|
||||
})
|
||||
console.log("Hardware has been added to the system");
|
||||
generateToast('info', 'bxs-hdd', $_("peripheralArrivalToast") + ' <b>' + peripheralInfo.Name + '</b>')
|
||||
})
|
||||
|
||||
// Handle the event when a peripheral is removed from the system
|
||||
EventsOn('PERIPHERAL_REMOVAL', function(peripheralInfo){
|
||||
console.log("Hardware has been removed from the system");
|
||||
// When a peripheral is disconnected, pass its isDetected key to false
|
||||
// If the isSaved key is set to false, we can completely remove the peripheral from the list
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
|
||||
let needToDelete = (lastSavedProperty !== true) ? true : false
|
||||
peripherals.update((storedPeripherals) => {
|
||||
if (needToDelete){
|
||||
delete storedPeripherals[peripheralInfo.SerialNumber];
|
||||
return { ...storedPeripherals };
|
||||
}
|
||||
storedPeripherals[peripheralInfo.SerialNumber].isDetected = false
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
generateToast('warning', 'bxs-hdd', $_("peripheralRemovalToast") + ' <b>' + peripheralInfo.Name + '</b>')
|
||||
})
|
||||
|
||||
// Handle the event when a peripheral status is updated
|
||||
EventsOn('PERIPHERAL_STATUS', function(peripheral, status){
|
||||
console.log("Hardware status has been updated to " + status);
|
||||
// When a peripheral status is updated, update it in the store
|
||||
peripherals.update((storedPeripherals) => {
|
||||
return {
|
||||
...storedPeripherals,
|
||||
[peripheral.SerialNumber]: {
|
||||
...storedPeripherals[peripheral.SerialNumber],
|
||||
isSaved: true,
|
||||
Status: status,
|
||||
},
|
||||
}})
|
||||
})
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
// Instanciate a new project
|
||||
CreateProject().then((showInfo) => {
|
||||
showInformation.set(showInfo)
|
||||
$needProjectSave = true
|
||||
})
|
||||
|
||||
// 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()
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<NavigationBar on:navigationChanged="{onNavigationChanged}"/>
|
||||
{#if $needProjectSave}
|
||||
<RoundIconButton on:mouseup={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}></RoundIconButton>
|
||||
{/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;
|
||||
padding: 1em;
|
||||
max-width: 240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
main {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
frontend/src/assets/images/stage_background.png
Normal file
BIN
frontend/src/assets/images/stage_background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 373 KiB |
1
frontend/src/components/Animation/Animation.svelte
Normal file
1
frontend/src/components/Animation/Animation.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Animation creator</h1>
|
||||
1
frontend/src/components/Console/GeneralConsole.svelte
Normal file
1
frontend/src/components/Console/GeneralConsole.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<h1>General console</h1>
|
||||
1
frontend/src/components/Devices/Devices.svelte
Normal file
1
frontend/src/components/Devices/Devices.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Devices configuration</h1>
|
||||
32
frontend/src/components/General/Clock.svelte
Normal file
32
frontend/src/components/General/Clock.svelte
Normal 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>
|
||||
|
||||
103
frontend/src/components/General/DropdownList.svelte
Normal file
103
frontend/src/components/General/DropdownList.svelte
Normal 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>
|
||||
63
frontend/src/components/General/InfoButton.svelte
Normal file
63
frontend/src/components/General/InfoButton.svelte
Normal 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>
|
||||
|
||||
90
frontend/src/components/General/Input.svelte
Normal file
90
frontend/src/components/General/Input.svelte
Normal 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>
|
||||
62
frontend/src/components/General/NavigationBar.svelte
Normal file
62
frontend/src/components/General/NavigationBar.svelte
Normal 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>
|
||||
115
frontend/src/components/General/RoundIconButton.svelte
Normal file
115
frontend/src/components/General/RoundIconButton.svelte
Normal 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>
|
||||
62
frontend/src/components/General/RoundedButton.svelte
Normal file
62
frontend/src/components/General/RoundedButton.svelte
Normal 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>
|
||||
53
frontend/src/components/General/Tab.svelte
Normal file
53
frontend/src/components/General/Tab.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<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"
|
||||
style='background-color: {$colors.third};'>
|
||||
{#each tabs as tab, index}
|
||||
<RoundedButton 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}; max-height: {maxHeight};'>
|
||||
{#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;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
.bodyContainer{
|
||||
padding: 0.5em;
|
||||
background-color: red;
|
||||
border-radius: 0.5em;
|
||||
overflow:auto;
|
||||
}
|
||||
</style>
|
||||
94
frontend/src/components/General/ToastNotification.svelte
Normal file
94
frontend/src/components/General/ToastNotification.svelte
Normal 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>
|
||||
113
frontend/src/components/General/Toggle.svelte
Normal file
113
frontend/src/components/General/Toggle.svelte
Normal 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>
|
||||
46
frontend/src/components/General/Tooltip.svelte
Normal file
46
frontend/src/components/General/Tooltip.svelte
Normal 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>
|
||||
1
frontend/src/components/Preparation/Preparation.svelte
Normal file
1
frontend/src/components/Preparation/Preparation.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Show preparation</h1>
|
||||
136
frontend/src/components/Settings/DeviceCard.svelte
Normal file
136
frontend/src/components/Settings/DeviceCard.svelte
Normal file
@@ -0,0 +1,136 @@
|
||||
<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 = "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 == "connecting" ? "waiting" : ""}" on:mousedown={click} style="color: {(status == "disconnected") ? $colors.first : $colors.white};">
|
||||
<div style="z-index: 1;">
|
||||
<p>{#if status == "disconnected" }<i class='bx bx-no-signal' style="font-size:100%; color: var(--nok-color);"></i> {/if}{title}</p>
|
||||
<h6 class="subtitle">{type} {location != '' ? "- " : ""}<i>{location}</i></h6>
|
||||
{#if status == "disconnected"}
|
||||
<h6><b>Disconnected</b></h6>
|
||||
{:else}
|
||||
<h6>{line1}</h6>
|
||||
<h6>{line2}</h6>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<InfoButton on:click={add} color="{(status == "disconnected") ? $colors.first : $colors.white}" style="margin: 0.2em; display: { addable ? 'flex' : 'none' }" icon='bxs-message-square-add' interactive message={$_("projectHardwareAddTooltip")}/>
|
||||
<InfoButton on:click={remove} color="{(status == "disconnected") ? $colors.first : $colors.white}" style="margin: 0.2em; display: { removable ? 'flex' : 'none' }" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")}/>
|
||||
<InfoButton style="margin: 0.2em; display: { (status == "activated" || status == "deactivated") ? 'flex' : 'none' }" background={ (status == "activated") ? $colors.ok : (status == "deactivated") ? $colors.nok : null} icon='bx-pulse' hide={!signalizable}/>
|
||||
</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(--first-color);
|
||||
margin: 0.2em;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
border-radius: 0.2em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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(--second-color), var(--first-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;
|
||||
}
|
||||
|
||||
.unselected{
|
||||
background-color: var(--third-color);
|
||||
background: fixed;
|
||||
position: relative;
|
||||
margin: 0.2em;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
border-radius: 0.2em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 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>
|
||||
207
frontend/src/components/Settings/InputsOutputsContent.svelte
Normal file
207
frontend/src/components/Settings/InputsOutputsContent.svelte
Normal file
@@ -0,0 +1,207 @@
|
||||
<script lang=ts>
|
||||
import DeviceCard from "./DeviceCard.svelte";
|
||||
import Tab from "../General/Tab.svelte";
|
||||
import Input from "../General/Input.svelte";
|
||||
import { t, _ } from 'svelte-i18n'
|
||||
import { generateToast, needProjectSave, peripherals } from "../../stores";
|
||||
import { get } from "svelte/store"
|
||||
import { UpdatePeripheralSettings, GetPeripheralSettings, RemovePeripheral, AddPeripheral } from "../../../wailsjs/go/main/App";
|
||||
import RoundedButton from "../General/RoundedButton.svelte";
|
||||
|
||||
// Add the peripheral to the project
|
||||
function addPeripheral(peripheral){
|
||||
// Add the peripheral to the project (backend)
|
||||
AddPeripheral(peripheral).then((serialNumber) => {
|
||||
peripherals.update((storedPeripherals) => {
|
||||
return {
|
||||
...storedPeripherals,
|
||||
[serialNumber]: {
|
||||
...storedPeripherals[serialNumber],
|
||||
Name: peripheral.Name,
|
||||
ProtocolName: peripheral.ProtocolName,
|
||||
SerialNumber: serialNumber,
|
||||
isSaved: true,
|
||||
},
|
||||
}})
|
||||
$needProjectSave = true
|
||||
}).catch((error) => {
|
||||
console.log("Unable to add the peripheral to the project: " + error)
|
||||
generateToast('danger', 'bx-error', $_("addPeripheralErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the peripheral from the project
|
||||
function removePeripheral(peripheral) {
|
||||
// Delete the peripheral from the project (backend)
|
||||
RemovePeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
|
||||
// If the peripheral is not detected, we can delete it form the store
|
||||
// If not, we only pass the isSaved key to false
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastDetectedProperty = peripheralsList[peripheral.SerialNumber]?.isDetected
|
||||
let needToDelete = (lastDetectedProperty !== true) ? true : false
|
||||
peripherals.update((storedPeripherals) => {
|
||||
if (needToDelete){
|
||||
delete storedPeripherals[peripheral.SerialNumber];
|
||||
return { ...storedPeripherals };
|
||||
}
|
||||
storedPeripherals[peripheral.SerialNumber].isSaved = false
|
||||
return { ...storedPeripherals };
|
||||
})
|
||||
$needProjectSave = true
|
||||
// If the peripheral is currently selected, unselect it
|
||||
if (selectedPeripheralSN == peripheral.SerialNumber) {
|
||||
selectedPeripheralSN = null
|
||||
selectedPeripheralSettings = {}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.log("Unable to remove the peripheral from the project: " + error)
|
||||
generateToast('danger', 'bx-error', $_("removePeripheralErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Select the peripheral to edit its settings
|
||||
let selectedPeripheralSN = null
|
||||
let selectedPeripheralSettings = {}
|
||||
function selectPeripheral(peripheral){
|
||||
// Load the settings array if the peripheral is detected
|
||||
if (peripheral.isSaved){
|
||||
GetPeripheralSettings(peripheral.ProtocolName, peripheral.SerialNumber).then((peripheralSettings) => {
|
||||
selectedPeripheralSettings = peripheralSettings
|
||||
// Select the current peripheral
|
||||
selectedPeripheralSN = peripheral.SerialNumber
|
||||
}).catch((error) => {
|
||||
console.log("Unable to get the peripheral settings: " + error)
|
||||
generateToast('danger', 'bx-error', $_("getPeripheralSettingsErrorToast"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Unselect the peripheral if it is disconnect
|
||||
$: {
|
||||
Object.entries($peripherals).filter(([serialNumber, peripheral]) => {
|
||||
if (!peripheral.isDetected && peripheral.isSaved && selectedPeripheralSN == serialNumber) {
|
||||
selectedPeripheralSN = null
|
||||
selectedPeripheralSettings = {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get the number of saved peripherals
|
||||
$: savedPeripheralNumber = Object.values($peripherals).filter(peripheral => peripheral.isSaved).length;
|
||||
|
||||
// Validate the peripheral settings
|
||||
function validate(settingName, settingValue){
|
||||
console.log("Peripheral 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(selectedPeripheralSettings[settingName])] || (x => x)
|
||||
selectedPeripheralSettings[settingName] = convert(settingValue)
|
||||
let peripheralProtocolName = get(peripherals)[selectedPeripheralSN].ProtocolName
|
||||
UpdatePeripheralSettings(peripheralProtocolName, selectedPeripheralSN, selectedPeripheralSettings).then(()=> {
|
||||
$needProjectSave = true
|
||||
}).catch((error) => {
|
||||
console.log("Unable to save the peripheral setting: " + error)
|
||||
generateToast('danger', 'bx-error', $_("peripheralSettingSaveErrorToast"))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hardware">
|
||||
<div style="padding: 0.5em;">
|
||||
<p style="margin-bottom: 1em;">{$_("projectHardwareAvailableLabel")}</p>
|
||||
<div class="availableHardware">
|
||||
<p style="color: var(--first-color);"><i class='bx bxs-plug'></i> {$_("projectHardwareDetectedLabel")}</p>
|
||||
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
|
||||
{#if peripheral.isDetected}
|
||||
<DeviceCard on:add={() => addPeripheral(peripheral)} on:dblclick={() => {
|
||||
if(!peripheral.isSaved)
|
||||
addPeripheral(peripheral)
|
||||
}}
|
||||
status="connected" title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/>
|
||||
{/if}
|
||||
{/each}
|
||||
<p style="color: var(--first-color);"><i class='bx bxs-network-chart' ></i> {$_("projectHardwareOthersLabel")}</p>
|
||||
<RoundedButton on:click={()=>addPeripheral({Name: "OS2L connection", ProtocolName: "OS2L"})} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0.5em; flex:2; width:100%;">
|
||||
<p style="margin-bottom: 1em;">{$_("projectHardwareSavedLabel")}</p>
|
||||
<div class="configuredHardware">
|
||||
{#if savedPeripheralNumber > 0}
|
||||
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
|
||||
{#if peripheral.isSaved}
|
||||
<DeviceCard status="{peripheral.Status}" on:delete={() => removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)} on:click={() => selectPeripheral(peripheral)}
|
||||
title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} selected={serialNumber == selectedPeripheralSN} removable signalizable/>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<i>{$_("projectHardwareEmptyLabel")}</i>
|
||||
{/if}
|
||||
</div>
|
||||
<p style="margin-bottom: 1em;">{$_("projectHardwareSettingsLabel")} (<b>{selectedPeripheralSN == null ? $_("projectHardwareNoSelection") : selectedPeripheralSN}</b>)</p>
|
||||
<div class='flexSettings'>
|
||||
{#if Object.keys(selectedPeripheralSettings).length > 0}
|
||||
{#each Object.entries(selectedPeripheralSettings) as [settingName, settingValue]}
|
||||
<div class="peripheralSetting">
|
||||
<Input on:blur={(event) => validate(settingName, event.detail.target.value)} label={$t(settingName)} type="{typeof(settingValue)}" width='100%' value="{settingValue}"/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<i>{$_("projectHardwareNoSettingLabel")}</i>
|
||||
{/if}
|
||||
|
||||
<!-- <button on:click={ftdiConnect}>Connect FTDI 0</button>
|
||||
<button on:click={ftdiActivate}>Activate FTDI 0</button>
|
||||
<div class="slidecontainer">
|
||||
<input type="range" min="0" max="255" class="slider" bind:value={sliderValue} on:input={() => ftdiSetDevice(sliderValue)}>
|
||||
</div>
|
||||
<button on:click={ftdiDeactivate}>Deactivate FTDI 0</button>
|
||||
<button on:click={ftdiDisconnect}>Disconnect FTDI 0</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.peripheralSetting{
|
||||
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);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
/* overflow: visible; */
|
||||
}
|
||||
.availableHardware::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.configuredHardware {
|
||||
background-color: var(--second-color);
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em;
|
||||
padding: 0.2em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
97
frontend/src/components/Settings/Settings.svelte
Normal file
97
frontend/src/components/Settings/Settings.svelte
Normal file
@@ -0,0 +1,97 @@
|
||||
<script lang=ts>
|
||||
import { generateToast, projectsList, showInformation, needProjectSave, peripherals } from '../../stores.js';
|
||||
import RoundedButton from "../General/RoundedButton.svelte";
|
||||
import ProjectPropertiesContent from "./ProjectPropertiesContent.svelte";
|
||||
import DropdownList from "../General/DropdownList.svelte";
|
||||
import InputsOutputsContent from "./InputsOutputsContent.svelte";
|
||||
import Tab from "../General/Tab.svelte";
|
||||
import { CreateProject, GetProjects, GetProjectInfo } from "../../../wailsjs/go/main/App";
|
||||
import { _ } from 'svelte-i18n'
|
||||
import {colors} from '../../stores.js';
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const tabs = [
|
||||
{ title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent },
|
||||
{ title: $_("projectInputOutputTab"), icon: 'bxs-plug', tooltip: $_("projectInputOutputTooltip"), component: InputsOutputsContent },
|
||||
];
|
||||
|
||||
// Refresh the projects list
|
||||
let choices = new Map()
|
||||
function loadProjectsList(){
|
||||
GetProjects().then((projects) => {
|
||||
choices = new Map(projects.map(item => [item.Save, item.Name]));
|
||||
$projectsList = projects
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to get the projects list: ${error}`)
|
||||
generateToast('danger', 'bx-error', $_("projectsLoadErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Unsave peripherals from the store and remove the disconnected peripherals
|
||||
function unsavePeripherals(){
|
||||
peripherals.update((storedPeripherals) => {
|
||||
// Set all the isSaved keys to false and delete the disconnected peripherals
|
||||
for (let peripheralID in storedPeripherals) {
|
||||
storedPeripherals[peripheralID].isSaved = false
|
||||
if (!storedPeripherals[peripheralID].isDetected) {
|
||||
delete storedPeripherals[peripheralID]
|
||||
}
|
||||
}
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
}
|
||||
|
||||
// Load the saved peripherals into the store
|
||||
function loadPeripherals(peripheralsInfo){
|
||||
peripherals.update((storedPeripherals) => {
|
||||
// Add the saved peripherals of the project
|
||||
// If already exists pass the isSaved key to true, if not create the peripheral and set it to disconnected
|
||||
for (let peripheralID in peripheralsInfo){
|
||||
// Add the peripheral to the list of peripherals, with the last isDetected key and the isSaved key to true
|
||||
let lastDetectedKey = storedPeripherals[peripheralID]?.isDetected
|
||||
storedPeripherals[peripheralID] = peripheralsInfo[peripheralID]
|
||||
storedPeripherals[peripheralID].isDetected = (lastDetectedKey === true) ? true : false
|
||||
storedPeripherals[peripheralID].isSaved = true
|
||||
}
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
}
|
||||
|
||||
// Open the selected project
|
||||
function openSelectedProject(event){
|
||||
let selectedOption = event.detail.key
|
||||
// Open the selected project
|
||||
GetProjectInfo(selectedOption).then((projectInfo) => {
|
||||
$showInformation = projectInfo.ShowInfo
|
||||
// Remove the saved peripherals ofthe current project
|
||||
unsavePeripherals()
|
||||
// Load the new project peripherals
|
||||
loadPeripherals(projectInfo.PeripheralsInfo)
|
||||
needProjectSave.set(false)
|
||||
generateToast('info', 'bx-folder-open', $_("projectOpenedToast") + ' <b>' + projectInfo.ShowInfo.Name + '</b>')
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to open the project: ${error}`)
|
||||
generateToast('danger', 'bx-error', $_("projectOpenErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
function initializeNewProject(){
|
||||
// Instanciate a new project
|
||||
CreateProject().then((showInfo) => {
|
||||
$showInformation = showInfo
|
||||
// Remove the saved peripherals ofthe current project
|
||||
unsavePeripherals()
|
||||
$needProjectSave = true
|
||||
generateToast('info', 'bxs-folder-plus', $_("projectCreatedToast"))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Project buttons -->
|
||||
<RoundedButton on:click={initializeNewProject} text={$_("newProjectString")} icon='bxs-plus-square' tooltip={$_("newProjectTooltip")}/>
|
||||
<DropdownList icon='bxs-folder-open' text={$_("openProjectString")} choices={choices} tooltip={$_("openProjectTooltip")} on:click={loadProjectsList} on:selected={openSelectedProject}/>
|
||||
<!-- Project tabcontrol -->
|
||||
<Tab { tabs } maxHeight='73vh'/>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
1
frontend/src/components/Show/Show.svelte
Normal file
1
frontend/src/components/Show/Show.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Show mapping</h1>
|
||||
66
frontend/src/lang/en.json
Normal file
66
frontend/src/lang/en.json
Normal 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",
|
||||
"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 this peripheral",
|
||||
"projectHardwareAddTooltip": "Add this peripheral to project",
|
||||
"projectHardwareNoSelection": "Empty",
|
||||
"projectHardwareAvailableLabel": "Available peripherals",
|
||||
"projectHardwareSavedLabel": "Project peripherals",
|
||||
"projectHardwareDetectedLabel": "Detected",
|
||||
"projectHardwareOthersLabel": "Others",
|
||||
"projectHardwareEmptyLabel": "No hardware saved for this project",
|
||||
"projectHardwareSettingsLabel": "Peripheral settings",
|
||||
"projectHardwareNoSettingLabel": "No setting can be displayed",
|
||||
|
||||
"peripheralArrivalToast": "Peripheral inserted:",
|
||||
"peripheralRemovalToast": "Peripheral removed:",
|
||||
"projectSavedToast": "The project has been saved",
|
||||
"projectSaveErrorToast": "Unable to save the project:",
|
||||
"addPeripheralErrorToast": "Unable to add this peripheral to project",
|
||||
"removePeripheralErrorToast": "Unable to remove this peripheral from project",
|
||||
"os2lPeripheralCreatedToast": "Your OS2L peripheral has been created",
|
||||
"os2lPeripheralCreateErrorToast": "Unable to create the OS2L peripheral",
|
||||
"getPeripheralSettingsErrorToast": "Unable to get the peripheral settings",
|
||||
"projectsLoadErrorToast": "Unable to get the projects list",
|
||||
"projectOpenedToast": "The project was opened:",
|
||||
"projectOpenErrorToast": "Unable to open the project",
|
||||
"projectCreatedToast": "The project was created",
|
||||
"peripheralSettingSaveErrorToast": "Unable to save the peripheral settings",
|
||||
|
||||
"os2lIp": "OS2L server IP",
|
||||
"os2lPort": "OS2L server port"
|
||||
|
||||
}
|
||||
27
frontend/src/main.js
Normal file
27
frontend/src/main.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import App from './App.svelte';
|
||||
|
||||
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
|
||||
|
||||
import {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;
|
||||
38
frontend/src/stores.js
Normal file
38
frontend/src/stores.js
Normal file
@@ -0,0 +1,38 @@
|
||||
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 peripherals = writable({})
|
||||
79
frontend/src/style.css
Normal file
79
frontend/src/style.css
Normal 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
2
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
7
frontend/vite.config.js
Normal file
7
frontend/vite.config.js
Normal 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()]
|
||||
})
|
||||
44
go.mod
Normal file
44
go.mod
Normal file
@@ -0,0 +1,44 @@
|
||||
module dmxconnect
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.3
|
||||
|
||||
require (
|
||||
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/wailsapp/wails/v2 v2.9.1
|
||||
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
|
||||
105
go.sum
Normal file
105
go.sum
Normal file
@@ -0,0 +1,105 @@
|
||||
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/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79 h1:CA1UHN3RuY70DlC0RlvgtB1e8h3kYzmvK7s8CFe+Ohw=
|
||||
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79/go.mod h1:oBuZjmjlKSj9CZKrNhcx/adNhHiiE0hZknECjIP8Z0Q=
|
||||
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=
|
||||
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=
|
||||
264
hardware/FTDIFinder.go
Normal file
264
hardware/FTDIFinder.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
goRuntime "runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
ftdiFinderExecutableName = "FTDI_finder.exe"
|
||||
)
|
||||
|
||||
// FTDIFinder represents how the protocol is defined
|
||||
type FTDIFinder struct {
|
||||
findTicker time.Ticker // Peripherals find ticker
|
||||
foundPeripherals map[string]PeripheralInfo // The list of peripherals handled by this finder
|
||||
registeredPeripherals map[string]FTDIPeripheral // The list of found peripherals
|
||||
scanChannel chan struct{} // The channel to trigger a scan event
|
||||
goWait sync.WaitGroup // Check goroutines execution
|
||||
}
|
||||
|
||||
// NewFTDIFinder creates a new FTDI finder
|
||||
func NewFTDIFinder(findPeriod time.Duration) *FTDIFinder {
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder created")
|
||||
return &FTDIFinder{
|
||||
findTicker: *time.NewTicker(findPeriod),
|
||||
foundPeripherals: make(map[string]PeripheralInfo),
|
||||
registeredPeripherals: make(map[string]FTDIPeripheral),
|
||||
scanChannel: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPeripheral registers a new peripheral
|
||||
func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) {
|
||||
ftdiPeripheral, err := NewFTDIPeripheral(peripheralData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create the FTDI peripheral: %v", err)
|
||||
}
|
||||
f.registeredPeripherals[peripheralData.SerialNumber] = *ftdiPeripheral
|
||||
log.Trace().Any("periph", &ftdiPeripheral).Str("file", "FTDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created")
|
||||
return peripheralData.SerialNumber, nil
|
||||
}
|
||||
|
||||
// UnregisterPeripheral unregisters an existing peripheral
|
||||
func (f *FTDIFinder) UnregisterPeripheral(peripheralID string) error {
|
||||
peripheral, registered := f.registeredPeripherals[peripheralID]
|
||||
if registered {
|
||||
err := peripheral.Disconnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(f.registeredPeripherals, peripheralID)
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed third-party/ftdi/detectFTDI.exe
|
||||
var finderExe []byte
|
||||
|
||||
// Initialize initializes the FTDI finder
|
||||
func (f *FTDIFinder) Initialize() error {
|
||||
// Check platform
|
||||
if goRuntime.GOOS != "windows" {
|
||||
log.Error().Str("file", "FTDIFinder").Str("platform", goRuntime.GOOS).Msg("FTDI finder not compatible with your platform")
|
||||
return fmt.Errorf("the FTDI finder is not compatible with your platform yet (%s)", goRuntime.GOOS)
|
||||
}
|
||||
// Create the FTDI executables
|
||||
err := createExecutable(ftdiFinderExecutableName, finderExe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createExecutable creates and writes an executable to the temporary directory of the system
|
||||
func createExecutable(fileName string, storedFile []byte) error {
|
||||
tempFile, err := os.Create(filepath.Join(os.TempDir(), fileName))
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to create an FTDI executable")
|
||||
return err
|
||||
}
|
||||
log.Trace().Str("file", "FTDIFinder").Str("filePath", tempFile.Name()).Msg("FTDI executable created")
|
||||
|
||||
// Write the embedded executable to the temp file
|
||||
if _, err := tempFile.Write(storedFile); err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to write the content to an FTDI executable")
|
||||
return err
|
||||
}
|
||||
tempFile.Close()
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("fileName", fileName).Msg("FTDI executable written")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the finder and search for peripherals
|
||||
func (f *FTDIFinder) Start(ctx context.Context) error {
|
||||
f.goWait.Add(1)
|
||||
go func() {
|
||||
defer f.goWait.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-f.findTicker.C:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals")
|
||||
}
|
||||
case <-f.scanChannel:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceScan explicily asks for scanning peripherals
|
||||
func (f *FTDIFinder) ForceScan() {
|
||||
f.scanChannel <- struct{}{}
|
||||
}
|
||||
|
||||
// Stop stops the finder
|
||||
func (f *FTDIFinder) Stop() error {
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...")
|
||||
// Wait for goroutines to stop
|
||||
f.goWait.Wait()
|
||||
// Stop the ticker
|
||||
f.findTicker.Stop()
|
||||
// Delete the FTDI executable files
|
||||
fileToDelete := filepath.Join(os.TempDir(), ftdiFinderExecutableName)
|
||||
err := os.Remove(fileToDelete)
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "FTDIFinder").Str("fileName", fileToDelete).AnErr("error", err).Msg("unable to remove the executable file")
|
||||
}
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (f *FTDIFinder) GetName() string {
|
||||
return "FTDI"
|
||||
}
|
||||
|
||||
// GetPeripheralSettings gets the peripheral settings
|
||||
func (f *FTDIFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.registeredPeripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder")
|
||||
return nil, fmt.Errorf("unable to found the peripheral")
|
||||
}
|
||||
log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder")
|
||||
return peripheral.GetSettings(), nil
|
||||
}
|
||||
|
||||
// SetPeripheralSettings sets the peripheral settings
|
||||
func (f *FTDIFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.registeredPeripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder")
|
||||
return fmt.Errorf("unable to found the peripheral")
|
||||
}
|
||||
log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder")
|
||||
return peripheral.SetSettings(settings)
|
||||
}
|
||||
|
||||
// scanPeripherals scans the FTDI peripherals
|
||||
func (f *FTDIFinder) scanPeripherals(ctx context.Context) error {
|
||||
detectionCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI scan triggered")
|
||||
|
||||
finder := exec.CommandContext(detectionCtx, filepath.Join(os.TempDir(), ftdiFinderExecutableName))
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("has executed the FIND executable")
|
||||
|
||||
stdout, err := finder.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the stdout pipe: %s", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
stderr, err := finder.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the stderr pipe: %s", err)
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
err = finder.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find FTDI peripherals: %s", err)
|
||||
}
|
||||
|
||||
scannerErr := bufio.NewScanner(stderr)
|
||||
for scannerErr.Scan() {
|
||||
return fmt.Errorf("unable to find FTDI peripherals: %s", scannerErr.Text())
|
||||
}
|
||||
|
||||
temporaryPeripherals := make(map[string]PeripheralInfo)
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
peripheralString := scanner.Text()
|
||||
// The program output is like '0:1:2:3' where 0 is the location, 1 is the S/N, 2 is the name and 3 is the open flag [O/C]
|
||||
peripheralInfo := strings.Split(peripheralString, ":")
|
||||
|
||||
log.Trace().Str("file", "FTDIFinder").Str("scannedString", peripheralString).Str("peripheralOpenFlag", peripheralInfo[3]).Str("peripheralName", peripheralInfo[2]).Str("peripheralSN", peripheralInfo[1]).Msg("new FTDI peripheral detected")
|
||||
// Convert the location to an integer
|
||||
location, err := strconv.Atoi(peripheralInfo[0])
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("no location provided for this FTDI peripheral")
|
||||
location = -1
|
||||
}
|
||||
// Add the peripheral info to the found list
|
||||
temporaryPeripherals[peripheralInfo[1]] = PeripheralInfo{
|
||||
Name: peripheralInfo[2],
|
||||
SerialNumber: peripheralInfo[1],
|
||||
IsOpen: peripheralInfo[3] == "O",
|
||||
ProtocolName: "FTDI",
|
||||
}
|
||||
|
||||
// If this peripheral is already registered, connect it and activate it
|
||||
peripheral, registered := f.registeredPeripherals[peripheralInfo[1]]
|
||||
if registered {
|
||||
runtime.EventsEmit(ctx, string(PeripheralStatus), peripheral.info, "connecting")
|
||||
err := peripheral.Connect(ctx, location)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralInfo[1]).Msg("unable to connect the peripheral")
|
||||
}
|
||||
runtime.EventsEmit(ctx, string(PeripheralStatus), peripheral.info, "deactivated")
|
||||
time.Sleep(2 * time.Second)
|
||||
err = peripheral.Activate(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralInfo[1]).Msg("unable to activate the peripheral")
|
||||
}
|
||||
runtime.EventsEmit(ctx, string(PeripheralStatus), peripheral.info, "activated")
|
||||
}
|
||||
|
||||
log.Trace().Any("periph", temporaryPeripherals).Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("successfully added the FTDI peripheral to the finder")
|
||||
}
|
||||
// Emit the peripherals changes to the front
|
||||
emitPeripheralsChanges(ctx, f.foundPeripherals, temporaryPeripherals)
|
||||
// Store the new peripherals list
|
||||
f.foundPeripherals = temporaryPeripherals
|
||||
return nil
|
||||
}
|
||||
140
hardware/FTDIPeripheral.go
Normal file
140
hardware/FTDIPeripheral.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"io"
|
||||
|
||||
"unsafe"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../build/bin -ldmxSender
|
||||
#include "cpp/include/dmxSenderBridge.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// FTDIPeripheral contains the data of an FTDI peripheral
|
||||
type FTDIPeripheral struct {
|
||||
info PeripheralInfo // The peripheral basic data
|
||||
settings map[string]interface{} // The settings of the peripheral
|
||||
dmxDevice unsafe.Pointer // The command object for piloting the DMX ouptut
|
||||
stdin io.WriteCloser // For writing in the DMX sender
|
||||
stdout io.ReadCloser // For reading from the DMX sender
|
||||
stderr io.ReadCloser // For reading the errors
|
||||
disconnectChan chan struct{} // Channel to cancel the connection
|
||||
errorsChan chan error // Channel to get the errors
|
||||
}
|
||||
|
||||
// NewFTDIPeripheral creates a new FTDI peripheral
|
||||
func NewFTDIPeripheral(info PeripheralInfo) (*FTDIPeripheral, error) {
|
||||
log.Info().Str("file", "FTDIPeripheral").Str("name", info.Name).Str("s/n", info.SerialNumber).Msg("FTDI peripheral created")
|
||||
settings := make(map[string]interface{})
|
||||
return &FTDIPeripheral{
|
||||
info: info,
|
||||
dmxDevice: C.dmx_create(),
|
||||
settings: settings,
|
||||
disconnectChan: make(chan struct{}),
|
||||
errorsChan: make(chan error, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect connects the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Connect(ctx context.Context, location int) error {
|
||||
// Check if the device has already been created
|
||||
if p.dmxDevice == nil {
|
||||
return errors.Errorf("the DMX device has not been created!")
|
||||
}
|
||||
|
||||
// Connect the peripheral
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("connecting FTDI peripheral...")
|
||||
err := C.dmx_connect(p.dmxDevice)
|
||||
if err {
|
||||
log.Error().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("unable to connect the DMX device")
|
||||
return errors.Errorf("Unable to connect the DMX Device on the specified port")
|
||||
}
|
||||
|
||||
//TODO: Destroy the object when context is done to avoid memory loss
|
||||
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device connected successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Disconnect() error {
|
||||
// Check if the device has already been created
|
||||
if p.dmxDevice == nil {
|
||||
return errors.Errorf("the DMX device has not been created!")
|
||||
}
|
||||
|
||||
//TODO: What actions for disconnecting the DMX device?
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate activates the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Activate(ctx context.Context) error {
|
||||
// Check if the device has already been created
|
||||
if p.dmxDevice == nil {
|
||||
return errors.Errorf("the DMX device has not been created!")
|
||||
}
|
||||
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("activating FTDI peripheral...")
|
||||
|
||||
C.dmx_activate(p.dmxDevice)
|
||||
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device activated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Deactivate(ctx context.Context) error {
|
||||
// Check if the device has already been created
|
||||
if p.dmxDevice == nil {
|
||||
return errors.Errorf("the DMX device has not been created!")
|
||||
}
|
||||
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("deactivating FTDI peripheral...")
|
||||
|
||||
C.dmx_deactivate(p.dmxDevice)
|
||||
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device deactivated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSettings sets a specific setting for this peripheral
|
||||
func (p *FTDIPeripheral) SetSettings(settings map[string]interface{}) error {
|
||||
p.settings = settings
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty sends a command to the specified device
|
||||
func (p *FTDIPeripheral) SetDeviceProperty(ctx context.Context, channelNumber uint32, channelValue byte) error {
|
||||
// Check if the device has already been created
|
||||
if p.dmxDevice == nil {
|
||||
return errors.Errorf("the DMX device has not been created!")
|
||||
}
|
||||
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("setting device property on FTDI peripheral...")
|
||||
|
||||
C.dmx_setValue(p.dmxDevice, C.int(channelNumber), C.int(channelValue))
|
||||
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("device property set on FTDI peripheral successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSettings gets the peripheral settings
|
||||
func (p *FTDIPeripheral) GetSettings() map[string]interface{} {
|
||||
return p.settings
|
||||
}
|
||||
|
||||
// GetInfo gets all the peripheral information
|
||||
func (p *FTDIPeripheral) GetInfo() PeripheralInfo {
|
||||
return p.info
|
||||
}
|
||||
210
hardware/MIDIFinder.go
Normal file
210
hardware/MIDIFinder.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattrtaylor/go-rtmidi"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// MIDIFinder represents how the protocol is defined
|
||||
type MIDIFinder struct {
|
||||
findTicker time.Ticker // Peripherals find ticker
|
||||
registeredPeripherals map[string]MIDIPeripheral // The list of peripherals
|
||||
scanChannel chan struct{} // The channel to trigger a scan event
|
||||
goWait sync.WaitGroup // Check goroutines execution
|
||||
}
|
||||
|
||||
// NewMIDIFinder creates a new DMXUSB protocol
|
||||
func NewMIDIFinder(findPeriod time.Duration) *MIDIFinder {
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder created")
|
||||
return &MIDIFinder{
|
||||
findTicker: *time.NewTicker(findPeriod),
|
||||
registeredPeripherals: make(map[string]MIDIPeripheral),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the MIDI driver
|
||||
func (f *MIDIFinder) Initialize() error {
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPeripheral registers a new peripheral
|
||||
func (f *MIDIFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) {
|
||||
peripheral, err := NewMIDIPeripheral(peripheralData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create the MIDI peripheral: %v", err)
|
||||
}
|
||||
f.registeredPeripherals[peripheralData.SerialNumber] = *peripheral
|
||||
log.Trace().Any("periph", &peripheral).Str("file", "MIDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created")
|
||||
return peripheralData.SerialNumber, nil
|
||||
}
|
||||
|
||||
// UnregisterPeripheral unregisters an existing peripheral
|
||||
func (f *MIDIFinder) UnregisterPeripheral(peripheralID string) error {
|
||||
peripheral, registered := f.registeredPeripherals[peripheralID]
|
||||
if registered {
|
||||
err := peripheral.Disconnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(f.registeredPeripherals, peripheralID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the finder and search for peripherals
|
||||
func (f *MIDIFinder) Start(ctx context.Context) error {
|
||||
f.goWait.Add(1)
|
||||
go func() {
|
||||
defer f.goWait.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-f.findTicker.C:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals")
|
||||
}
|
||||
case <-f.scanChannel:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the finder
|
||||
func (f *MIDIFinder) Stop() error {
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("stopping the MIDI finder...")
|
||||
// Wait for goroutines to stop
|
||||
f.goWait.Wait()
|
||||
// Stop the ticker
|
||||
f.findTicker.Stop()
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (f *MIDIFinder) GetName() string {
|
||||
return "MIDI"
|
||||
}
|
||||
|
||||
// GetPeripheralSettings gets the peripheral settings
|
||||
func (f *MIDIFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.registeredPeripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder")
|
||||
return nil, fmt.Errorf("unable to found the peripheral")
|
||||
}
|
||||
log.Debug().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder")
|
||||
return peripheral.GetSettings(), nil
|
||||
}
|
||||
|
||||
// SetPeripheralSettings sets the peripheral settings
|
||||
func (f *MIDIFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.registeredPeripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder")
|
||||
return fmt.Errorf("unable to found the peripheral")
|
||||
}
|
||||
log.Debug().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder")
|
||||
return peripheral.SetSettings(settings)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// ForceScan explicily asks for scanning peripherals
|
||||
func (f *MIDIFinder) ForceScan() {
|
||||
f.scanChannel <- struct{}{}
|
||||
}
|
||||
|
||||
// scanPeripherals scans the MIDI peripherals
|
||||
func (f *MIDIFinder) scanPeripherals(ctx context.Context) error {
|
||||
// midiPeripherals := make(map[string]Peripheral)
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("opening MIDI scanner port...")
|
||||
midiScanner, err := rtmidi.NewMIDIInDefault()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to open the MIDI scanner port...")
|
||||
return fmt.Errorf("unable to open the MIDI scanner: %s", err)
|
||||
}
|
||||
defer midiScanner.Close()
|
||||
midiScanner.SetCallback(func(m rtmidi.MIDIIn, b []byte, f float64) {})
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("scanning MIDI peripherals...")
|
||||
devicesCount, err := midiScanner.PortCount()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals...")
|
||||
return fmt.Errorf("unable to scan MIDI peripherals: %s", err)
|
||||
}
|
||||
for i := 0; i < devicesCount; i++ {
|
||||
portName, err := midiScanner.PortName(i)
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "MIDIPeripheral").Msg("found peripheral without a correct name, set it to unknown")
|
||||
portName = "Unknown device 0"
|
||||
}
|
||||
// Separate data
|
||||
name, location, err := splitStringAndNumber(portName)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Str("description", portName).Msg("invalid peripheral description")
|
||||
return fmt.Errorf("invalid pripheral description: %s", err)
|
||||
}
|
||||
log.Info().Str("file", "MIDIFinder").Str("name", name).Int("location", location).Msg("MIDI peripheral found")
|
||||
// Add the peripheral to the temporary list
|
||||
// sn := strings.ToLower(strings.Replace(name, " ", "_", -1))
|
||||
// midiPeripherals[sn] = NewMIDIPeripheral(name, location, sn)
|
||||
}
|
||||
// Compare with the current peripherals to detect arrivals/removals
|
||||
// removedList, addedList := comparePeripherals(f.peripherals, midiPeripherals)
|
||||
// Emit the events
|
||||
// emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
|
||||
log.Info().Str("file", "MIDIFinder").Msg("MIDI remove list emitted to the front")
|
||||
// emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
|
||||
log.Info().Str("file", "MIDIFinder").Msg("MIDI add list emitted to the front")
|
||||
// Store the new peripherals list
|
||||
// f.peripherals = midiPeripherals
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePeripheral is not implemented here
|
||||
func (f *MIDIFinder) CreatePeripheral(context.Context) (Peripheral, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeletePeripheral is not implemented here
|
||||
func (f *MIDIFinder) DeletePeripheral(serialNumber string) error {
|
||||
return nil
|
||||
}
|
||||
64
hardware/MIDIPeripheral.go
Normal file
64
hardware/MIDIPeripheral.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// MIDIPeripheral contains the data of a MIDI peripheral
|
||||
type MIDIPeripheral struct {
|
||||
info PeripheralInfo // The peripheral info
|
||||
location int // The location of the peripheral
|
||||
settings map[string]interface{} // The settings of the peripheral
|
||||
}
|
||||
|
||||
// NewMIDIPeripheral creates a new MIDI peripheral
|
||||
func NewMIDIPeripheral(peripheralData PeripheralInfo) (*MIDIPeripheral, error) {
|
||||
log.Trace().Str("file", "MIDIPeripheral").Str("name", peripheralData.Name).Str("s/n", peripheralData.SerialNumber).Msg("MIDI peripheral created")
|
||||
return &MIDIPeripheral{
|
||||
info: peripheralData,
|
||||
settings: peripheralData.Settings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect connects the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Connect(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate activates the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Activate(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Deactivate(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSettings sets a specific setting for this peripheral
|
||||
func (p *MIDIPeripheral) SetSettings(settings map[string]interface{}) error {
|
||||
p.settings = settings
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty - not implemented for this kind of peripheral
|
||||
func (p *MIDIPeripheral) SetDeviceProperty(context.Context, uint32, uint32, byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSettings gets the peripheral settings
|
||||
func (p *MIDIPeripheral) GetSettings() map[string]interface{} {
|
||||
return p.settings
|
||||
}
|
||||
|
||||
// GetInfo gets the peripheral information
|
||||
func (p *MIDIPeripheral) GetInfo() PeripheralInfo {
|
||||
return p.info
|
||||
}
|
||||
106
hardware/OS2LFinder.go
Normal file
106
hardware/OS2LFinder.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// OS2LFinder represents how the protocol is defined
|
||||
type OS2LFinder struct {
|
||||
registeredPeripherals map[string]OS2LPeripheral // The list of found peripherals
|
||||
}
|
||||
|
||||
// NewOS2LFinder creates a new OS2L finder
|
||||
func NewOS2LFinder() *OS2LFinder {
|
||||
log.Trace().Str("file", "OS2LFinder").Msg("OS2L finder created")
|
||||
return &OS2LFinder{
|
||||
registeredPeripherals: make(map[string]OS2LPeripheral),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the finder
|
||||
func (f *OS2LFinder) Initialize() error {
|
||||
log.Trace().Str("file", "OS2LFinder").Msg("OS2L finder initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPeripheral registers a new peripheral
|
||||
func (f *OS2LFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) {
|
||||
// Create a random serial number for this peripheral
|
||||
peripheralData.SerialNumber = strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32)))
|
||||
log.Trace().Str("file", "OS2LFinder").Str("serialNumber", peripheralData.SerialNumber).Msg("OS2L peripheral created")
|
||||
|
||||
os2lPeripheral, err := NewOS2LPeripheral(peripheralData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create the OS2L peripheral: %v", err)
|
||||
}
|
||||
// Connect this peripheral
|
||||
err = os2lPeripheral.Connect(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
f.registeredPeripherals[peripheralData.SerialNumber] = *os2lPeripheral
|
||||
log.Trace().Any("periph", &os2lPeripheral).Str("file", "OS2LFinder").Str("peripheralName", peripheralData.Name).Msg("OS2L peripheral has been created")
|
||||
return peripheralData.SerialNumber, nil
|
||||
}
|
||||
|
||||
// UnregisterPeripheral unregisters an existing peripheral
|
||||
func (f *OS2LFinder) UnregisterPeripheral(peripheralID string) error {
|
||||
peripheral, registered := f.registeredPeripherals[peripheralID]
|
||||
if registered {
|
||||
err := peripheral.Disconnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(f.registeredPeripherals, peripheralID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (f *OS2LFinder) GetName() string {
|
||||
return "OS2L"
|
||||
}
|
||||
|
||||
// GetPeripheralSettings gets the peripheral settings
|
||||
func (f *OS2LFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.registeredPeripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the OS2L finder")
|
||||
return nil, fmt.Errorf("unable to found the peripheral")
|
||||
}
|
||||
log.Debug().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the OS2L finder")
|
||||
return peripheral.GetSettings(), nil
|
||||
}
|
||||
|
||||
// SetPeripheralSettings sets the peripheral settings
|
||||
func (f *OS2LFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.registeredPeripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder")
|
||||
return fmt.Errorf("unable to found the peripheral")
|
||||
}
|
||||
log.Debug().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder")
|
||||
return peripheral.SetSettings(settings)
|
||||
}
|
||||
|
||||
// Start starts the finder
|
||||
func (f *OS2LFinder) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops this finder
|
||||
func (f *OS2LFinder) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceScan scans the interfaces (not implemented)
|
||||
func (f *OS2LFinder) ForceScan() {
|
||||
}
|
||||
104
hardware/OS2LPeripheral.go
Normal file
104
hardware/OS2LPeripheral.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// OS2LPeripheral contains the data of an OS2L peripheral
|
||||
type OS2LPeripheral struct {
|
||||
info PeripheralInfo // The basic info for this peripheral
|
||||
serverIP string // OS2L server IP
|
||||
serverPort int // OS2L server port
|
||||
}
|
||||
|
||||
// NewOS2LPeripheral creates a new OS2L peripheral
|
||||
func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) {
|
||||
log.Trace().Str("file", "OS2LPeripheral").Str("name", peripheralData.Name).Str("s/n", peripheralData.SerialNumber).Msg("OS2L peripheral created")
|
||||
return &OS2LPeripheral{
|
||||
info: peripheralData,
|
||||
serverIP: "127.0.0.1",
|
||||
serverPort: 9005,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect connects the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Connect(ctx context.Context) error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected")
|
||||
go func() {
|
||||
runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "connecting")
|
||||
time.Sleep(5 * time.Second)
|
||||
runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "disconnected")
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Disconnect() error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral disconnected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate activates the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Activate(ctx context.Context) error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral activated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Deactivate(ctx context.Context) error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral deactivated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSettings sets a specific setting for this peripheral
|
||||
func (p *OS2LPeripheral) SetSettings(settings map[string]interface{}) error {
|
||||
// Check if the IP exists
|
||||
serverIP, found := settings["os2lIp"]
|
||||
if !found {
|
||||
return fmt.Errorf("Unable to find the OS2L server IP")
|
||||
}
|
||||
// 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 {
|
||||
return fmt.Errorf("Unable to find the OS2L server port")
|
||||
}
|
||||
// Check if it is a float and convert to int
|
||||
portFloat, ok := serverPort.(float64)
|
||||
if ok {
|
||||
p.serverPort = int(portFloat)
|
||||
} else {
|
||||
return fmt.Errorf("The specified port is not an int")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty - not implemented for this kind of peripheral
|
||||
func (p *OS2LPeripheral) SetDeviceProperty(context.Context, uint32, uint32, byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSettings gets the peripheral settings
|
||||
func (p *OS2LPeripheral) GetSettings() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"os2lIp": p.serverIP,
|
||||
"os2lPort": p.serverPort,
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo gets the peripheral information
|
||||
func (p *OS2LPeripheral) GetInfo() PeripheralInfo {
|
||||
return p.info
|
||||
}
|
||||
21
hardware/cpp/generate.bat
Normal file
21
hardware/cpp/generate.bat
Normal file
@@ -0,0 +1,21 @@
|
||||
@REM windres dmxSender.rc dmxSender.o
|
||||
@REM windres detectFTDI.rc detectFTDI.o
|
||||
|
||||
@REM g++ -o dmxSender.exe dmxSender.cpp dmxSender.o -I"include" -L"lib" -lftd2xx -mwindows
|
||||
@REM g++ -o detectFTDI.exe detectFTDI.cpp detectFTDI.o -I"include" -L"lib" -lftd2xx -mwindows
|
||||
|
||||
@REM g++ -o dmxSender.exe dmxSender.cpp -I"include" -L"lib" -lftd2xx
|
||||
@REM g++ -o detectFTDI.exe detectFTDI.cpp -I"include" -L"lib" -lftd2xx
|
||||
|
||||
@REM g++ -c dmxSender.cpp -o dmxSender.o -I"include" -L"lib" -lftd2xx -mwindows
|
||||
|
||||
@REM g++ -c dmxSender_wrapper.cpp -o dmxSender_wrapper.o -I"include" -L"lib" -lftd2xx -mwindows
|
||||
|
||||
@REM g++ -shared -o ../../build/bin/libdmxSender.dll src/dmxSender.cpp -fPIC -Wl,--out-implib,../../build/bin/libdmxSender.dll.a -L"lib" -lftd2xx
|
||||
|
||||
@REM Compiling DMXSENDER library
|
||||
|
||||
g++ -shared -o ../../build/bin/libdmxSender.dll src/dmxSender.cpp -fPIC -L"lib" -lftd2xx
|
||||
|
||||
|
||||
@REM g++ -shared -o libdmxSender.so dmxSender.cpp -fPIC -I"include" -L"lib" -lftd2xx -mwindows
|
||||
15
hardware/cpp/include/dmxSenderBridge.h
Normal file
15
hardware/cpp/include/dmxSenderBridge.h
Normal file
@@ -0,0 +1,15 @@
|
||||
// Declare the C++ function from the shared library
|
||||
|
||||
typedef void DMXDevice;
|
||||
|
||||
extern DMXDevice* dmx_create();
|
||||
|
||||
extern void* dmx_destroy(DMXDevice* dev);
|
||||
|
||||
extern bool dmx_connect(DMXDevice* dev);
|
||||
|
||||
extern void dmx_activate(DMXDevice* dev);
|
||||
|
||||
extern void dmx_deactivate(DMXDevice* dev);
|
||||
|
||||
extern void dmx_setValue(DMXDevice* dev, int channel, int value);
|
||||
BIN
hardware/cpp/lib/ftd2xx.lib
Normal file
BIN
hardware/cpp/lib/ftd2xx.lib
Normal file
Binary file not shown.
127
hardware/cpp/src/dmxSender.cpp
Normal file
127
hardware/cpp/src/dmxSender.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
//dmxSender.cpp
|
||||
|
||||
#include "dmxSender.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)
|
||||
|
||||
// Initialize default values for starting the DMX device
|
||||
DMXDevice::DMXDevice(){
|
||||
ftHandle = nullptr;
|
||||
isOutputActivated = false;
|
||||
}
|
||||
|
||||
// Properly close the DMX device
|
||||
DMXDevice::~DMXDevice(){
|
||||
deactivate();
|
||||
if (ftHandle != nullptr){
|
||||
FT_Close(ftHandle);
|
||||
ftHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect the device on a specific port
|
||||
bool DMXDevice::connect(int port){
|
||||
ftStatus = FT_Open(port, &ftHandle);
|
||||
if (ftStatus != FT_OK) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ftStatus = FT_SetBaudRate(ftHandle, 250000);
|
||||
ftStatus |= FT_SetDataCharacteristics(ftHandle, 8, FT_STOP_BITS_2, FT_PARITY_NONE); // 8 bits, no parity, 1 stop bit
|
||||
ftStatus |= FT_SetFlowControl(ftHandle, FT_FLOW_NONE, 0, 0);
|
||||
if (ftStatus != FT_OK) {
|
||||
FT_Close(ftHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Send the DMX frames
|
||||
std::thread updateThread([this]() {
|
||||
this->sendDMX(ftHandle);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Activate the DMX flow
|
||||
void DMXDevice::activate(){
|
||||
isOutputActivated.store(true);
|
||||
}
|
||||
|
||||
// Deactivate the DMX flow
|
||||
void DMXDevice::deactivate(){
|
||||
isOutputActivated.store(false);
|
||||
}
|
||||
|
||||
// Set the value of a DMX channe
|
||||
void DMXDevice::setValue(int channel, int value){
|
||||
dmxData[channel].store(value);
|
||||
}
|
||||
|
||||
// Send a break line
|
||||
void DMXDevice::sendBreak(FT_HANDLE ftHandle) {
|
||||
FT_SetBreakOn(ftHandle); // Set BREAK ON
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(BREAK_DURATION_US));
|
||||
FT_SetBreakOff(ftHandle); // Set BREAK OFF
|
||||
}
|
||||
|
||||
// Continuously send the DMX frame
|
||||
void DMXDevice::sendDMX(FT_HANDLE ftHandle) {
|
||||
while (true) {
|
||||
if(isOutputActivated){
|
||||
// Send the BREAK
|
||||
sendBreak(ftHandle);
|
||||
|
||||
// Send the MAB
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(MAB_DURATION_US));
|
||||
|
||||
DWORD bytesWritten = 0;
|
||||
|
||||
// Send the DMX frame
|
||||
FT_STATUS status = FT_Write(ftHandle, dmxData, DMX_CHANNELS, &bytesWritten);
|
||||
if (status != FT_OK || bytesWritten != DMX_CHANNELS) { // Error detected when trying to send the frame. Deactivate the line.
|
||||
deactivate();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait before sending the next frame
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(INTERVAL - BREAK_DURATION_US - MAB_DURATION_US));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
bool dmx_connect(DMXDevice* dev, int port) {
|
||||
return dev->connect(port);
|
||||
}
|
||||
|
||||
// Activate a DMX device
|
||||
void dmx_activate(DMXDevice* dev) {
|
||||
dev->activate();
|
||||
}
|
||||
|
||||
// Deactivate a DMX device
|
||||
void dmx_deactivate(DMXDevice* dev) {
|
||||
dev->deactivate();
|
||||
}
|
||||
|
||||
// Set the channel value of a DMX device
|
||||
void dmx_setValue(DMXDevice* dev, int channel, int value) {
|
||||
dev->setValue(channel, value);
|
||||
}
|
||||
}
|
||||
49
hardware/cpp/src/dmxSender.h
Normal file
49
hardware/cpp/src/dmxSender.h
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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)
|
||||
|
||||
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
|
||||
bool connect(int port);
|
||||
|
||||
// Activate the DMX flow
|
||||
void activate();
|
||||
|
||||
// Deactivate the DMX flow
|
||||
void deactivate();
|
||||
|
||||
// Set the value of a DMX channel
|
||||
void setValue(int channel, int value);
|
||||
|
||||
private:
|
||||
FT_STATUS ftStatus; // FTDI peripheral status
|
||||
FT_HANDLE ftHandle = nullptr; // FTDI object
|
||||
std::atomic<unsigned char> 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
|
||||
void sendBreak(FT_HANDLE ftHandle);
|
||||
|
||||
// Continuously send the DMX frame
|
||||
void sendDMX(FT_HANDLE ftHandle);
|
||||
};
|
||||
1667
hardware/cpp/src/ftd2xx.h
Normal file
1667
hardware/cpp/src/ftd2xx.h
Normal file
File diff suppressed because it is too large
Load Diff
153
hardware/hardware.go
Normal file
153
hardware/hardware.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// PeripheralEvent is trigger by the finders when the scan is complete
|
||||
type PeripheralEvent string
|
||||
|
||||
const (
|
||||
// PeripheralArrival is triggerd when a peripheral has been connected to the system
|
||||
PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL"
|
||||
// PeripheralRemoval is triggered when a peripheral has been disconnected from the system
|
||||
PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL"
|
||||
// PeripheralStatus is triggered when a peripheral status has been updated (disconnected - connecting - connected)
|
||||
PeripheralStatus PeripheralEvent = "PERIPHERAL_STATUS"
|
||||
// debounceDuration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
debounceTimer *time.Timer
|
||||
)
|
||||
|
||||
// HardwareManager is the class who manages the hardware
|
||||
type HardwareManager struct {
|
||||
finders map[string]PeripheralFinder // The map of peripherals finders
|
||||
peripherals []Peripheral // The current list of peripherals
|
||||
peripheralsScanTrigger chan struct{} // Trigger the peripherals scans
|
||||
goWait sync.WaitGroup // Wait for goroutines to terminate
|
||||
}
|
||||
|
||||
// NewHardwareManager creates a new HardwareManager
|
||||
func NewHardwareManager() *HardwareManager {
|
||||
log.Trace().Str("package", "hardware").Msg("Hardware instance created")
|
||||
return &HardwareManager{
|
||||
finders: make(map[string]PeripheralFinder),
|
||||
peripherals: make([]Peripheral, 0),
|
||||
peripheralsScanTrigger: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts to find new peripheral events
|
||||
func (h *HardwareManager) Start(ctx context.Context) error {
|
||||
for finderName, finder := range h.finders {
|
||||
err := finder.Initialize()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to initialize finder")
|
||||
return err
|
||||
}
|
||||
err = finder.Start(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to start finder")
|
||||
return err
|
||||
}
|
||||
}
|
||||
h.goWait.Add(1)
|
||||
go func() {
|
||||
defer h.goWait.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-h.peripheralsScanTrigger:
|
||||
for finderName, finder := range h.finders {
|
||||
log.Trace().Str("file", "hardware").Str("finderName", finderName).Msg("force a finder to scan peripherals")
|
||||
finder.ForceScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFinder returns a register finder
|
||||
func (h *HardwareManager) GetFinder(finderName string) (PeripheralFinder, error) {
|
||||
finder, exists := h.finders[finderName]
|
||||
if !exists {
|
||||
log.Error().Str("file", "hardware").Str("finderName", finderName).Msg("unable to get the finder")
|
||||
return nil, fmt.Errorf("unable to locate the '%s' finder", finderName)
|
||||
}
|
||||
log.Debug().Str("file", "hardware").Str("finderName", finderName).Msg("got finder")
|
||||
return finder, nil
|
||||
}
|
||||
|
||||
// RegisterFinder registers a new peripherals finder
|
||||
func (h *HardwareManager) RegisterFinder(finder PeripheralFinder) {
|
||||
h.finders[finder.GetName()] = finder
|
||||
log.Info().Str("file", "hardware").Str("finderName", finder.GetName()).Msg("finder registered")
|
||||
}
|
||||
|
||||
// // GetPeripheral gets the peripheral object from the parent finder
|
||||
// func (h *HardwareManager) GetPeripheral(finderName string, peripheralID string) (Peripheral, bool) {
|
||||
// // Get the finder
|
||||
// parentFinder, found := h.finders[finderName]
|
||||
// // If no finder found, return false
|
||||
// if !found {
|
||||
// log.Error().Str("file", "hardware").Str("finderName", finderName).Msg("unable to get the finder")
|
||||
// return nil, false
|
||||
// }
|
||||
// log.Trace().Str("file", "hardware").Str("finderName", parentFinder.GetName()).Msg("finder got")
|
||||
// // Contact the finder to get the peripheral
|
||||
// return parentFinder.GetPeripheral(peripheralID)
|
||||
// }
|
||||
|
||||
// Scan scans all the peripherals for the registered finders
|
||||
func (h *HardwareManager) Scan() error {
|
||||
h.peripheralsScanTrigger <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the hardware manager
|
||||
func (h *HardwareManager) Stop() error {
|
||||
log.Trace().Str("file", "hardware").Msg("closing the hardware manager")
|
||||
// Stop each finder
|
||||
for finderName, finder := range h.finders {
|
||||
err := finder.Stop()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to stop the finder")
|
||||
}
|
||||
}
|
||||
// Wait for goroutines to finish
|
||||
h.goWait.Wait()
|
||||
log.Info().Str("file", "hardware").Msg("hardware manager stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// emitPeripheralsChanges compares the old and new peripherals to determine which ones have been added or removed.
|
||||
func emitPeripheralsChanges(ctx context.Context, oldPeripherals map[string]PeripheralInfo, newPeripherals map[string]PeripheralInfo) {
|
||||
log.Trace().Any("oldList", oldPeripherals).Any("newList", newPeripherals).Msg("emitting peripherals changes to the front")
|
||||
|
||||
// Identify removed peripherals: present in the old list but not in the new list
|
||||
for oldPeriphName := range oldPeripherals {
|
||||
if _, exists := newPeripherals[oldPeriphName]; !exists {
|
||||
runtime.EventsEmit(ctx, string(PeripheralRemoval), oldPeripherals[oldPeriphName])
|
||||
log.Trace().Str("file", "hardware").Str("event", string(PeripheralRemoval)).Msg("emit peripheral removal event")
|
||||
}
|
||||
}
|
||||
|
||||
// Identify added peripherals: present in the new list but not in the old list
|
||||
for newPeriphName := range newPeripherals {
|
||||
if _, exists := oldPeripherals[newPeriphName]; !exists {
|
||||
runtime.EventsEmit(ctx, string(PeripheralArrival), newPeripherals[newPeriphName])
|
||||
log.Trace().Str("file", "hardware").Str("event", string(PeripheralArrival)).Msg("emit peripheral arrival event")
|
||||
}
|
||||
}
|
||||
}
|
||||
38
hardware/interfaces.go
Normal file
38
hardware/interfaces.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package hardware
|
||||
|
||||
import "context"
|
||||
|
||||
// Peripheral represents the methods used to manage a peripheral (input or output hardware)
|
||||
type Peripheral interface {
|
||||
Connect(context.Context) error // Connect the peripheral
|
||||
Disconnect() error // Disconnect the peripheral
|
||||
Activate(context.Context) error // Activate the peripheral
|
||||
Deactivate(context.Context) error // Deactivate the peripheral
|
||||
SetSettings(map[string]interface{}) error // Set a peripheral setting
|
||||
SetDeviceProperty(context.Context, uint32, byte) error // Update a device property
|
||||
|
||||
GetInfo() PeripheralInfo // Get the peripheral information
|
||||
GetSettings() map[string]interface{} // Get the peripheral settings
|
||||
}
|
||||
|
||||
// PeripheralInfo represents a peripheral information
|
||||
type PeripheralInfo struct {
|
||||
Name string `yaml:"name"` // Name of the peripheral
|
||||
SerialNumber string `yaml:"sn"` // S/N of the peripheral
|
||||
ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral
|
||||
IsOpen bool // Open flag for peripheral connection
|
||||
Settings map[string]interface{} `yaml:"settings"` // Peripheral settings
|
||||
}
|
||||
|
||||
// PeripheralFinder represents how compatible peripheral drivers are implemented
|
||||
type PeripheralFinder interface {
|
||||
Initialize() error // Initializes the protocol
|
||||
Start(context.Context) error // Start the detection
|
||||
Stop() error // Stop the detection
|
||||
ForceScan() // Explicitly scans for peripherals
|
||||
RegisterPeripheral(context.Context, PeripheralInfo) (string, error) // Registers a new peripheral data
|
||||
UnregisterPeripheral(string) error // Unregisters an existing peripheral
|
||||
GetPeripheralSettings(string) (map[string]interface{}, error) // Gets the peripheral settings
|
||||
SetPeripheralSettings(string, map[string]interface{}) error // Sets the peripheral settings
|
||||
GetName() string // Get the name of the finder
|
||||
}
|
||||
44
hardware/third-party/ftdi/detectFTDI.cpp
vendored
Normal file
44
hardware/third-party/ftdi/detectFTDI.cpp
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include "ftd2xx.h"
|
||||
|
||||
int main() {
|
||||
FT_STATUS ftStatus;
|
||||
FT_HANDLE ftHandle = nullptr;
|
||||
FT_DEVICE_LIST_INFO_NODE *devInfo;
|
||||
DWORD numDevs;
|
||||
|
||||
// create the device information list
|
||||
ftStatus = FT_CreateDeviceInfoList(&numDevs);
|
||||
if (ftStatus != FT_OK) {
|
||||
std::cerr << "Unable to get the FTDI devices : create list error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (numDevs > 0) {
|
||||
// allocate storage for list based on numDevs
|
||||
devInfo =
|
||||
(FT_DEVICE_LIST_INFO_NODE*)malloc(sizeof(FT_DEVICE_LIST_INFO_NODE)*numDevs);
|
||||
// get the device information list
|
||||
ftStatus = FT_GetDeviceInfoList(devInfo, &numDevs);
|
||||
if (ftStatus != FT_OK) {
|
||||
std::cerr << "Unable to get the FTDI devices : get list error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < numDevs; i++) {
|
||||
if (devInfo[i].SerialNumber[0] != '\0') {
|
||||
std::cout << i << ":" << devInfo[i].SerialNumber << ":" << devInfo[i].Description << ":";
|
||||
|
||||
// Add information about the hardware open state
|
||||
if (devInfo[i].Flags & FT_FLAGS_OPENED) {
|
||||
std::cout << "O" << std::endl;
|
||||
} else {
|
||||
std::cout << "C" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(devInfo);
|
||||
}
|
||||
}
|
||||
12
hardware/third-party/ftdi/detectFTDI.manifest
vendored
Normal file
12
hardware/third-party/ftdi/detectFTDI.manifest
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="DMXSender" type="win32"/>
|
||||
<description>Detect FTDI</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
1667
hardware/third-party/ftdi/include/ftd2xx.h
vendored
Normal file
1667
hardware/third-party/ftdi/include/ftd2xx.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
hardware/third-party/ftdi/lib/ftd2xx.lib
vendored
Normal file
BIN
hardware/third-party/ftdi/lib/ftd2xx.lib
vendored
Normal file
Binary file not shown.
55
main.go
Normal file
55
main.go
Normal 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
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "dmxconnect",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
1
package.json
Normal file
1
package.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
125
peripherals.go
Normal file
125
peripherals.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"dmxconnect/hardware"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// AddPeripheral adds a peripheral to the project
|
||||
func (a *App) AddPeripheral(peripheralData hardware.PeripheralInfo) (string, error) {
|
||||
// Get the peripheral from its finder
|
||||
f, err := a.hardwareManager.GetFinder(peripheralData.ProtocolName)
|
||||
if err != nil {
|
||||
log.Error().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Msg("unable to found the specified finder")
|
||||
return "", fmt.Errorf("unable to found the peripheral ID '%s'", peripheralData.SerialNumber)
|
||||
}
|
||||
// Register this new peripheral
|
||||
serialNumber, err := f.RegisterPeripheral(a.ctx, peripheralData)
|
||||
if err != nil {
|
||||
log.Trace().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Str("periphID", serialNumber).Msg("device registered to the finder")
|
||||
return "", fmt.Errorf("unable to register the peripheral '%s'", serialNumber)
|
||||
}
|
||||
// Rewrite the serialnumber for virtual devices
|
||||
peripheralData.SerialNumber = serialNumber
|
||||
|
||||
// Add the peripheral ID to the project
|
||||
if a.projectInfo.PeripheralsInfo == nil {
|
||||
a.projectInfo.PeripheralsInfo = make(map[string]hardware.PeripheralInfo)
|
||||
}
|
||||
|
||||
a.projectInfo.PeripheralsInfo[peripheralData.SerialNumber] = peripheralData
|
||||
log.Info().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Str("periphID", peripheralData.SerialNumber).Msg("peripheral added to project")
|
||||
return peripheralData.SerialNumber, nil
|
||||
}
|
||||
|
||||
// GetPeripheralSettings gets the peripheral settings
|
||||
func (a *App) GetPeripheralSettings(protocolName, peripheralID string) (map[string]interface{}, error) {
|
||||
// Get the peripheral from its finder
|
||||
f, err := a.hardwareManager.GetFinder(protocolName)
|
||||
if err != nil {
|
||||
log.Error().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("unable to found the specified peripheral")
|
||||
return nil, fmt.Errorf("unable to found the peripheral ID '%s'", peripheralID)
|
||||
}
|
||||
return f.GetPeripheralSettings(peripheralID)
|
||||
}
|
||||
|
||||
// UpdatePeripheralSettings updates a specific setting of a peripheral
|
||||
func (a *App) UpdatePeripheralSettings(protocolName, peripheralID string, settings map[string]interface{}) error {
|
||||
// Sets the settings with the finder
|
||||
f, err := a.hardwareManager.GetFinder(protocolName)
|
||||
if err != nil {
|
||||
log.Error().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("unable to found the specified peripheral")
|
||||
return fmt.Errorf("unable to found the peripheral ID '%s'", peripheralID)
|
||||
}
|
||||
// Save the settings in the application
|
||||
if a.projectInfo.PeripheralsInfo == nil {
|
||||
a.projectInfo.PeripheralsInfo = make(map[string]hardware.PeripheralInfo)
|
||||
}
|
||||
pInfo := a.projectInfo.PeripheralsInfo[peripheralID]
|
||||
pInfo.Settings = settings
|
||||
a.projectInfo.PeripheralsInfo[peripheralID] = pInfo
|
||||
// Apply changes in the peripheral
|
||||
return f.SetPeripheralSettings(peripheralID, pInfo.Settings)
|
||||
}
|
||||
|
||||
// RemovePeripheral removes a peripheral from the project
|
||||
func (a *App) RemovePeripheral(protocolName string, peripheralID string) error {
|
||||
// Unregister the peripheral from the finder
|
||||
f, err := a.hardwareManager.GetFinder(protocolName)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "peripherals").Str("protocolName", protocolName).Msg("unable to find the finder")
|
||||
return fmt.Errorf("unable to find the finder")
|
||||
}
|
||||
err = f.UnregisterPeripheral(peripheralID)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "peripherals").Str("peripheralID", peripheralID).Msg("unable to unregister this peripheral")
|
||||
return fmt.Errorf("unable to unregister this peripheral")
|
||||
}
|
||||
// Remove the peripheral ID from the project
|
||||
delete(a.projectInfo.PeripheralsInfo, peripheralID)
|
||||
log.Info().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("peripheral removed from project")
|
||||
return nil
|
||||
}
|
||||
|
||||
// FOR TESTING PURPOSE ONLY
|
||||
|
||||
// func (a *App) ActivateFTDI() error {
|
||||
// // Connect the FTDI
|
||||
// driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// periph, found := driver.GetPeripheral("A50285BI")
|
||||
// if !found {
|
||||
// return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
// }
|
||||
// return periph.Activate(a.ctx)
|
||||
// }
|
||||
|
||||
// func (a *App) SetDeviceFTDI(channelValue byte) error {
|
||||
// // Connect the FTDI
|
||||
// driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// periph, found := driver.GetPeripheral("A50285BI")
|
||||
// if !found {
|
||||
// return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
// }
|
||||
// return periph.SetDeviceProperty(a.ctx, 0, 0, channelValue)
|
||||
// }
|
||||
|
||||
// func (a *App) DeactivateFTDI() error {
|
||||
// // Connect the FTDI
|
||||
// driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// periph, found := driver.GetPeripheral("A50285BI")
|
||||
// if !found {
|
||||
// return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
// }
|
||||
// return periph.Deactivate(a.ctx)
|
||||
// }
|
||||
191
project.go
Normal file
191
project.go
Normal file
@@ -0,0 +1,191 @@
|
||||
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() ShowInfo {
|
||||
date := time.Now()
|
||||
a.projectSave = ""
|
||||
a.projectInfo.ShowInfo = ShowInfo{
|
||||
Name: "My new show",
|
||||
Date: fmt.Sprintf("%04d-%02d-%02dT%02d:%02d", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute()),
|
||||
Avatar: "appicon.png",
|
||||
Comments: "Write your comments here",
|
||||
}
|
||||
log.Info().Str("file", "project").Any("showInfo", a.projectInfo.ShowInfo).Msg("project has been created")
|
||||
return a.projectInfo.ShowInfo
|
||||
}
|
||||
|
||||
// GetProjectInfo returns the information of the saved project
|
||||
func (a *App) GetProjectInfo(projectFile string) (ProjectInfo, 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 ProjectInfo{}, fmt.Errorf("unable to read the project file: %v", err)
|
||||
}
|
||||
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project file read")
|
||||
a.projectInfo = ProjectInfo{}
|
||||
err = yaml.Unmarshal(content, &a.projectInfo)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to get the project information")
|
||||
return ProjectInfo{}, fmt.Errorf("unable to get the project information: %v", err)
|
||||
}
|
||||
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project information got")
|
||||
// Load it into the app
|
||||
a.projectSave = projectFile
|
||||
// Return the show information
|
||||
log.Info().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("got the project information")
|
||||
return a.projectInfo, nil
|
||||
}
|
||||
|
||||
// ChooseAvatarPath opens a filedialog to choose the show avatar
|
||||
func (a *App) ChooseAvatarPath() (string, error) {
|
||||
// Open the file dialog box
|
||||
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Choose your show avatar",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "Images",
|
||||
Pattern: "*.png;*.jpg;*.jpeg",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "project").Msg("unable to open the avatar dialog")
|
||||
return "", err
|
||||
}
|
||||
log.Debug().Str("file", "project").Msg("avatar dialog is opened")
|
||||
// Copy the avatar to the application avatars path
|
||||
avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath))
|
||||
log.Trace().Str("file", "project").Str("avatarPath", avatarPath).Msg("avatar path is created")
|
||||
_, err = copy(filePath, avatarPath)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "project").Str("avatarsDirectory", avatarsDirectory).Str("fileBase", filepath.Base(filePath)).Msg("unable to copy the avatar file")
|
||||
return "", err
|
||||
}
|
||||
log.Info().Str("file", "project").Str("avatarFileName", filepath.Base(filePath)).Msg("got the new avatar file")
|
||||
return filepath.Base(filePath), nil
|
||||
}
|
||||
|
||||
// UpdateShowInfo updates the show information
|
||||
func (a *App) UpdateShowInfo(showInfo ShowInfo) {
|
||||
a.projectInfo.ShowInfo = showInfo
|
||||
log.Info().Str("file", "project").Any("showInfo", showInfo).Msg("show information was updated")
|
||||
}
|
||||
|
||||
// SaveProject saves the project
|
||||
func (a *App) SaveProject() (string, error) {
|
||||
// If there is no save file, create a new one with the show name
|
||||
if a.projectSave == "" {
|
||||
date := time.Now()
|
||||
a.projectSave = fmt.Sprintf("%04d%02d%02d%02d%02d%02d%s", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), projectExtension)
|
||||
log.Debug().Str("file", "project").Str("newProjectSave", a.projectSave).Msg("projectSave is null, getting a new one")
|
||||
}
|
||||
data, err := yaml.Marshal(a.projectInfo)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "project").Any("projectInfo", a.projectInfo).Msg("unable to format the project information")
|
||||
return "", err
|
||||
}
|
||||
log.Trace().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("projectInfo has been marshalled")
|
||||
// Create the project directory if not exists
|
||||
err = os.MkdirAll(projectsDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("unable to create the projects directory")
|
||||
return "", err
|
||||
}
|
||||
log.Trace().Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("projects directory has been created")
|
||||
|
||||
err = os.WriteFile(filepath.Join(projectsDirectory, a.projectSave), data, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Str("projectSave", a.projectSave).Msg("unable to save the project")
|
||||
return "", err
|
||||
}
|
||||
log.Info().Str("file", "project").Str("projectFileName", a.projectSave).Msg("project has been saved")
|
||||
return a.projectSave, nil
|
||||
}
|
||||
|
||||
// ShowInfo defines the information of the show
|
||||
type ShowInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Date string `yaml:"date"`
|
||||
Avatar string `yaml:"avatar"`
|
||||
Comments string `yaml:"comments"`
|
||||
}
|
||||
|
||||
// ProjectMetaData defines all the minimum information for a lighting project
|
||||
type ProjectMetaData struct {
|
||||
Name string // Show name
|
||||
Save string // The save file of the project
|
||||
}
|
||||
|
||||
// ProjectInfo defines all the information for a lighting project
|
||||
type ProjectInfo struct {
|
||||
ShowInfo ShowInfo `yaml:"show"` // Show information
|
||||
PeripheralsInfo map[string]hardware.PeripheralInfo `yaml:"peripherals"` // Peripherals information
|
||||
}
|
||||
13
wails.json
Normal file
13
wails.json
Normal 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": ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user