diff --git a/.vscode/settings.json b/.vscode/settings.json index 4396ea7..3cad51b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,6 +58,9 @@ "streambuf": "cpp", "thread": "cpp", "typeinfo": "cpp", - "variant": "cpp" + "variant": "cpp", + "queue": "cpp", + "ranges": "cpp", + "text_encoding": "cpp" } } \ No newline at end of file diff --git a/app.go b/app.go index 36d1024..8708e62 100644 --- a/app.go +++ b/app.go @@ -16,21 +16,24 @@ import ( // App struct type App struct { - ctx context.Context - cancelFunc context.CancelFunc + ctx context.Context + cancelFunc context.CancelFunc + wait sync.WaitGroup + 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 + projectCancel context.CancelFunc // The project cancel function } // 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.NewMIDIFinder(5 * time.Second)) hardwareManager.RegisterFinder(hardware.NewFTDIFinder(5 * time.Second)) - hardwareManager.RegisterFinder(hardware.NewOS2LFinder()) + // hardwareManager.RegisterFinder(hardware.NewOS2LFinder()) return &App{ hardwareManager: hardwareManager, projectSave: "", @@ -44,11 +47,17 @@ 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 @@ -69,10 +78,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 } diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..b10b859 --- /dev/null +++ b/build.bat @@ -0,0 +1,55 @@ +@echo off +setlocal + +echo ============================================ +echo [INFO] Starting Wails build script +echo ============================================ + +rem Détection du mode (par défaut : build) +set "MODE=build" +if /i "%~1"=="-dev" set "MODE=dev" + +rem 1️⃣ Essayer de récupérer le dernier tag +for /f "tokens=*" %%i in ('git describe --tags --abbrev=0 2^>nul') do set "GIT_TAG=%%i" + +rem 2️⃣ Si pas de tag, utiliser le hash du commit +if "%GIT_TAG%"=="" ( + for /f "tokens=*" %%i in ('git rev-parse --short HEAD 2^>nul') do set "GIT_TAG=%%i" +) + +rem 3️⃣ Si Git n’est pas dispo, mettre "unknown" +if "%GIT_TAG%"=="" set "GIT_TAG=unknown" + +echo [INFO] Git version detected: %GIT_TAG% + + +echo [INFO] Mode selectionne : %MODE% + +echo [INFO] Moving to the C++ folder... +cd /d "%~dp0hardware\cpp" || ( + echo [ERROR] Impossible d'accéder à hardware\cpp + exit /b 1 +) + +echo [INFO] Compiling C++ libraries... +call generate.bat || ( + echo [ERROR] Échec de la compilation C++ + exit /b 1 +) + +echo [INFO] Returning to project root... +cd /d "%~dp0" || exit /b 1 + +if /i "%MODE%"=="dev" ( + echo [INFO] Launching Wails in DEV mode... + wails dev +) else ( + echo [INFO] Building Wails application... + wails build -o "dmxconnect-%GIT_TAG%.exe" +) + +echo ============================================ +echo [SUCCESS] Done! +echo ============================================ + +endlocal \ No newline at end of file diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 032ecde..b7d7ba7 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -54,9 +54,24 @@ generateToast('warning', 'bxs-hdd', $_("peripheralRemovalToast") + ' ' + peripheralInfo.Name + '') }) + // Handle the event when a peripheral status is updated + EventsOn('PERIPHERAL_STATUS', function(peripheral, status){ + console.log("Hardware status has been updated to " + status); + // When a peripheral status is updated, update it in the store + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [peripheral.SerialNumber]: { + ...storedPeripherals[peripheral.SerialNumber], + isSaved: true, + Status: status, + }, + }}) + }) + // Set the window title $: { - WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (unsaved)" : "")) + WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (" + $_("unsavedProjectFlag") + ")" : "")) } let selectedMenu = "settings" @@ -77,12 +92,6 @@ }) } - // Instanciate a new project - CreateProject().then((showInfo) => { - showInformation.set(showInfo) - $needProjectSave = true - }) - // Handle window shortcuts document.addEventListener('keydown', function(event) { // Check the CTRL+S keys diff --git a/frontend/src/components/Settings/DeviceCard.svelte b/frontend/src/components/Settings/DeviceCard.svelte index 85158e0..8910208 100644 --- a/frontend/src/components/Settings/DeviceCard.svelte +++ b/frontend/src/components/Settings/DeviceCard.svelte @@ -14,8 +14,8 @@ export let addable = false; export let signalizable = false; export let signalized = false; - export let disconnected = false; export let selected = false; + export let status = "disconnected"; // Emit a delete event when the device is being removed const dispatch = createEventDispatcher(); @@ -37,11 +37,11 @@
-
-
-

{#if disconnected} {/if}{title}

+
+
+

{#if status == "disconnected" } {/if}{title}

{type} {location != '' ? "- " : ""}{location}
- {#if disconnected} + {#if status == "disconnected"}
Disconnected
{:else}
{line1}
@@ -50,15 +50,13 @@
- - - + + +
- - \ No newline at end of file diff --git a/frontend/src/components/Settings/InputsOutputsContent.svelte b/frontend/src/components/Settings/InputsOutputsContent.svelte index 415e380..0f5b2c8 100644 --- a/frontend/src/components/Settings/InputsOutputsContent.svelte +++ b/frontend/src/components/Settings/InputsOutputsContent.svelte @@ -5,61 +5,24 @@ import { t, _ } from 'svelte-i18n' import { generateToast, needProjectSave, peripherals } from "../../stores"; import { get } from "svelte/store" - import { UpdatePeripheralSettings, GetPeripheralSettings, AddOS2LPeripheral, RemovePeripheral, ConnectFTDI, ActivateFTDI, DeactivateFTDI, DisconnectFTDI, SetDeviceFTDI, AddPeripheral } from "../../../wailsjs/go/main/App"; + import { UpdatePeripheralSettings, GetPeripheralSettings, RemovePeripheral, AddPeripheral } from "../../../wailsjs/go/main/App"; import RoundedButton from "../General/RoundedButton.svelte"; - function ftdiConnect(){ - ConnectFTDI().then(() => - console.log("FTDI connected")) - .catch((error) => { - console.log("Error when trying to connect: " + error) - }) - } - - function ftdiActivate(){ - ActivateFTDI().then(() => - console.log("FTDI activated")) - .catch((error) => { - console.log("Error when trying to activate: " + error) - }) - } - - function ftdiDeactivate(){ - DeactivateFTDI().then(() => - console.log("FTDI deactivated")) - .catch((error) => { - console.log("Error when trying to deactivate: " + error) - }) - } - - let sliderValue = 0 - function ftdiSetDevice(value){ - console.log("value is " + value) - SetDeviceFTDI(value).then(() => - console.log("FTDI device set up")) - .catch((error) => { - console.log("Error when trying to set the device: " + error) - }) - } - - function ftdiDisconnect(){ - DisconnectFTDI().then(() => - console.log("FTDI disconnected")) - .catch((error) => { - console.log("Error when trying to disconnect: " + error) - }) - } - // Add the peripheral to the project function addPeripheral(peripheral){ // Add the peripheral to the project (backend) - AddPeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => { - peripherals.update((value) => { - if (value[peripheral.SerialNumber]) { - value[peripheral.SerialNumber].isSaved = true; - } - return {...value} - }) + AddPeripheral(peripheral).then((serialNumber) => { + peripherals.update((storedPeripherals) => { + return { + ...storedPeripherals, + [serialNumber]: { + ...storedPeripherals[serialNumber], + Name: peripheral.Name, + ProtocolName: peripheral.ProtocolName, + SerialNumber: serialNumber, + isSaved: true, + }, + }}) $needProjectSave = true }).catch((error) => { console.log("Unable to add the peripheral to the project: " + error) @@ -96,37 +59,20 @@ }) } - // 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")) - }) - } - // Select the peripheral to edit its settings let selectedPeripheralSN = null let selectedPeripheralSettings = {} function selectPeripheral(peripheral){ // Load the settings array if the peripheral is detected - if (peripheral.isDetected){ + if (peripheral.isSaved){ GetPeripheralSettings(peripheral.ProtocolName, peripheral.SerialNumber).then((peripheralSettings) => { selectedPeripheralSettings = peripheralSettings + // Select the current peripheral + selectedPeripheralSN = peripheral.SerialNumber }).catch((error) => { console.log("Unable to get the peripheral settings: " + error) generateToast('danger', 'bx-error', $_("getPeripheralSettingsErrorToast")) }) - // Select the current peripheral - selectedPeripheralSN = peripheral.SerialNumber } } @@ -153,7 +99,6 @@ boolean: Boolean, }[typeof(selectedPeripheralSettings[settingName])] || (x => x) selectedPeripheralSettings[settingName] = convert(settingValue) - console.log(typeof(selectedPeripheralSettings[settingName])) let peripheralProtocolName = get(peripherals)[selectedPeripheralSN].ProtocolName UpdatePeripheralSettings(peripheralProtocolName, selectedPeripheralSN, selectedPeripheralSettings).then(()=> { $needProjectSave = true @@ -175,11 +120,11 @@ if(!peripheral.isSaved) addPeripheral(peripheral) }} - title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/> + status="connected" title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/> {/if} {/each}

{$_("projectHardwareOthersLabel")}

- + addPeripheral({Name: "OS2L connection", ProtocolName: "OS2L"})} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/>
@@ -189,8 +134,8 @@ {#if savedPeripheralNumber > 0} {#each Object.entries($peripherals) as [serialNumber, peripheral]} {#if peripheral.isSaved} - removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)} on:click={() => selectPeripheral(peripheral)} - disconnected={!peripheral.isDetected} title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} selected={serialNumber == selectedPeripheralSN} removable signalizable/> + removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)} on:click={() => selectPeripheral(peripheral)} + title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} selected={serialNumber == selectedPeripheralSN} removable signalizable/> {/if} {/each} {:else} @@ -208,14 +153,6 @@ {:else} {$_("projectHardwareNoSettingLabel")} {/if} - - diff --git a/frontend/src/components/Settings/Settings.svelte b/frontend/src/components/Settings/Settings.svelte index dfcc601..a2e7b48 100644 --- a/frontend/src/components/Settings/Settings.svelte +++ b/frontend/src/components/Settings/Settings.svelte @@ -5,10 +5,11 @@ 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 { CreateProject, GetProjects, OpenProjectFromDisk } from "../../../wailsjs/go/main/App"; import { _ } from 'svelte-i18n' import {colors} from '../../stores.js'; import { get } from "svelte/store" + import { EventsOn } from '../../../wailsjs/runtime' const tabs = [ { title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent }, @@ -57,18 +58,28 @@ }) } + // Handle the event when a new project need to be loaded + EventsOn('LOAD_PROJECT', function(projectInfo){ + // Store project information + showInformation.set(projectInfo.ShowInfo) + + // Remove the saved peripherals of the current project + unsavePeripherals() + + // Load new project peripherals + loadPeripherals(projectInfo.PeripheralsInfo) + + console.log("Project has been opened"); + generateToast('info', 'bx-folder-open', $_("projectOpenedToast") + ' ' + projectInfo.ShowInfo.Name + '') + }) + // 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) + OpenProjectFromDisk(selectedOption).then(() => { + // Project opened, we set the needSave flag to false (already saved) needProjectSave.set(false) - generateToast('info', 'bx-folder-open', $_("projectOpenedToast") + ' ' + projectInfo.ShowInfo.Name + '') }).catch((error) => { console.error(`Unable to open the project: ${error}`) generateToast('danger', 'bx-error', $_("projectOpenErrorToast")) @@ -77,14 +88,17 @@ 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")) + 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")) }) } + + // Instantiate a new project + initializeNewProject() diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index 7e7de71..f185eb7 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -13,6 +13,7 @@ "newProjectTooltip": "Create a new project", "openProjectString": "Open", "openProjectTooltip": "Open an existing project", + "unsavedProjectFlag": "unsaved", "projectPropertiesTab": "Project properties", "projectPropertiesTooltip": "The project properties", "projectInputOutputTab": "Hardware", @@ -57,6 +58,7 @@ "projectOpenedToast": "The project was opened:", "projectOpenErrorToast": "Unable to open the project", "projectCreatedToast": "The project was created", + "projectCreateErrorToast": "Unable to create the project", "peripheralSettingSaveErrorToast": "Unable to save the peripheral settings", "os2lIp": "OS2L server IP", diff --git a/go.mod b/go.mod index 0601c5e..f721582 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.21.3 require ( github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79 + github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.33.0 github.com/wailsapp/wails/v2 v2.9.1 gopkg.in/yaml.v2 v2.4.0 @@ -26,7 +27,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 diff --git a/hardware/FTDIFinder.go b/hardware/FTDIFinder.go index 4d1fc29..3a748e2 100644 --- a/hardware/FTDIFinder.go +++ b/hardware/FTDIFinder.go @@ -1,50 +1,89 @@ package hardware import ( - "bufio" "context" _ "embed" + "errors" "fmt" - "os" - "os/exec" - "path/filepath" goRuntime "runtime" - "strconv" - "strings" "sync" "time" + "unsafe" "github.com/rs/zerolog/log" ) -const ( - ftdiFinderExecutableName = "FTDI_finder.exe" - ftdiSenderExecutableName = "FTDI_sender.exe" -) +/* +#include +#cgo LDFLAGS: -L${SRCDIR}/../build/bin -ldetectFTDI +#include "cpp/include/detectFTDIBridge.h" +*/ +import "C" -// FTDIFinder represents how the protocol is defined +// FTDIFinder manages all the FTDI peripherals 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 + wg sync.WaitGroup + + findTicker *time.Ticker // Peripherals find ticker + foundPeripherals map[string]PeripheralInfo // The list of peripherals handled by this finder + registeredPeripherals map[string]*FTDIPeripheral // The list of found peripherals + scanChannel chan struct{} // The channel to trigger a scan event } // 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{}), + findTicker: time.NewTicker(findPeriod), + foundPeripherals: make(map[string]PeripheralInfo), + registeredPeripherals: make(map[string]*FTDIPeripheral), + scanChannel: make(chan struct{}), } } -//go:embed third-party/ftdi/detectFTDI.exe -var finderExe []byte +// RegisterPeripheral registers a new peripheral +func (f *FTDIFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) { + // Create a new FTDI peripheral + ftdiPeripheral, err := NewFTDIPeripheral(peripheralData) + if err != nil { + return "", fmt.Errorf("unable to create the FTDI peripheral: %v", err) + } + // Register it in the finder + f.registeredPeripherals[peripheralData.SerialNumber] = ftdiPeripheral + log.Trace().Any("periph", &ftdiPeripheral).Str("file", "FTDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created") -//go:embed third-party/ftdi/dmxSender.exe -var senderExe []byte + // Peripheral created, connect it + err = ftdiPeripheral.Connect(ctx) + if err != nil { + log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to connect the peripheral") + } + // Peripheral connected, activate it + err = ftdiPeripheral.Activate(ctx) + if err != nil { + log.Err(err).Str("file", "FTDIFinder").Str("peripheralSN", peripheralData.SerialNumber).Msg("unable to activate the peripheral") + } + // Peripheral activated + return peripheralData.SerialNumber, nil +} + +// UnregisterPeripheral unregisters an existing peripheral +func (f *FTDIFinder) UnregisterPeripheral(ctx context.Context, peripheralID string) error { + peripheral, registered := f.registeredPeripherals[peripheralID] + if registered { + // Deactivating peripheral + err := peripheral.Deactivate(ctx) + if err != nil { + return err + } + // Disconnecting peripheral + err = peripheral.Disconnect() + if err != nil { + return err + } + } + delete(f.registeredPeripherals, peripheralID) + return nil +} // Initialize initializes the FTDI finder func (f *FTDIFinder) Initialize() error { @@ -53,43 +92,15 @@ func (f *FTDIFinder) Initialize() error { 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) + f.wg.Add(1) go func() { - defer f.goWait.Done() + defer f.wg.Done() for { select { case <-ctx.Done(): @@ -114,29 +125,11 @@ func (f *FTDIFinder) Start(ctx context.Context) error { // 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") + select { + case f.scanChannel <- struct{}{}: + default: + // Ignore if the channel is full or if it is closed } - 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 @@ -144,88 +137,103 @@ 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) { +// GetPeripheralSettings gets the peripheral settings +func (f *FTDIFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) { // Return the specified peripheral - peripheral := f.peripherals[peripheralID] - if peripheral == nil { + peripheral, found := f.registeredPeripherals[peripheralID] + if !found { log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") - return nil, false + return nil, fmt.Errorf("unable to found the peripheral") } log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") - return peripheral, true + return peripheral.GetSettings(), nil +} + +// SetPeripheralSettings sets the peripheral settings +func (f *FTDIFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error { + // Return the specified peripheral + peripheral, found := f.registeredPeripherals[peripheralID] + if !found { + log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") + return fmt.Errorf("unable to found the peripheral") + } + log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") + return peripheral.SetSettings(settings) } // scanPeripherals scans the FTDI peripherals func (f *FTDIFinder) scanPeripherals(ctx context.Context) error { - detectionCtx, cancel := context.WithCancel(ctx) - defer cancel() - log.Trace().Str("file", "FTDIFinder").Msg("FTDI scan triggered") - ftdiPeripherals := make(map[string]Peripheral) + count := int(C.get_peripherals_number()) - finder := exec.CommandContext(detectionCtx, filepath.Join(os.TempDir(), ftdiFinderExecutableName)) - log.Trace().Str("file", "FTDIFinder").Msg("has executed the FIND executable") + log.Info().Int("number", count).Msg("number of FTDI devices connected") - stdout, err := finder.StdoutPipe() - if err != nil { - return fmt.Errorf("unable to create the stdout pipe: %s", err) - } - defer stdout.Close() + // Allocating C array + size := C.size_t(count) * C.size_t(unsafe.Sizeof(C.FTDIPeripheralC{})) + devicesPtr := C.malloc(size) + defer C.free(devicesPtr) - stderr, err := finder.StderrPipe() - if err != nil { - return fmt.Errorf("unable to create the stderr pipe: %s", err) - } - defer stderr.Close() + devices := (*[1 << 20]C.FTDIPeripheralC)(devicesPtr)[:count:count] - err = finder.Start() - if err != nil { - return fmt.Errorf("unable to find FTDI peripherals: %s", err) - } + C.get_ftdi_devices((*C.FTDIPeripheralC)(devicesPtr), C.int(count)) - scannerErr := bufio.NewScanner(stderr) - for scannerErr.Scan() { - return fmt.Errorf("unable to find FTDI peripherals: %s", scannerErr.Text()) - } + temporaryPeripherals := make(map[string]PeripheralInfo) - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - peripheralString := scanner.Text() - // The program output is like '0:1:2' where 0 is the location, 1 is the S/N and 2 is the name - peripheralInfo := strings.Split(peripheralString, ":") + for i := 0; i < count; i++ { + d := devices[i] - 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 + sn := C.GoString(d.serialNumber) + desc := C.GoString(d.description) + isOpen := d.isOpen != 0 + + temporaryPeripherals[sn] = PeripheralInfo{ + SerialNumber: sn, + Name: desc, + IsOpen: isOpen, + ProtocolName: "FTDI", } - // 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") + // Free C memory + C.free_ftdi_device(&d) } + + log.Info().Any("peripherals", temporaryPeripherals).Msg("available FTDI peripherals") + // Emit the peripherals changes to the front - emitPeripheralsChanges(ctx, f.peripherals, ftdiPeripherals) + emitPeripheralsChanges(ctx, f.foundPeripherals, temporaryPeripherals) // Store the new peripherals list - f.peripherals = ftdiPeripherals + f.foundPeripherals = temporaryPeripherals return nil } -// CreatePeripheral is not implemented here -func (f *FTDIFinder) CreatePeripheral(context.Context) (Peripheral, error) { - return nil, nil -} +// WaitStop stops the finder +func (f *FTDIFinder) WaitStop() error { + log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...") -// DeletePeripheral is not implemented here -func (f *FTDIFinder) DeletePeripheral(serialNumber string) error { + // Stop the ticker + f.findTicker.Stop() + + // Close the channel + close(f.scanChannel) + + // Wait for all the peripherals to close + log.Trace().Str("file", "FTDIFinder").Msg("closing all FTDI peripherals") + var errs []error + for registeredPeripheralSN, registeredPeripheral := range f.registeredPeripherals { + err := registeredPeripheral.WaitStop() + if err != nil { + errs = append(errs, fmt.Errorf("%s: %w", registeredPeripheralSN, err)) + } + } + + // Wait for goroutines to stop + f.wg.Wait() + + // Returning errors + if len(errs) > 0 { + return errors.Join(errs...) + } + log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped") return nil } diff --git a/hardware/FTDIPeripheral.go b/hardware/FTDIPeripheral.go index a005cc9..1c22062 100644 --- a/hardware/FTDIPeripheral.go +++ b/hardware/FTDIPeripheral.go @@ -1,208 +1,167 @@ package hardware import ( - "bufio" "context" _ "embed" - "fmt" - "io" + "sync" + "unsafe" + + "github.com/pkg/errors" "github.com/rs/zerolog/log" - - "os" - "os/exec" + "github.com/wailsapp/wails/v2/pkg/runtime" ) -const ( - activateCommandString = 0x01 - deactivateCommandString = 0x02 - setCommandString = 0x03 -) +/* +#include +#cgo LDFLAGS: -L${SRCDIR}/../build/bin -ldmxSender +#include "cpp/include/dmxSenderBridge.h" +*/ +import "C" // FTDIPeripheral contains the data of an FTDI peripheral type FTDIPeripheral struct { - name string // The name of the peripheral - serialNumber string // The S/N of the FTDI peripheral - location int // The location of the peripheral - programName string // The temp file name of the executable - settings map[string]interface{} // The settings of the peripheral - dmxSender *exec.Cmd // The command to pilot the DMX sender program - stdin io.WriteCloser // For writing in the DMX sender - stdout io.ReadCloser // For reading from the DMX sender - stderr io.ReadCloser // For reading the errors - disconnectChan chan struct{} // Channel to cancel the connection - errorsChan chan error // Channel to get the errors + wg sync.WaitGroup + + info PeripheralInfo // The peripheral basic data + dmxSender unsafe.Pointer // The command object for piloting the DMX ouptut } // 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") - settings := make(map[string]interface{}) +func NewFTDIPeripheral(info PeripheralInfo) (*FTDIPeripheral, error) { + log.Info().Str("file", "FTDIPeripheral").Str("name", info.Name).Str("s/n", info.SerialNumber).Msg("FTDI peripheral created") return &FTDIPeripheral{ - name: name, - dmxSender: nil, - serialNumber: serialNumber, - location: location, - settings: settings, - disconnectChan: make(chan struct{}), - errorsChan: make(chan error, 1), + info: info, + dmxSender: nil, }, 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 + // Check if the device has already been created if p.dmxSender != nil { - log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender already initialized") - return nil + return errors.Errorf("the DMX device has already been created!") } - // 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 DMX sender + p.dmxSender = C.dmx_create() - // 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) + // Connect the FTDI + runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "connecting") + serialNumber := C.CString(p.info.SerialNumber) + defer C.free(unsafe.Pointer(serialNumber)) + if C.dmx_connect(p.dmxSender, serialNumber) != C.DMX_OK { + log.Error().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("unable to connect the DMX device") + return errors.Errorf("unable to connect '%s'") } - // Launch a goroutine to read stderr asynchronously + p.wg.Add(1) 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") - } + defer p.wg.Done() + <-ctx.Done() + _ = p.Disconnect() }() - // 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") - } - } - }() + // Send deactivated state (connected but not activated for now) + runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "deactivated") + log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device connected 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 +func (p *FTDIPeripheral) Disconnect() error { + // Check if the device has already been created + if p.dmxSender == nil { + return errors.Errorf("the DMX device has not been connected!") } - log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while disconnecting: not connected") - return fmt.Errorf("unable to disconnect: not connected") + + // Destroy the dmx sender + C.dmx_destroy(p.dmxSender) + return nil } // 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 + // Check if the device has already been created + if p.dmxSender == nil { + return errors.Errorf("the DMX sender has not been created!") } - log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while activating: not connected") - return fmt.Errorf("unable to activate: not connected") + + log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("activating FTDI peripheral...") + + err := C.dmx_activate(p.dmxSender) + if err != C.DMX_OK { + return errors.Errorf("unable to activate the DMX sender!") + } + + 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(PeripheralStatus), p.info, "activated") + + log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device activated successfully") + + return nil } // Deactivate deactivates the FTDI peripheral func (p *FTDIPeripheral) Deactivate(ctx context.Context) error { - 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 + // Check if the device has already been created + if p.dmxSender == nil { + return errors.Errorf("the DMX device has not been created!") } - log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while deactivating: not connected") - return fmt.Errorf("unable to deactivate: not connected") -} -// SetPeripheralSettings sets a specific setting for this peripheral -func (p *FTDIPeripheral) SetPeripheralSettings(settings map[string]interface{}) error { - p.settings = settings + log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("deactivating FTDI peripheral...") + + err := C.dmx_deactivate(p.dmxSender) + if err != C.DMX_OK { + return errors.Errorf("unable to deactivate the DMX sender!") + } + + log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("DMX device deactivated successfully") + return nil } +// SetSettings sets a specific setting for this peripheral +func (p *FTDIPeripheral) SetSettings(settings map[string]interface{}) error { + return errors.Errorf("unable to set the settings: not implemented") +} + // 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 +func (p *FTDIPeripheral) 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.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") + + log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("setting device property on FTDI peripheral...") + + 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", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("device property set on FTDI peripheral successfully") + + return nil } // GetSettings gets the peripheral settings func (p *FTDIPeripheral) GetSettings() map[string]interface{} { - return p.settings + return map[string]interface{}{} } // GetInfo gets all the peripheral information func (p *FTDIPeripheral) GetInfo() PeripheralInfo { - return PeripheralInfo{ - Name: p.name, - SerialNumber: p.serialNumber, - ProtocolName: "FTDI", - } + return p.info +} + +// WaitStop wait about the peripheral to close +func (p *FTDIPeripheral) WaitStop() error { + log.Info().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("waiting for FTDI peripheral to close...") + p.wg.Wait() + log.Info().Str("file", "FTDIPeripheral").Str("s/n", p.info.SerialNumber).Msg("FTDI peripheral closed!") + return nil } diff --git a/hardware/MIDIFinder.go b/hardware/MIDIFinder.go index 4d7a65e..4feb2d8 100644 --- a/hardware/MIDIFinder.go +++ b/hardware/MIDIFinder.go @@ -15,18 +15,18 @@ import ( // 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 + findTicker time.Ticker // Peripherals find ticker + registeredPeripherals map[string]MIDIPeripheral // The list of peripherals + scanChannel chan struct{} // The channel to trigger a scan event + goWait sync.WaitGroup // Check goroutines execution } // NewMIDIFinder creates a new DMXUSB protocol func NewMIDIFinder(findPeriod time.Duration) *MIDIFinder { log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder created") return &MIDIFinder{ - findTicker: *time.NewTicker(findPeriod), - peripherals: make(map[string]Peripheral), + findTicker: *time.NewTicker(findPeriod), + registeredPeripherals: make(map[string]MIDIPeripheral), } } @@ -36,6 +36,30 @@ func (f *MIDIFinder) Initialize() error { return nil } +// RegisterPeripheral registers a new peripheral +func (f *MIDIFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, error) { + peripheral, err := NewMIDIPeripheral(peripheralData) + if err != nil { + return "", fmt.Errorf("unable to create the MIDI peripheral: %v", err) + } + f.registeredPeripherals[peripheralData.SerialNumber] = *peripheral + log.Trace().Any("periph", &peripheral).Str("file", "MIDIFinder").Str("peripheralName", peripheralData.Name).Msg("FTDI peripheral has been created") + return peripheralData.SerialNumber, nil +} + +// UnregisterPeripheral unregisters an existing peripheral +func (f *MIDIFinder) UnregisterPeripheral(peripheralID string) error { + peripheral, registered := f.registeredPeripherals[peripheralID] + if registered { + err := peripheral.Disconnect() + if err != nil { + return err + } + } + delete(f.registeredPeripherals, peripheralID) + return nil +} + // Start starts the finder and search for peripherals func (f *MIDIFinder) Start(ctx context.Context) error { f.goWait.Add(1) @@ -79,16 +103,28 @@ 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) { +// GetPeripheralSettings gets the peripheral settings +func (f *MIDIFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) { // Return the specified peripheral - peripheral, found := f.peripherals[peripheralID] + peripheral, found := f.registeredPeripherals[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.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") + return nil, fmt.Errorf("unable to found the peripheral") } - log.Trace().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("MIDI peripheral found in the driver") - return peripheral, true + log.Debug().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") + return peripheral.GetSettings(), nil +} + +// SetPeripheralSettings sets the peripheral settings +func (f *MIDIFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error { + // Return the specified peripheral + peripheral, found := f.registeredPeripherals[peripheralID] + if !found { + log.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") + return fmt.Errorf("unable to found the peripheral") + } + log.Debug().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") + return peripheral.SetSettings(settings) } func splitStringAndNumber(input string) (string, int, error) { @@ -119,7 +155,7 @@ func (f *MIDIFinder) ForceScan() { // scanPeripherals scans the MIDI peripherals func (f *MIDIFinder) scanPeripherals(ctx context.Context) error { - midiPeripherals := make(map[string]Peripheral) + // midiPeripherals := make(map[string]Peripheral) log.Trace().Str("file", "MIDIFinder").Msg("opening MIDI scanner port...") midiScanner, err := rtmidi.NewMIDIInDefault() if err != nil { @@ -148,8 +184,8 @@ func (f *MIDIFinder) scanPeripherals(ctx context.Context) error { } 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) + // 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) @@ -159,7 +195,7 @@ func (f *MIDIFinder) scanPeripherals(ctx context.Context) error { // 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 + // f.peripherals = midiPeripherals return nil } diff --git a/hardware/MIDIPeripheral.go b/hardware/MIDIPeripheral.go index e3083ba..16aadef 100644 --- a/hardware/MIDIPeripheral.go +++ b/hardware/MIDIPeripheral.go @@ -8,22 +8,18 @@ import ( // 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 - settings map[string]interface{} // The settings of the peripheral + info PeripheralInfo // The peripheral info + location int // The location of the peripheral + settings map[string]interface{} // The settings of the peripheral } // NewMIDIPeripheral creates a new MIDI peripheral -func NewMIDIPeripheral(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") - settings := make(map[string]interface{}) +func NewMIDIPeripheral(peripheralData PeripheralInfo) (*MIDIPeripheral, error) { + log.Trace().Str("file", "MIDIPeripheral").Str("name", peripheralData.Name).Str("s/n", peripheralData.SerialNumber).Msg("MIDI peripheral created") return &MIDIPeripheral{ - name: name, - location: location, - serialNumber: serialNumber, - settings: settings, - } + info: peripheralData, + settings: peripheralData.Settings, + }, nil } // Connect connects the MIDI peripheral @@ -32,7 +28,7 @@ func (p *MIDIPeripheral) Connect(ctx context.Context) error { } // Disconnect disconnects the MIDI peripheral -func (p *MIDIPeripheral) Disconnect(ctx context.Context) error { +func (p *MIDIPeripheral) Disconnect() error { return nil } @@ -46,8 +42,8 @@ func (p *MIDIPeripheral) Deactivate(ctx context.Context) error { return nil } -// SetPeripheralSettings sets a specific setting for this peripheral -func (p *MIDIPeripheral) SetPeripheralSettings(settings map[string]interface{}) error { +// SetSettings sets a specific setting for this peripheral +func (p *MIDIPeripheral) SetSettings(settings map[string]interface{}) error { p.settings = settings return nil } @@ -64,9 +60,5 @@ func (p *MIDIPeripheral) GetSettings() map[string]interface{} { // GetInfo gets the peripheral information func (p *MIDIPeripheral) GetInfo() PeripheralInfo { - return PeripheralInfo{ - Name: p.name, - ProtocolName: "MIDI", - SerialNumber: p.serialNumber, - } + return p.info } diff --git a/hardware/OS2LFinder.go b/hardware/OS2LFinder.go index 33fae6b..24a8d42 100644 --- a/hardware/OS2LFinder.go +++ b/hardware/OS2LFinder.go @@ -11,14 +11,14 @@ import ( // OS2LFinder represents how the protocol is defined type OS2LFinder struct { - peripherals map[string]Peripheral // The list of peripherals + registeredPeripherals map[string]OS2LPeripheral // The list of found peripherals } // NewOS2LFinder creates a new OS2L finder func NewOS2LFinder() *OS2LFinder { log.Trace().Str("file", "OS2LFinder").Msg("OS2L finder created") return &OS2LFinder{ - peripherals: make(map[string]Peripheral), + registeredPeripherals: make(map[string]OS2LPeripheral), } } @@ -28,21 +28,37 @@ func (f *OS2LFinder) Initialize() error { return nil } -// CreatePeripheral creates a new OS2L peripheral -func (f *OS2LFinder) CreatePeripheral(ctx context.Context) (Peripheral, error) { +// RegisterPeripheral registers a new peripheral +func (f *OS2LFinder) RegisterPeripheral(ctx context.Context, peripheralData PeripheralInfo) (string, 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 + peripheralData.SerialNumber = strings.ToUpper(fmt.Sprintf("%08x", rand.Intn(1<<32))) + log.Trace().Str("file", "OS2LFinder").Str("serialNumber", peripheralData.SerialNumber).Msg("OS2L peripheral created") + + os2lPeripheral, err := NewOS2LPeripheral(peripheralData) + if err != nil { + return "", fmt.Errorf("unable to create the OS2L peripheral: %v", err) + } + // Connect this peripheral + err = os2lPeripheral.Connect(ctx) + if err != nil { + return "", err + } + + f.registeredPeripherals[peripheralData.SerialNumber] = *os2lPeripheral + log.Trace().Any("periph", &os2lPeripheral).Str("file", "OS2LFinder").Str("peripheralName", peripheralData.Name).Msg("OS2L peripheral has been created") + return peripheralData.SerialNumber, nil } -// 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") +// UnregisterPeripheral unregisters an existing peripheral +func (f *OS2LFinder) UnregisterPeripheral(peripheralID string) error { + peripheral, registered := f.registeredPeripherals[peripheralID] + if registered { + err := peripheral.Disconnect() + if err != nil { + return err + } + } + delete(f.registeredPeripherals, peripheralID) return nil } @@ -51,16 +67,28 @@ 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) { +// GetPeripheralSettings gets the peripheral settings +func (f *OS2LFinder) GetPeripheralSettings(peripheralID string) (map[string]interface{}, error) { // Return the specified peripheral - peripheral, found := f.peripherals[peripheralID] + peripheral, found := f.registeredPeripherals[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.Error().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the OS2L finder") + return nil, fmt.Errorf("unable to found the peripheral") } - log.Trace().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("OS2L peripheral found in the finder") - return peripheral, true + log.Debug().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the OS2L finder") + return peripheral.GetSettings(), nil +} + +// SetPeripheralSettings sets the peripheral settings +func (f *OS2LFinder) SetPeripheralSettings(peripheralID string, settings map[string]interface{}) error { + // Return the specified peripheral + peripheral, found := f.registeredPeripherals[peripheralID] + if !found { + log.Error().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder") + return fmt.Errorf("unable to found the peripheral") + } + log.Debug().Str("file", "OS2LFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder") + return peripheral.SetSettings(settings) } // Start starts the finder diff --git a/hardware/OS2LPeripheral.go b/hardware/OS2LPeripheral.go index e6845e5..07e69a0 100644 --- a/hardware/OS2LPeripheral.go +++ b/hardware/OS2LPeripheral.go @@ -3,37 +3,42 @@ package hardware import ( "context" "fmt" + "time" "github.com/rs/zerolog/log" + "github.com/wailsapp/wails/v2/pkg/runtime" ) // OS2LPeripheral contains the data of an OS2L peripheral type OS2LPeripheral struct { - name string // The name of the peripheral - serialNumber string // The serial number of the peripheral - serverIP string // OS2L server IP - serverPort int // OS2L server port + info PeripheralInfo // The basic info for this peripheral + serverIP string // OS2L server IP + serverPort int // OS2L server port } // NewOS2LPeripheral creates a new OS2L peripheral -func NewOS2LPeripheral(name string, serialNumber string) *OS2LPeripheral { - log.Trace().Str("file", "OS2LPeripheral").Str("name", name).Str("s/n", serialNumber).Msg("OS2L peripheral created") +func NewOS2LPeripheral(peripheralData PeripheralInfo) (*OS2LPeripheral, error) { + log.Trace().Str("file", "OS2LPeripheral").Str("name", peripheralData.Name).Str("s/n", peripheralData.SerialNumber).Msg("OS2L peripheral created") return &OS2LPeripheral{ - name: name, - serverIP: "127.0.0.1", - serverPort: 9995, - serialNumber: serialNumber, - } + info: peripheralData, + serverIP: "127.0.0.1", + serverPort: 9005, + }, nil } // Connect connects the MIDI peripheral func (p *OS2LPeripheral) Connect(ctx context.Context) error { log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected") + go func() { + runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "connecting") + time.Sleep(5 * time.Second) + runtime.EventsEmit(ctx, string(PeripheralStatus), p.info, "disconnected") + }() return nil } // Disconnect disconnects the MIDI peripheral -func (p *OS2LPeripheral) Disconnect(ctx context.Context) error { +func (p *OS2LPeripheral) Disconnect() error { log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral disconnected") return nil } @@ -50,8 +55,8 @@ func (p *OS2LPeripheral) Deactivate(ctx context.Context) error { return nil } -// SetPeripheralSettings sets a specific setting for this peripheral -func (p *OS2LPeripheral) SetPeripheralSettings(settings map[string]interface{}) error { +// SetSettings sets a specific setting for this peripheral +func (p *OS2LPeripheral) SetSettings(settings map[string]interface{}) error { // Check if the IP exists serverIP, found := settings["os2lIp"] if !found { @@ -95,9 +100,5 @@ func (p *OS2LPeripheral) GetSettings() map[string]interface{} { // GetInfo gets the peripheral information func (p *OS2LPeripheral) GetInfo() PeripheralInfo { - return PeripheralInfo{ - Name: p.name, - SerialNumber: p.serialNumber, - ProtocolName: "OS2L", - } + return p.info } diff --git a/hardware/cpp/generate.bat b/hardware/cpp/generate.bat new file mode 100644 index 0000000..4deba9c --- /dev/null +++ b/hardware/cpp/generate.bat @@ -0,0 +1,22 @@ +@REM windres dmxSender.rc dmxSender.o +@REM windres detectFTDI.rc detectFTDI.o + +@REM g++ -o dmxSender.exe dmxSender.cpp dmxSender.o -I"include" -L"lib" -lftd2xx -mwindows +@REM g++ -o detectFTDI.exe detectFTDI.cpp detectFTDI.o -I"include" -L"lib" -lftd2xx -mwindows + +@REM g++ -o dmxSender.exe dmxSender.cpp -I"include" -L"lib" -lftd2xx +@REM g++ -o detectFTDI.exe detectFTDI.cpp -I"include" -L"lib" -lftd2xx + +@REM g++ -c dmxSender.cpp -o dmxSender.o -I"include" -L"lib" -lftd2xx -mwindows + +@REM g++ -c dmxSender_wrapper.cpp -o dmxSender_wrapper.o -I"include" -L"lib" -lftd2xx -mwindows + +@REM g++ -shared -o ../../build/bin/libdmxSender.dll src/dmxSender.cpp -fPIC -Wl,--out-implib,../../build/bin/libdmxSender.dll.a -L"lib" -lftd2xx + +@REM Compiling 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 \ No newline at end of file diff --git a/hardware/cpp/include/detectFTDIBridge.h b/hardware/cpp/include/detectFTDIBridge.h new file mode 100644 index 0000000..8cab84c --- /dev/null +++ b/hardware/cpp/include/detectFTDIBridge.h @@ -0,0 +1,19 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char* serialNumber; + char* description; + int isOpen; +} FTDIPeripheralC; + +int get_peripherals_number(); +void get_ftdi_devices(FTDIPeripheralC* devices, int count); +void free_ftdi_device(FTDIPeripheralC* device); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/hardware/cpp/include/dmxSenderBridge.h b/hardware/cpp/include/dmxSenderBridge.h new file mode 100644 index 0000000..1cc9ed4 --- /dev/null +++ b/hardware/cpp/include/dmxSenderBridge.h @@ -0,0 +1,30 @@ +// Declare the C++ function from the shared library + +#include + +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); \ No newline at end of file diff --git a/hardware/third-party/ftdi/lib/ftd2xx.lib b/hardware/cpp/lib/ftd2xx.lib similarity index 100% rename from hardware/third-party/ftdi/lib/ftd2xx.lib rename to hardware/cpp/lib/ftd2xx.lib diff --git a/hardware/cpp/src/detectFTDI.cpp b/hardware/cpp/src/detectFTDI.cpp new file mode 100644 index 0000000..a6266ee --- /dev/null +++ b/hardware/cpp/src/detectFTDI.cpp @@ -0,0 +1,91 @@ +#include "../include/detectFTDIBridge.h" +#include "detectFTDI.h" +#include +#include +#include +#include + +int getFTDIPeripheralsNumber() { + DWORD numDevs = 0; + if (FT_CreateDeviceInfoList(&numDevs) != FT_OK) { + std::cerr << "Unable to get FTDI devices: create list error\n"; + } + return numDevs; +} + +std::vector scanFTDIPeripherals() { + 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 devInfo(numDevs); + if (FT_GetDeviceInfoList(devInfo.data(), &numDevs) != FT_OK) { + std::cerr << "Unable to get FTDI devices: get list error\n"; + return {}; + } + + std::vector peripherals; + peripherals.reserve(numDevs); + + for (const auto& info : devInfo) { + if (info.SerialNumber[0] != '\0') { + peripherals.push_back({ + info.SerialNumber, + info.Description, + static_cast(info.Flags & FT_FLAGS_OPENED) + }); + } + } + + return peripherals; +} + +extern "C" { + +int get_peripherals_number() { + return getFTDIPeripheralsNumber(); +} + +void get_ftdi_devices(FTDIPeripheralC* devices, int count) { + if (!devices || count <= 0) { + return; + } + + auto list = scanFTDIPeripherals(); + int n = std::min(count, static_cast(list.size())); + + for (int i = 0; i < n; ++i) { + const auto& src = list[i]; + auto& dst = devices[i]; + + dst.serialNumber = static_cast(std::malloc(src.serialNumber.size() + 1)); + std::strcpy(dst.serialNumber, src.serialNumber.c_str()); + + dst.description = static_cast(std::malloc(src.description.size() + 1)); + std::strcpy(dst.description, src.description.c_str()); + + dst.isOpen = src.isOpen ? 1 : 0; + } +} + +void free_ftdi_device(FTDIPeripheralC* 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" \ No newline at end of file diff --git a/hardware/cpp/src/detectFTDI.h b/hardware/cpp/src/detectFTDI.h new file mode 100644 index 0000000..d22d4fb --- /dev/null +++ b/hardware/cpp/src/detectFTDI.h @@ -0,0 +1,14 @@ +#pragma once + +#include "ftd2xx.h" +#include +#include + +struct FTDIPeripheral { + std::string serialNumber; + std::string description; + bool isOpen; +}; + +int getFTDIPeripheralsNumber(); +std::vector scanFTDIPeripherals(); \ No newline at end of file diff --git a/hardware/cpp/src/dmxSender.cpp b/hardware/cpp/src/dmxSender.cpp new file mode 100644 index 0000000..9f372ca --- /dev/null +++ b/hardware/cpp/src/dmxSender.cpp @@ -0,0 +1,230 @@ +//dmxSender.cpp + +#include "dmxSender.h" +#include + +#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 set up!" << std::endl; + std::cout << " [DMXSENDER] " << "Preparing sending job..." << std::endl; + + // Send the DMX frames + std::thread updateThread([this]() { + this->sendDMX(ftHandle); + }); + + updateThread.detach(); + + std::cout << " [DMXSENDER] " << "Sending job executed!" << std::endl; + 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); + 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 (true) { + if(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; + } + } +} \ No newline at end of file diff --git a/hardware/cpp/src/dmxSender.h b/hardware/cpp/src/dmxSender.h new file mode 100644 index 0000000..f0d3db1 --- /dev/null +++ b/hardware/cpp/src/dmxSender.h @@ -0,0 +1,65 @@ +// dmxSender.h + +#pragma once + +#include +#include +#include +#include +#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 peripheral status + FT_HANDLE ftHandle = nullptr; // FTDI object + std::atomic dmxData[DMX_CHANNELS + 1]; // For storing dynamically the DMX data + std::atomic 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); +}; \ No newline at end of file diff --git a/hardware/third-party/ftdi/include/ftd2xx.h b/hardware/cpp/src/ftd2xx.h similarity index 100% rename from hardware/third-party/ftdi/include/ftd2xx.h rename to hardware/cpp/src/ftd2xx.h diff --git a/hardware/cpp/test/detectFTDI_test.cpp b/hardware/cpp/test/detectFTDI_test.cpp new file mode 100644 index 0000000..181dded --- /dev/null +++ b/hardware/cpp/test/detectFTDI_test.cpp @@ -0,0 +1,14 @@ +#include "../src/detectFTDI.h" +#include + +int main(){ + int peripheralsNumber = getFTDIPeripheralsNumber(); + + + std::vector peripherals = scanFTDIPeripherals(); + + // for (const auto& peripheral : peripherals) { + // std::cout << peripheral.serialNumber << " (" << peripheral.description << ") -> IS OPEN: " << peripheral.isOpen << std::endl; + // } + +} \ No newline at end of file diff --git a/hardware/cpp/test/dmxSender_test.cpp b/hardware/cpp/test/dmxSender_test.cpp new file mode 100644 index 0000000..12e4e77 --- /dev/null +++ b/hardware/cpp/test/dmxSender_test.cpp @@ -0,0 +1,122 @@ +#include "../src/dmxSender.h" + +#include +#include +#include + +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; +} \ No newline at end of file diff --git a/hardware/hardware.go b/hardware/hardware.go index f7aeabf..cbc841d 100644 --- a/hardware/hardware.go +++ b/hardware/hardware.go @@ -2,9 +2,9 @@ package hardware import ( "context" + "errors" "fmt" "sync" - "time" "github.com/rs/zerolog/log" @@ -19,19 +19,18 @@ const ( PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL" // PeripheralRemoval is triggered when a peripheral has been disconnected from the system PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL" + // PeripheralStatus is triggered when a peripheral status has been updated (disconnected - connecting - connected) + PeripheralStatus PeripheralEvent = "PERIPHERAL_STATUS" // debounceDuration = 500 * time.Millisecond ) -var ( - debounceTimer *time.Timer -) - // HardwareManager is the class who manages the hardware type HardwareManager struct { + wg sync.WaitGroup + finders map[string]PeripheralFinder // The map of peripherals finders peripherals []Peripheral // The current list of peripherals peripheralsScanTrigger chan struct{} // Trigger the peripherals scans - goWait sync.WaitGroup // Wait for goroutines to terminate } // NewHardwareManager creates a new HardwareManager @@ -46,6 +45,7 @@ func NewHardwareManager() *HardwareManager { // Start starts to find new peripheral events func (h *HardwareManager) Start(ctx context.Context) error { + // Initialize all the finders for finderName, finder := range h.finders { err := finder.Initialize() if err != nil { @@ -58,9 +58,11 @@ func (h *HardwareManager) Start(ctx context.Context) error { return err } } - h.goWait.Add(1) + + // Periodically scan all the finders + h.wg.Add(1) go func() { - defer h.goWait.Done() + defer h.wg.Done() for { select { case <-ctx.Done(): @@ -93,50 +95,24 @@ func (h *HardwareManager) RegisterFinder(finder PeripheralFinder) { log.Info().Str("file", "hardware").Str("finderName", finder.GetName()).Msg("finder registered") } -// GetPeripheral gets the peripheral object from the parent finder -func (h *HardwareManager) GetPeripheral(finderName string, peripheralID string) (Peripheral, bool) { - // Get the finder - parentFinder, found := h.finders[finderName] - // If no finder found, return false - if !found { - log.Error().Str("file", "hardware").Str("finderName", finderName).Msg("unable to get the finder") - return nil, false - } - log.Trace().Str("file", "hardware").Str("finderName", parentFinder.GetName()).Msg("finder got") - // Contact the finder to get the peripheral - return parentFinder.GetPeripheral(peripheralID) -} - // Scan scans all the peripherals for the registered finders func (h *HardwareManager) Scan() error { - h.peripheralsScanTrigger <- struct{}{} - return nil -} - -// Stop stops the hardware manager -func (h *HardwareManager) Stop() error { - log.Trace().Str("file", "hardware").Msg("closing the hardware manager") - // Stop each finder - for finderName, finder := range h.finders { - err := finder.Stop() - if err != nil { - log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to stop the finder") - } + select { + case h.peripheralsScanTrigger <- struct{}{}: + return nil + default: + return fmt.Errorf("scan trigger not available (manager stopped?)") } - // Wait for goroutines to finish - h.goWait.Wait() - log.Info().Str("file", "hardware").Msg("hardware manager stopped") - return nil } // emitPeripheralsChanges compares the old and new peripherals to determine which ones have been added or removed. -func emitPeripheralsChanges(ctx context.Context, oldPeripherals map[string]Peripheral, newPeripherals map[string]Peripheral) { +func emitPeripheralsChanges(ctx context.Context, oldPeripherals map[string]PeripheralInfo, newPeripherals map[string]PeripheralInfo) { log.Trace().Any("oldList", oldPeripherals).Any("newList", newPeripherals).Msg("emitting peripherals changes to the front") // Identify removed peripherals: present in the old list but not in the new list for oldPeriphName := range oldPeripherals { if _, exists := newPeripherals[oldPeriphName]; !exists { - runtime.EventsEmit(ctx, string(PeripheralRemoval), oldPeripherals[oldPeriphName].GetInfo()) + runtime.EventsEmit(ctx, string(PeripheralRemoval), oldPeripherals[oldPeriphName]) log.Trace().Str("file", "hardware").Str("event", string(PeripheralRemoval)).Msg("emit peripheral removal event") } } @@ -144,8 +120,35 @@ func emitPeripheralsChanges(ctx context.Context, oldPeripherals map[string]Perip // 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()) + runtime.EventsEmit(ctx, string(PeripheralArrival), newPeripherals[newPeriphName]) log.Trace().Str("file", "hardware").Str("event", string(PeripheralArrival)).Msg("emit peripheral arrival event") } } } + +// WaitStop stops the hardware manager +func (h *HardwareManager) WaitStop() error { + log.Trace().Str("file", "hardware").Msg("closing the hardware manager") + + // Closing trigger channel + close(h.peripheralsScanTrigger) + + // Stop each finder + var errs []error + for name, f := range h.finders { + if err := f.WaitStop(); err != nil { + errs = append(errs, fmt.Errorf("%s: %w", name, err)) + } + } + + // Wait for goroutines to finish + h.wg.Wait() + + // Returning errors + if len(errs) > 0 { + return errors.Join(errs...) + } + log.Info().Str("file", "hardware").Msg("hardware manager stopped") + + return nil +} diff --git a/hardware/interfaces.go b/hardware/interfaces.go index 213c3e3..039cf13 100644 --- a/hardware/interfaces.go +++ b/hardware/interfaces.go @@ -4,12 +4,13 @@ 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 - SetPeripheralSettings(map[string]interface{}) error // Set a peripheral setting - SetDeviceProperty(context.Context, uint32, uint32, byte) error // Update a device property + Connect(context.Context) error // Connect the peripheral + IsConnected() bool // Return if the peripheral is connected or not + Disconnect() error // Disconnect the peripheral + Activate(context.Context) error // Activate the peripheral + Deactivate(context.Context) error // Deactivate the peripheral + SetSettings(map[string]interface{}) error // Set a peripheral setting + SetDeviceProperty(context.Context, uint32, byte) error // Update a device property GetInfo() PeripheralInfo // Get the peripheral information GetSettings() map[string]interface{} // Get the peripheral settings @@ -20,17 +21,19 @@ type PeripheralInfo struct { Name string `yaml:"name"` // Name of the peripheral SerialNumber string `yaml:"sn"` // S/N of the peripheral ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral + IsOpen bool // Open flag for peripheral connection Settings map[string]interface{} `yaml:"settings"` // Peripheral settings } // PeripheralFinder represents how compatible peripheral drivers are implemented type PeripheralFinder interface { - Initialize() error // Initializes the protocol - Start(context.Context) error // Start the detection - Stop() error // Stop the detection - ForceScan() // Explicitly scans for peripherals - 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 + Initialize() error // Initializes the protocol + Start(context.Context) error // Start the detection + WaitStop() error // Waiting for finder to close + ForceScan() // Explicitly scans for peripherals + RegisterPeripheral(context.Context, PeripheralInfo) (string, error) // Registers a new peripheral data + UnregisterPeripheral(context.Context, string) error // Unregisters an existing peripheral + GetPeripheralSettings(string) (map[string]interface{}, error) // Gets the peripheral settings + SetPeripheralSettings(string, map[string]interface{}) error // Sets the peripheral settings + GetName() string // Get the name of the finder } diff --git a/hardware/third-party/ftdi/detectFTDI.cpp b/hardware/third-party/ftdi/detectFTDI.cpp deleted file mode 100644 index c1e8722..0000000 --- a/hardware/third-party/ftdi/detectFTDI.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include -#include -#include -#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; - } - } - } -} \ No newline at end of file diff --git a/hardware/third-party/ftdi/detectFTDI.manifest b/hardware/third-party/ftdi/detectFTDI.manifest deleted file mode 100644 index a048981..0000000 --- a/hardware/third-party/ftdi/detectFTDI.manifest +++ /dev/null @@ -1,12 +0,0 @@ - - - - Detect FTDI - - - - - - - - \ No newline at end of file diff --git a/hardware/third-party/ftdi/dmxSender.cpp b/hardware/third-party/ftdi/dmxSender.cpp deleted file mode 100644 index 20a6995..0000000 --- a/hardware/third-party/ftdi/dmxSender.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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 dmxData[DMX_CHANNELS + 1]; -std::atomic 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(buffer[1]) | - (static_cast(buffer[2]) << 8)); - // Get the channel value - uint8_t channelValue = static_cast(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; -} \ No newline at end of file diff --git a/hardware/third-party/ftdi/dmxSender.manifest b/hardware/third-party/ftdi/dmxSender.manifest deleted file mode 100644 index c87b039..0000000 --- a/hardware/third-party/ftdi/dmxSender.manifest +++ /dev/null @@ -1,12 +0,0 @@ - - - - DMXSender - - - - - - - - \ No newline at end of file diff --git a/hardware/third-party/ftdi/generate.bat b/hardware/third-party/ftdi/generate.bat deleted file mode 100644 index 000979c..0000000 --- a/hardware/third-party/ftdi/generate.bat +++ /dev/null @@ -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 \ No newline at end of file diff --git a/peripherals.go b/peripherals.go index d98ea46..e128c1d 100644 --- a/peripherals.go +++ b/peripherals.go @@ -8,41 +8,48 @@ import ( ) // AddPeripheral adds a peripheral to the project -func (a *App) AddPeripheral(protocolName string, peripheralID string) error { +func (a *App) AddPeripheral(peripheralData hardware.PeripheralInfo) (string, error) { // Get the peripheral 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) + f, err := a.hardwareManager.GetFinder(peripheralData.ProtocolName) + if err != nil { + log.Error().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Msg("unable to found the specified finder") + return "", fmt.Errorf("unable to found the peripheral ID '%s'", peripheralData.SerialNumber) } + // Register this new peripheral + serialNumber, err := f.RegisterPeripheral(a.ctx, peripheralData) + if err != nil { + log.Trace().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Str("periphID", serialNumber).Msg("device registered to the finder") + return "", fmt.Errorf("unable to register the peripheral '%s'", serialNumber) + } + // Rewrite the serialnumber for virtual devices + peripheralData.SerialNumber = serialNumber + // Add the peripheral ID to the project if a.projectInfo.PeripheralsInfo == nil { a.projectInfo.PeripheralsInfo = make(map[string]hardware.PeripheralInfo) } - a.projectInfo.PeripheralsInfo[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 + a.projectInfo.PeripheralsInfo[peripheralData.SerialNumber] = peripheralData + log.Info().Str("file", "peripheral").Str("protocolName", peripheralData.ProtocolName).Str("periphID", peripheralData.SerialNumber).Msg("peripheral added to project") + return peripheralData.SerialNumber, nil } // GetPeripheralSettings gets the peripheral settings func (a *App) GetPeripheralSettings(protocolName, peripheralID string) (map[string]interface{}, error) { // Get the peripheral from its finder - p, found := a.hardwareManager.GetPeripheral(protocolName, peripheralID) - if !found { + f, err := a.hardwareManager.GetFinder(protocolName) + if err != nil { log.Error().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("unable to found the specified peripheral") return nil, fmt.Errorf("unable to found the peripheral ID '%s'", peripheralID) } - // Return the peripheral settings - return p.GetSettings(), nil + return f.GetPeripheralSettings(peripheralID) } // UpdatePeripheralSettings updates a specific setting of a peripheral func (a *App) UpdatePeripheralSettings(protocolName, peripheralID string, settings map[string]interface{}) error { - // Get the peripheral from its finder - p, found := a.hardwareManager.GetPeripheral(protocolName, peripheralID) - if !found { + // Sets the settings with the finder + f, err := a.hardwareManager.GetFinder(protocolName) + if err != nil { log.Error().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("unable to found the specified peripheral") return fmt.Errorf("unable to found the peripheral ID '%s'", peripheralID) } @@ -54,104 +61,65 @@ func (a *App) UpdatePeripheralSettings(protocolName, peripheralID string, settin pInfo.Settings = settings a.projectInfo.PeripheralsInfo[peripheralID] = pInfo // Apply changes in the peripheral - return p.SetPeripheralSettings(pInfo.Settings) + return f.SetPeripheralSettings(peripheralID, pInfo.Settings) } -// RemovePeripheral adds a peripheral to the project +// RemovePeripheral removes a peripheral from the project func (a *App) RemovePeripheral(protocolName string, peripheralID string) error { - // TODO: Disconnect the peripheral + // Unregister the peripheral from the finder + f, err := a.hardwareManager.GetFinder(protocolName) + if err != nil { + log.Err(err).Str("file", "peripherals").Str("protocolName", protocolName).Msg("unable to find the finder") + return fmt.Errorf("unable to find the finder") + } + err = f.UnregisterPeripheral(a.ctx, peripheralID) + if err != nil { + log.Err(err).Str("file", "peripherals").Str("peripheralID", peripheralID).Msg("unable to unregister this peripheral") + return fmt.Errorf("unable to unregister this peripheral") + } // Remove the peripheral ID from the project delete(a.projectInfo.PeripheralsInfo, peripheralID) log.Info().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("peripheral removed from project") return nil } -// AddOS2LPeripheral adds a new OS2L peripheral -func (a *App) AddOS2LPeripheral() (hardware.PeripheralInfo, error) { - // Get the OS2L finder - 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 finder - 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) 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) 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) -} +// 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) +// } diff --git a/project.go b/project.go deleted file mode 100644 index e2afa78..0000000 --- a/project.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "dmxconnect/hardware" - "fmt" - - "github.com/rs/zerolog/log" - - "os" - "path/filepath" - "time" - - "github.com/wailsapp/wails/v2/pkg/runtime" - "gopkg.in/yaml.v2" -) - -const ( - projectsDirectory = "projects" // The directory were are stored all the projects - avatarsDirectory = "frontend/public" // The directory were are stored all the avatars - projectExtension = ".dmxproj" // The extension of a DMX Connect project -) - -// GetProjects gets all the projects in the projects directory -func (a *App) GetProjects() ([]ProjectMetaData, error) { - projects := []ProjectMetaData{} - - f, err := os.Open(projectsDirectory) - if err != nil { - log.Err(err).Str("file", "project").Msg("unable to open the projects directory") - return nil, fmt.Errorf("unable to open the projects directory: %v", err) - } - log.Trace().Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("projects directory opened") - - files, err := f.Readdir(0) - if err != nil { - log.Err(err).Str("file", "project").Msg("unable to read the projects directory") - return nil, fmt.Errorf("unable to read the projects directory: %v", err) - } - log.Trace().Str("file", "project").Any("projectsFiles", files).Msg("project files got") - - for _, fileInfo := range files { - // Open the file and get the show name - fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name())) - if err != nil { - log.Warn().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("unable to open the project file") - continue - } - log.Trace().Str("file", "project").Str("projectFile", fileInfo.Name()).Any("fileData", fileData).Msg("project file read") - - projectObject := ProjectInfo{} - err = yaml.Unmarshal(fileData, &projectObject) - if err != nil { - log.Warn().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("project has invalid format") - continue - } - log.Trace().Str("file", "project").Str("projectFile", fileInfo.Name()).Msg("project file unmarshalled") - - // Add the SaveFile property - projects = append(projects, ProjectMetaData{ - Name: projectObject.ShowInfo.Name, - Save: fileInfo.Name(), - }) - } - log.Info().Str("file", "project").Any("projectsList", projects).Msg("got the projects list") - return projects, nil -} - -// CreateProject creates a new blank project -func (a *App) CreateProject() ShowInfo { - date := time.Now() - a.projectSave = "" - a.projectInfo.ShowInfo = ShowInfo{ - Name: "My new show", - Date: fmt.Sprintf("%04d-%02d-%02dT%02d:%02d", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute()), - Avatar: "appicon.png", - Comments: "Write your comments here", - } - log.Info().Str("file", "project").Any("showInfo", a.projectInfo.ShowInfo).Msg("project has been created") - return a.projectInfo.ShowInfo -} - -// GetProjectInfo returns the information of the saved project -func (a *App) GetProjectInfo(projectFile string) (ProjectInfo, error) { - // Open the project file - projectPath := filepath.Join(projectsDirectory, projectFile) - log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project path is created") - content, err := os.ReadFile(projectPath) - if err != nil { - log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to read the project file") - return ProjectInfo{}, fmt.Errorf("unable to read the project file: %v", err) - } - log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project file read") - a.projectInfo = ProjectInfo{} - err = yaml.Unmarshal(content, &a.projectInfo) - if err != nil { - log.Err(err).Str("file", "project").Str("projectFile", projectFile).Msg("Unable to get the project information") - return ProjectInfo{}, fmt.Errorf("unable to get the project information: %v", err) - } - log.Trace().Str("file", "project").Str("projectPath", projectPath).Msg("project information got") - // Load it into the app - a.projectSave = projectFile - // Return the show information - log.Info().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("got the project information") - return a.projectInfo, nil -} - -// ChooseAvatarPath opens a filedialog to choose the show avatar -func (a *App) ChooseAvatarPath() (string, error) { - // Open the file dialog box - filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ - Title: "Choose your show avatar", - Filters: []runtime.FileFilter{ - { - DisplayName: "Images", - Pattern: "*.png;*.jpg;*.jpeg", - }, - }, - }) - if err != nil { - log.Err(err).Str("file", "project").Msg("unable to open the avatar dialog") - return "", err - } - log.Debug().Str("file", "project").Msg("avatar dialog is opened") - // Copy the avatar to the application avatars path - avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath)) - log.Trace().Str("file", "project").Str("avatarPath", avatarPath).Msg("avatar path is created") - _, err = copy(filePath, avatarPath) - if err != nil { - log.Err(err).Str("file", "project").Str("avatarsDirectory", avatarsDirectory).Str("fileBase", filepath.Base(filePath)).Msg("unable to copy the avatar file") - return "", err - } - log.Info().Str("file", "project").Str("avatarFileName", filepath.Base(filePath)).Msg("got the new avatar file") - return filepath.Base(filePath), nil -} - -// UpdateShowInfo updates the show information -func (a *App) UpdateShowInfo(showInfo ShowInfo) { - a.projectInfo.ShowInfo = showInfo - log.Info().Str("file", "project").Any("showInfo", showInfo).Msg("show information was updated") -} - -// SaveProject saves the project -func (a *App) SaveProject() (string, error) { - // If there is no save file, create a new one with the show name - if a.projectSave == "" { - date := time.Now() - a.projectSave = fmt.Sprintf("%04d%02d%02d%02d%02d%02d%s", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), projectExtension) - log.Debug().Str("file", "project").Str("newProjectSave", a.projectSave).Msg("projectSave is null, getting a new one") - } - data, err := yaml.Marshal(a.projectInfo) - if err != nil { - log.Err(err).Str("file", "project").Any("projectInfo", a.projectInfo).Msg("unable to format the project information") - return "", err - } - log.Trace().Str("file", "project").Any("projectInfo", a.projectInfo).Msg("projectInfo has been marshalled") - // Create the project directory if not exists - err = os.MkdirAll(projectsDirectory, os.ModePerm) - if err != nil { - log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("unable to create the projects directory") - return "", err - } - log.Trace().Str("file", "project").Str("projectsDirectory", projectsDirectory).Msg("projects directory has been created") - - err = os.WriteFile(filepath.Join(projectsDirectory, a.projectSave), data, os.ModePerm) - if err != nil { - log.Err(err).Str("file", "project").Str("projectsDirectory", projectsDirectory).Str("projectSave", a.projectSave).Msg("unable to save the project") - return "", err - } - log.Info().Str("file", "project").Str("projectFileName", a.projectSave).Msg("project has been saved") - return a.projectSave, nil -} - -// ShowInfo defines the information of the show -type ShowInfo struct { - Name string `yaml:"name"` - Date string `yaml:"date"` - Avatar string `yaml:"avatar"` - Comments string `yaml:"comments"` -} - -// ProjectMetaData defines all the minimum information for a lighting project -type ProjectMetaData struct { - Name string // Show name - Save string // The save file of the project -} - -// ProjectInfo defines all the information for a lighting project -type ProjectInfo struct { - ShowInfo ShowInfo `yaml:"show"` // Show information - PeripheralsInfo map[string]hardware.PeripheralInfo `yaml:"peripherals"` // Peripherals information -} diff --git a/wails.json b/wails.json index b3676fe..279d20c 100644 --- a/wails.json +++ b/wails.json @@ -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",