19 Commits

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

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

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

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

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

Reviewed-on: #17
2025-01-18 14:53:29 +00:00
1052dcc8d5 added the toast notifications center (#19) 2024-12-24 13:36:11 +00:00
a231263825 project management feature (#15)
Reviewed-on: #15
2024-11-01 20:10:28 +00:00
364dabee69 10-project-settings (#13)
Added the project properties page.
2024-07-08 11:19:35 +00:00
b33df4b447 Added the application logo (#12)
Added the application logo.
2024-07-07 16:34:44 +00:00
d6dc8405dd added final navigation bar (#9)
Reviewed-on: #9
2024-06-24 07:02:39 +00:00
75 changed files with 6310 additions and 365 deletions

11
.gitignore vendored
View File

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

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

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

112
app.go
View File

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

55
build.bat Normal file
View File

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

BIN
build/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

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

View File

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

61
endpoints.go Normal file
View File

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

View File

@@ -5,7 +5,6 @@
<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'>
<link rel="shortcut icon" type="image/x-icon" href="./src/assets/images/logo-universal.png" />
<title>DMXConnect</title>
</head>
<body style="background-color:#1B262C;">

View File

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

BIN
frontend/public/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -1,41 +1,138 @@
<script>
import logo from './assets/images/logo-universal.png'
import {Greet} from '../wailsjs/go/main/App.js'
import { _ } from 'svelte-i18n'
import NavigationBar from './components/NavigationBar.svelte';
import Clock from './components/Clock.svelte'
import Stage from './components/Stage.svelte';
import Atmo from './components/Atmo.svelte';
import Settings from './components/Settings.svelte';
import DevicesLibrary from './components/DevicesLibrary.svelte';
import NavigationBar from './components/General/NavigationBar.svelte';
import Clock from './components/General/Clock.svelte'
import Preparation from './components/Preparation/Preparation.svelte';
import Animation from './components/Animation/Animation.svelte';
import Settings from './components/Settings/Settings.svelte';
import Devices from './components/Devices/Devices.svelte';
import Show from './components/Show/Show.svelte';
import RoundDropdownList from "./components/General/RoundDropdownList.svelte";
import GeneralConsole from './components/Console/GeneralConsole.svelte';
import RoundIconButton from './components/General/RoundIconButton.svelte';
import { generateToast, showInformation, needProjectSave, projectsList } from './stores.js';
import { GetProjects, CreateProject, OpenProjectFromDisk, SaveProject } from '../wailsjs/go/main/App.js';
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
import ToastNotification from './components/General/ToastNotification.svelte';
import { onMount, onDestroy } from 'svelte'
import { destroyRuntimeEvents, initRuntimeEvents } from './runtime-events.js'
let selectedMenu = "stage"
function initializeNewProject(){
// Instanciate a new project
CreateProject().then(() => {
// Project created, we set the needSave flag to true (not already saved)
needProjectSave.set(true)
}).catch((error) => {
console.error(`Unable to create the project: ${error}`)
generateToast('danger', 'bx-error', $_("projectCreateErrorToast"))
})
}
// Initialize runtime events at startup
onMount(() => {
initRuntimeEvents()
// Handle window shortcuts
document.addEventListener('keydown', function(event) {
// Check the CTRL+S keys
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
// Avoid the natural behaviour
event.preventDefault();
// Save the current project
saveProject()
}
});
// Initialize a new project
initializeNewProject()
})
// Destroy runtime events at shutdown
onDestroy(() => {
destroyRuntimeEvents()
})
// Set the window title
$: {
WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (" + $_("unsavedProjectFlag") + ")" : ""))
}
let selectedMenu = "settings"
// When the navigation menu changed, update the selected menu
function onNavigationChanged(event){
selectedMenu = event.detail.menu;
}
// Save the project
function saveProject(){
SaveProject().then((filePath) => {
needProjectSave.set(false)
console.log("Project has been saved to " + filePath)
generateToast('info', 'bxs-save', $_("projectSavedToast"))
}).catch((error) => {
console.error(`Unable to save the project: ${error}`)
generateToast('danger', 'bx-error', $_("projectSaveErrorToast") + ' ' + error)
})
}
// Open the selected project
function openSelectedProject(event){
let selectedOption = event.detail.key
// Open the selected project
OpenProjectFromDisk(selectedOption).then(() => {
// Project opened, we set the needSave flag to false (already saved)
needProjectSave.set(false)
}).catch((error) => {
console.error(`Unable to open the project: ${error}`)
generateToast('danger', 'bx-error', $_("projectOpenErrorToast"))
})
}
// Refresh the projects list
let choices = new Map()
function loadProjectsList(){
GetProjects().then((projects) => {
choices = new Map(projects.map(item => [item.Save, item.Name]));
$projectsList = projects
}).catch((error) => {
console.error(`Unable to get the projects list: ${error}`)
generateToast('danger', 'bx-error', $_("projectsLoadErrorToast"))
})
}
</script>
<header>
<!-- Project buttons -->
<RoundIconButton on:mouseup={initializeNewProject} icon='bxs-plus-square' width="2.5em" tooltip={$_("newProjectTooltip")}/>
<RoundDropdownList on:click={loadProjectsList} on:selected={openSelectedProject} choices={choices} icon='bxs-folder-open' width="2.5em" tooltip={$_("openProjectTooltip")}/>
<NavigationBar on:navigationChanged="{onNavigationChanged}"/>
{#if $needProjectSave}
<RoundIconButton on:mouseup={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}/>
{/if}
<Clock/>
</header>
<main>
{#if selectedMenu === "stage"}
<Stage />
{:else if selectedMenu === "atmo"}
<Atmo />
{:else if selectedMenu === "settings"}
{#if selectedMenu === "settings"}
<Settings />
{:else if selectedMenu === "devicesLibrary"}
<DevicesLibrary />
{:else if selectedMenu === "devices"}
<Devices />
{:else if selectedMenu === "preparation"}
<Preparation />
{:else if selectedMenu === "animation"}
<Animation />
{:else if selectedMenu === "show"}
<Show />
{:else if selectedMenu === "console"}
<GeneralConsole />
{/if}
<ToastNotification/>
</main>
<style>
main {
text-align: left;
padding: 1em;
max-width: 240px;
margin: 0 auto;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View File

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

View File

@@ -1 +0,0 @@
<h1>Atmo</h1>

View File

@@ -1,50 +0,0 @@
<script>
import { onDestroy } from 'svelte';
import * as SoftwareVariables from '../stores.js';
// Import the main colors from the store
let firstColor, secondColor, thirdColor, fourthColor, okColor, nokColor
const unsubscribeFirstColor = SoftwareVariables.firstColor.subscribe((value) => (firstColor = value));
const unsubscribeSecondColor = SoftwareVariables.secondColor.subscribe((value) => (secondColor = value));
const unsubscribeThirdColor = SoftwareVariables.thirdColor.subscribe((value) => (thirdColor = value));
const unsubscribeFourthColor = SoftwareVariables.fourthColor.subscribe((value) => (fourthColor = value));
const unsubscribeOkColor = SoftwareVariables.okColor.subscribe((value) => (okColor = value));
const unsubscribeNokColor = SoftwareVariables.nokColor.subscribe((value) => (nokColor = value));
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);
onDestroy(() => {
clearInterval(interval);
unsubscribeFirstColor();
unsubscribeSecondColor();
unsubscribeThirdColor();
unsubscribeFourthColor();
unsubscribeOkColor();
unsubscribeNokColor();
});
</script>
<div style='color:{fourthColor}'>
<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 @@
<h1>General console</h1>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<script>
import { createEventDispatcher, onDestroy } from 'svelte';
import * as SoftwareVariables from '../stores.js';
import {colors} from '../../stores.js';
import Tooltip from './Tooltip.svelte';
import { _ } from 'svelte-i18n'
@@ -10,31 +10,24 @@
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 // The optional button status
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
// Import the main colors from the store
let firstColor, secondColor, thirdColor, fourthColor, okColor, nokColor
const unsubscribeFirstColor = SoftwareVariables.firstColor.subscribe((value) => (firstColor = value));
const unsubscribeSecondColor = SoftwareVariables.secondColor.subscribe((value) => (secondColor = value));
const unsubscribeThirdColor = SoftwareVariables.thirdColor.subscribe((value) => (thirdColor = value));
const unsubscribeFourthColor = SoftwareVariables.fourthColor.subscribe((value) => (fourthColor = value));
const unsubscribeOkColor = SoftwareVariables.okColor.subscribe((value) => (okColor = value));
const unsubscribeNokColor = SoftwareVariables.nokColor.subscribe((value) => (nokColor = value));
// Default values for background and foreground
$: background = firstColor
$: foreground = firstColor
$: background = $colors.first
$: foreground = $colors.first
// Change the background when the selected prop changed
$: {
if (active === true) {
background = thirdColor
background = $colors.third
foreground = $colors.fourth
} else {
background = fourthColor
background = $colors.fourth
foreground = $colors.second
}
}
@@ -42,62 +35,70 @@
// undefined => no status displayed
// operationalStatus = true => OK color displayed
// operationalStatus = false => NOK color displayed
$: statusColor = nokColor
$: statusColor = $colors.nok
$: {
if (operationalStatus === true){
statusColor = okColor
statusColor = $colors.ok
tooltipMessage = tooltip + " " + okStatusLabel
} else {
statusColor = nokColor
statusColor = $colors.nok
tooltipMessage = tooltip + " " + nokStatusLabel
}
}
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function emitClick() {
dispatch('click');
function emitMouseDown() {
dispatch('mousedown');
}
function emitMouseUp() {
dispatch('mouseup');
}
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
function toggleTooltip(){
tooltipShowing = !tooltipShowing
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
}
// Unsubscribe for all variables used from the store
onDestroy(() => {
unsubscribeFirstColor();
unsubscribeSecondColor();
unsubscribeThirdColor();
unsubscribeFourthColor();
unsubscribeOkColor();
unsubscribeNokColor();
})
</script>
<div>
<Tooltip message={tooltipMessage} show={tooltipShowing}></Tooltip>
<button
style="width:{width}; height:{width}; border:none; border-radius:{width}; background-color:{background}; color:{foreground};"
on:mousedown={emitClick}
on:mouseenter={toggleTooltip}
on:mouseleave={toggleTooltip}>
<i class='bx {icon}' style="font-size:calc({width} - 80%);"></i>
<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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,72 +0,0 @@
<script>
import RoundIconButton from './RoundIconButton.svelte';
import { createEventDispatcher, onDestroy } from 'svelte';
import * as SoftwareVariables from '../stores.js';
import { _ } from 'svelte-i18n'
// Import the main colors from the store
let firstColor, secondColor, thirdColor, fourthColor, okColor, nokColor
const unsubscribeFirstColor = SoftwareVariables.firstColor.subscribe((value) => (firstColor = value));
const unsubscribeSecondColor = SoftwareVariables.secondColor.subscribe((value) => (secondColor = value));
const unsubscribeThirdColor = SoftwareVariables.thirdColor.subscribe((value) => (thirdColor = value));
const unsubscribeFourthColor = SoftwareVariables.fourthColor.subscribe((value) => (fourthColor = value));
const unsubscribeOkColor = SoftwareVariables.okColor.subscribe((value) => (okColor = value));
const unsubscribeNokColor = SoftwareVariables.nokColor.subscribe((value) => (nokColor = value));
//---Navigation System---//
let menuStates = {
stage: true,
atmo: false,
devicesLibrary: false,
settings: 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
});
}
// Unsubscribe for all variables used from the store
onDestroy(() => {
unsubscribeFirstColor();
unsubscribeSecondColor();
unsubscribeThirdColor();
unsubscribeFourthColor();
unsubscribeOkColor();
unsubscribeNokColor();
})
</script>
<div style="background-color: {secondColor};">
<RoundIconButton id="stageMenu" on:click="{() => handleNavigation("stage")}" icon="bx-shape-square" width="2.5em" tooltip="{$_("stageMenuTooltip")}" active={menuStates.stage}></RoundIconButton>
<RoundIconButton id="atmoMenu" on:click="{() => handleNavigation("atmo")}" icon="bxl-deezer" width="2.5em" tooltip="{$_("atmoMenuTooltip")}" active={menuStates.atmo}></RoundIconButton>
<RoundIconButton id="devicesLibraryMenu" on:click="{() => handleNavigation("devicesLibrary")}" icon="bx-hdd" width="2.5em" tooltip={$_("devicesLibraryMenuTooltip")} active={menuStates.devicesLibrary}></RoundIconButton>
<RoundIconButton id="settingsMenu" on:click="{() => handleNavigation("settings")}" icon="bx-cog" width="2.5em" tooltip={$_("settingsMenuTooltip")} active={menuStates.settings}></RoundIconButton>
<RoundIconButton icon="bx-broadcast" width="2.5em" tooltip="{$_("dmxBoxStatusTooltip")}" operationalStatus=false okStatusLabel="{$_("dmxBoxOkStatusTooltip")}" nokStatusLabel="{$_("dmxBoxNokStatusTooltip")}"></RoundIconButton>
</div>
<style>
div {
display: inline-block;
padding: 4px;
border-radius: 40px;
}
</style>

View File

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

View File

@@ -1 +0,0 @@
<h1>Settings</h1>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
<div id="background"></div>
<h1>Stage</h1>
<style>
#background {
background-image: url('../assets/images/stage_background.png');
background-size: cover;
background-position: center;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
}
</style>

View File

@@ -1,44 +0,0 @@
<script>
export let message = "Default tooltip"
export let show = false
import * as SoftwareVariables from '../stores.js';
import { onDestroy } from 'svelte';
// Import the main colors from the store
let firstColor, fourthColor
const unsubscribeFirstColor = SoftwareVariables.firstColor.subscribe((value) => (firstColor = value));
const unsubscribeFourthColor = SoftwareVariables.fourthColor.subscribe((value) => (fourthColor = value));
let mustBeDisplayed = "none"
$: {
if (show === true){
mustBeDisplayed = "inline-block"
} else {
mustBeDisplayed = "none"
}
}
// Unsubscribe for all variables used from the store
onDestroy(() => {
unsubscribeFirstColor();
unsubscribeFourthColor();
})
</script>
<div style="background-color:{fourthColor}; display:{mustBeDisplayed}">
<p style="color:{firstColor};">{message}</p>
</div>
<style>
div {
position: absolute;
border-radius: 15px;
transform: translate(0, 200%);
}
p{
margin:5px;
font-size: 10px;
}
</style>

View File

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

View File

@@ -1,5 +1,10 @@
import App from './App.svelte';
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
import { _ } from 'svelte-i18n'
import {messages, showInformation, needProjectSave} from './stores.js';
// Load dictionaries
import { addMessages, init } from 'svelte-i18n';
@@ -13,7 +18,7 @@ addMessages('en', en);
init({
fallbackLocale: 'en',
initialLocale: 'en',
});
});
// Create the main app
const app = new App({

View File

@@ -0,0 +1,225 @@
import { EventsOn, EventsOff } from "../wailsjs/runtime/runtime.js"
import { endpoints, generateToast, needProjectSave, showInformation } from './stores'
import { get } from "svelte/store"
import { _ } from 'svelte-i18n'
// New endpoint has been added to the system
function endpointArrival (endpointInfo){
// If not exists, add it to the map
// isDetected key to true
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
isDetected: true,
},
}})
console.log("Hardware has been added to the system");
generateToast('info', 'bxs-hdd', get(_)("endpointArrivalToast") + ' <b>' + endpointInfo.Name + '</b>')
}
// Endpoint is removed from the system
function endpointRemoval (endpointInfo){
// If not exists, add it to the map
// isDetected key to false
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
isDetected: false,
status: "PERIPHERAL_DISCONNECTED",
},
}})
console.log("Hardware has been removed from the system");
generateToast('warning', 'bxs-hdd', get(_)("endpointRemovalToast") + ' <b>' + endpointInfo.Name + '</b>')
}
// Update endpoint status
function endpointUpdateStatus(endpointInfo, status){
// If not exists, add it to the map
// change status key
endpoints.update((storedEndpoints) => {
console.log(status)
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
status: status,
},
}})
console.log("Hardware status has been updated to " + status);
}
// Load the endpoint in the project
function loadEndpoint (endpointInfo) {
// If not exists, add it to the map
// isSaved key to true
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
isSaved: true,
},
}})
console.log("Hardware has been added to the project");
//TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder
needProjectSave.set(true)
}
function loadProject (showInfo){
// Store project information
showInformation.set(showInfo)
console.log("Project has been opened");
generateToast('info', 'bx-folder-open', get(_)("projectOpenedToast") + ' <b>' + showInfo.Name + '</b>')
}
// Unload the hardware from the project
function unloadEndpoint (endpointInfo) {
// If not exists, add it to the map
// isSaved key to false
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[endpointInfo.SerialNumber]: {
...storedEndpoints[endpointInfo.SerialNumber],
Name: endpointInfo.Name,
ProtocolName: endpointInfo.ProtocolName,
SerialNumber: endpointInfo.SerialNumber,
Settings: endpointInfo.Settings,
isSaved: false,
},
}})
console.log("Hardware has been removed from the project");
//TODO: Lors d'un chargement/déchargement natif au démarrage, il ne doit pas y avoir de nécessité de sauvegarder
needProjectSave.set(true)
}
// A endpoint event has been emitted
function onEndpointEvent(sn, event) {
// If not exists, add it to the map
// eventEmitted key to true for 0.2 sec
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[sn]: {
...storedEndpoints[sn],
eventEmitted: true
},
}})
setTimeout(() => {
endpoints.update((storedEndpoints) => {
return {
...storedEndpoints,
[sn]: {
...storedEndpoints[sn],
eventEmitted: false
},
}})
}, 200);
}
// When a new device is added
function onDeviceArrival(deviceInfo) {
console.log("New device arrival")
console.log(deviceInfo)
}
// When a new device is removed
function onDeviceRemoval(sn){
console.log("New device removal")
console.log(sn)
}
let initialized = false
export function initRuntimeEvents(){
if (initialized) return
initialized = true
// Handle the event when a new endpoint is detected
EventsOn('PERIPHERAL_ARRIVAL', endpointArrival)
// Handle the event when a endpoint is removed from the system
EventsOn('PERIPHERAL_REMOVAL', endpointRemoval)
// Handle the event when a endpoint status is updated
EventsOn('PERIPHERAL_STATUS', endpointUpdateStatus)
// Handle the event when a new project need to be loaded
EventsOn('LOAD_PROJECT', loadProject)
// Handle a endpoint loaded in the project
EventsOn('PERIPHERAL_LOAD', loadEndpoint)
// Handle a endpoint unloaded from the project
EventsOn('PERIPHERAL_UNLOAD', unloadEndpoint)
// Handle a endpoint event
EventsOn('PERIPHERAL_EVENT_EMITTED', onEndpointEvent)
// Handle a device arrival
EventsOn('DEVICE_ARRIVAL', onDeviceArrival)
// Handle a device removal
EventsOn('DEVICE_REMOVAL', onDeviceRemoval)
}
export function destroyRuntimeEvents(){
if (!initialized) return
initialized = false
// Handle the event when a new endpoint is detected
EventsOff('PERIPHERAL_ARRIVAL')
// Handle the event when a endpoint is removed from the system
EventsOff('PERIPHERAL_REMOVAL')
// Handle the event when a endpoint status is updated
EventsOff('PERIPHERAL_STATUS')
// Handle the event when a new project need to be loaded
EventsOff('LOAD_PROJECT')
// Handle a endpoint loaded in the project
EventsOff('PERIPHERAL_LOAD')
// Handle a endpoint unloaded from the project
EventsOff('PERIPHERAL_UNLOAD')
// Handle a endpoint event
EventsOff('PERIPHERAL_EVENT_EMITTED')
// Handle a device arrival
EventsOff('DEVICE_ARRIVAL')
// Handle a device removal
EventsOff('DEVICE_REMOVAL')
}

