14 Commits

Author SHA1 Message Date
037735fb85 add OS2L device creation 2024-12-23 17:22:37 +01:00
7cf222c4f9 run third-party programs silently 2024-12-21 23:28:26 +01:00
7832d744b7 fixed the application logo 2024-12-21 21:54:24 +01:00
2496d49634 peripherals saving improvements 2024-12-21 13:24:00 +01:00
9964ccef7e save peripherals in project file 2024-12-20 17:18:57 +01:00
17b5d39fc4 fixed tooltip 2024-12-15 13:45:46 +01:00
4690f771fa added the device card component 2024-11-02 00:29:46 +01:00
a231263825 project management feature (#15)
Reviewed-on: #15
2024-11-01 20:10:28 +00:00
364dabee69 10-project-settings (#13)
Added the project properties page.
2024-07-08 11:19:35 +00:00
b33df4b447 Added the application logo (#12)
Added the application logo.
2024-07-07 16:34:44 +00:00
d6dc8405dd added final navigation bar (#9)
Reviewed-on: #9
2024-06-24 07:02:39 +00:00
d9a01d440b feat: restored the project 2023-08-26 09:33:05 +00:00
8fe9c0a4e8 feat: added the build status 2023-08-25 20:37:16 +00:00
7ac2d71b4d feat: initialize the wails project 2023-08-25 20:33:11 +00:00
63 changed files with 5051 additions and 2 deletions

12
.gitignore vendored
View File

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

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

@@ -0,0 +1,63 @@
{
"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"
}
}

View File

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

83
app.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import (
"changeme/hardware"
"context"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
)
// App struct
type App struct {
ctx context.Context
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
// FOR TESTING PURPOSE ONLY
ftdi *hardware.FTDIPeripheral
}
// NewApp creates a new App application struct
func NewApp() *App {
// Create a new hadware manager
hardwareManager := hardware.NewHardwareManager()
hardwareManager.RegisterDriver(hardware.NewMIDIDriver())
hardwareManager.RegisterDriver(hardware.NewFTDIDriver())
hardwareManager.RegisterDriver(hardware.NewOS2LDriver())
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) startup(ctx context.Context) {
a.ctx = ctx
err := a.hardwareManager.Start(ctx)
if err != nil {
log.Fatalf("Unable to start the device manager: %s", err)
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
}

BIN
build/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

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

View File

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

63
frontend/README.md Normal file
View File

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

13
frontend/index.html Normal file
View File

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

38
frontend/jsconfig.json Normal file
View 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
View File

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

BIN
frontend/public/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

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

@@ -0,0 +1,134 @@
<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 { 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, GetPeripherals } from "../wailsjs/go/main/App";
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
import { get } from "svelte/store"
// Handle the event when a new peripheral is detected
EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){
console.log("Hardware has been added to the system");
// 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}
})
})
// 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}
})
})
// Set the window title
$: {
WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (unsaved)" : ""))
}
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)
}).catch((error) => {
console.error(`Unable to save the project: ${error}`)
})
}
// Instanciate a new project
CreateProject().then((showInfo) => {
showInformation.set(showInfo)
$needProjectSave = true
})
// Request the list of peripherals
// TODO: Handle the error here
GetPeripherals()
// 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}
</main>
<style>
main {
text-align: left;
padding: 1em;
max-width: 240px;
margin: 0 auto;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
<script lang=ts>
import { createEventDispatcher, onDestroy } from 'svelte';
import {colors} from '../../stores.js';
import Tooltip from './Tooltip.svelte';
export let text = 'Default button';
export let icon = ''
export let tooltip = "Default tooltip"
export let choices = new Map()
export let active = false;
export let style = '';
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
let buttonRef
function toggleTooltip(active){
const rect = buttonRef.getBoundingClientRect();
tooltipPosition = {
top: rect.bottom + 5, // Ajouter une marge en bas
left: rect.left, // Centrer horizontalement
};
tooltipShowing = active
}
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function handleclick(key){
// Deactivate the list visibility
hideList()
dispatch('selected', key)
}
// Show the option list
let listShowing = false
function toggleList(){
if (!listShowing) {
dispatch('click')
}
listShowing = !listShowing
}
function hideList(){
listShowing = false
}
</script>
<!-- <Tooltip message={tooltip} show={tooltipShowing}></Tooltip> -->
<div class="container">
<button bind:this={buttonRef}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
on:click={toggleList}
style='color: {$colors.white}; background-color: { active ? $colors.second : $colors.third }; border:none; {style}'><i class='bx { icon}'></i> { text }
</button>
<Tooltip message={tooltip} show={tooltipShowing} position={tooltipPosition}></Tooltip>
<div class="list" style="color: {$colors.white}; display: {listShowing ? "block" : "none"};"
on:mouseleave={hideList}>
{#each Array.from(choices) as [key, value]}
<div class="item" on:click={() => handleclick({key})}>{value}</div>
{/each}
</div>
</div>
<style>
.item{
border-radius: 0.3em;
padding: 0.3em;
}
.item:hover {
background-color: var(--second-color);
color: var(--white-color);
}
.container{
position: relative;
display: inline-block;
}
.list {
z-index: 200;
padding: 0.2em;
backdrop-filter: blur(20px);
margin-top: 0.2em;
position: absolute;
width: auto;
cursor: pointer;
border-radius: 0.5em;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 30em;
max-height: 40vh;
overflow-y: scroll;
scrollbar-width: none;
}
button{
cursor: pointer;
border-radius: 0.5em;
margin: 0;
}
button:hover{
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.25) inset;
}
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,115 @@
<!-- Create a round icon button -->
<script>
import { createEventDispatcher, onDestroy } from 'svelte';
import {colors} from '../../stores.js';
import Tooltip from './Tooltip.svelte';
import { _ } from 'svelte-i18n'
export let icon = "bxs-heart" // The icon wanted
export let width = "10em" // The button width
export let active = false // If the button is active or not
export let tooltip = "Default tooltip" // The description shown in the tooltip
export let operationalStatus = undefined// The optional button status
export let okStatusLabel = "" // The label shown when the button is OK
export let nokStatusLabel = "" // The label shown when the button is NOK
let tooltipMessage = tooltip
// Default values for background and foreground
$: background = $colors.first
$: foreground = $colors.first
// Change the background when the selected prop changed
$: {
if (active === true) {
background = $colors.third
foreground = $colors.fourth
} else {
background = $colors.fourth
foreground = $colors.second
}
}
// Show the operational status if specified
// undefined => no status displayed
// operationalStatus = true => OK color displayed
// operationalStatus = false => NOK color displayed
$: statusColor = $colors.nok
$: {
if (operationalStatus === true){
statusColor = $colors.ok
tooltipMessage = tooltip + " " + okStatusLabel
} else {
statusColor = $colors.nok
tooltipMessage = tooltip + " " + nokStatusLabel
}
}
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function emitMouseDown() {
dispatch('mousedown');
}
function emitMouseUp() {
dispatch('mouseup');
}
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
let buttonRef
function toggleTooltip(active){
const rect = buttonRef.getBoundingClientRect();
tooltipPosition = {
top: rect.bottom + 5, // Ajouter une marge en bas
left: rect.left, // Centrer horizontalement
};
tooltipShowing = active
}
</script>
<div class="container">
<button bind:this={buttonRef}
style="width:{width}; height:{width}; border-radius:{width}; background-color:{background}; color:{foreground};"
on:mousedown={emitMouseDown}
on:mouseup={emitMouseUp}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}>
<i class='bx {icon}' style="font-size:100%;"></i>
</button>
<!-- Showing the badge status if the button has an operational status -->
{#if (operationalStatus !== undefined)}
<div class="badge"
style="width: calc({width} / 3); height: calc({width} / 3); border-radius: calc({width}); background-color:{statusColor}; display:block;">
</div>
{/if}
<Tooltip message={tooltipMessage} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>
.container {
position: relative;
display: inline-block;
}
button{
display: inline-block;
margin: 0;
border:none;
cursor: pointer;
}
button:hover{
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.25) inset;
}
.badge{
position: absolute;
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
transform: translate(200%, -100%);
}
div{
display:inline-block;
}
</style>

