generated from thinkode/modelRepository
Compare commits
13 Commits
0e3f57f5fb
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fbb75ad19 | |||
| 932c288a9c | |||
| 121a14ac61 | |||
| 6dd555265c | |||
| 9e8cbed73f | |||
| 15d0f8b61b | |||
| abcc3e0b5e | |||
| cb5c5b688e | |||
| 65e2def501 | |||
| bc15407cad | |||
| 0db468bfef | |||
| 4e0829e821 | |||
| c7fe171cb4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
build/bin
|
||||
projects
|
||||
mapping
|
||||
node_modules
|
||||
frontend/.vscode
|
||||
frontend/dist
|
||||
@@ -9,4 +10,6 @@ frontend/wailsjs
|
||||
*.exe
|
||||
*.o
|
||||
*.rc
|
||||
*.dll
|
||||
*.dll.a
|
||||
frontend/public
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -58,6 +58,9 @@
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp"
|
||||
"variant": "cpp",
|
||||
"queue": "cpp",
|
||||
"ranges": "cpp",
|
||||
"text_encoding": "cpp"
|
||||
}
|
||||
}
|
||||
61
app.go
61
app.go
@@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"dmxconnect/hardware"
|
||||
genericmidi "dmxconnect/hardware/genericMIDI"
|
||||
"dmxconnect/hardware/genericftdi"
|
||||
"dmxconnect/hardware/os2l"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
@@ -16,26 +19,29 @@ import (
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
hardwareManager *hardware.HardwareManager // For managing all the hardware
|
||||
wmiMutex sync.Mutex // Avoid some WMI operations at the same time
|
||||
projectInfo ProjectInfo // The project information structure
|
||||
projectSave string // The file name of the project
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
wait sync.WaitGroup
|
||||
|
||||
hardwareManager *hardware.Manager // For managing all the hardware
|
||||
wmiMutex sync.Mutex // Avoid some WMI operations at the same time
|
||||
projectInfo ProjectInfo // The project information structure
|
||||
projectSave string // The file name of the project
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
// Create a new hadware manager
|
||||
hardwareManager := hardware.NewHardwareManager()
|
||||
// hardwareManager.RegisterFinder(hardware.NewMIDIFinder(5 * time.Second))
|
||||
hardwareManager.RegisterFinder(hardware.NewFTDIFinder(5 * time.Second))
|
||||
// hardwareManager.RegisterFinder(hardware.NewOS2LFinder())
|
||||
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{
|
||||
PeripheralsInfo: make(map[string]hardware.PeripheralInfo),
|
||||
EndpointsInfo: make(map[string]hardware.EndpointInfo),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -44,20 +50,26 @@ func NewApp() *App {
|
||||
// so we can call the runtime methods
|
||||
func (a *App) onStartup(ctx context.Context) {
|
||||
a.ctx, a.cancelFunc = context.WithCancel(ctx)
|
||||
err := a.hardwareManager.Start(a.ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "app").Msg("unable to start the hardware manager")
|
||||
return
|
||||
}
|
||||
|
||||
// Starting the hardware manager
|
||||
a.wait.Add(1)
|
||||
go func() {
|
||||
defer a.wait.Done()
|
||||
err := a.hardwareManager.Start(a.ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "app").Msg("unable to start the hardware manager")
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// onReady is called when the DOM is ready
|
||||
// We get the current peripherals connected
|
||||
// We get the current endpoints connected
|
||||
func (a *App) onReady(ctx context.Context) {
|
||||
// log.Debug().Str("file", "peripherals").Msg("getting peripherals...")
|
||||
// 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 peripherals")
|
||||
// log.Err(err).Str("file", "app").Msg("unable to get the endpoints")
|
||||
// }
|
||||
return
|
||||
}
|
||||
@@ -69,10 +81,13 @@ func (a *App) onShutdown(ctx context.Context) {
|
||||
log.Trace().Str("file", "app").Msg("app is closing")
|
||||
// Explicitly close the context
|
||||
a.cancelFunc()
|
||||
err := a.hardwareManager.Stop()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "app").Msg("unable to stop the hardware manager")
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
55
build.bat
Normal file
55
build.bat
Normal file
@@ -0,0 +1,55 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
echo ============================================
|
||||
echo [INFO] Starting Wails build script
|
||||
echo ============================================
|
||||
|
||||
rem Détection du mode (par défaut : build)
|
||||
set "MODE=build"
|
||||
if /i "%~1"=="-dev" set "MODE=dev"
|
||||
|
||||
rem 1️⃣ Essayer de récupérer le dernier tag
|
||||
for /f "tokens=*" %%i in ('git describe --tags --abbrev=0 2^>nul') do set "GIT_TAG=%%i"
|
||||
|
||||
rem 2️⃣ Si pas de tag, utiliser le hash du commit
|
||||
if "%GIT_TAG%"=="" (
|
||||
for /f "tokens=*" %%i in ('git rev-parse --short HEAD 2^>nul') do set "GIT_TAG=%%i"
|
||||
)
|
||||
|
||||
rem 3️⃣ Si Git n’est pas dispo, mettre "unknown"
|
||||
if "%GIT_TAG%"=="" set "GIT_TAG=unknown"
|
||||
|
||||
echo [INFO] Git version detected: %GIT_TAG%
|
||||
|
||||
|
||||
echo [INFO] Mode selectionne : %MODE%
|
||||
|
||||
echo [INFO] Moving to the C++ folder...
|
||||
cd /d "%~dp0hardware\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
|
||||
61
endpoints.go
Normal file
61
endpoints.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -7,56 +7,54 @@
|
||||
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, peripherals } from './stores';
|
||||
import { SaveProject } from '../wailsjs/go/main/App.js';
|
||||
import { construct_svelte_component } from 'svelte/internal';
|
||||
import { EventsOn } from '../wailsjs/runtime'
|
||||
import { CreateProject } from "../wailsjs/go/main/App";
|
||||
import { generateToast, showInformation, needProjectSave, projectsList } from './stores.js';
|
||||
import { GetProjects, CreateProject, OpenProjectFromDisk, SaveProject } from '../wailsjs/go/main/App.js';
|
||||
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
|
||||
import { get } from "svelte/store"
|
||||
import ToastNotification from './components/General/ToastNotification.svelte';
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { destroyRuntimeEvents, initRuntimeEvents } from './runtime-events.js'
|
||||
|
||||
// Handle the event when a new peripheral is detected
|
||||
EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){
|
||||
// When a new peripheral is detected, add it to the map and:
|
||||
// - Pass the isDetected key to true
|
||||
// - Set the isSaved key to the last value
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
|
||||
peripheralInfo.isDetected = true
|
||||
peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false
|
||||
peripherals.update((peripherals) => {
|
||||
peripherals[peripheralInfo.SerialNumber] = peripheralInfo
|
||||
return {...peripherals}
|
||||
})
|
||||
console.log("Hardware has been added to the system");
|
||||
generateToast('info', 'bxs-hdd', $_("peripheralArrivalToast") + ' <b>' + peripheralInfo.Name + '</b>')
|
||||
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()
|
||||
})
|
||||
|
||||
// Handle the event when a peripheral is removed from the system
|
||||
EventsOn('PERIPHERAL_REMOVAL', function(peripheralInfo){
|
||||
console.log("Hardware has been removed from the system");
|
||||
// When a peripheral is disconnected, pass its isDetected key to false
|
||||
// If the isSaved key is set to false, we can completely remove the peripheral from the list
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
|
||||
let needToDelete = (lastSavedProperty !== true) ? true : false
|
||||
peripherals.update((storedPeripherals) => {
|
||||
if (needToDelete){
|
||||
delete storedPeripherals[peripheralInfo.SerialNumber];
|
||||
return { ...storedPeripherals };
|
||||
}
|
||||
storedPeripherals[peripheralInfo.SerialNumber].isDetected = false
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
generateToast('warning', 'bxs-hdd', $_("peripheralRemovalToast") + ' <b>' + peripheralInfo.Name + '</b>')
|
||||
// Destroy runtime events at shutdown
|
||||
onDestroy(() => {
|
||||
destroyRuntimeEvents()
|
||||
})
|
||||
|
||||
// Set the window title
|
||||
$: {
|
||||
WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (unsaved)" : ""))
|
||||
WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (" + $_("unsavedProjectFlag") + ")" : ""))
|
||||
}
|
||||
|
||||
let selectedMenu = "settings"
|
||||
@@ -77,29 +75,41 @@
|
||||
})
|
||||
}
|
||||
|
||||
// Instanciate a new project
|
||||
CreateProject().then((showInfo) => {
|
||||
showInformation.set(showInfo)
|
||||
$needProjectSave = true
|
||||
})
|
||||
// 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"))
|
||||
})
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
});
|
||||
// Refresh the projects list
|
||||
let choices = new Map()
|
||||
function loadProjectsList(){
|
||||
GetProjects().then((projects) => {
|
||||
choices = new Map(projects.map(item => [item.Save, item.Name]));
|
||||
$projectsList = projects
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to get the projects list: ${error}`)
|
||||
generateToast('danger', 'bx-error', $_("projectsLoadErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<!-- Project buttons -->
|
||||
<RoundIconButton on:mouseup={initializeNewProject} icon='bxs-plus-square' width="2.5em" tooltip={$_("newProjectTooltip")}/>
|
||||
<RoundDropdownList on:click={loadProjectsList} on:selected={openSelectedProject} choices={choices} icon='bxs-folder-open' width="2.5em" tooltip={$_("openProjectTooltip")}/>
|
||||
|
||||
<NavigationBar on:navigationChanged="{onNavigationChanged}"/>
|
||||
{#if $needProjectSave}
|
||||
<RoundIconButton on:mouseup={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}></RoundIconButton>
|
||||
<RoundIconButton on:mouseup={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}/>
|
||||
{/if}
|
||||
<Clock/>
|
||||
</header>
|
||||
@@ -123,7 +133,6 @@
|
||||
<style>
|
||||
main {
|
||||
text-align: left;
|
||||
padding: 1em;
|
||||
max-width: 240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
156
frontend/src/components/General/RoundDropdownList.svelte
Normal file
156
frontend/src/components/General/RoundDropdownList.svelte
Normal 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>
|
||||
@@ -15,15 +15,14 @@
|
||||
</script>
|
||||
|
||||
<div class="tabContainer" style="color: {$colors.white};">
|
||||
<div class="headerContainer"
|
||||
style='background-color: {$colors.third};'>
|
||||
<div class="headerContainer">
|
||||
{#each tabs as tab, index}
|
||||
<RoundedButton text={tab.title} icon={tab.icon} tooltip={tab.tooltip} active={ (activeTab == index) ? true : false } on:click={() => setActiveTab(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}; max-height: {maxHeight};'>
|
||||
style='background-color: {$colors.first}; max-width: {maxWidth};'>
|
||||
{#if tabs[activeTab]}
|
||||
<svelte:component this={tabs[activeTab].component} />
|
||||
{/if}
|
||||
@@ -41,13 +40,11 @@
|
||||
}
|
||||
.headerContainer{
|
||||
padding: 0.1em;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
.bodyContainer{
|
||||
padding: 0.5em;
|
||||
background-color: red;
|
||||
border-radius: 0.5em;
|
||||
overflow:auto;
|
||||
}
|
||||
</style>
|
||||
@@ -14,13 +14,15 @@
|
||||
export let addable = false;
|
||||
export let signalizable = false;
|
||||
export let signalized = false;
|
||||
export let disconnected = 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')
|
||||
}
|
||||
@@ -36,11 +38,11 @@
|
||||
</script>
|
||||
|
||||
<div class="card" on:dblclick={dblclick}>
|
||||
<div class="profile" on:mousedown={click} style="color: {disconnected ? $colors.first : $colors.white};">
|
||||
<div>
|
||||
<p>{#if disconnected}<i class='bx bx-no-signal' style="font-size:100%; color: var(--nok-color);"></i> {/if}{title}</p>
|
||||
<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 disconnected}
|
||||
{#if status == "PERIPHERAL_DISCONNECTED"}
|
||||
<h6><b>Disconnected</b></h6>
|
||||
{:else}
|
||||
<h6>{line1}</h6>
|
||||
@@ -49,37 +51,51 @@
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<InfoButton on:click={add} color="{disconnected ? $colors.first : $colors.white}" style="margin: 0.2em; display: { addable ? 'flex' : 'none' }" icon='bxs-message-square-add' interactive message={$_("projectHardwareAddTooltip")}/>
|
||||
<InfoButton on:click={remove} color="{disconnected ? $colors.first : $colors.white}" style="margin: 0.2em; display: { removable ? 'flex' : 'none' }" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")}/>
|
||||
<InfoButton style="margin: 0.2em;" background={ signalized ? $colors.orange : $colors.first } icon='bx-pulse' hide={!signalizable}/>
|
||||
<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>
|
||||
.profile:hover{
|
||||
background-color: var(--third-color);
|
||||
.unselected:hover{
|
||||
background: linear-gradient(to bottom right, var(--second-color), var(--third-color));
|
||||
}
|
||||
.card{
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
.profile {
|
||||
background-color: var(--second-color);
|
||||
.selected {
|
||||
background-color: var(--third-color);
|
||||
position: relative;
|
||||
margin: 0.2em;
|
||||
padding-left: 0.3em;
|
||||
border-radius: 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;
|
||||
@@ -88,4 +104,31 @@
|
||||
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>
|
||||
@@ -1,164 +1,151 @@
|
||||
<script lang=ts>
|
||||
import DeviceCard from "./DeviceCard.svelte";
|
||||
import Tab from "../General/Tab.svelte";
|
||||
import { _ } from 'svelte-i18n'
|
||||
import { generateToast, needProjectSave, peripherals } from "../../stores";
|
||||
import Input from "../General/Input.svelte";
|
||||
import { t, _ } from 'svelte-i18n'
|
||||
import { generateToast, needProjectSave, endpoints, colors } from "../../stores";
|
||||
import { get } from "svelte/store"
|
||||
import { AddOS2LPeripheral, RemovePeripheral, ConnectFTDI, ActivateFTDI, DeactivateFTDI, DisconnectFTDI, SetDeviceFTDI, AddPeripheral } from "../../../wailsjs/go/main/App";
|
||||
import { UpdateEndpointSettings, GetEndpointSettings, RemoveEndpoint, AddEndpoint } from "../../../wailsjs/go/main/App";
|
||||
import RoundedButton from "../General/RoundedButton.svelte";
|
||||
|
||||
function ftdiConnect(){
|
||||
ConnectFTDI().then(() =>
|
||||
console.log("FTDI connected"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to connect: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
function ftdiActivate(){
|
||||
ActivateFTDI().then(() =>
|
||||
console.log("FTDI activated"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to activate: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
function ftdiDeactivate(){
|
||||
DeactivateFTDI().then(() =>
|
||||
console.log("FTDI deactivated"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to deactivate: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
let sliderValue = 0
|
||||
function ftdiSetDevice(value){
|
||||
console.log("value is " + value)
|
||||
SetDeviceFTDI(value).then(() =>
|
||||
console.log("FTDI device set up"))
|
||||
// Create the endpoint to the project
|
||||
function createEndpoint(endpoint){
|
||||
// Create the endpoint to the project (backend)
|
||||
AddEndpoint(endpoint)
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to set the device: " + error)
|
||||
})
|
||||
console.log("Unable to create the endpoint: " + error)
|
||||
generateToast('danger', 'bx-error', $_("addEndpointErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
function ftdiDisconnect(){
|
||||
DisconnectFTDI().then(() =>
|
||||
console.log("FTDI disconnected"))
|
||||
// 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("Error when trying to disconnect: " + error)
|
||||
console.log("Unable to remove the endpoint from the project: " + error)
|
||||
generateToast('danger', 'bx-error', $_("removeEndpointErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Add the peripheral to the project
|
||||
function addPeripheral(peripheral){
|
||||
// Add the peripheral to the project (backend)
|
||||
AddPeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
|
||||
peripherals.update((value) => {
|
||||
if (value[peripheral.SerialNumber]) {
|
||||
value[peripheral.SerialNumber].isSaved = true;
|
||||
}
|
||||
return {...value}
|
||||
// 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 add the peripheral to the project: " + error)
|
||||
generateToast('danger', 'bx-error', $_("addPeripheralErrorToast"))
|
||||
console.log("Unable to save the endpoint setting: " + error)
|
||||
generateToast('danger', 'bx-error', $_("endpointSettingSaveErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the peripheral from the project
|
||||
function removePeripheral(peripheral) {
|
||||
// Delete the peripheral from the project (backend)
|
||||
RemovePeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
|
||||
// If the peripheral is not detected, we can delete it form the store
|
||||
// If not, we only pass the isSaved key to false
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastDetectedProperty = peripheralsList[peripheral.SerialNumber]?.isDetected
|
||||
let needToDelete = (lastDetectedProperty !== true) ? true : false
|
||||
peripherals.update((storedPeripherals) => {
|
||||
if (needToDelete){
|
||||
delete storedPeripherals[peripheral.SerialNumber];
|
||||
return { ...storedPeripherals };
|
||||
}
|
||||
storedPeripherals[peripheral.SerialNumber].isSaved = false
|
||||
return { ...storedPeripherals };
|
||||
})
|
||||
$needProjectSave = true
|
||||
}).catch((error) => {
|
||||
console.log("Unable to remove the peripheral from the project: " + error)
|
||||
generateToast('danger', 'bx-error', $_("removePeripheralErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Create the OS2L peripheral
|
||||
function createOS2L(){
|
||||
AddOS2LPeripheral().then(os2lDevice => {
|
||||
peripherals.update(currentPeriph => {
|
||||
os2lDevice.isSaved = true
|
||||
os2lDevice.isDetected = true
|
||||
currentPeriph[os2lDevice.SerialNumber] = os2lDevice
|
||||
return {...currentPeriph}
|
||||
})
|
||||
$needProjectSave = true
|
||||
generateToast('info', 'bx-signal-5', $_("os2lPeripheralCreatedToast"))
|
||||
}).catch(error => {
|
||||
console.log("Unable to add the OS2L peripheral: " + error)
|
||||
generateToast('danger', 'bx-error', $_("os2lPeripheralCreateErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Get the number of saved peripherals
|
||||
$: savedPeripheralNumber = Object.values($peripherals).filter(peripheral => peripheral.isSaved).length;
|
||||
</script>
|
||||
|
||||
<div class="hardware">
|
||||
<div style="padding: 0.5em;">
|
||||
<p style="margin-bottom: 1em;">Available peripherals</p>
|
||||
<div class="libraryPanel">
|
||||
<div class="availableHardware">
|
||||
<p style="color: var(--first-color);"><i class='bx bxs-plug'></i> Detected</p>
|
||||
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
|
||||
{#if peripheral.isDetected}
|
||||
<DeviceCard on:add={() => addPeripheral(peripheral)} on:dblclick={() => {
|
||||
if(!peripheral.isSaved)
|
||||
addPeripheral(peripheral)
|
||||
}}
|
||||
title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/>
|
||||
{/if}
|
||||
{/each}
|
||||
<p style="color: var(--first-color);"><i class='bx bxs-network-chart' ></i> Others</p>
|
||||
<RoundedButton on:click={createOS2L} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/>
|
||||
<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;">Project peripherals</p>
|
||||
<p style="margin-bottom: 1em;">{$_("projectHardwareSavedLabel")}</p>
|
||||
<div class="configuredHardware">
|
||||
{#if savedPeripheralNumber > 0}
|
||||
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
|
||||
{#if peripheral.isSaved}
|
||||
<DeviceCard on:delete={() => removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)}
|
||||
disconnected={!peripheral.isDetected} title={peripheral.Name == "" ? "Please wait..." : peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} removable signalizable/>
|
||||
{#if 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>No hardware saved for this project.</i>
|
||||
<i>{$_("projectHardwareEmptyLabel")}</i>
|
||||
{/if}
|
||||
</div>
|
||||
<p style="margin-bottom: 1em;">Peripheral settings</p>
|
||||
<div>
|
||||
<p><i>Select a peripheral to edit its settings</i></p>
|
||||
<button on:click={ftdiConnect}>Connect FTDI 0</button>
|
||||
<button on:click={ftdiActivate}>Activate FTDI 0</button>
|
||||
<div class="slidecontainer">
|
||||
<input type="range" min="0" max="255" class="slider" bind:value={sliderValue} on:input={() => ftdiSetDevice(sliderValue)}>
|
||||
</div>
|
||||
<button on:click={ftdiDeactivate}>Deactivate FTDI 0</button>
|
||||
<button on:click={ftdiDisconnect}>Disconnect FTDI 0</button>
|
||||
<div 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;
|
||||
}
|
||||
@@ -171,19 +158,23 @@
|
||||
border-radius: 0.5em;
|
||||
padding: 0.2em;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
/* overflow: visible; */
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.availableHardware::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.configuredHardware {
|
||||
background-color: var(--second-color);
|
||||
border-radius: 0.5em;
|
||||
|
||||
.libraryPanel {
|
||||
padding: 0.5em;
|
||||
padding: 0.2em;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
// Validate the project information
|
||||
function validate(field, value){
|
||||
$showInformation[field] = value
|
||||
console.log($showInformation)
|
||||
|
||||
@@ -1,97 +1,18 @@
|
||||
<script lang=ts>
|
||||
import { generateToast, projectsList, showInformation, needProjectSave, peripherals } from '../../stores.js';
|
||||
import RoundedButton from "../General/RoundedButton.svelte";
|
||||
import ProjectPropertiesContent from "./ProjectPropertiesContent.svelte";
|
||||
import DropdownList from "../General/DropdownList.svelte";
|
||||
import InputsOutputsContent from "./InputsOutputsContent.svelte";
|
||||
import Tab from "../General/Tab.svelte";
|
||||
import { CreateProject, GetProjects, GetProjectInfo } from "../../../wailsjs/go/main/App";
|
||||
import { _ } from 'svelte-i18n'
|
||||
import {colors} from '../../stores.js';
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const tabs = [
|
||||
{ title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent },
|
||||
{ title: $_("projectInputOutputTab"), icon: 'bxs-plug', tooltip: $_("projectInputOutputTooltip"), component: InputsOutputsContent },
|
||||
];
|
||||
|
||||
// Refresh the projects list
|
||||
let choices = new Map()
|
||||
function loadProjectsList(){
|
||||
GetProjects().then((projects) => {
|
||||
choices = new Map(projects.map(item => [item.Save, item.Name]));
|
||||
$projectsList = projects
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to get the projects list: ${error}`)
|
||||
generateToast('danger', 'bx-error', $_("projectsLoadErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
// Unsave peripherals from the store and remove the disconnected peripherals
|
||||
function unsavePeripherals(){
|
||||
peripherals.update((storedPeripherals) => {
|
||||
// Set all the isSaved keys to false and delete the disconnected peripherals
|
||||
for (let peripheralID in storedPeripherals) {
|
||||
storedPeripherals[peripheralID].isSaved = false
|
||||
if (!storedPeripherals[peripheralID].isDetected) {
|
||||
delete storedPeripherals[peripheralID]
|
||||
}
|
||||
}
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
}
|
||||
|
||||
// Load the saved peripherals into the store
|
||||
function loadPeripherals(peripheralsInfo){
|
||||
peripherals.update((storedPeripherals) => {
|
||||
// Add the saved peripherals of the project
|
||||
// If already exists pass the isSaved key to true, if not create the peripheral and set it to disconnected
|
||||
for (let peripheralID in peripheralsInfo){
|
||||
// Add the peripheral to the list of peripherals, with the last isDetected key and the isSaved key to true
|
||||
let lastDetectedKey = storedPeripherals[peripheralID]?.isDetected
|
||||
storedPeripherals[peripheralID] = peripheralsInfo[peripheralID]
|
||||
storedPeripherals[peripheralID].isDetected = (lastDetectedKey === true) ? true : false
|
||||
storedPeripherals[peripheralID].isSaved = true
|
||||
}
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
}
|
||||
|
||||
// Open the selected project
|
||||
function openSelectedProject(event){
|
||||
let selectedOption = event.detail.key
|
||||
// Open the selected project
|
||||
GetProjectInfo(selectedOption).then((projectInfo) => {
|
||||
$showInformation = projectInfo.ShowInfo
|
||||
// Remove the saved peripherals ofthe current project
|
||||
unsavePeripherals()
|
||||
// Load the new project peripherals
|
||||
loadPeripherals(projectInfo.PeripheralsInfo)
|
||||
needProjectSave.set(false)
|
||||
generateToast('info', 'bx-folder-open', $_("projectOpenedToast") + ' <b>' + projectInfo.ShowInfo.Name + '</b>')
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to open the project: ${error}`)
|
||||
generateToast('danger', 'bx-error', $_("projectOpenErrorToast"))
|
||||
})
|
||||
}
|
||||
|
||||
function initializeNewProject(){
|
||||
// Instanciate a new project
|
||||
CreateProject().then((showInfo) => {
|
||||
$showInformation = showInfo
|
||||
// Remove the saved peripherals ofthe current project
|
||||
unsavePeripherals()
|
||||
$needProjectSave = true
|
||||
generateToast('info', 'bxs-folder-plus', $_("projectCreatedToast"))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Project buttons -->
|
||||
<RoundedButton on:click={initializeNewProject} text={$_("newProjectString")} icon='bxs-plus-square' tooltip={$_("newProjectTooltip")}/>
|
||||
<DropdownList icon='bxs-folder-open' text={$_("openProjectString")} choices={choices} tooltip={$_("openProjectTooltip")} on:click={loadProjectsList} on:selected={openSelectedProject}/>
|
||||
<!-- Project tabcontrol -->
|
||||
<Tab { tabs } maxHeight='73vh'/>
|
||||
<Tab { tabs }/>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -13,9 +13,11 @@
|
||||
"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": "Inputs & outputs",
|
||||
"projectInputOutputTab": "Hardware",
|
||||
"projectInputOutputTooltip": "The input/output hardware definition",
|
||||
|
||||
"projectShowNameLabel": "Show name",
|
||||
@@ -33,19 +35,32 @@
|
||||
"projectHardwareShowLabel" : "My Show",
|
||||
"projectHardwareInputsLabel": "INPUTS",
|
||||
"projectHardwareOutputsLabel": "OUTPUTS",
|
||||
"projectHardwareDeleteTooltip": "Delete this peripheral",
|
||||
"projectHardwareAddTooltip": "Add this peripheral to project",
|
||||
"projectHardwareDeleteTooltip": "Delete",
|
||||
"projectHardwareAddTooltip": "Add to project",
|
||||
"projectHardwareNoSelection": "Empty",
|
||||
"projectHardwareSavedLabel": "Saved in project",
|
||||
"projectHardwareDetectedLabel": "Detected",
|
||||
"projectHardwareOthersLabel": "Others",
|
||||
"projectHardwareEmptyLabel": "No hardware saved for this project",
|
||||
|
||||
"peripheralArrivalToast": "Peripheral inserted:",
|
||||
"peripheralRemovalToast": "Peripheral removed:",
|
||||
"endpointArrivalToast": "Endpoint inserted:",
|
||||
"endpointRemovalToast": "Endpoint removed:",
|
||||
"projectSavedToast": "The project has been saved",
|
||||
"projectSaveErrorToast": "Unable to save the project:",
|
||||
"addPeripheralErrorToast": "Unable to add this peripheral to project",
|
||||
"removePeripheralErrorToast": "Unable to remove this peripheral from project",
|
||||
"os2lPeripheralCreatedToast": "Your OS2L peripheral has been created",
|
||||
"os2lPeripheralCreateErrorToast": "Unable to create the OS2L peripheral",
|
||||
"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"
|
||||
"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"
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import App from './App.svelte';
|
||||
|
||||
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
|
||||
import { _ } from 'svelte-i18n'
|
||||
|
||||
import {showInformation, needProjectSave} from './stores.js';
|
||||
import {messages, showInformation, needProjectSave} from './stores.js';
|
||||
|
||||
// Load dictionaries
|
||||
import { addMessages, init } from 'svelte-i18n';
|
||||
|
||||
225
frontend/src/runtime-events.js
Normal file
225
frontend/src/runtime-events.js
Normal 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')
|
||||
}
|
||||
@@ -10,13 +10,12 @@ export let showInformation = writable({})
|
||||
// Toasts notifications
|
||||
export let messages = writable([])
|
||||
export function generateToast(type, icon, text){
|
||||
messages.update((value) => {
|
||||
value.push( { id: Date.now(), type: type, icon: icon, text: text } )
|
||||
return value.slice(-5)
|
||||
})
|
||||
messages.update((value) => {
|
||||
value.push( { id: Date.now(), type: type, icon: icon, text: text } )
|
||||
return value.slice(-5)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Application colors
|
||||
export const colors = writable({
|
||||
first: "#1B262C",
|
||||
@@ -35,4 +34,16 @@ export const secondSize = writable("14px")
|
||||
export const thirdSize = writable("20px")
|
||||
|
||||
// List of current hardware
|
||||
export let peripherals = writable({})
|
||||
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)
|
||||
5
go.mod
5
go.mod
@@ -5,9 +5,11 @@ go 1.21
|
||||
toolchain go1.21.3
|
||||
|
||||
require (
|
||||
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/wailsapp/wails/v2 v2.9.1
|
||||
gitlab.com/gomidi/midi v1.23.7
|
||||
gitlab.com/gomidi/rtmididrv v0.15.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@@ -26,7 +28,6 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.6 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -37,8 +37,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79 h1:CA1UHN3RuY70DlC0RlvgtB1e8h3kYzmvK7s8CFe+Ohw=
|
||||
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79/go.mod h1:oBuZjmjlKSj9CZKrNhcx/adNhHiiE0hZknECjIP8Z0Q=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -70,6 +68,11 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlEdc=
|
||||
github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
|
||||
gitlab.com/gomidi/midi v1.21.0/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c=
|
||||
gitlab.com/gomidi/midi v1.23.7 h1:I6qKoIk9s9dcX+pNf0jC+tziCzJFn82bMpuntRkLeik=
|
||||
gitlab.com/gomidi/midi v1.23.7/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c=
|
||||
gitlab.com/gomidi/rtmididrv v0.15.0 h1:52Heco8Y3Jjcl4t0yDUVikOxfI8FMF1Zq+qsG++TUeo=
|
||||
gitlab.com/gomidi/rtmididrv v0.15.0/go.mod h1:p/6IL1LGgj7utcv3wXudsDWiD9spgAdn0O8LDsGIPG0=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
goRuntime "runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
ftdiFinderExecutableName = "FTDI_finder.exe"
|
||||
ftdiSenderExecutableName = "FTDI_sender.exe"
|
||||
)
|
||||
|
||||
// FTDIFinder represents how the protocol is defined
|
||||
type FTDIFinder struct {
|
||||
findTicker time.Ticker // Peripherals find ticker
|
||||
peripherals map[string]Peripheral // The list of peripherals handled by this finder
|
||||
scanChannel chan struct{} // The channel to trigger a scan event
|
||||
goWait sync.WaitGroup // Check goroutines execution
|
||||
}
|
||||
|
||||
// NewFTDIFinder creates a new FTDI finder
|
||||
func NewFTDIFinder(findPeriod time.Duration) *FTDIFinder {
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder created")
|
||||
return &FTDIFinder{
|
||||
findTicker: *time.NewTicker(findPeriod),
|
||||
peripherals: make(map[string]Peripheral),
|
||||
scanChannel: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed third-party/ftdi/detectFTDI.exe
|
||||
var finderExe []byte
|
||||
|
||||
//go:embed third-party/ftdi/dmxSender.exe
|
||||
var senderExe []byte
|
||||
|
||||
// Initialize initializes the FTDI finder
|
||||
func (f *FTDIFinder) Initialize() error {
|
||||
// Check platform
|
||||
if goRuntime.GOOS != "windows" {
|
||||
log.Error().Str("file", "FTDIFinder").Str("platform", goRuntime.GOOS).Msg("FTDI finder not compatible with your platform")
|
||||
return fmt.Errorf("the FTDI finder is not compatible with your platform yet (%s)", goRuntime.GOOS)
|
||||
}
|
||||
// Create the FTDI executables
|
||||
err := createExecutable(ftdiFinderExecutableName, finderExe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createExecutable(ftdiSenderExecutableName, senderExe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createExecutable creates and writes an executable to the temporary directory of the system
|
||||
func createExecutable(fileName string, storedFile []byte) error {
|
||||
tempFile, err := os.Create(filepath.Join(os.TempDir(), fileName))
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to create an FTDI executable")
|
||||
return err
|
||||
}
|
||||
log.Trace().Str("file", "FTDIFinder").Str("filePath", tempFile.Name()).Msg("FTDI executable created")
|
||||
|
||||
// Write the embedded executable to the temp file
|
||||
if _, err := tempFile.Write(storedFile); err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to write the content to an FTDI executable")
|
||||
return err
|
||||
}
|
||||
tempFile.Close()
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("fileName", fileName).Msg("FTDI executable written")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the finder and search for peripherals
|
||||
func (f *FTDIFinder) Start(ctx context.Context) error {
|
||||
f.goWait.Add(1)
|
||||
go func() {
|
||||
defer f.goWait.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-f.findTicker.C:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals")
|
||||
}
|
||||
case <-f.scanChannel:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceScan explicily asks for scanning peripherals
|
||||
func (f *FTDIFinder) ForceScan() {
|
||||
f.scanChannel <- struct{}{}
|
||||
}
|
||||
|
||||
// Stop stops the finder
|
||||
func (f *FTDIFinder) Stop() error {
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...")
|
||||
// Wait for goroutines to stop
|
||||
f.goWait.Wait()
|
||||
// Stop the ticker
|
||||
f.findTicker.Stop()
|
||||
// Delete the FTDI executable files
|
||||
fileToDelete := filepath.Join(os.TempDir(), ftdiFinderExecutableName)
|
||||
err := os.Remove(fileToDelete)
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "FTDIFinder").Str("fileName", fileToDelete).AnErr("error", err).Msg("unable to remove the executable file")
|
||||
}
|
||||
fileToDelete = filepath.Join(os.TempDir(), ftdiSenderExecutableName)
|
||||
err = os.Remove(fileToDelete)
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "FTDIFinder").Str("fileName", fileToDelete).AnErr("error", err).Msg("unable to remove the executable file")
|
||||
}
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (f *FTDIFinder) GetName() string {
|
||||
return "FTDI"
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral that correspond to the specified ID
|
||||
func (f *FTDIFinder) GetPeripheral(peripheralID string) (Peripheral, bool) {
|
||||
// Return the specified peripheral
|
||||
peripheral := f.peripherals[peripheralID]
|
||||
if peripheral == nil {
|
||||
log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder")
|
||||
return nil, false
|
||||
}
|
||||
log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder")
|
||||
return peripheral, true
|
||||
}
|
||||
|
||||
// scanPeripherals scans the FTDI peripherals
|
||||
func (f *FTDIFinder) scanPeripherals(ctx context.Context) error {
|
||||
detectionCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("FTDI scan triggered")
|
||||
|
||||
ftdiPeripherals := make(map[string]Peripheral)
|
||||
|
||||
finder := exec.CommandContext(detectionCtx, filepath.Join(os.TempDir(), ftdiFinderExecutableName))
|
||||
log.Trace().Str("file", "FTDIFinder").Msg("has executed the FIND executable")
|
||||
|
||||
stdout, err := finder.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the stdout pipe: %s", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
stderr, err := finder.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the stderr pipe: %s", err)
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
err = finder.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find FTDI peripherals: %s", err)
|
||||
}
|
||||
|
||||
scannerErr := bufio.NewScanner(stderr)
|
||||
for scannerErr.Scan() {
|
||||
return fmt.Errorf("unable to find FTDI peripherals: %s", scannerErr.Text())
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
peripheralString := scanner.Text()
|
||||
// The program output is like '0:1:2' where 0 is the location, 1 is the S/N and 2 is the name
|
||||
peripheralInfo := strings.Split(peripheralString, ":")
|
||||
|
||||
log.Trace().Str("file", "FTDIFinder").Str("scannedString", peripheralString).Str("peripheralName", peripheralInfo[2]).Str("peripheralSN", peripheralInfo[1]).Msg("new FTDI peripheral detected")
|
||||
// Convert the location to an integer
|
||||
location, err := strconv.Atoi(peripheralInfo[0])
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("no location provided for this FTDI peripheral")
|
||||
location = -1
|
||||
}
|
||||
// Add the peripheral to the temporary list
|
||||
peripheral, err := NewFTDIPeripheral(peripheralInfo[2], peripheralInfo[1], location)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the FTDI peripheral: %v", err)
|
||||
}
|
||||
log.Trace().Any("periph", &peripheral).Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("has been created")
|
||||
|
||||
ftdiPeripherals[peripheralInfo[1]] = peripheral
|
||||
log.Trace().Any("periph", ftdiPeripherals).Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("successfully added the FTDI peripheral to the finder")
|
||||
}
|
||||
// Emit the peripherals changes to the front
|
||||
emitPeripheralsChanges(ctx, f.peripherals, ftdiPeripherals)
|
||||
// Store the new peripherals list
|
||||
f.peripherals = ftdiPeripherals
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePeripheral is not implemented here
|
||||
func (f *FTDIFinder) CreatePeripheral(context.Context) (Peripheral, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeletePeripheral is not implemented here
|
||||
func (f *FTDIFinder) DeletePeripheral(serialNumber string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
activateCommandString = 0x01
|
||||
deactivateCommandString = 0x02
|
||||
setCommandString = 0x03
|
||||
)
|
||||
|
||||
// FTDIPeripheral contains the data of an FTDI peripheral
|
||||
type FTDIPeripheral struct {
|
||||
name string // The name of the peripheral
|
||||
serialNumber string // The S/N of the FTDI peripheral
|
||||
location int // The location of the peripheral
|
||||
universesNumber int // The number of DMX universes handled by this peripheral
|
||||
programName string // The temp file name of the executable
|
||||
dmxSender *exec.Cmd // The command to pilot the DMX sender program
|
||||
stdin io.WriteCloser // For writing in the DMX sender
|
||||
stdout io.ReadCloser // For reading from the DMX sender
|
||||
stderr io.ReadCloser // For reading the errors
|
||||
disconnectChan chan struct{} // Channel to cancel the connection
|
||||
errorsChan chan error // Channel to get the errors
|
||||
}
|
||||
|
||||
// NewFTDIPeripheral creates a new FTDI peripheral
|
||||
func NewFTDIPeripheral(name string, serialNumber string, location int) (*FTDIPeripheral, error) {
|
||||
log.Info().Str("file", "FTDIPeripheral").Str("name", name).Str("s/n", serialNumber).Int("location", location).Msg("FTDI peripheral created")
|
||||
return &FTDIPeripheral{
|
||||
name: name,
|
||||
dmxSender: nil,
|
||||
serialNumber: serialNumber,
|
||||
location: location,
|
||||
universesNumber: 1,
|
||||
disconnectChan: make(chan struct{}),
|
||||
errorsChan: make(chan error, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect connects the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Connect(ctx context.Context) error {
|
||||
// Connect if no connection is already running
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("connecting FTDI peripheral...")
|
||||
|
||||
// Check if the connection has already been established
|
||||
if p.dmxSender != nil {
|
||||
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender already initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize the exec.Command for running the process
|
||||
p.dmxSender = exec.Command(p.programName, fmt.Sprintf("%d", p.location))
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender instance created")
|
||||
|
||||
// Create the pipes for stdin, stdout, and stderr asynchronously without blocking
|
||||
var err error
|
||||
if p.stdout, err = p.dmxSender.StdoutPipe(); err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stdout pipe")
|
||||
return fmt.Errorf("unable to create stdout pipe: %v", err)
|
||||
}
|
||||
if p.stdin, err = p.dmxSender.StdinPipe(); err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stdin pipe")
|
||||
return fmt.Errorf("unable to create stdin pipe: %v", err)
|
||||
}
|
||||
if p.stderr, err = p.dmxSender.StderrPipe(); err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stderr pipe")
|
||||
return fmt.Errorf("unable to create stderr pipe: %v", err)
|
||||
}
|
||||
|
||||
// Launch a goroutine to read stderr asynchronously
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(p.stderr)
|
||||
for scanner.Scan() {
|
||||
// Process each line read from stderr
|
||||
log.Err(fmt.Errorf(scanner.Text())).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error detected in dmx sender")
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error reading from stderr")
|
||||
}
|
||||
}()
|
||||
|
||||
// Launch the command asynchronously in another goroutine
|
||||
go func() {
|
||||
// Run the command, respecting the context cancellation
|
||||
err := p.dmxSender.Run()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// If the context is canceled, handle it gracefully
|
||||
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender was canceled by context")
|
||||
return
|
||||
default:
|
||||
// Handle command exit normally
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while execution of dmx sender")
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
log.Warn().Str("file", "FTDIPeripheral").Int("exitCode", exitError.ExitCode()).Str("s/n", p.serialNumber).Msg("dmx sender exited with code")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmx sender exited successfully")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender process started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Disconnect(ctx context.Context) error {
|
||||
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("disconnecting FTDI peripheral...")
|
||||
if p.dmxSender != nil {
|
||||
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
|
||||
_, err := io.WriteString(p.stdin, string([]byte{0x04, 0x00, 0x00, 0x00}))
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
|
||||
return fmt.Errorf("unable to disconnect: %v", err)
|
||||
}
|
||||
p.stdin.Close()
|
||||
p.stdout.Close()
|
||||
p.dmxSender = nil
|
||||
err = os.Remove(p.programName)
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Str("senderPath", p.programName).Msg("unable to delete the dmx sender temporary file")
|
||||
return fmt.Errorf("unable to delete the temporary file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while disconnecting: not connected")
|
||||
return fmt.Errorf("unable to disconnect: not connected")
|
||||
}
|
||||
|
||||
// Activate activates the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Activate(ctx context.Context) error {
|
||||
if p.dmxSender != nil {
|
||||
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
|
||||
_, err := io.WriteString(p.stdin, string([]byte{0x01, 0x00, 0x00, 0x00}))
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
|
||||
return fmt.Errorf("unable to activate: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while activating: not connected")
|
||||
return fmt.Errorf("unable to activate: not connected")
|
||||
}
|
||||
|
||||
// Deactivate deactivates the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Deactivate(ctx context.Context) error {
|
||||
if p.dmxSender != nil {
|
||||
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
|
||||
_, err := io.WriteString(p.stdin, string([]byte{0x02, 0x00, 0x00, 0x00}))
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
|
||||
return fmt.Errorf("unable to deactivate: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while deactivating: not connected")
|
||||
return fmt.Errorf("unable to deactivate: not connected")
|
||||
}
|
||||
|
||||
// SetDeviceProperty sends a command to the specified device
|
||||
func (p *FTDIPeripheral) SetDeviceProperty(ctx context.Context, uint32, channelNumber uint32, channelValue byte) error {
|
||||
if p.dmxSender != nil {
|
||||
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
|
||||
commandString := []byte{0x03, 0x01, 0x00, 0xff, 0x03, 0x02, 0x00, channelValue}
|
||||
_, err := io.WriteString(p.stdin, string(commandString))
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
|
||||
return fmt.Errorf("unable to set device property: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while setting device property: not connected")
|
||||
return fmt.Errorf("unable to set device property: not connected")
|
||||
}
|
||||
|
||||
// GetInfo gets all the peripheral information
|
||||
func (p *FTDIPeripheral) GetInfo() PeripheralInfo {
|
||||
return PeripheralInfo{
|
||||
Name: p.name,
|
||||
SerialNumber: p.serialNumber,
|
||||
ProtocolName: "FTDI",
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattrtaylor/go-rtmidi"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// MIDIFinder represents how the protocol is defined
|
||||
type MIDIFinder struct {
|
||||
findTicker time.Ticker // Peripherals find ticker
|
||||
peripherals map[string]Peripheral // The list of peripherals
|
||||
scanChannel chan struct{} // The channel to trigger a scan event
|
||||
goWait sync.WaitGroup // Check goroutines execution
|
||||
}
|
||||
|
||||
// NewMIDIFinder creates a new DMXUSB protocol
|
||||
func NewMIDIFinder(findPeriod time.Duration) *MIDIFinder {
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder created")
|
||||
return &MIDIFinder{
|
||||
findTicker: *time.NewTicker(findPeriod),
|
||||
peripherals: make(map[string]Peripheral),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the MIDI driver
|
||||
func (f *MIDIFinder) Initialize() error {
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the finder and search for peripherals
|
||||
func (f *MIDIFinder) Start(ctx context.Context) error {
|
||||
f.goWait.Add(1)
|
||||
go func() {
|
||||
defer f.goWait.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-f.findTicker.C:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals")
|
||||
}
|
||||
case <-f.scanChannel:
|
||||
// Scan the peripherals
|
||||
err := f.scanPeripherals(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the finder
|
||||
func (f *MIDIFinder) Stop() error {
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("stopping the MIDI finder...")
|
||||
// Wait for goroutines to stop
|
||||
f.goWait.Wait()
|
||||
// Stop the ticker
|
||||
f.findTicker.Stop()
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (f *MIDIFinder) GetName() string {
|
||||
return "MIDI"
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral that correspond to the specified ID
|
||||
func (f *MIDIFinder) GetPeripheral(peripheralID string) (Peripheral, bool) {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.peripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral in the MIDI finder")
|
||||
return nil, false
|
||||
}
|
||||
log.Trace().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("MIDI peripheral found in the driver")
|
||||
return peripheral, true
|
||||
}
|
||||
|
||||
func splitStringAndNumber(input string) (string, int, error) {
|
||||
// Regular expression to match the text part and the number at the end
|
||||
re := regexp.MustCompile(`^(.*?)(\d+)$`)
|
||||
matches := re.FindStringSubmatch(input)
|
||||
|
||||
// Check if the regex found both a text part and a number
|
||||
if len(matches) == 3 {
|
||||
// matches[1]: text part (might contain trailing spaces)
|
||||
// matches[2]: numeric part as a string
|
||||
textPart := strings.TrimSpace(matches[1]) // Remove any trailing spaces from the text
|
||||
numberPart, err := strconv.Atoi(matches[2])
|
||||
if err != nil {
|
||||
return "", 0, err // Return error if the number conversion fails
|
||||
}
|
||||
return textPart, numberPart, nil
|
||||
}
|
||||
|
||||
// Return an error if no trailing number is found
|
||||
return "", 0, fmt.Errorf("no number found at the end of the string")
|
||||
}
|
||||
|
||||
// ForceScan explicily asks for scanning peripherals
|
||||
func (f *MIDIFinder) ForceScan() {
|
||||
f.scanChannel <- struct{}{}
|
||||
}
|
||||
|
||||
// scanPeripherals scans the MIDI peripherals
|
||||
func (f *MIDIFinder) scanPeripherals(ctx context.Context) error {
|
||||
midiPeripherals := make(map[string]Peripheral)
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("opening MIDI scanner port...")
|
||||
midiScanner, err := rtmidi.NewMIDIInDefault()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to open the MIDI scanner port...")
|
||||
return fmt.Errorf("unable to open the MIDI scanner: %s", err)
|
||||
}
|
||||
defer midiScanner.Close()
|
||||
midiScanner.SetCallback(func(m rtmidi.MIDIIn, b []byte, f float64) {})
|
||||
log.Trace().Str("file", "MIDIFinder").Msg("scanning MIDI peripherals...")
|
||||
devicesCount, err := midiScanner.PortCount()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals...")
|
||||
return fmt.Errorf("unable to scan MIDI peripherals: %s", err)
|
||||
}
|
||||
for i := 0; i < devicesCount; i++ {
|
||||
portName, err := midiScanner.PortName(i)
|
||||
if err != nil {
|
||||
log.Warn().Str("file", "MIDIPeripheral").Msg("found peripheral without a correct name, set it to unknown")
|
||||
portName = "Unknown device 0"
|
||||
}
|
||||
// Separate data
|
||||
name, location, err := splitStringAndNumber(portName)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "MIDIFinder").Str("description", portName).Msg("invalid peripheral description")
|
||||
return fmt.Errorf("invalid pripheral description: %s", err)
|
||||
}
|
||||
log.Info().Str("file", "MIDIFinder").Str("name", name).Int("location", location).Msg("MIDI peripheral found")
|
||||
// Add the peripheral to the temporary list
|
||||
sn := strings.ToLower(strings.Replace(name, " ", "_", -1))
|
||||
midiPeripherals[sn] = NewMIDIPeripheral(name, location, sn)
|
||||
}
|
||||
// Compare with the current peripherals to detect arrivals/removals
|
||||
// removedList, addedList := comparePeripherals(f.peripherals, midiPeripherals)
|
||||
// Emit the events
|
||||
// emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
|
||||
log.Info().Str("file", "MIDIFinder").Msg("MIDI remove list emitted to the front")
|
||||
// emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
|
||||
log.Info().Str("file", "MIDIFinder").Msg("MIDI add list emitted to the front")
|
||||
// Store the new peripherals list
|
||||
f.peripherals = midiPeripherals
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePeripheral is not implemented here
|
||||
func (f *MIDIFinder) CreatePeripheral(context.Context) (Peripheral, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeletePeripheral is not implemented here
|
||||
func (f *MIDIFinder) DeletePeripheral(serialNumber string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// MIDIPeripheral contains the data of a MIDI peripheral
|
||||
type MIDIPeripheral struct {
|
||||
name string // The name of the peripheral
|
||||
location int // The location of the peripheral
|
||||
serialNumber string // The S/N of the peripheral
|
||||
}
|
||||
|
||||
// NewMIDIPeripheral creates a new MIDI peripheral
|
||||
func NewMIDIPeripheral(name string, location int, serialNumber string) *MIDIPeripheral {
|
||||
log.Trace().Str("file", "MIDIPeripheral").Str("name", name).Str("s/n", serialNumber).Int("location", location).Msg("MIDI peripheral created")
|
||||
return &MIDIPeripheral{
|
||||
name: name,
|
||||
location: location,
|
||||
serialNumber: serialNumber,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Connect(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Disconnect(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate activates the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Activate(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Deactivate(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty - not implemented for this kind of peripheral
|
||||
func (p *MIDIPeripheral) SetDeviceProperty(context.Context, uint32, uint32, byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInfo gets the peripheral information
|
||||
func (p *MIDIPeripheral) GetInfo() PeripheralInfo {
|
||||
return PeripheralInfo{
|
||||
Name: p.name,
|
||||
ProtocolName: "MIDI",
|
||||
SerialNumber: p.serialNumber,
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// OS2LFinder represents how the protocol is defined
|
||||
type OS2LFinder struct {
|
||||
peripherals map[string]Peripheral // The list of peripherals
|
||||
}
|
||||
|
||||
// NewOS2LFinder creates a new OS2L finder
|
||||
func NewOS2LFinder() *OS2LFinder {
|
||||
log.Trace().Str("file", "OS2LFinder").Msg("OS2L finder created")
|
||||
return &OS2LFinder{
|
||||
peripherals: make(map[string]Peripheral),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the finder
|
||||
func (f *OS2LFinder) Initialize() error {
|
||||
log.Trace().Str("file", "OS2LFinder").Msg("OS2L finder initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePeripheral creates a new OS2L peripheral
|
||||
func (f *OS2LFinder) CreatePeripheral(ctx context.Context) (Peripheral, error) {
|
||||
// Create a random serial number for this peripheral
|
||||
randomSerialNumber := strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32)))
|
||||
log.Trace().Str("file", "OS2LFinder").Str("serialNumber", randomSerialNumber).Msg("OS2L peripheral created")
|
||||
peripheral := NewOS2LPeripheral("OS2L", randomSerialNumber)
|
||||
f.peripherals[randomSerialNumber] = peripheral
|
||||
log.Info().Str("file", "OS2LFinder").Str("serialNumber", randomSerialNumber).Msg("OS2L peripheral created and registered")
|
||||
return peripheral, nil
|
||||
}
|
||||
|
||||
// DeletePeripheral removes an OS2L peripheral
|
||||
func (f *OS2LFinder) DeletePeripheral(serialNumber string) error {
|
||||
delete(f.peripherals, serialNumber)
|
||||
log.Info().Str("file", "OS2LFinder").Str("serialNumber", serialNumber).Msg("OS2L peripheral removed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (f *OS2LFinder) GetName() string {
|
||||
return "OS2L"
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral that correspond to the specified ID
|
||||
func (f *OS2LFinder) GetPeripheral(peripheralID string) (Peripheral, bool) {
|
||||
// Return the specified peripheral
|
||||
peripheral, found := f.peripherals[peripheralID]
|
||||
if !found {
|
||||
log.Error().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral in the OS2L finder")
|
||||
return nil, false
|
||||
}
|
||||
log.Trace().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("OS2L peripheral found in the finder")
|
||||
return peripheral, true
|
||||
}
|
||||
|
||||
// Start starts the finder
|
||||
func (f *OS2LFinder) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops this finder
|
||||
func (f *OS2LFinder) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceScan scans the interfaces (not implemented)
|
||||
func (f *OS2LFinder) ForceScan() {
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// OS2LPeripheral contains the data of an OS2L peripheral
|
||||
type OS2LPeripheral struct {
|
||||
name string // The name of the peripheral
|
||||
serialNumber string // The serial number of the peripheral
|
||||
}
|
||||
|
||||
// NewOS2LPeripheral creates a new OS2L peripheral
|
||||
func NewOS2LPeripheral(name string, serialNumber string) *OS2LPeripheral {
|
||||
log.Trace().Str("file", "OS2LPeripheral").Str("name", name).Str("s/n", serialNumber).Msg("OS2L peripheral created")
|
||||
return &OS2LPeripheral{
|
||||
name: name,
|
||||
serialNumber: serialNumber,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Connect(ctx context.Context) error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Disconnect(ctx context.Context) error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral disconnected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate activates the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Activate(ctx context.Context) error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral activated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Deactivate(ctx context.Context) error {
|
||||
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral deactivated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty - not implemented for this kind of peripheral
|
||||
func (p *OS2LPeripheral) SetDeviceProperty(context.Context, uint32, uint32, byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInfo gets the peripheral information
|
||||
func (p *OS2LPeripheral) GetInfo() PeripheralInfo {
|
||||
return PeripheralInfo{
|
||||
Name: p.name,
|
||||
SerialNumber: p.serialNumber,
|
||||
ProtocolName: "OS2L",
|
||||
}
|
||||
}
|
||||
72
hardware/devicesHandler.go
Normal file
72
hardware/devicesHandler.go
Normal 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
|
||||
}
|
||||
221
hardware/endpointsHandler.go
Normal file
221
hardware/endpointsHandler.go
Normal 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())
|
||||
}
|
||||
180
hardware/genericftdi/FTDIEndpoint.go
Normal file
180
hardware/genericftdi/FTDIEndpoint.go
Normal 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
|
||||
}
|
||||
181
hardware/genericftdi/FTDIProvider.go
Normal file
181
hardware/genericftdi/FTDIProvider.go
Normal 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
|
||||
}
|
||||
7
hardware/genericftdi/cpp/generate.bat
Normal file
7
hardware/genericftdi/cpp/generate.bat
Normal 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
|
||||
19
hardware/genericftdi/cpp/include/detectFTDIBridge.h
Normal file
19
hardware/genericftdi/cpp/include/detectFTDIBridge.h
Normal 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
|
||||
30
hardware/genericftdi/cpp/include/dmxSenderBridge.h
Normal file
30
hardware/genericftdi/cpp/include/dmxSenderBridge.h
Normal 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);
|
||||
91
hardware/genericftdi/cpp/src/detectFTDI.cpp
Normal file
91
hardware/genericftdi/cpp/src/detectFTDI.cpp
Normal 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"
|
||||
14
hardware/genericftdi/cpp/src/detectFTDI.h
Normal file
14
hardware/genericftdi/cpp/src/detectFTDI.h
Normal 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();
|
||||
225
hardware/genericftdi/cpp/src/dmxSender.cpp
Normal file
225
hardware/genericftdi/cpp/src/dmxSender.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
hardware/genericftdi/cpp/src/dmxSender.h
Normal file
65
hardware/genericftdi/cpp/src/dmxSender.h
Normal 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);
|
||||
};
|
||||
14
hardware/genericftdi/cpp/test/detectFTDI_test.cpp
Normal file
14
hardware/genericftdi/cpp/test/detectFTDI_test.cpp
Normal 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;
|
||||
// }
|
||||
|
||||
}
|
||||
122
hardware/genericftdi/cpp/test/dmxSender_test.cpp
Normal file
122
hardware/genericftdi/cpp/test/dmxSender_test.cpp
Normal 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;
|
||||
}
|
||||
140
hardware/genericmidi/MIDIEndpoint.go
Normal file
140
hardware/genericmidi/MIDIEndpoint.go
Normal 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
|
||||
}
|
||||
248
hardware/genericmidi/MIDIProvider.go
Normal file
248
hardware/genericmidi/MIDIProvider.go
Normal 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, " ")
|
||||
}
|
||||
@@ -2,150 +2,89 @@ package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// PeripheralEvent is trigger by the finders when the scan is complete
|
||||
type PeripheralEvent string
|
||||
// Manager is the class who manages the hardware
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
|
||||
const (
|
||||
// PeripheralArrival is triggerd when a peripheral has been connected to the system
|
||||
PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL"
|
||||
// PeripheralRemoval is triggered when a peripheral has been disconnected from the system
|
||||
PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL"
|
||||
// debounceDuration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
debounceTimer *time.Timer
|
||||
)
|
||||
|
||||
// HardwareManager is the class who manages the hardware
|
||||
type HardwareManager struct {
|
||||
finders map[string]PeripheralFinder // The map of peripherals finders
|
||||
peripherals []Peripheral // The current list of peripherals
|
||||
peripheralsScanTrigger chan struct{} // Trigger the peripherals scans
|
||||
goWait sync.WaitGroup // Wait for goroutines to terminate
|
||||
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
|
||||
}
|
||||
|
||||
// NewHardwareManager creates a new HardwareManager
|
||||
func NewHardwareManager() *HardwareManager {
|
||||
// NewManager creates a new hardware manager
|
||||
func NewManager() *Manager {
|
||||
log.Trace().Str("package", "hardware").Msg("Hardware instance created")
|
||||
return &HardwareManager{
|
||||
finders: make(map[string]PeripheralFinder),
|
||||
peripherals: make([]Peripheral, 0),
|
||||
peripheralsScanTrigger: make(chan struct{}),
|
||||
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 peripheral events
|
||||
func (h *HardwareManager) Start(ctx context.Context) error {
|
||||
for finderName, finder := range h.finders {
|
||||
err := finder.Initialize()
|
||||
// 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("finderName", finderName).Msg("unable to initialize finder")
|
||||
log.Err(err).Str("file", "hardware").Str("providerName", providerName).Msg("unable to initialize provider")
|
||||
return err
|
||||
}
|
||||
err = finder.Start(ctx)
|
||||
|
||||
// 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("finderName", finderName).Msg("unable to start finder")
|
||||
log.Err(err).Str("file", "hardware").Str("providerName", providerName).Msg("unable to start provider")
|
||||
return err
|
||||
}
|
||||
}
|
||||
h.goWait.Add(1)
|
||||
go func() {
|
||||
defer h.goWait.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-h.peripheralsScanTrigger:
|
||||
for finderName, finder := range h.finders {
|
||||
log.Trace().Str("file", "hardware").Str("finderName", finderName).Msg("force a finder to scan peripherals")
|
||||
finder.ForceScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFinder returns a register finder
|
||||
func (h *HardwareManager) GetFinder(finderName string) (PeripheralFinder, error) {
|
||||
finder, exists := h.finders[finderName]
|
||||
if !exists {
|
||||
log.Error().Str("file", "hardware").Str("finderName", finderName).Msg("unable to get the finder")
|
||||
return nil, fmt.Errorf("unable to locate the '%s' finder", finderName)
|
||||
}
|
||||
log.Debug().Str("file", "hardware").Str("finderName", finderName).Msg("got finder")
|
||||
return finder, nil
|
||||
}
|
||||
|
||||
// RegisterFinder registers a new peripherals finder
|
||||
func (h *HardwareManager) RegisterFinder(finder PeripheralFinder) {
|
||||
h.finders[finder.GetName()] = finder
|
||||
log.Info().Str("file", "hardware").Str("finderName", finder.GetName()).Msg("finder registered")
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral object from the parent finder
|
||||
func (h *HardwareManager) GetPeripheral(finderName string, peripheralID string) (Peripheral, bool) {
|
||||
// Get the finder
|
||||
parentFinder, found := h.finders[finderName]
|
||||
// If no finder found, return false
|
||||
if !found {
|
||||
log.Error().Str("file", "hardware").Str("finderName", finderName).Msg("unable to get the finder")
|
||||
return nil, false
|
||||
}
|
||||
log.Trace().Str("file", "hardware").Str("finderName", parentFinder.GetName()).Msg("finder got")
|
||||
// Contact the finder to get the peripheral
|
||||
return parentFinder.GetPeripheral(peripheralID)
|
||||
}
|
||||
|
||||
// Scan scans all the peripherals for the registered finders
|
||||
func (h *HardwareManager) Scan() error {
|
||||
h.peripheralsScanTrigger <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the hardware manager
|
||||
func (h *HardwareManager) Stop() error {
|
||||
// WaitStop stops the hardware manager
|
||||
func (h *Manager) WaitStop() error {
|
||||
log.Trace().Str("file", "hardware").Msg("closing the hardware manager")
|
||||
// Stop each finder
|
||||
for finderName, finder := range h.finders {
|
||||
err := finder.Stop()
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to stop the finder")
|
||||
|
||||
// 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.goWait.Wait()
|
||||
h.wg.Wait()
|
||||
|
||||
// Returning errors
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
log.Info().Str("file", "hardware").Msg("hardware manager stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// emitPeripheralsChanges compares the old and new peripherals to determine which ones have been added or removed.
|
||||
func emitPeripheralsChanges(ctx context.Context, oldPeripherals map[string]Peripheral, newPeripherals map[string]Peripheral) {
|
||||
log.Trace().Any("oldList", oldPeripherals).Any("newList", newPeripherals).Msg("emitting peripherals changes to the front")
|
||||
|
||||
// Identify removed peripherals: present in the old list but not in the new list
|
||||
for oldPeriphName := range oldPeripherals {
|
||||
if _, exists := newPeripherals[oldPeriphName]; !exists {
|
||||
runtime.EventsEmit(ctx, string(PeripheralRemoval), oldPeripherals[oldPeriphName].GetInfo())
|
||||
log.Trace().Str("file", "hardware").Str("event", string(PeripheralRemoval)).Msg("emit peripheral removal event")
|
||||
}
|
||||
}
|
||||
|
||||
// Identify added peripherals: present in the new list but not in the old list
|
||||
for newPeriphName := range newPeripherals {
|
||||
if _, exists := oldPeripherals[newPeriphName]; !exists {
|
||||
runtime.EventsEmit(ctx, string(PeripheralArrival), newPeripherals[newPeriphName].GetInfo())
|
||||
log.Trace().Str("file", "hardware").Str("event", string(PeripheralArrival)).Msg("emit peripheral arrival event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package hardware
|
||||
|
||||
import "context"
|
||||
|
||||
// Peripheral represents the methods used to manage a peripheral (input or output hardware)
|
||||
type Peripheral interface {
|
||||
Connect(context.Context) error // Connect the peripheral
|
||||
Disconnect(context.Context) error // Disconnect the peripheral
|
||||
Activate(context.Context) error // Activate the peripheral
|
||||
Deactivate(context.Context) error // Deactivate the peripheral
|
||||
SetDeviceProperty(context.Context, uint32, uint32, byte) error // Update a device property
|
||||
|
||||
GetInfo() PeripheralInfo // Get the peripheral information
|
||||
}
|
||||
|
||||
// PeripheralInfo represents a peripheral information
|
||||
type PeripheralInfo struct {
|
||||
Name string `yaml:"name"` // Name of the peripheral
|
||||
SerialNumber string `yaml:"sn"` // S/N of the peripheral
|
||||
ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral
|
||||
Settings []interface{} `yaml:"settings"` // Number of DMX universes handled by the peripheral
|
||||
}
|
||||
|
||||
// PeripheralFinder represents how compatible peripheral drivers are implemented
|
||||
type PeripheralFinder interface {
|
||||
Initialize() error // Initializes the protocol
|
||||
Start(context.Context) error // Start the detection
|
||||
Stop() error // Stop the detection
|
||||
ForceScan() // Explicitly scans for peripherals
|
||||
CreatePeripheral(ctx context.Context) (Peripheral, error) // Creates a new peripheral
|
||||
DeletePeripheral(serialNumber string) error // Removes a peripheral
|
||||
GetName() string // Get the name of the finder
|
||||
GetPeripheral(string) (Peripheral, bool) // Get the peripheral
|
||||
}
|
||||
309
hardware/os2l/OS2LEndpoint.go
Normal file
309
hardware/os2l/OS2LEndpoint.go
Normal 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
|
||||
}
|
||||
106
hardware/os2l/OS2LProvider.go
Normal file
106
hardware/os2l/OS2LProvider.go
Normal 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
|
||||
}
|
||||
37
hardware/providersHandler.go
Normal file
37
hardware/providersHandler.go
Normal 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")
|
||||
}
|
||||
35
hardware/third-party/ftdi/detectFTDI.cpp
vendored
35
hardware/third-party/ftdi/detectFTDI.cpp
vendored
@@ -1,35 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include "ftd2xx.h"
|
||||
|
||||
int main() {
|
||||
FT_STATUS ftStatus;
|
||||
FT_HANDLE ftHandle = nullptr;
|
||||
FT_DEVICE_LIST_INFO_NODE *devInfo;
|
||||
DWORD numDevs;
|
||||
|
||||
// create the device information list
|
||||
ftStatus = FT_CreateDeviceInfoList(&numDevs);
|
||||
if (ftStatus != FT_OK) {
|
||||
std::cerr << "Unable to get the FTDI devices : create list error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (numDevs > 0) {
|
||||
// allocate storage for list based on numDevs
|
||||
devInfo =
|
||||
(FT_DEVICE_LIST_INFO_NODE*)malloc(sizeof(FT_DEVICE_LIST_INFO_NODE)*numDevs);
|
||||
// get the device information list
|
||||
ftStatus = FT_GetDeviceInfoList(devInfo, &numDevs);
|
||||
if (ftStatus != FT_OK) {
|
||||
std::cerr << "Unable to get the FTDI devices : get list error" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < numDevs; i++) {
|
||||
if (devInfo[i].SerialNumber[0] != '\0') {
|
||||
std::cout << i << ":" << devInfo[i].SerialNumber << ":" << devInfo[i].Description << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
hardware/third-party/ftdi/detectFTDI.manifest
vendored
12
hardware/third-party/ftdi/detectFTDI.manifest
vendored
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="DMXSender" type="win32"/>
|
||||
<description>Detect FTDI</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
142
hardware/third-party/ftdi/dmxSender.cpp
vendored
142
hardware/third-party/ftdi/dmxSender.cpp
vendored
@@ -1,142 +0,0 @@
|
||||
#include <cstring>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
#include <fstream>
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <math.h>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include "ftd2xx.h"
|
||||
|
||||
#define DMX_START_CODE 0x00
|
||||
#define BREAK_DURATION_US 110
|
||||
#define MAB_DURATION_US 16
|
||||
#define DMX_CHANNELS 512
|
||||
#define FREQUENCY 44
|
||||
#define INTERVAL (1000000 / FREQUENCY)
|
||||
|
||||
std::atomic<unsigned char> dmxData[DMX_CHANNELS + 1];
|
||||
std::atomic<bool> isOutputActivated = false;
|
||||
|
||||
using namespace std;
|
||||
|
||||
void sendBreak(FT_HANDLE ftHandle) {
|
||||
FT_SetBreakOn(ftHandle); // Envoie le signal de BREAK
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(BREAK_DURATION_US));
|
||||
FT_SetBreakOff(ftHandle); // Arrête le signal de BREAK
|
||||
}
|
||||
|
||||
void sendDMX(FT_HANDLE ftHandle) {
|
||||
while (true) {
|
||||
if(isOutputActivated){
|
||||
// Envoi du BREAK suivi du MAB
|
||||
sendBreak(ftHandle);
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(MAB_DURATION_US));
|
||||
|
||||
// Envoi de la trame DMX512
|
||||
DWORD bytesWritten = 0;
|
||||
|
||||
// Envoyer la trame DMX512
|
||||
FT_STATUS status = FT_Write(ftHandle, dmxData, DMX_CHANNELS, &bytesWritten);
|
||||
if (status != FT_OK || bytesWritten != DMX_CHANNELS) {
|
||||
std::cerr << "Unable to send the DMX frame" << std::endl;
|
||||
FT_Close(ftHandle);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attendre avant d'envoyer la prochaine trame
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(INTERVAL - BREAK_DURATION_US - MAB_DURATION_US));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void processCommand(const char* buffer) {
|
||||
if (buffer[0] == 0x01) {
|
||||
// Activate the DMX512
|
||||
isOutputActivated.store(true);
|
||||
} else if(buffer[0] == 0x02) {
|
||||
// Deactivate the DMX512
|
||||
isOutputActivated.store(false);
|
||||
} else if(buffer[0] == 0x03) {
|
||||
// Get the channel number
|
||||
uint16_t channelNumber = (static_cast<unsigned char>(buffer[1]) |
|
||||
(static_cast<unsigned char>(buffer[2]) << 8));
|
||||
// Get the channel value
|
||||
uint8_t channelValue = static_cast<unsigned char>(buffer[3]);
|
||||
// // Update the DMX array
|
||||
dmxData[channelNumber].store(channelValue);
|
||||
} else if(buffer[0] == 0x04) {
|
||||
// Close this sender
|
||||
exit(0);
|
||||
} else {
|
||||
std::cerr << "Unknown command" << endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
_setmode(_fileno(stdin), _O_BINARY);
|
||||
#endif
|
||||
|
||||
FT_STATUS ftStatus;
|
||||
FT_HANDLE ftHandle = nullptr;
|
||||
|
||||
// Check if the serial port is specified
|
||||
if (argc != 2) {
|
||||
std::cerr << "Invalid call to DMX sender" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Connect the serial port
|
||||
int deviceDev;
|
||||
try {
|
||||
deviceDev = std::stoi(argv[1]);
|
||||
}catch(const std::exception& e){
|
||||
std::cerr << "Invalid call to DMX sender" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
ftStatus = FT_Open(deviceDev, &ftHandle);
|
||||
if (ftStatus != FT_OK) {
|
||||
std::cerr << "Unable to open the FTDI device" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
ftStatus = FT_SetBaudRate(ftHandle, 250000);
|
||||
ftStatus |= FT_SetDataCharacteristics(ftHandle, 8, FT_STOP_BITS_2, FT_PARITY_NONE); // 8 bits, pas de parité, 1 bit de stop
|
||||
ftStatus |= FT_SetFlowControl(ftHandle, FT_FLOW_NONE, 0, 0);
|
||||
if (ftStatus != FT_OK) {
|
||||
std::cerr << "Unable to configure the FTDI device" << std::endl;
|
||||
FT_Close(ftHandle);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Send the DMX frames
|
||||
std::thread updateThread(sendDMX, ftHandle);
|
||||
|
||||
// Intercept commands from the GO program
|
||||
char buffer[4]; // Tampon pour stocker les 4 octets d'une commande
|
||||
|
||||
while (true) {
|
||||
std::cin.read(buffer, 4); // Attente bloquante jusqu'à ce que 4 octets soient lus
|
||||
if (std::cin.gcount() == 4) { // Vérifier que 4 octets ont été lus
|
||||
processCommand(buffer);
|
||||
} else if (std::cin.eof()) {
|
||||
std::cerr << "Fin de l'entrée standard (EOF)" << std::endl;
|
||||
break;
|
||||
} else if (std::cin.fail()) {
|
||||
std::cerr << "Erreur de lecture sur stdin" << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
12
hardware/third-party/ftdi/dmxSender.manifest
vendored
12
hardware/third-party/ftdi/dmxSender.manifest
vendored
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="DMXSender" type="win32"/>
|
||||
<description>DMXSender</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
8
hardware/third-party/ftdi/generate.bat
vendored
8
hardware/third-party/ftdi/generate.bat
vendored
@@ -1,8 +0,0 @@
|
||||
windres dmxSender.rc dmxSender.o
|
||||
windres detectFTDI.rc detectFTDI.o
|
||||
|
||||
g++ -o dmxSender.exe dmxSender.cpp dmxSender.o -I"include" -L"lib" -lftd2xx -mwindows
|
||||
g++ -o detectFTDI.exe detectFTDI.cpp detectFTDI.o -I"include" -L"lib" -lftd2xx -mwindows
|
||||
|
||||
@REM g++ -o dmxSender.exe dmxSender.cpp -I"include" -L"lib" -lftd2xx
|
||||
@REM g++ -o detectFTDI.exe detectFTDI.cpp -I"include" -L"lib" -lftd2xx
|
||||
123
peripherals.go
123
peripherals.go
@@ -1,123 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"dmxconnect/hardware"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// AddPeripheral adds a peripheral to the project
|
||||
func (a *App) AddPeripheral(protocolName string, peripheralID string) error {
|
||||
// Get the device from its finder
|
||||
p, found := a.hardwareManager.GetPeripheral(protocolName, peripheralID)
|
||||
if !found {
|
||||
log.Error().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("unable to found the specified peripheral")
|
||||
return fmt.Errorf("unable to found the peripheral ID '%s'", peripheralID)
|
||||
}
|
||||
// Add the peripheral ID to the project
|
||||
a.projectInfo.PeripheralsInfo[peripheralID] = p.GetInfo()
|
||||
log.Info().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("peripheral added to project")
|
||||
|
||||
// TODO: Connect the peripheral
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePeripheral adds a peripheral to the project
|
||||
func (a *App) RemovePeripheral(protocolName string, peripheralID string) error {
|
||||
// TODO: Disconnect the peripheral
|
||||
// Remove the peripheral ID from the project
|
||||
delete(a.projectInfo.PeripheralsInfo, peripheralID)
|
||||
log.Info().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("peripheral removed from project")
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddOS2LPeripheral adds a new OS2L peripheral
|
||||
func (a *App) AddOS2LPeripheral() (hardware.PeripheralInfo, error) {
|
||||
// Get the OS2L driver
|
||||
os2lDriver, err := a.hardwareManager.GetFinder("OS2L")
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "peripheral").Msg("unable to found the OS2L driver")
|
||||
return hardware.PeripheralInfo{}, err
|
||||
}
|
||||
log.Trace().Str("file", "peripheral").Msg("OS2L driver got")
|
||||
|
||||
// Create a new OS2L peripheral with this driver
|
||||
os2lPeripheral, err := os2lDriver.CreatePeripheral(a.ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", "peripheral").Msg("unable to create the OS2L peripheral")
|
||||
return hardware.PeripheralInfo{}, err
|
||||
}
|
||||
|
||||
os2lInfo := os2lPeripheral.GetInfo()
|
||||
log.Info().Str("file", "peripheral").Str("s/n", os2lInfo.SerialNumber).Msg("OS2L peripheral created, adding to project")
|
||||
// Add this new peripheral to the project
|
||||
return os2lInfo, a.AddPeripheral(os2lDriver.GetName(), os2lInfo.SerialNumber)
|
||||
}
|
||||
|
||||
// FOR TESTING PURPOSE ONLY
|
||||
|
||||
func (a *App) ConnectFTDI() error {
|
||||
// Connect the FTDI
|
||||
driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
periph, found := driver.GetPeripheral("A50285BI")
|
||||
if !found {
|
||||
return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
}
|
||||
return periph.Connect(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) ActivateFTDI() error {
|
||||
// Connect the FTDI
|
||||
driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
periph, found := driver.GetPeripheral("A50285BI")
|
||||
if !found {
|
||||
return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
}
|
||||
return periph.Activate(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) SetDeviceFTDI(channelValue byte) error {
|
||||
// Connect the FTDI
|
||||
driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
periph, found := driver.GetPeripheral("A50285BI")
|
||||
if !found {
|
||||
return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
}
|
||||
return periph.SetDeviceProperty(a.ctx, 0, 0, channelValue)
|
||||
}
|
||||
|
||||
func (a *App) DeactivateFTDI() error {
|
||||
// Connect the FTDI
|
||||
driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
periph, found := driver.GetPeripheral("A50285BI")
|
||||
if !found {
|
||||
return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
}
|
||||
return periph.Deactivate(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) DisconnectFTDI() error {
|
||||
// Connect the FTDI
|
||||
driver, err := a.hardwareManager.GetFinder("FTDI")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
periph, found := driver.GetPeripheral("A50285BI")
|
||||
if !found {
|
||||
return fmt.Errorf("unable to find the peripheral s/n %s", "A50285BI")
|
||||
}
|
||||
return periph.Disconnect(a.ctx)
|
||||
}
|
||||
94
project.go
94
project.go
@@ -66,42 +66,91 @@ func (a *App) GetProjects() ([]ProjectMetaData, error) {
|
||||
}
|
||||
|
||||
// CreateProject creates a new blank project
|
||||
func (a *App) CreateProject() ShowInfo {
|
||||
func (a *App) CreateProject() error {
|
||||
|
||||
// Create new project information
|
||||
date := time.Now()
|
||||
a.projectSave = ""
|
||||
a.projectInfo.ShowInfo = ShowInfo{
|
||||
Name: "My new show",
|
||||
Date: fmt.Sprintf("%04d-%02d-%02dT%02d:%02d", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute()),
|
||||
Avatar: "appicon.png",
|
||||
Comments: "Write your comments here",
|
||||
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),
|
||||
}
|
||||
log.Info().Str("file", "project").Any("showInfo", a.projectInfo.ShowInfo).Msg("project has been created")
|
||||
return a.projectInfo.ShowInfo
|
||||
|
||||
// The project isn't saved for now
|
||||
a.projectSave = ""
|
||||
|
||||
return a.OpenProject(projectInfo)
|
||||
}
|
||||
|
||||
// GetProjectInfo returns the information of the saved project
|
||||
func (a *App) GetProjectInfo(projectFile string) (ProjectInfo, error) {
|
||||
// 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 ProjectInfo{}, fmt.Errorf("unable to read the project file: %v", err)
|
||||
return fmt.Errorf("unable to read the project file: %v", err)
|
||||
}
|
||||
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project file read")
|
||||
a.projectInfo = ProjectInfo{}
|
||||
err = yaml.Unmarshal(content, &a.projectInfo)
|
||||
|
||||
// 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 ProjectInfo{}, fmt.Errorf("unable to get the project information: %v", err)
|
||||
return fmt.Errorf("unable to get the project information: %v", err)
|
||||
}
|
||||
log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project information got")
|
||||
// Load it into the app
|
||||
|
||||
// The project is saved
|
||||
a.projectSave = projectFile
|
||||
// Return the show information
|
||||
log.Info().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("got the project information")
|
||||
return a.projectInfo, nil
|
||||
|
||||
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
|
||||
@@ -147,6 +196,9 @@ func (a *App) SaveProject() (string, error) {
|
||||
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")
|
||||
@@ -186,6 +238,6 @@ type ProjectMetaData struct {
|
||||
|
||||
// ProjectInfo defines all the information for a lighting project
|
||||
type ProjectInfo struct {
|
||||
ShowInfo ShowInfo `yaml:"show"` // Show information
|
||||
PeripheralsInfo map[string]hardware.PeripheralInfo `yaml:"peripherals"` // Peripherals information
|
||||
ShowInfo ShowInfo `yaml:"show"` // Show information
|
||||
EndpointsInfo map[string]hardware.EndpointInfo `yaml:"endpoints"` // Endpoints information
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user