View File

@@ -1,14 +1,49 @@
import { writable } from 'svelte/store';
// Colors defined in the software
export const firstColor = writable("#1B262C");
export const secondColor = writable("#0F4C75");
export const thirdColor = writable("#3282B8");
export const fourthColor = writable("#BBE1FA");
export const okColor = writable("#2BA646");
export const nokColor = writable("#A6322B");
// Projects management
export let projectsList = writable([])
export let needProjectSave = writable(true)
// Show settings
export let showInformation = writable({})
// Toasts notifications
export let messages = writable([])
export function generateToast(type, icon, text){
messages.update((value) => {
value.push( { id: Date.now(), type: type, icon: icon, text: text } )
return value.slice(-5)
})
}
// Application colors
export const colors = writable({
first: "#1B262C",
second: "#0F4C75",
third: "#3282B8",
fourth: "#BBE1FA",
ok: "#2BA646",
nok: "#A6322B",
white: "#FFFFFF",
orange: "#BC9714"
})
// Font sizes defined in the software
export const firstSize = writable("10px")
export const secondSize = writable("14px")
export const thirdSize = writable("20px")
// List of current hardware
export let endpoints = writable({})
// Endpoint structure :
// Name string `yaml:"name"` // Name of the endpoint
// SerialNumber string `yaml:"sn"` // S/N of the endpoint
// ProtocolName string `yaml:"protocol"` // Protocol name of the endpoint
// Settings map[string]interface{} `yaml:"settings"` // Endpoint settings
// isSaved // if the endpoint is saved in the project
// isDetected // if the endpoint is detected by the system
// status // the status of connection
// eventEmitted // if an event has been emitted for this endpoint (disappear after a delay)