View File

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

View File

@@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
<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 disconnected = false;
// 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="profile" on:mousedown={click} style="color: {disconnected ? $colors.first : $colors.white};">
<div>
<p>{#if 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 disconnected}
<h6><b>Disconnected</b></h6>
{:else}
<h6>{line1}</h6>
<h6>{line2}</h6>
{/if}
</div>
<div class="actions">
<InfoButton on:click={add} color="{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="{disconnected ? $colors.first : $colors.white}" style="margin: 0.2em; display: { removable ? 'flex' : 'none' }" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")}/>
<InfoButton style="margin: 0.2em;" background={ signalized ? $colors.orange : $colors.first } icon='bx-pulse' hide={!signalizable}/>
</div>
</div>
</div>
<style>
.profile:hover{
background-color: var(--third-color);
}
.card{
position: relative;
}
.profile {
background-color: var(--second-color);
margin: 0.2em;
padding-left: 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;
}
p{
margin: 0;
}
h6 {
margin: 0;
font-weight: 1;
}
</style>

View File

@@ -0,0 +1,178 @@
<script lang=ts>
import DeviceCard from "./DeviceCard.svelte";
import Tab from "../General/Tab.svelte";
import { _ } from 'svelte-i18n'
import { needProjectSave, peripherals } from "../../stores";
import { get } from "svelte/store"
import { AddOS2LPeripheral, RemovePeripheral, ConnectFTDI, ActivateFTDI, DeactivateFTDI, DisconnectFTDI, SetDeviceFTDI, AddPeripheral } from "../../../wailsjs/go/main/App";
import RoundedButton from "../General/RoundedButton.svelte";
function ftdiConnect(){
ConnectFTDI().then(() =>
console.log("FTDI connected"))
.catch((error) => {
console.log("Error when trying to connect: " + error)
})
}
function ftdiActivate(){
ActivateFTDI().then(() =>
console.log("FTDI activated"))
.catch((error) => {
console.log("Error when trying to activate: " + error)
})
}
function ftdiDeactivate(){
DeactivateFTDI().then(() =>
console.log("FTDI deactivated"))
.catch((error) => {
console.log("Error when trying to deactivate: " + error)
})
}
let sliderValue = 0
function ftdiSetDevice(value){
console.log("value is " + value)
SetDeviceFTDI(value).then(() =>
console.log("FTDI device set up"))
.catch((error) => {
console.log("Error when trying to set the device: " + error)
})
}
function ftdiDisconnect(){
DisconnectFTDI().then(() =>
console.log("FTDI disconnected"))
.catch((error) => {
console.log("Error when trying to disconnect: " + error)
})
}
// Add the peripheral to the project
function addPeripheral(peripheral){
// Add the peripheral to the project (backend)
AddPeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
peripherals.update((value) => {
if (value[peripheral.SerialNumber]) {
value[peripheral.SerialNumber].isSaved = true;
}
return {...value}
})
$needProjectSave = true
}).catch((error) => {
console.log("Unable to add the peripheral to the project: " + error)
})
}
// 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
}).catch((error) => {
console.log("Unable to remove the peripheral from the project: " + error)
})
}
// Create the OS2L peripheral
function createOS2L(){
AddOS2LPeripheral().then(os2lDevice => {
peripherals.update(currentPeriph => {
os2lDevice.isSaved = true
os2lDevice.isDetected = true
currentPeriph[os2lDevice.SerialNumber] = os2lDevice
return {...currentPeriph}
})
}).catch(error => {
console.log("Unable to add the OS2L peripheral: " + error)
})
}
</script>
<div class="hardware">
<div style="padding: 0.5em;">
<p style="margin-bottom: 1em;">Available peripherals</p>
<div class="availableHardware">
<p style="color: var(--first-color);"><i class='bx bxs-plug'></i> Detected</p>
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
{#if peripheral.isDetected}
<DeviceCard on:add={() => addPeripheral(peripheral)} on:dblclick={() => {
if(!peripheral.isSaved)
addPeripheral(peripheral)
}}
title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} addable={!peripheral.isSaved}/>
{/if}
{/each}
<p style="color: var(--first-color);"><i class='bx bxs-network-chart' ></i> Others</p>
<RoundedButton on:click={createOS2L} 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;">Project peripherals</p>
<div class="configuredHardware">
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
{#if peripheral.isSaved}
<DeviceCard on:delete={() => removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)}
disconnected={!peripheral.isDetected} title={peripheral.Name == "" ? "Please wait..." : peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} removable signalizable/>
{/if}
{/each}
</div>
<p style="margin-bottom: 1em;">Peripheral settings</p>
<div>
<p><i>Select a peripheral to edit its settings</i></p>
<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>
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>