View File

@@ -1,7 +1,23 @@
: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 {

52
go.mod
View File

@@ -1,33 +1,45 @@
module changeme
module dmxconnect
go 1.18
go 1.21
require github.com/wailsapp/wails/v2 v2.5.1
toolchain go1.21.3
require (
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.33.0
github.com/wailsapp/wails/v2 v2.9.1
gitlab.com/gomidi/midi v1.23.7
gitlab.com/gomidi/rtmididrv v0.15.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/uuid v1.1.2 // 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.9.0 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/leaanthony/go-ansi-parser v1.0.1 // 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.5.0 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/samber/lo v1.27.1 // indirect
github.com/tkrajina/go-reflector v0.5.5 // indirect
github.com/leaanthony/slicer v1.6.0 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // 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.1.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.5.1 => /home/dev/go/pkg/mod

95
go.sum
View File

@@ -1,61 +1,85 @@
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
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.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4=
github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM=
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 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
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/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg=
github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ=
github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
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 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
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.5.1 h1:mfG+2kWqQXYOwdgI43HEILjOZDXbk5woPYI3jP2b+js=
github.com/wailsapp/wails/v2 v2.5.1/go.mod h1:jbOZbcr/zm79PxXxAjP8UoVlDd9wLW3uDs+isIthDfs=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlEdc=
github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
gitlab.com/gomidi/midi v1.21.0/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c=
gitlab.com/gomidi/midi v1.23.7 h1:I6qKoIk9s9dcX+pNf0jC+tziCzJFn82bMpuntRkLeik=
gitlab.com/gomidi/midi v1.23.7/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c=
gitlab.com/gomidi/rtmididrv v0.15.0 h1:52Heco8Y3Jjcl4t0yDUVikOxfI8FMF1Zq+qsG++TUeo=
gitlab.com/gomidi/rtmididrv v0.15.0/go.mod h1:p/6IL1LGgj7utcv3wXudsDWiD9spgAdn0O8LDsGIPG0=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -64,14 +88,21 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc
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.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

90
hardware/hardware.go Normal file
View File

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

View File

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

View File

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

View File

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

31
main.go
View File

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

6
package-lock.json generated Normal file
View File

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

1
package.json Normal file
View File

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

243
project.go Normal file
View File

@@ -0,0 +1,243 @@
package main
import (
"dmxconnect/hardware"
"fmt"
"github.com/rs/zerolog/log"
"os"
"path/filepath"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
"gopkg.in/yaml.v2"
)
const (
projectsDirectory = "projects" // The directory were are stored all the projects
avatarsDirectory = "frontend/public" // The directory were are stored all the avatars
projectExtension = ".dmxproj" // The extension of a DMX Connect project
)
// GetProjects gets all the projects in the projects directory
func (a *App) GetProjects() ([]ProjectMetaData, error) {
projects := []ProjectMetaData{}
f, err := os.Open(projectsDirectory)
if err != nil {
log.Err(err).Str("file", "project").Msg("unable to open the projects directory")
return nil, fmt.Errorf("unable to open the projects directory: %v", err)
}
log.Trace().Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("projects directory opened")
files, err := f.Readdir(0)
if err != nil {
log.Err(err).Str("file", "project").Msg("unable to read the projects directory")
return nil, fmt.Errorf("unable to read the projects directory: %v", err)
}
log.Trace().Str("file", "project").Any("projectsFiles", files).Msg("project files got")
for _, fileInfo := range files {
// Open the file and get the show name
fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name()))
if err != nil {
log.Warn().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("unable to open the project file")
continue
}
log.Trace().Str("file", "project").Str("projectFile", fileInfo.Name()).Any("fileData", fileData).Msg("project file read")
projectObject := ProjectInfo{}
err = yaml.Unmarshal(fileData, &projectObject)
if err != nil {
log.Warn().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("project has invalid format")
continue
}
log.Trace().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("project file unmarshalled")
// Add the SaveFile property
projects = append(projects, ProjectMetaData{
Name: projectObject.ShowInfo.Name,
Save: fileInfo.Name(),
})
}
log.Info().Str("file", "project").Any("projectsList", projects).Msg("got the projects list")
return projects, nil
}
// CreateProject creates a new blank project
func (a *App) CreateProject() error {
// Create new project information
date := time.Now()
projectInfo := ProjectInfo{
ShowInfo{
Name: "My new show",
Date: fmt.Sprintf("%04d-%02d-%02dT%02d:%02d", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute()),
Avatar: "appicon.png",
Comments: "Write your comments here",
},
make(map[string]hardware.EndpointInfo),
}
// The project isn't saved for now
a.projectSave = ""
return a.OpenProject(projectInfo)
}
// OpenProjectFromDisk opens a project based on its filename
func (a *App) OpenProjectFromDisk(projectFile string) error {
// Open the project file
projectPath := filepath.Join(projectsDirectory, projectFile)
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project path is created")
content, err := os.ReadFile(projectPath)
if err != nil {
log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to read the project file")
return fmt.Errorf("unable to read the project file: %v", err)
}
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project file read")
// Import project data structure
projectInfo := ProjectInfo{}
err = yaml.Unmarshal(content, &projectInfo)
if err != nil {
log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to get the project information")
return fmt.Errorf("unable to get the project information: %v", err)
}
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project information got")
// The project is saved
a.projectSave = projectFile
return a.OpenProject(projectInfo)
}
// OpenProject opens a project based on its information
func (a *App) OpenProject(projectInfo ProjectInfo) error {
// Close the current project
err := a.CloseCurrentProject()
if err != nil {
return fmt.Errorf("unable to close project: %w", err)
}
// Open the project
a.projectInfo = projectInfo
// Send an event with the project data
runtime.EventsEmit(a.ctx, "LOAD_PROJECT", projectInfo.ShowInfo)
// Load all endpoints of the project
projectEndpoints := a.projectInfo.EndpointsInfo
for key, value := range projectEndpoints {
_, err = a.hardwareManager.RegisterEndpoint(a.ctx, value)
if err != nil {
return fmt.Errorf("unable to register the endpoint S/N '%s': %w", key, err)
}
}
return nil
}
// CloseCurrentProject closes the current project
func (a *App) CloseCurrentProject() error {
// Unregister all endpoints of the project
projectEndpoints := a.hardwareManager.SavedEndpoints
for key, value := range projectEndpoints {
err := a.hardwareManager.UnregisterEndpoint(a.ctx, value)
if err != nil {
return fmt.Errorf("unable to unregister the endpoint S/N '%s': %w", key, err)
}
}
// Unload project info in the front
return nil
}
// ChooseAvatarPath opens a filedialog to choose the show avatar
func (a *App) ChooseAvatarPath() (string, error) {
// Open the file dialog box
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Choose your show avatar",
Filters: []runtime.FileFilter{
{
DisplayName: "Images",
Pattern: "*.png;*.jpg;*.jpeg",
},
},
})
if err != nil {
log.Err(err).Str("file", "project").Msg("unable to open the avatar dialog")
return "", err
}
log.Debug().Str("file", "project").Msg("avatar dialog is opened")
// Copy the avatar to the application avatars path
avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath))
log.Trace().Str("file", "project").Str("avatarPath", avatarPath).Msg("avatar path is created")
_, err = copy(filePath, avatarPath)
if err != nil {
log.Err(err).Str("file", "project").Str("avatarsDirectory", avatarsDirectory).Str("fileBase", filepath.Base(filePath)).Msg("unable to copy the avatar file")
return "", err
}
log.Info().Str("file", "project").Str("avatarFileName", filepath.Base(filePath)).Msg("got the new avatar file")
return filepath.Base(filePath), nil
}
// UpdateShowInfo updates the show information
func (a *App) UpdateShowInfo(showInfo ShowInfo) {
a.projectInfo.ShowInfo = showInfo
log.Info().Str("file", "project").Any("showInfo", showInfo).Msg("show information was updated")
}
// SaveProject saves the project
func (a *App) SaveProject() (string, error) {
// If there is no save file, create a new one with the show name
if a.projectSave == "" {
date := time.Now()
a.projectSave = fmt.Sprintf("%04d%02d%02d%02d%02d%02d%s", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), projectExtension)
log.Debug().Str("file", "project").Str("newProjectSave", a.projectSave).Msg("projectSave is null, getting a new one")
}
// Get hardware info
a.projectInfo.EndpointsInfo = a.hardwareManager.SavedEndpoints
// Marshal the project
data, err := yaml.Marshal(a.projectInfo)
if err != nil {
log.Err(err).Str("file", "project").Any("projectInfo", a.projectInfo).Msg("unable to format the project information")
return "", err
}
log.Trace().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("projectInfo has been marshalled")
// Create the project directory if not exists
err = os.MkdirAll(projectsDirectory, os.ModePerm)
if err != nil {
log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("unable to create the projects directory")
return "", err
}
log.Trace().Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("projects directory has been created")
err = os.WriteFile(filepath.Join(projectsDirectory, a.projectSave), data, os.ModePerm)
if err != nil {
log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Str("projectSave", a.projectSave).Msg("unable to save the project")
return "", err
}
log.Info().Str("file", "project").Str("projectFileName", a.projectSave).Msg("project has been saved")
return a.projectSave, nil
}
// ShowInfo defines the information of the show
type ShowInfo struct {
Name string `yaml:"name"`
Date string `yaml:"date"`
Avatar string `yaml:"avatar"`
Comments string `yaml:"comments"`
}
// ProjectMetaData defines all the minimum information for a lighting project
type ProjectMetaData struct {
Name string // Show name
Save string // The save file of the project
}
// ProjectInfo defines all the information for a lighting project
type ProjectInfo struct {
ShowInfo ShowInfo `yaml:"show"` // Show information
EndpointsInfo map[string]hardware.EndpointInfo `yaml:"endpoints"` // Endpoints information
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "dmxconnect",
"outputfilename": "dmxconnect",
"outputfilename": "dmxconnect.exe",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",