View File

@@ -0,0 +1,50 @@
<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}`)
})
}
function validate(field, value){
$showInformation[field] = value
console.log($showInformation)
UpdateShowInfo($showInformation).then(()=> {
$needProjectSave = true
})
}
</script>
<div class='flexSettings'>
<div>
<Input on:blur={(event) => validate("Name", event.detail.target.value)} label={$_("projectShowNameLabel")} type='text' value={$showInformation.Name}/>
<Input on:blur={(event) => validate("Date", event.detail.target.value)} label={$_("projectShowDateLabel")} type='datetime-local' value={$showInformation.Date}/>
</div>
<div>
<Input on:dblclick={chooseAvatar} label={$_("projectAvatarLabel")} type='image' alt={$_("projectAvatarLabel")} width='11em' src={$showInformation.Avatar}/>
</div>
</div>
<div>
<Input on:blur={(event) => validate("Comments", event.detail.target.value)} label={$_("projectCommentsLabel")} type='large' width='100%' value={$showInformation.Comments}/>
</div>
<style>
.flexSettings{
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,76 @@
<script lang=ts>
import { 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}`)
})
}
function openSelectedProject(event){
let selectedOption = event.detail.key
// Open the selected project
GetProjectInfo(selectedOption).then((projectInfo) => {
$showInformation = projectInfo.ShowInfo
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]
}
}
// 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 projectInfo.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] = projectInfo.PeripheralsInfo[peripheralID]
storedPeripherals[peripheralID].isDetected = (lastDetectedKey === true) ? true : false
storedPeripherals[peripheralID].isSaved = true
}
return {...storedPeripherals}
})
needProjectSave.set(false)
}).catch((error) => {
console.error(`Unable to open the project: ${error}`)
})
}
function initializeNewProject(){
// Instanciate a new project
CreateProject().then((showInfo) => {
$showInformation = showInfo
$needProjectSave = true
})
}
</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>

View File

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

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

@@ -0,0 +1,38 @@
{
"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",
"projectPropertiesTab": "Project properties",
"projectPropertiesTooltip": "The project properties",
"projectInputOutputTab": "Inputs & outputs",
"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"
}

27
frontend/src/main.js Normal file
View 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;

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

@@ -0,0 +1,28 @@
import { writable } from 'svelte/store';
// Projects management
export let projectsList = writable([])
export let needProjectSave = writable(true)
// Show settings
export let showInformation = writable({})
// 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
View File

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

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

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

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

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

44
go.mod Normal file
View File

@@ -0,0 +1,44 @@
module changeme
go 1.21
toolchain go1.21.3
require (
github.com/lxn/win v0.0.0-20210218163916-a377121e959e
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79
github.com/wailsapp/wails/v2 v2.9.1
golang.org/x/sys v0.20.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.10.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // 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/text v0.15.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.5.1 => /home/dev/go/pkg/mod

102
go.sum Normal file
View File

@@ -0,0 +1,102 @@
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
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.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/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
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/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-20201018230417-eeed37f84f13/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.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=

140
hardware/FTDIDriver.go Normal file
View File

@@ -0,0 +1,140 @@
package hardware
import (
"bufio"
"context"
_ "embed"
"fmt"
"os"
"os/exec"
goRuntime "runtime"
"strconv"
"strings"
"time"
)
const (
scanDelay = 4 * time.Second // Waiting delay before scanning the FTDI devices
)
// FTDIDriver represents how the protocol is defined
type FTDIDriver struct {
peripherals map[string]Peripheral
}
// NewFTDIDriver creates a new FTDI finder
func NewFTDIDriver() *FTDIDriver {
return &FTDIDriver{
peripherals: make(map[string]Peripheral),
}
}
// Initialize initializes the FTDI driver
func (d *FTDIDriver) Initialize() error {
if goRuntime.GOOS != "windows" {
return fmt.Errorf("<!> The FTDI driver is not compatible with your platform yet (%s)", goRuntime.GOOS)
}
fmt.Println("FTDI driver initialized")
return nil
}
// GetName returns the name of the driver
func (d *FTDIDriver) GetName() string {
return "FTDI"
}
// GetPeripheral gets the peripheral that correspond to the specified ID
func (d *FTDIDriver) GetPeripheral(peripheralID string) (Peripheral, bool) {
// Return the specified peripheral
peripheral := d.peripherals[peripheralID]
if peripheral == nil {
fmt.Println("Unable to get the peripheral with the driver")
return nil, false
}
fmt.Println("Peripheral found by the FTDI driver")
return peripheral, true
}
//go:embed third-party/ftdi/detectFTDI.exe
var findFTDI []byte
// Scan scans the FTDI peripherals
func (d *FTDIDriver) Scan(ctx context.Context) error {
time.Sleep(scanDelay)
// Create a temporary file
tempFile, err := os.CreateTemp("", "findFTDI*.exe")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
// Write the embedded executable to the temp file
if _, err := tempFile.Write(findFTDI); err != nil {
return err
}
tempFile.Close()
ftdiPeripherals := make(map[string]Peripheral)
finder := exec.Command(tempFile.Name())
stdout, err := finder.StdoutPipe()
if err != nil {
return fmt.Errorf("Unable to create the stdout pipe: %s", err)
}
stderr, err := finder.StderrPipe()
if err != nil {
return fmt.Errorf("Unable to create the stderr pipe: %s", err)
}
err = finder.Start()
if err != nil {
return fmt.Errorf("Unable to find FTDI devices: %s", err)
}
scannerErr := bufio.NewScanner(stderr)
for scannerErr.Scan() {
return fmt.Errorf("Unable to find FTDI devices: %s", scannerErr.Text())
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
deviceString := scanner.Text()
// The program output is like '0:1:2' where 0 is the location, 1 is the S/N and 2 is the name
deviceInfo := strings.Split(deviceString, ":")
fmt.Println("FTDI device: " + deviceString)
// Convert the location to an integer
location, err := strconv.Atoi(deviceInfo[0])
if err != nil {
location = -1
}
// Add the peripheral to the temporary list
peripheral, err := NewFTDIPeripheral(deviceInfo[2], deviceInfo[1], location)
if err != nil {
return fmt.Errorf("Unable to create the FTDI peripheral: %v", err)
}
ftdiPeripherals[deviceInfo[1]] = peripheral
}
// Compare with the current peripherals to detect arrivals/removals
removedList, addedList := comparePeripherals(d.peripherals, ftdiPeripherals)
// Emit the events
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
// Store the new peripherals list
d.peripherals = ftdiPeripherals
return nil
}
// CreatePeripheral is not implemented here
func (d *FTDIDriver) CreatePeripheral(context.Context) (Peripheral, error) {
return nil, nil
}
// RemovePeripheral is not implemented here
func (d *FTDIDriver) RemovePeripheral(serialNumber string) error {
return nil
}

201
hardware/FTDIPeripheral.go Normal file
View File

@@ -0,0 +1,201 @@
package hardware
import (
"bufio"
_ "embed"
"fmt"
"io"
"log"
"os"
"os/exec"
"sync"
)
const (
activateCommandString = 0x01
deactivateCommandString = 0x02
setCommandString = 0x03
)
//go:embed third-party/ftdi/dmxSender.exe
var dmxSender []byte
// FTDIPeripheral contains the data of an FTDI peripheral
type FTDIPeripheral struct {
name string // The name of the peripheral
serialNumber string // The S/N of the FTDI peripheral
location int // The location of the peripheral
universesNumber int // The number of DMX universes handled by this peripheral
programName string // The temp file name of the executable
dmxSender *exec.Cmd // The command to pilot the DMX sender program
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
wg sync.WaitGroup // Tasks management
}
// NewFTDIPeripheral creates a new FTDI peripheral
func NewFTDIPeripheral(name string, serialNumber string, location int) (*FTDIPeripheral, error) {
// Create a temporary file
tempFile, err := os.CreateTemp("", "dmxSender*.exe")
if err != nil {
return nil, err
}
// Write the embedded executable to the temp file
if _, err := tempFile.Write(dmxSender); err != nil {
return nil, err
}
tempFile.Close()
return &FTDIPeripheral{
name: name,
programName: tempFile.Name(),
serialNumber: serialNumber,
location: location,
universesNumber: 1,
disconnectChan: make(chan struct{}),
errorsChan: make(chan error, 1),
}, nil
}
// Connect connects the FTDI peripheral
func (p *FTDIPeripheral) Connect() error {
// Connect if no connection is already running
fmt.Println(p.dmxSender)
if p.dmxSender == nil {
// Executing the command
p.dmxSender = exec.Command(p.programName, fmt.Sprintf("%d", p.location))
var err error
p.stdout, err = p.dmxSender.StdoutPipe()
if err != nil {
log.Fatalf("Unable to create the stdout pipe: %v", err)
}
p.stdin, err = p.dmxSender.StdinPipe()
if err != nil {
log.Fatalf("Unable to create the stdin pipe: %v", err)
}
p.stderr, err = p.dmxSender.StderrPipe()
if err != nil {
log.Fatalf("Unable to create the stderr pipe: %v", err)
}
go func() {
scanner := bufio.NewScanner(p.stderr)
for scanner.Scan() {
// Traitez chaque ligne lue depuis stderr
fmt.Printf("Erreur : %s\n", scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Printf("Error reading from stderr: %v", err)
}
}()
p.wg.Add(1)
go func() {
defer p.wg.Done()
err = p.dmxSender.Run()
if err != nil {
// Affiche l'erreur (cela inclut également les erreurs de retour du programme)
fmt.Printf("Erreur lors de l'exécution : %v\n", err)
// Vérifie si l'erreur est liée au code de retour
if exitError, ok := err.(*exec.ExitError); ok {
// Récupère et affiche le code de retour
fmt.Printf("Le programme s'est terminé avec le code : %d\n", exitError.ExitCode())
fmt.Println(p.stderr)
}
} else {
fmt.Println("Le programme s'est terminé avec succès.")
}
}()
}
return nil
}
// Disconnect disconnects the FTDI peripheral
func (p *FTDIPeripheral) Disconnect() error {
if p.dmxSender != nil {
_, err := io.WriteString(p.stdin, string([]byte{0x04, 0x00, 0x00, 0x00}))
if err != nil {
return fmt.Errorf("Unable to disconnect: %v", err)
}
p.stdin.Close()
p.stdout.Close()
p.dmxSender = nil
err = os.Remove(p.programName)
if err != nil {
return fmt.Errorf("Unable to delete the temporary file: %v", err)
}
return nil
}
return fmt.Errorf("Unable to disconnect: not connected")
// if p.dmxSender != nil {
// err := p.dmxSender.Process.Kill()
// if err != nil {
// return fmt.Errorf("Unable to disconnect: %v", err)
// }
// p.stdin.Close()
// p.stdout.Close()
// p.dmxSender = nil
// err = os.Remove(p.programName)
// if err != nil {
// return fmt.Errorf("Unable to delete the temporary file: %v", err)
// }
// return nil
// }
// return fmt.Errorf("Unable to disconnect: not connected")
}
// Activate activates the FTDI peripheral
func (p *FTDIPeripheral) Activate() error {
if p.dmxSender != nil {
_, err := io.WriteString(p.stdin, string([]byte{0x01, 0x00, 0x00, 0x00}))
if err != nil {
return fmt.Errorf("Unable to activate: %v", err)
}
return nil
}
return fmt.Errorf("Unable to activate: not connected")
}
// Deactivate deactivates the FTDI peripheral
func (p *FTDIPeripheral) Deactivate() error {
if p.dmxSender != nil {
_, err := io.WriteString(p.stdin, string([]byte{0x02, 0x00, 0x00, 0x00}))
if err != nil {
return fmt.Errorf("Unable to deactivate: %v", err)
}
return nil
}
return fmt.Errorf("Unable to deactivate: not connected")
}
// SetDeviceProperty sends a command to the specified device
func (p *FTDIPeripheral) SetDeviceProperty(uint32, channelNumber uint32, channelValue byte) error {
if p.dmxSender != nil {
fmt.Println(channelValue)
commandString := []byte{0x03, 0x01, 0x00, 0xff, 0x03, 0x02, 0x00, channelValue}
_, err := io.WriteString(p.stdin, string(commandString))
if err != nil {
return fmt.Errorf("Unable to set device property: %v", err)
}
return nil
}
return fmt.Errorf("Unable to set device property: not connected")
}
// GetInfo gets all the peripheral information
func (p *FTDIPeripheral) GetInfo() PeripheralInfo {
return PeripheralInfo{
Name: p.name,
SerialNumber: p.serialNumber,
ProtocolName: "FTDI",
}
}

132
hardware/MIDIDriver.go Normal file
View File

@@ -0,0 +1,132 @@
package hardware
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/mattrtaylor/go-rtmidi"
)
/*
DMXConnect
DMXUSBProtocol.go
Copyright (c) Valentin Boulanger
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.txt
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// MIDIDriver represents how the protocol is defined
type MIDIDriver struct {
peripherals map[string]Peripheral // The list of peripherals
}
// NewMIDIDriver creates a new DMXUSB protocol
func NewMIDIDriver() *MIDIDriver {
return &MIDIDriver{
peripherals: make(map[string]Peripheral),
}
}
// Initialize initializes the MIDI driver
func (d *MIDIDriver) Initialize() error {
fmt.Println("MIDI driver initialized")
return nil
}
// GetName returns the name of the driver
func (d *MIDIDriver) GetName() string {
return "MIDI"
}
// GetPeripheral gets the peripheral that correspond to the specified ID
func (d *MIDIDriver) GetPeripheral(peripheralID string) (Peripheral, bool) {
// Return the specified peripheral
peripheral := d.peripherals[peripheralID]
if peripheral == nil {
return nil, false
}
return peripheral, true
}
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")
}
// Scan scans the interfaces compatible with the MIDI protocol
func (d *MIDIDriver) Scan(ctx context.Context) error {
midiPeripherals := make(map[string]Peripheral)
fmt.Println("Opening MIDI scanner port...")
midiScanner, err := rtmidi.NewMIDIInDefault()
if err != nil {
return fmt.Errorf("Error when opening the MIDI scanner: %s", err)
}
defer midiScanner.Close()
fmt.Println("Scanning MIDI devices...")
devicesCount, err := midiScanner.PortCount()
if err != nil {
return fmt.Errorf("Error when scanning MIDI devices: %s", err)
}
for i := 0; i < devicesCount; i++ {
portName, err := midiScanner.PortName(i)
if err != nil {
portName = "Unknown device 0"
}
// Separate data
name, location, err := splitStringAndNumber(portName)
if err != nil {
return fmt.Errorf("Unable to get information from the string: %s", err)
}
fmt.Printf("New MIDI device found: %s on %i\n", name, location)
// Add the peripheral to the temporary list
midiPeripherals[portName] = NewMIDIPeripheral(name, location)
}
// Compare with the current peripherals to detect arrivals/removals
removedList, addedList := comparePeripherals(d.peripherals, midiPeripherals)
// Emit the events
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
// Store the new peripherals list
d.peripherals = midiPeripherals
return nil
}
// CreatePeripheral is not implemented here
func (d *MIDIDriver) CreatePeripheral(context.Context) (Peripheral, error) {
return nil, nil
}
// RemovePeripheral is not implemented here
func (d *MIDIDriver) RemovePeripheral(serialNumber string) error {
return nil
}

View File

@@ -0,0 +1,48 @@
package hardware
// MIDIPeripheral contains the data of a MIDI peripheral
type MIDIPeripheral struct {
name string // The name of the peripheral
location int // The location of the peripheral
}
// NewMIDIPeripheral creates a new MIDI peripheral
func NewMIDIPeripheral(name string, location int) *MIDIPeripheral {
return &MIDIPeripheral{
name: name,
location: location,
}
}
// Connect connects the MIDI peripheral
func (p *MIDIPeripheral) Connect() error {
return nil
}
// Disconnect disconnects the MIDI peripheral
func (p *MIDIPeripheral) Disconnect() error {
return nil
}
// Activate activates the MIDI peripheral
func (p *MIDIPeripheral) Activate() error {
return nil
}
// Deactivate deactivates the MIDI peripheral
func (p *MIDIPeripheral) Deactivate() error {
return nil
}
// SetDeviceProperty - not implemented for this kind of peripheral
func (p *MIDIPeripheral) SetDeviceProperty(uint32, uint32, byte) error {
return nil
}
// GetInfo gets the peripheral information
func (p *MIDIPeripheral) GetInfo() PeripheralInfo {
return PeripheralInfo{
Name: p.name,
ProtocolName: "MIDI",
}
}

60
hardware/OS2LDriver.go Normal file
View File

@@ -0,0 +1,60 @@
package hardware
import (
"context"
"fmt"
"math/rand"
)
// OS2LDriver represents how the protocol is defined
type OS2LDriver struct {
peripherals map[string]Peripheral // The list of peripherals
}
// NewOS2LDriver creates a new OS2L driver
func NewOS2LDriver() *OS2LDriver {
return &OS2LDriver{
peripherals: make(map[string]Peripheral),
}
}
// Initialize initializes the MIDI driver
func (d *OS2LDriver) Initialize() error {
fmt.Println("OS2L driver initialized")
return nil
}
// CreatePeripheral creates a new OS2L peripheral
func (d *OS2LDriver) CreatePeripheral(ctx context.Context) (Peripheral, error) {
// Create a random serial number for this peripheral
randomSerialNumber := fmt.Sprintf("%08x", rand.Intn(1<<32))
peripheral := NewOS2LPeripheral("OS2L", randomSerialNumber)
d.peripherals[randomSerialNumber] = peripheral
return peripheral, nil
}
// RemovePeripheral removes an OS2L dev
func (d *OS2LDriver) RemovePeripheral(serialNumber string) error {
delete(d.peripherals, serialNumber)
return nil
}
// GetName returns the name of the driver
func (d *OS2LDriver) GetName() string {
return "OS2L"
}
// GetPeripheral gets the peripheral that correspond to the specified ID
func (d *OS2LDriver) GetPeripheral(peripheralID string) (Peripheral, bool) {
// Return the specified peripheral
peripheral := d.peripherals[peripheralID]
if peripheral == nil {
return nil, false
}
return peripheral, true
}
// Scan scans the interfaces compatible with the MIDI protocol
func (d *OS2LDriver) Scan(ctx context.Context) error {
return nil
}

View File

@@ -0,0 +1,55 @@
package hardware
import "fmt"
// OS2LPeripheral contains the data of an OS2L peripheral
type OS2LPeripheral struct {
name string // The name of the peripheral
serialNumber string // The serial number of the peripheral
}
// NewOS2LPeripheral creates a new OS2L peripheral
func NewOS2LPeripheral(name string, serialNumber string) *OS2LPeripheral {
return &OS2LPeripheral{
name: name,
serialNumber: serialNumber,
}
}
// Connect connects the MIDI peripheral
func (p *OS2LPeripheral) Connect() error {
fmt.Println("OS2L peripheral connected")
return nil
}
// Disconnect disconnects the MIDI peripheral
func (p *OS2LPeripheral) Disconnect() error {
fmt.Println("OS2L peripheral disconnected")
return nil
}
// Activate activates the MIDI peripheral
func (p *OS2LPeripheral) Activate() error {
fmt.Println("OS2L peripheral activated")
return nil
}
// Deactivate deactivates the MIDI peripheral
func (p *OS2LPeripheral) Deactivate() error {
fmt.Println("OS2L peripheral deactivated")
return nil
}
// SetDeviceProperty - not implemented for this kind of peripheral
func (p *OS2LPeripheral) SetDeviceProperty(uint32, uint32, byte) error {
return nil
}
// GetInfo gets the peripheral information
func (p *OS2LPeripheral) GetInfo() PeripheralInfo {
return PeripheralInfo{
Name: p.name,
SerialNumber: p.serialNumber,
ProtocolName: "OS2L",
}
}

212
hardware/hardware.go Normal file
View File

@@ -0,0 +1,212 @@
package hardware
import (
"context"
"fmt"
"syscall"
"time"
"unsafe"
"github.com/lxn/win"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/sys/windows"
)
// 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"
debounceDuration = 500 * time.Millisecond
)
var (
debounceTimer *time.Timer
)
// HardwareManager is the class who manages the hardware
type HardwareManager struct {
drivers map[string]PeripheralDriver // The map of peripherals finders
peripherals []Peripheral // The current list of peripherals
deviceChangedEvent chan struct{} // The event when the devices list changed
ctx context.Context
}
// NewHardwareManager creates a new HardwareManager
func NewHardwareManager() *HardwareManager {
return &HardwareManager{
drivers: make(map[string]PeripheralDriver),
peripherals: make([]Peripheral, 0),
deviceChangedEvent: make(chan struct{}),
}
}
// Start starts to finding new device events
func (h *HardwareManager) Start(ctx context.Context) error {
cb := windows.NewCallback(h.wndProc)
inst := win.GetModuleHandle(nil)
cn, err := syscall.UTF16PtrFromString("DMXConnect device watcher")
if err != nil {
return fmt.Errorf("failed to convert window class name to UTF16: %w", err)
}
wc := win.WNDCLASSEX{
HInstance: inst,
LpfnWndProc: cb,
LpszClassName: cn,
}
wc.CbSize = uint32(unsafe.Sizeof(wc))
if win.RegisterClassEx(&wc) == 0 {
return fmt.Errorf("failed to register window class: %w", syscall.GetLastError())
}
wName, err := syscall.UTF16PtrFromString("usbevent.exe")
if err != nil {
return fmt.Errorf("failed to convert window name to UTF16: %w", err)
}
wdw := win.CreateWindowEx(
0,
wc.LpszClassName,
wName,
win.WS_MINIMIZE|win.WS_OVERLAPPEDWINDOW,
win.CW_USEDEFAULT,
win.CW_USEDEFAULT,
100,
100,
0,
0,
wc.HInstance,
nil)
if wdw == 0 {
return fmt.Errorf("failed to create window: %w", syscall.GetLastError())
}
_ = win.ShowWindow(wdw, win.SW_HIDE)
win.UpdateWindow(wdw)
// To continuously get the devices events from Windows
go func() {
for {
select {
case <-ctx.Done():
return
default:
var msg win.MSG
got := win.GetMessage(&msg, win.HWND(windows.HWND(wdw)), 0, 0)
if got == 0 {
win.TranslateMessage(&msg)
win.DispatchMessage(&msg)
}
}
}
}()
// To handle the peripheral changed
go func() {
for {
select {
case <-ctx.Done():
return
case <-h.deviceChangedEvent:
fmt.Println("This is the list of devices")
h.Scan(ctx)
}
}
}()
return nil
}
// GetDriver returns a register driver
func (h *HardwareManager) GetDriver(driverName string) (PeripheralDriver, error) {
driver, exists := h.drivers[driverName]
if !exists {
return nil, fmt.Errorf("Unable to locate the '%s' driver", driverName)
}
return driver, nil
}
// RegisterDriver registers a new peripherals driver
func (h *HardwareManager) RegisterDriver(driver PeripheralDriver) {
h.drivers[driver.GetName()] = driver
fmt.Printf("Success registered the %s driver\n", driver.GetName())
}
// GetPeripheral gets the peripheral object from the parent driver
func (h *HardwareManager) GetPeripheral(driverName string, peripheralID string) (Peripheral, bool) {
// Get the driver
parentDriver := h.drivers[driverName]
// If no driver found, return false
if parentDriver == nil {
fmt.Println("Unable to get the driver")
return nil, false
}
// Contact the driver to get the device
return parentDriver.GetPeripheral(peripheralID)
}
// Scan scans all the peripherals for the registered finders
func (h *HardwareManager) Scan(ctx context.Context) error {
if len(h.drivers) == 0 {
return fmt.Errorf("No peripherals driver registered")
}
for _, driver := range h.drivers {
finder := driver
go func() {
err := driver.Scan(ctx)
if err != nil {
fmt.Printf("Unable to scan peripherals with the %s driver: %s\n", finder.GetName(), err)
return
}
}()
}
return nil
}
func (h *HardwareManager) wndProc(hwnd windows.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case win.WM_DEVICECHANGE:
// Trigger the devices scan when the last DEVICE_CHANGE event is received
if debounceTimer != nil {
debounceTimer.Stop()
}
debounceTimer = time.AfterFunc(debounceDuration, func() {
fmt.Printf("Devices list has changed, refresh the devices list\n")
h.deviceChangedEvent <- struct{}{}
})
}
return win.DefWindowProc(win.HWND(hwnd), msg, wParam, lParam)
}
// peripheralsList emits a peripheral event
func emitPeripheralsEvents(ctx context.Context, peripheralsList map[string]Peripheral, peripheralEvent PeripheralEvent) {
for _, peripheral := range peripheralsList {
runtime.EventsEmit(ctx, string(peripheralEvent), peripheral.GetInfo())
}
}
// comparePeripherals compares the peripherals to determine which has been inserted or removed
func comparePeripherals(oldPeripherals map[string]Peripheral, newPeripherals map[string]Peripheral) (map[string]Peripheral, map[string]Peripheral) {
// Duplicate the lists
oldList := make(map[string]Peripheral)
newList := make(map[string]Peripheral)
for key, value := range oldPeripherals {
oldList[key] = value
}
for key, value := range newPeripherals {
newList[key] = value
}
// Remove in these lists all the commons peripherals
for key := range newList {
if _, exists := oldList[key]; exists {
delete(oldList, key)
delete(newList, key)
}
}
// Now the old list contains the removed peripherals, and the new list contains the added peripherals
fmt.Printf("%s\n", oldList)
fmt.Printf("%s\n", newList)
return oldList, newList
}

32
hardware/interfaces.go Normal file
View File

@@ -0,0 +1,32 @@
package hardware
import "context"
// Peripheral represents the methods used to manage a peripheral (input or output hardware)
type Peripheral interface {
Connect() error // Connect the peripheral
Disconnect() error // Disconnect the peripheral
Activate() error // Activate the peripheral
Deactivate() error // Deactivate the peripheral
SetDeviceProperty(uint32, uint32, byte) error // Update a device property
GetInfo() PeripheralInfo // Get the peripheral information
}
// 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
Settings []interface{} `yaml:"settings"` // Number of DMX universes handled by the peripheral
}
// PeripheralDriver represents how compatible peripheral drivers are implemented
type PeripheralDriver interface {
Initialize() error // Initializes the protocol
GetName() string // Get the name of the finder
GetPeripheral(string) (Peripheral, bool) // Get the peripheral
Scan(context.Context) error // Scan for peripherals
CreatePeripheral(ctx context.Context) (Peripheral, error) // Creates a new peripheral
RemovePeripheral(serialNumber string) error // Removes a peripheral
}

View File

@@ -0,0 +1,34 @@
#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 << std::endl;
}
}
}
}

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

142
hardware/third-party/ftdi/dmxSender.cpp vendored Normal file
View File

@@ -0,0 +1,142 @@
#include <cstring>
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
#include <fstream>
#include <windows.h>
#include <string>
#include <math.h>
#include <chrono>
#include <thread>
#include <cstdint>
#include <vector>
#include <atomic>
#include <fcntl.h>
#include <io.h>
#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)
std::atomic<unsigned char> dmxData[DMX_CHANNELS + 1];
std::atomic<bool> isOutputActivated = false;
using namespace std;
void sendBreak(FT_HANDLE ftHandle) {
FT_SetBreakOn(ftHandle); // Envoie le signal de BREAK
std::this_thread::sleep_for(std::chrono::microseconds(BREAK_DURATION_US));
FT_SetBreakOff(ftHandle); // Arrête le signal de BREAK
}
void sendDMX(FT_HANDLE ftHandle) {
while (true) {
if(isOutputActivated){
// Envoi du BREAK suivi du MAB
sendBreak(ftHandle);
std::this_thread::sleep_for(std::chrono::microseconds(MAB_DURATION_US));
// Envoi de la trame DMX512
DWORD bytesWritten = 0;
// Envoyer la trame DMX512
FT_STATUS status = FT_Write(ftHandle, dmxData, DMX_CHANNELS, &bytesWritten);
if (status != FT_OK || bytesWritten != DMX_CHANNELS) {
std::cerr << "Unable to send the DMX frame" << std::endl;
FT_Close(ftHandle);
return;
}
// Attendre avant d'envoyer la prochaine trame
std::this_thread::sleep_for(std::chrono::microseconds(INTERVAL - BREAK_DURATION_US - MAB_DURATION_US));
}
}
}
void processCommand(const char* buffer) {
if (buffer[0] == 0x01) {
// Activate the DMX512
isOutputActivated.store(true);
} else if(buffer[0] == 0x02) {
// Deactivate the DMX512
isOutputActivated.store(false);
} else if(buffer[0] == 0x03) {
// Get the channel number
uint16_t channelNumber = (static_cast<unsigned char>(buffer[1]) |
(static_cast<unsigned char>(buffer[2]) << 8));
// Get the channel value
uint8_t channelValue = static_cast<unsigned char>(buffer[3]);
// // Update the DMX array
dmxData[channelNumber].store(channelValue);
} else if(buffer[0] == 0x04) {
// Close this sender
exit(0);
} else {
std::cerr << "Unknown command" << endl;
}
}
// Entry point
int main(int argc, char* argv[]) {
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
#endif
FT_STATUS ftStatus;
FT_HANDLE ftHandle = nullptr;
// Check if the serial port is specified
if (argc != 2) {
std::cerr << "Invalid call to DMX sender" << std::endl;
return 1;
}
// Connect the serial port
int deviceDev;
try {
deviceDev = std::stoi(argv[1]);
}catch(const std::exception& e){
std::cerr << "Invalid call to DMX sender" << std::endl;
return 1;
}
ftStatus = FT_Open(deviceDev, &ftHandle);
if (ftStatus != FT_OK) {
std::cerr << "Unable to open the FTDI device" << std::endl;
return 1;
}
ftStatus = FT_SetBaudRate(ftHandle, 250000);
ftStatus |= FT_SetDataCharacteristics(ftHandle, 8, FT_STOP_BITS_2, FT_PARITY_NONE); // 8 bits, pas de parité, 1 bit de stop
ftStatus |= FT_SetFlowControl(ftHandle, FT_FLOW_NONE, 0, 0);
if (ftStatus != FT_OK) {
std::cerr << "Unable to configure the FTDI device" << std::endl;
FT_Close(ftHandle);
return 1;
}
// Send the DMX frames
std::thread updateThread(sendDMX, ftHandle);
// Intercept commands from the GO program
char buffer[4]; // Tampon pour stocker les 4 octets d'une commande
while (true) {
std::cin.read(buffer, 4); // Attente bloquante jusqu'à ce que 4 octets soient lus
if (std::cin.gcount() == 4) { // Vérifier que 4 octets ont été lus
processCommand(buffer);
} else if (std::cin.eof()) {
std::cerr << "Fin de l'entrée standard (EOF)" << std::endl;
break;
} else if (std::cin.fail()) {
std::cerr << "Erreur de lecture sur stdin" << std::endl;
break;
}
}
return 0;
}

View 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>DMXSender</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@@ -0,0 +1,8 @@
windres dmxSender.rc dmxSender.o
windres detectFTDI.rc detectFTDI.o
g++ -o dmxSender.exe dmxSender.cpp dmxSender.o -I"include" -L"lib" -lftd2xx -mwindows
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

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

Binary file not shown.

36
main.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"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() {
// 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.startup,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}

6
package-lock.json generated Normal file
View File

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

1
package.json Normal file
View File

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

77
peripherals.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
"changeme/hardware"
"fmt"
)
// GetPeripherals gets all the peripherals connected
func (a *App) GetPeripherals() error {
return a.hardwareManager.Scan(a.ctx)
}
// AddPeripheral adds a peripheral to the project
func (a *App) AddPeripheral(protocolName string, peripheralID string) error {
// Get the device from its finder
p, found := a.hardwareManager.GetPeripheral(protocolName, peripheralID)
if !found {
return fmt.Errorf("Unable to localize the peripheral %s", peripheralID)
}
// Add the peripheral ID to the project
a.projectInfo.PeripheralsInfo[peripheralID] = p.GetInfo()
// TODO: Connect the peripheral
return nil
}
// RemovePeripheral adds a peripheral to the project
func (a *App) RemovePeripheral(protocolName string, peripheralID string) error {
// TODO: Disconnect the peripheral
// Remove the peripheral ID from the project
delete(a.projectInfo.PeripheralsInfo, peripheralID)
return nil
}
// AddOS2LPeripheral adds a new OS2L peripheral
func (a *App) AddOS2LPeripheral() (hardware.PeripheralInfo, error) {
// Get the OS2L driver
os2lDriver, err := a.hardwareManager.GetDriver("OS2L")
if err != nil {
return hardware.PeripheralInfo{}, err
}
// Create a new OS2L peripheral with this driver
os2lPeripheral, err := os2lDriver.CreatePeripheral(a.ctx)
if err != nil {
return hardware.PeripheralInfo{}, nil
}
os2lInfo := os2lPeripheral.GetInfo()
// Add this new peripheral to the project
return os2lInfo, a.AddPeripheral(os2lDriver.GetName(), os2lInfo.SerialNumber)
}
// FOR TESTING PURPOSE ONLY
func (a *App) ConnectFTDI() error {
// Create a new FTDI object
var err error
a.ftdi, err = hardware.NewFTDIPeripheral("FTDI TEST INTERFACE", "A50825I", 0)
if err != nil {
return err
}
return a.ftdi.Connect()
}
func (a *App) ActivateFTDI() error {
return a.ftdi.Activate()
}
func (a *App) SetDeviceFTDI(channelValue byte) error {
return a.ftdi.SetDeviceProperty(0, 0, channelValue)
}
func (a *App) DeactivateFTDI() error {
return a.ftdi.Deactivate()
}
func (a *App) DisconnectFTDI() error {
return a.ftdi.Disconnect()
}

161
project.go Normal file
View File

@@ -0,0 +1,161 @@
package main
import (
"changeme/hardware"
"fmt"
"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.Fatalf("Unable to open the projects directory: %v", err)
return nil, err
}
files, err := f.Readdir(0)
if err != nil {
log.Fatalf("Unable to read the projects directory: %v", err)
return nil, err
}
for _, fileInfo := range files {
// Open the file and get the show name
fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name()))
if err == nil {
projectObject := ProjectInfo{}
err = yaml.Unmarshal(fileData, &projectObject)
if err == nil {
// Add the SaveFile property
projects = append(projects, ProjectMetaData{
Name: projectObject.ShowInfo.Name,
Save: fileInfo.Name(),
})
}
}
}
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",
}
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)
content, err := os.ReadFile(projectPath)
if err != nil {
log.Fatalf("Unable to read the project file: %v", err)
return ProjectInfo{}, err
}
a.projectInfo = ProjectInfo{}
err = yaml.Unmarshal(content, &a.projectInfo)
if err != nil {
log.Fatalf("Unable to get the project information: %v", err)
return ProjectInfo{}, err
}
// Load it into the app
a.projectSave = projectFile
// Return the show 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 {
return "", err
}
// Copy the avatar to the application avatars path
avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath))
_, err = copy(filePath, avatarPath)
if err != nil {
return "", err
}
return filepath.Base(filePath), nil
}
// UpdateShowInfo updates the show information
func (a *App) UpdateShowInfo(showInfo ShowInfo) {
fmt.Printf("%s\n", showInfo)
a.projectInfo.ShowInfo = showInfo
}
// SaveProject saves the project
func (a *App) SaveProject() (string, error) {
log.Printf("Saving the project %s to %s", a.projectInfo.ShowInfo.Name, a.projectSave)
// 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)
}
data, err := yaml.Marshal(a.projectInfo)
if err != nil {
return "", err
}
// Create the project directory if not exists
err = os.MkdirAll(projectsDirectory, os.ModePerm)
if err != nil {
return "", err
}
err = os.WriteFile(filepath.Join(projectsDirectory, a.projectSave), data, os.ModePerm)
if err != nil {
return "", err
}
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
View File

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