11 Commits

45 changed files with 3972 additions and 293 deletions

5
.gitignore vendored
View File

@@ -6,4 +6,7 @@ frontend/dist
frontend/wailsjs
*/package-lock.json
*/package.json.md5
*.exe
*.exe
*.o
*.rc
frontend/public

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

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

172
app.go
View File

@@ -2,150 +2,78 @@ package main
import (
"context"
"dmxconnect/hardware"
"fmt"
"io"
"log"
"time"
"github.com/rs/zerolog/log"
"os"
"path/filepath"
"strings"
"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
"sync"
)
// App struct
type App struct {
ctx context.Context
ctx context.Context
cancelFunc context.CancelFunc
hardwareManager *hardware.HardwareManager // For managing all the hardware
wmiMutex sync.Mutex // Avoid some WMI operations at the same time
projectInfo ProjectInfo // The project information structure
projectSave string // The file name of the project
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
// Create a new hadware manager
hardwareManager := hardware.NewHardwareManager()
hardwareManager.RegisterDriver(hardware.NewMIDIFinder(5 * time.Second))
hardwareManager.RegisterDriver(hardware.NewFTDIFinder(5 * time.Second))
// hardwareManager.RegisterDriver(hardware.NewOS2LDriver())
return &App{
hardwareManager: hardwareManager,
projectSave: "",
projectInfo: ProjectInfo{
PeripheralsInfo: make(map[string]hardware.PeripheralInfo),
},
}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
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
}
}
// GetProjects gets all the projects in the projects directory
func (a *App) GetProjects() ([]ProjectInfo, error) {
projects := []ProjectInfo{}
f, err := os.Open(projectsDirectory)
if err != nil {
log.Fatalf("Unable to open the projects directory: %v", err)
return nil, err
}
files, err := f.Readdir(0)
if err != nil {
log.Fatalf("Unable to read the projects directory: %v", err)
return nil, err
}
for _, fileInfo := range files {
// Open the file and get the show name
fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name()))
if err == nil {
projectObject := ProjectInfo{}
err = yaml.Unmarshal(fileData, &projectObject)
if err == nil {
// Add the SaveFile property
projectObject.ShowInfo.SaveFile = fileInfo.Name()
projects = append(projects, projectObject)
}
}
}
return projects, nil
// onReady is called when the DOM is ready
// We get the current peripherals connected
func (a *App) onReady(ctx context.Context) {
// log.Debug().Str("file", "peripherals").Msg("getting peripherals...")
// err := a.hardwareManager.Scan()
// if err != nil {
// log.Err(err).Str("file", "app").Msg("unable to get the peripherals")
// }
return
}
// GetProjectInfo returns the information of the saved project
func (a *App) GetProjectInfo(projectFile string) (ShowInfo, error) {
projectPath := filepath.Join(projectsDirectory, projectFile)
content, err := os.ReadFile(projectPath)
// onShutdown is called when the app is closing
// We stop all the pending processes
func (a *App) onShutdown(ctx context.Context) {
// Close the application properly
log.Trace().Str("file", "app").Msg("app is closing")
// Explicitly close the context
a.cancelFunc()
err := a.hardwareManager.Stop()
if err != nil {
log.Fatalf("Unable to read the project file: %v", err)
return ShowInfo{}, err
log.Err(err).Str("file", "app").Msg("unable to stop the hardware manager")
}
projectInfo := ProjectInfo{}
err = yaml.Unmarshal(content, &projectInfo)
if err != nil {
log.Fatalf("Unable to get the project information: %v", err)
return ShowInfo{}, err
}
projectInfo.ShowInfo.SaveFile = projectFile
return projectInfo.ShowInfo, nil
}
// ChooseAvatarPath opens a filedialog to choose the show avatar
func (a *App) ChooseAvatarPath() (string, error) {
// Open the file dialog box
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Choose your show avatar",
Filters: []runtime.FileFilter{
{
DisplayName: "Images",
Pattern: "*.png;*.jpg;*.jpeg",
},
},
})
if err != nil {
return "", err
}
// Copy the avatar to the application avatars path
avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath))
_, err = copy(filePath, avatarPath)
if err != nil {
return "", err
}
return filepath.Base(filePath), nil
}
// SaveProject saves the project
func (a *App) SaveProject(showInfo ShowInfo) (string, error) {
log.Printf("Saving the project %s to %s", showInfo.Name, showInfo.SaveFile)
// If there is no save file, create a new one with the show name
if showInfo.SaveFile == "" {
showInfo.SaveFile = fmt.Sprintf("%s%s", formatString(showInfo.Name), projectExtension)
}
project := ProjectInfo{}
log.Printf("The number of universes: %d", showInfo.UniversesNumber)
project.ShowInfo = showInfo
data, err := yaml.Marshal(project)
if err != nil {
return "", err
}
err = os.WriteFile(filepath.Join(projectsDirectory, showInfo.SaveFile), data, os.ModePerm)
if err != nil {
return "", err
}
return showInfo.SaveFile, nil
}
// ShowInfo defines the information of the show
type ShowInfo struct {
Name string `yaml:"name"`
Date string `yaml:"date"`
UniversesNumber int `yaml:"universesNumber"`
Avatar string `yaml:"avatar"`
Comments string `yaml:"comments"`
SaveFile string `yaml:"-"`
}
// ProjectInfo defines all the information for a lighting project
type ProjectInfo struct {
ShowInfo ShowInfo `yaml:"show"` // Show information
return
}
func formatString(input string) string {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>

View File

@@ -9,10 +9,55 @@
import Show from './components/Show/Show.svelte';
import GeneralConsole from './components/Console/GeneralConsole.svelte';
import RoundIconButton from './components/General/RoundIconButton.svelte';
import { generateToast, currentProject, needProjectSave } from './stores';
import { generateToast, showInformation, needProjectSave, peripherals } from './stores';
import { SaveProject } from '../wailsjs/go/main/App.js';
import { construct_svelte_component } from 'svelte/internal';
import ToastNotification from './components/General/ToastNotification.svelte';
import { construct_svelte_component } from 'svelte/internal';
import { EventsOn } from '../wailsjs/runtime'
import { CreateProject } from "../wailsjs/go/main/App";
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
import { get } from "svelte/store"
import ToastNotification from './components/General/ToastNotification.svelte';
// Handle the event when a new peripheral is detected
EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){
// When a new peripheral is detected, add it to the map and:
// - Pass the isDetected key to true
// - Set the isSaved key to the last value
let peripheralsList = get(peripherals)
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
peripheralInfo.isDetected = true
peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false
peripherals.update((peripherals) => {
peripherals[peripheralInfo.SerialNumber] = peripheralInfo
return {...peripherals}
})
console.log("Hardware has been added to the system");
generateToast('info', 'bxs-hdd', 'Your <b>' + peripheralInfo.Name + '</b> device has been detected')
})
// Handle the event when a peripheral is removed from the system
EventsOn('PERIPHERAL_REMOVAL', function(peripheralInfo){
console.log("Hardware has been removed from the system");
// When a peripheral is disconnected, pass its isDetected key to false
// If the isSaved key is set to false, we can completely remove the peripheral from the list
let peripheralsList = get(peripherals)
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
let needToDelete = (lastSavedProperty !== true) ? true : false
peripherals.update((storedPeripherals) => {
if (needToDelete){
delete storedPeripherals[peripheralInfo.SerialNumber];
return { ...storedPeripherals };
}
storedPeripherals[peripheralInfo.SerialNumber].isDetected = false
return {...storedPeripherals}
})
generateToast('warning', 'bxs-hdd', 'Your <b>' + peripheralInfo.Name + '</b> device has been removed')
})
// Set the window title
$: {
WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (unsaved)" : ""))
}
let selectedMenu = "settings"
// When the navigation menu changed, update the selected menu
@@ -22,11 +67,9 @@
// Save the project
function saveProject(){
SaveProject($currentProject).then((saveFile) => {
console.log($currentProject)
$currentProject.SaveFile = saveFile
SaveProject().then((filePath) => {
needProjectSave.set(false)
console.log("Project has been saved")
console.log("Project has been saved to " + filePath)
generateToast('info', 'bxs-save', 'The project has been saved')
}).catch((error) => {
console.error(`Unable to save the project: ${error}`)
@@ -34,23 +77,21 @@
})
}
function formatDate(date) {
const pad = (number) => number.toString().padStart(2, '0');
const year = date.getFullYear();
const month = pad(date.getMonth() + 1); // Les mois commencent à 0
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
// Instanciate a new project
CreateProject().then((showInfo) => {
showInformation.set(showInfo)
$needProjectSave = true
})
currentProject.set({
Name: "My new show",
Date: formatDate(new Date()),
Avatar: "appicon.png",
UniversesNumber: 1,
Comments: "Write your comments here",
SaveFile: "",
// Handle window shortcuts
document.addEventListener('keydown', function(event) {
// Check the CTRL+S keys
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
// Avoid the natural behaviour
event.preventDefault();
// Save the current project
saveProject()
}
});
</script>
@@ -58,7 +99,7 @@
<header>
<NavigationBar on:navigationChanged="{onNavigationChanged}"/>
{#if $needProjectSave}
<RoundIconButton on:click={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}></RoundIconButton>
<RoundIconButton on:mouseup={saveProject} icon="bx-save" width="2.5em" tooltip={$_("saveButtonTooltip")}></RoundIconButton>
{/if}
<Clock/>
</header>

View File

@@ -10,10 +10,18 @@
export let active = false;
export let style = '';
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
function toggleTooltip(){
tooltipShowing = !tooltipShowing
let buttonRef
function toggleTooltip(active){
const rect = buttonRef.getBoundingClientRect();
tooltipPosition = {
top: rect.bottom + 5, // Ajouter une marge en bas
left: rect.left, // Centrer horizontalement
};
tooltipShowing = active
}
// Emit a click event when the button is clicked
@@ -39,13 +47,13 @@
<!-- <Tooltip message={tooltip} show={tooltipShowing}></Tooltip> -->
<div class="container">
<button
on:mouseenter={toggleTooltip}
on:mouseleave={toggleTooltip}
<button bind:this={buttonRef}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
on:click={toggleList}
style='color: {$colors.white}; background-color: { active ? $colors.second : $colors.third }; border:none; {style}'><i class='bx { icon}'></i> { text }
</button>
<Tooltip message={tooltip} show={tooltipShowing}></Tooltip>
<Tooltip message={tooltip} show={tooltipShowing} position={tooltipPosition}></Tooltip>
<div class="list" style="color: {$colors.white}; display: {listShowing ? "block" : "none"};"
on:mouseleave={hideList}>
{#each Array.from(choices) as [key, value]}

View File

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

View File

@@ -15,21 +15,27 @@
const dispatch = createEventDispatcher();
function handleInput(event){
value = event.target.value
dispatch('input', value)
function handleInput(){
dispatch('input')
}
function handleBlur(event){
dispatch('blur', event)
}
function handleDblClick(){
dispatch('dblclick')
}
</script>
<div style="width: {width}; height: {height};">
<p style="color: {$colors.first};">{label}</p>
<p style="color: {$colors.white};">{label}</p>
<!-- Handle the textarea input -->
{#if type === 'large'}
<textarea style="background-color: {$colors.first}; color: {$colors.white};" placeholder={placeholder} value={value} on:input={handleInput}/>
<textarea style="background-color: {$colors.second}; color: {$colors.white};" placeholder={placeholder} value={value} on:dblclick={handleDblClick} on:input={handleInput} on:blur={handleBlur}/>
<!-- Handle the simple inputs -->
{:else}
<input style="background-color: {$colors.first}; color: {$colors.white};" type={type} min={min} max={max} src={src} alt={alt} value={value} placeholder={placeholder} on:input={handleInput}/>
<input style="background-color: {$colors.second}; color: {$colors.white};" type={type} min={min} max={max} src={src} alt={alt} value={value} placeholder={placeholder} on:dblclick={handleDblClick} on:input={handleInput} on:blur={handleBlur}/>
{/if}
</div>
@@ -48,14 +54,14 @@
}
input::selection {
background: #0F4C75; /* Couleur de fond de la sélection */
color: #FFFFFF; /* Couleur du texte de la sélection */
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
/* Pour Firefox */
input::-moz-selection {
background: #0F4C75; /* Couleur de fond de la sélection */
color: #FFFFFF; /* Couleur du texte de la sélection */
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
input:focus {
outline: 1px solid #BBE1FA;
@@ -69,14 +75,14 @@
}
textarea::selection {
background: #0F4C75; /* Couleur de fond de la sélection */
color: #FFFFFF; /* Couleur du texte de la sélection */
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
/* Pour Firefox */
textarea::-moz-selection {
background: #0F4C75; /* Couleur de fond de la sélection */
color: #FFFFFF; /* Couleur du texte de la sélection */
background: var(--first-color); /* Couleur de fond de la sélection */
color: var(--white-color); /* Couleur du texte de la sélection */
}
textarea:focus {
outline: 1px solid #BBE1FA;

View File

@@ -41,12 +41,12 @@
</script>
<div style="background-color: {$colors.second};">
<RoundIconButton on:click="{() => handleNavigation("settings")}" icon="bx-cog" width="2.5em" tooltip={$_("settingsMenuTooltip")} active={menuStates.settings}></RoundIconButton>
<RoundIconButton on:click="{() => handleNavigation("devices")}" icon="bx-video-plus" width="2.5em" tooltip={$_("devicesMenuTooltip")} active={menuStates.devices}></RoundIconButton>
<RoundIconButton on:click="{() => handleNavigation("preparation")}" icon="bx-layer" width="2.5em" tooltip="{$_("preparationMenuTooltip")}" active={menuStates.preparation}></RoundIconButton>
<RoundIconButton on:click="{() => handleNavigation("animation")}" icon="bx-film" width="2.5em" tooltip="{$_("animationMenuTooltip")}" active={menuStates.animation}></RoundIconButton>
<RoundIconButton on:click="{() => handleNavigation("show")}" icon="bxs-grid" width="2.5em" tooltip="{$_("showMenuTooltip")}" active={menuStates.show}></RoundIconButton>
<RoundIconButton on:click="{() => handleNavigation("console")}" icon="bx-slider" width="2.5em" tooltip="{$_("consoleMenuTooltip")}" active={menuStates.console}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("settings")}" icon="bx-cog" width="2.5em" tooltip={$_("settingsMenuTooltip")} active={menuStates.settings}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("devices")}" icon="bx-video-plus" width="2.5em" tooltip={$_("devicesMenuTooltip")} active={menuStates.devices}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("preparation")}" icon="bx-layer" width="2.5em" tooltip="{$_("preparationMenuTooltip")}" active={menuStates.preparation}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("animation")}" icon="bx-film" width="2.5em" tooltip="{$_("animationMenuTooltip")}" active={menuStates.animation}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("show")}" icon="bxs-grid" width="2.5em" tooltip="{$_("showMenuTooltip")}" active={menuStates.show}></RoundIconButton>
<RoundIconButton on:mousedown="{() => handleNavigation("console")}" icon="bx-slider" width="2.5em" tooltip="{$_("consoleMenuTooltip")}" active={menuStates.console}></RoundIconButton>
<Toggle icon="bx-shape-square" width="2.5em" height="1.3em" tooltip="{$_("stageRenderingToggleTooltip")}"></Toggle>
<Toggle icon="bx-play" width="2.5em" height="1.3em" tooltip="{$_("showActivationToggleTooltip")}"></Toggle>
</div>

View File

@@ -48,24 +48,36 @@
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function emitClick() {
dispatch('click');
function emitMouseDown() {
dispatch('mousedown');
}
function emitMouseUp() {
dispatch('mouseup');
}
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
function toggleTooltip(){
tooltipShowing = !tooltipShowing
let buttonRef
function toggleTooltip(active){
const rect = buttonRef.getBoundingClientRect();
tooltipPosition = {
top: rect.bottom + 5, // Ajouter une marge en bas
left: rect.left, // Centrer horizontalement
};
tooltipShowing = active
}
</script>
<div class="container">
<button
<button bind:this={buttonRef}
style="width:{width}; height:{width}; border-radius:{width}; background-color:{background}; color:{foreground};"
on:mousedown={emitClick}
on:mouseenter={toggleTooltip}
on:mouseleave={toggleTooltip}>
on:mousedown={emitMouseDown}
on:mouseup={emitMouseUp}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}>
<i class='bx {icon}' style="font-size:100%;"></i>
</button>
<!-- Showing the badge status if the button has an operational status -->
@@ -74,7 +86,7 @@
style="width: calc({width} / 3); height: calc({width} / 3); border-radius: calc({width}); background-color:{statusColor}; display:block;">
</div>
{/if}
<Tooltip message={tooltipMessage} show={tooltipShowing}></Tooltip>
<Tooltip message={tooltipMessage} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>

View File

@@ -9,28 +9,41 @@
export let active = false;
export let style = '';
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
function toggleTooltip(){
tooltipShowing = !tooltipShowing
let buttonRef
function toggleTooltip(active){
const rect = buttonRef.getBoundingClientRect();
tooltipPosition = {
top: rect.bottom + 5, // Ajouter une marge en bas
left: rect.left, // Centrer horizontalement
};
tooltipShowing = active
}
// Emit a click event when the button is clicked
// Emit a click event when the button is clicked
const dispatch = createEventDispatcher();
function emitClick() {
dispatch('click');
}
function handleBlur(){
dispatch('blur')
}
</script>
<div class="container">
<button
<button bind:this={buttonRef}
on:blur={handleBlur}
on:mousedown={emitClick}
on:mouseenter={toggleTooltip}
on:mouseleave={toggleTooltip}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
style='color: {$colors.white}; background-color: { active ? $colors.second : $colors.third }; {style}'><i class='bx { icon}'></i> { text }
</button>
<Tooltip message={tooltip} show={tooltipShowing}></Tooltip>
<Tooltip message={tooltip} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>
.container{

View File

@@ -23,7 +23,7 @@
</div>
<div class="bodyContainer"
style='background-color: {$colors.third}; max-width: {maxWidth}; max-height: {maxHeight};'>
style='background-color: {$colors.first}; max-width: {maxWidth}; max-height: {maxHeight};'>
{#if tabs[activeTab]}
<svelte:component this={tabs[activeTab].component} />
{/if}
@@ -32,7 +32,6 @@
<style>
.headerContainer{
cursor: pointer;
margin:0;
border-radius: 0.5em;
}

View File

@@ -24,25 +24,33 @@
dispatch('click', event);
}
let tooltipPosition = {top: 0, left: 0}
// Show a tooltip on mouse hover
let tooltipShowing = false
function toggleTooltip(){
tooltipShowing = !tooltipShowing
let buttonRef
function toggleTooltip(active){
const rect = buttonRef.getBoundingClientRect();
tooltipPosition = {
top: rect.bottom + 5, // Ajouter une marge en bas
left: rect.left, // Centrer horizontalement
};
tooltipShowing = active
}
</script>
<div class="container" style="{cssVarStyles}">
<label class="customToggle"
<label class="customToggle" bind:this={buttonRef}
on:mousedown={emitClick}
on:mouseenter={toggleTooltip}
on:mouseleave={toggleTooltip}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
style="width:{width}; height:{height}; border-radius:{width}; background-color:{$colors.fourth};">
<input type="checkbox" {checked}>
<span class="checkmark" style="width: {height}; height: 100%; border-radius:{height};">
<i class='bx {icon}' style="font-size:{height};"/>
</span>
</label>
<Tooltip message={tooltipMessage} show={tooltipShowing}></Tooltip>
<Tooltip message={tooltipMessage} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>

View File

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

View File

@@ -0,0 +1,91 @@
<script lang=ts>
import {colors} from '../../stores.js';
import InfoButton from "../General/InfoButton.svelte";
import { _ } from 'svelte-i18n'
import { createEventDispatcher } from 'svelte';
import Input from "../General/Input.svelte";
export let title = 'Default card';
export let type = '';
export let location = '';
export let line1 = '';
export let line2 = '';
export let removable = false;
export let addable = false;
export let signalizable = false;
export let signalized = false;
export let disconnected = false;
// Emit a delete event when the device is being removed
const dispatch = createEventDispatcher();
function remove(event){
dispatch('delete')
}
function add(event){
dispatch('add')
}
function click(){
dispatch('click')
}
function dblclick(){
dispatch('dblclick')
}
</script>
<div class="card" on:dblclick={dblclick}>
<div class="profile" on:mousedown={click} style="color: {disconnected ? $colors.first : $colors.white};">
<div>
<p>{#if disconnected}<i class='bx bx-no-signal' style="font-size:100%; color: var(--nok-color);"></i> {/if}{title}</p>
<h6 class="subtitle">{type} {location != '' ? "- " : ""}<i>{location}</i></h6>
{#if disconnected}
<h6><b>Disconnected</b></h6>
{:else}
<h6>{line1}</h6>
<h6>{line2}</h6>
{/if}
</div>
<div class="actions">
<InfoButton on:click={add} color="{disconnected ? $colors.first : $colors.white}" style="margin: 0.2em; display: { addable ? 'flex' : 'none' }" icon='bxs-message-square-add' interactive message={$_("projectHardwareAddTooltip")}/>
<InfoButton on:click={remove} color="{disconnected ? $colors.first : $colors.white}" style="margin: 0.2em; display: { removable ? 'flex' : 'none' }" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")}/>
<InfoButton style="margin: 0.2em;" background={ signalized ? $colors.orange : $colors.first } icon='bx-pulse' hide={!signalizable}/>
</div>
</div>
</div>
<style>
.profile:hover{
background-color: var(--third-color);
}
.card{
position: relative;
}
.profile {
background-color: var(--second-color);
margin: 0.2em;
padding-left: 0.3em;
border-radius: 0.2em;
display: flex;
justify-content: space-between;
text-align: left;
cursor: pointer;
}
.subtitle{
margin-bottom: 0.5em;
}
.actions {
margin-left: 0.2em;
}
p{
margin: 0;
}
h6 {
margin: 0;
font-weight: 1;
}
</style>

View File

@@ -1,9 +1,190 @@
<script lang=ts>
import DeviceCard from "./DeviceCard.svelte";
import Tab from "../General/Tab.svelte";
import { _ } from 'svelte-i18n'
import { generateToast, needProjectSave, peripherals } from "../../stores";
import { get } from "svelte/store"
import { AddOS2LPeripheral, RemovePeripheral, ConnectFTDI, ActivateFTDI, DeactivateFTDI, DisconnectFTDI, SetDeviceFTDI, AddPeripheral } from "../../../wailsjs/go/main/App";
import RoundedButton from "../General/RoundedButton.svelte";
function ftdiConnect(){
ConnectFTDI().then(() =>
console.log("FTDI connected"))
.catch((error) => {
console.log("Error when trying to connect: " + error)
})
}
function ftdiActivate(){
ActivateFTDI().then(() =>
console.log("FTDI activated"))
.catch((error) => {
console.log("Error when trying to activate: " + error)
})
}
function ftdiDeactivate(){
DeactivateFTDI().then(() =>
console.log("FTDI deactivated"))
.catch((error) => {
console.log("Error when trying to deactivate: " + error)
})
}
let sliderValue = 0
function ftdiSetDevice(value){
console.log("value is " + value)
SetDeviceFTDI(value).then(() =>
console.log("FTDI device set up"))
.catch((error) => {
console.log("Error when trying to set the device: " + error)
})
}
function ftdiDisconnect(){
DisconnectFTDI().then(() =>
console.log("FTDI disconnected"))
.catch((error) => {
console.log("Error when trying to disconnect: " + error)
})
}
// Add the peripheral to the project
function addPeripheral(peripheral){
// Add the peripheral to the project (backend)
AddPeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
peripherals.update((value) => {
if (value[peripheral.SerialNumber]) {
value[peripheral.SerialNumber].isSaved = true;
}
return {...value}
})
$needProjectSave = true
}).catch((error) => {
console.log("Unable to add the peripheral to the project: " + error)
generateToast('danger', 'bx-error', 'Unable to add this device to project')
})
}
// Remove the peripheral from the project
function removePeripheral(peripheral) {
// Delete the peripheral from the project (backend)
RemovePeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
// If the peripheral is not detected, we can delete it form the store
// If not, we only pass the isSaved key to false
let peripheralsList = get(peripherals)
let lastDetectedProperty = peripheralsList[peripheral.SerialNumber]?.isDetected
let needToDelete = (lastDetectedProperty !== true) ? true : false
peripherals.update((storedPeripherals) => {
if (needToDelete){
delete storedPeripherals[peripheral.SerialNumber];
return { ...storedPeripherals };
}
storedPeripherals[peripheral.SerialNumber].isSaved = false
return { ...storedPeripherals };
})
$needProjectSave = true
}).catch((error) => {
console.log("Unable to remove the peripheral from the project: " + error)
generateToast('danger', 'bx-error', 'Unable to remove this device from project')
})
}
// 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', 'Your OS2L peripheral has been created')
}).catch(error => {
console.log("Unable to add the OS2L peripheral: " + error)
generateToast('danger', 'bx-error', 'Unable to create the OS2L peripheral')
})
}
// Get the number of saved peripherals
$: savedPeripheralNumber = Object.values($peripherals).filter(peripheral => peripheral.isSaved).length;
</script>
<p>This is the Inputs & outputs page</p>
<div class="hardware">
<div style="padding: 0.5em;">
<p style="margin-bottom: 1em;">Available peripherals</p>
<div class="availableHardware">
<p style="color: var(--first-color);"><i class='bx bxs-plug'></i> Detected</p>
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
{#if peripheral.isDetected}
<DeviceCard on:add={() => addPeripheral(peripheral)} on:dblclick={() => {
if(!peripheral.isSaved)
addPeripheral(peripheral)
}}
title={peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={"S/N: " + peripheral.SerialNumber} addable={!peripheral.isSaved}/>
{/if}
{/each}
<p style="color: var(--first-color);"><i class='bx bxs-network-chart' ></i> Others</p>
<RoundedButton on:click={createOS2L} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/>
</div>
</div>
<div style="padding: 0.5em; flex:2; width:100%;">
<p style="margin-bottom: 1em;">Project peripherals</p>
<div class="configuredHardware">
{#if savedPeripheralNumber > 0}
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
{#if peripheral.isSaved}
<DeviceCard on:delete={() => removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)}
disconnected={!peripheral.isDetected} title={peripheral.Name == "" ? "Please wait..." : peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} removable signalizable/>
{/if}
{/each}
{:else}
<i>No hardware saved for this project.</i>
{/if}
</div>
<p style="margin-bottom: 1em;">Peripheral settings</p>
<div>
<p><i>Select a peripheral to edit its settings</i></p>
<button on:click={ftdiConnect}>Connect FTDI 0</button>
<button on:click={ftdiActivate}>Activate FTDI 0</button>
<div class="slidecontainer">
<input type="range" min="0" max="255" class="slider" bind:value={sliderValue} on:input={() => ftdiSetDevice(sliderValue)}>
</div>
<button on:click={ftdiDeactivate}>Deactivate FTDI 0</button>
<button on:click={ftdiDisconnect}>Disconnect FTDI 0</button>
</div>
</div>
</div>
<style>
p {
margin: 0;
}
.hardware {
display: flex;
width: 100%;
}
.availableHardware {
background-color: var(--second-color);
border-radius: 0.5em;
padding: 0.2em;
max-height: calc(100vh - 300px);
overflow-y: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
/* overflow: visible; */
}
.availableHardware::-webkit-scrollbar {
display: none;
}
.configuredHardware {
background-color: var(--second-color);
border-radius: 0.5em;
padding: 0.5em;
padding: 0.2em;
display: flex;
flex-wrap: wrap;
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang=ts>
import { ChooseAvatarPath } from '../../../wailsjs/go/main/App.js';
import { currentProject } from '../../stores.js';
import { set_data_contenteditable } from 'svelte/internal';
import { ChooseAvatarPath, UpdateShowInfo } from '../../../wailsjs/go/main/App.js';
import { showInformation, needProjectSave } from '../../stores.js';
import Input from "../General/Input.svelte";
import RoundedButton from '../General/RoundedButton.svelte';
import { _ } from 'svelte-i18n'
@@ -8,41 +9,42 @@
// Choose the avatar path
function chooseAvatar(){
ChooseAvatarPath().then((avatarPath) => {
console.log(`Avatar path is ${avatarPath}`)
$currentProject.Avatar = avatarPath
$showInformation["Avatar"] = avatarPath
UpdateShowInfo($showInformation).then(()=> {
$needProjectSave = true
})
}).catch((error) => {
console.error(`An error occured: ${error}`)
})
}
function updateUniverses(event) {
currentProject.update(obj => {
return {
...obj,
UniversesNumber: parseInt(event.detail, 10) // Conversion en entier
};
});
function validate(field, value){
$showInformation[field] = value
console.log($showInformation)
UpdateShowInfo($showInformation).then(()=> {
$needProjectSave = true
})
}
</script>
<div class='flexSettings'>
<div>
<Input bind:value={$currentProject.Name} label={$_("projectShowNameLabel")} type='text'/>
<Input bind:value={$currentProject.Date} label={$_("projectShowDateLabel")} type='datetime-local'/>
<Input bind:value={$currentProject.UniversesNumber} on:input={updateUniverses} label={$_("projectUniversesLabel")} type='number' min=1 max=10/>
<Input on:blur={(event) => validate("Name", event.detail.target.value)} label={$_("projectShowNameLabel")} type='text' value={$showInformation.Name}/>
<Input on:blur={(event) => validate("Date", event.detail.target.value)} label={$_("projectShowDateLabel")} type='datetime-local' value={$showInformation.Date}/>
</div>
<div>
<Input bind:src={$currentProject.Avatar} label={$_("projectAvatarLabel")} type='image' alt={$_("projectAvatarLabel")} width='11em'/>
<RoundedButton on:click={chooseAvatar} style='display:block;' tooltip={$_("projectAvatarTooltip")} text={$_("projectLoadAvatarButton")} icon='bxs-image' active/>
</div>
<div>
<Input bind:value={$currentProject.Comments} label={$_("projectCommentsLabel")} type='large' width='100%'/>
<Input on:dblclick={chooseAvatar} label={$_("projectAvatarLabel")} type='image' alt={$_("projectAvatarLabel")} width='11em' src={$showInformation.Avatar}/>
</div>
</div>
<div>
<Input on:blur={(event) => validate("Comments", event.detail.target.value)} label={$_("projectCommentsLabel")} type='large' width='100%' value={$showInformation.Comments}/>
</div>
<style>
.flexSettings{
display: flex;
flex-direction: column;
flex-wrap: wrap;
width: 100%;
justify-content: space-between;
}
</style>

View File

@@ -1,40 +1,74 @@
<script lang=ts>
import { projectsList, currentProject, needProjectSave } from '../../stores.js';
import { generateToast, projectsList, showInformation, needProjectSave, peripherals } from '../../stores.js';
import RoundedButton from "../General/RoundedButton.svelte";
import ProjectPropertiesContent from "./ProjectPropertiesContent.svelte";
import DropdownList from "../General/DropdownList.svelte";
import InputsOutputsContent from "./InputsOutputsContent.svelte";
import Tab from "../General/Tab.svelte";
import { GetProjects, GetProjectInfo } from "../../../wailsjs/go/main/App";
import { CreateProject, GetProjects, GetProjectInfo } from "../../../wailsjs/go/main/App";
import { _ } from 'svelte-i18n'
import {generateToast, colors} from '../../stores.js';
import {colors} from '../../stores.js';
import { get } from "svelte/store"
const tabs = [
{ title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent },
{ title: $_("projectInputOutputTab"), icon: 'bxs-plug', tooltip: $_("projectInputOutputTooltip"), component: InputsOutputsContent },
];
// Refresh the projects list
let choices = new Map()
function loadProjectsList(){
GetProjects().then((projects) => {
choices = new Map(projects.map(item => [item.ShowInfo.SaveFile, item.ShowInfo.Name]));
$projectsList = projects
choices = new Map(projects.map(item => [item.Save, item.Name]));
$projectsList = projects
}).catch((error) => {
console.error(`Unable to get the projects list: ${error}`)
generateToast('danger', 'bx-error', 'Unable to get the projects list')
})
}
// Unsave peripherals from the store and remove the disconnected peripherals
function unsavePeripherals(){
peripherals.update((storedPeripherals) => {
// Set all the isSaved keys to false and delete the disconnected peripherals
for (let peripheralID in storedPeripherals) {
storedPeripherals[peripheralID].isSaved = false
if (!storedPeripherals[peripheralID].isDetected) {
delete storedPeripherals[peripheralID]
}
}
return {...storedPeripherals}
})
}
// Load the saved peripherals into the store
function loadPeripherals(peripheralsInfo){
peripherals.update((storedPeripherals) => {
// Add the saved peripherals of the project
// If already exists pass the isSaved key to true, if not create the peripheral and set it to disconnected
for (let peripheralID in peripheralsInfo){
// Add the peripheral to the list of peripherals, with the last isDetected key and the isSaved key to true
let lastDetectedKey = storedPeripherals[peripheralID]?.isDetected
storedPeripherals[peripheralID] = peripheralsInfo[peripheralID]
storedPeripherals[peripheralID].isDetected = (lastDetectedKey === true) ? true : false
storedPeripherals[peripheralID].isSaved = true
}
return {...storedPeripherals}
})
}
// Open the selected project
function openSelectedProject(event){
let selectedOption = event.detail.key
// Open the selected project
GetProjectInfo(selectedOption).then((projectInfo) => {
console.log("Project opened")
$currentProject = projectInfo
$showInformation = projectInfo.ShowInfo
// Remove the saved peripherals ofthe current project
unsavePeripherals()
// Load the new project peripherals
loadPeripherals(projectInfo.PeripheralsInfo)
needProjectSave.set(false)
generateToast('info', 'bx-folder-open', 'The project <b>' + projectInfo.Name + '</b> was opened')
generateToast('info', 'bx-folder-open', 'The project <b>' + projectInfo.ShowInfo.Name + '</b> was opened')
}).catch((error) => {
console.error(`Unable to open the project: ${error}`)
generateToast('danger', 'bx-error', 'Unable to open the project')
@@ -42,32 +76,20 @@
}
function initializeNewProject(){
currentProject.set({
Name: "My new show",
Date: formatDate(new Date()),
Avatar: "appicon.png",
UniversesNumber: 1,
Comments: "Write your comments here",
SaveFile: "",
});
generateToast('info', 'bxs-folder-plus', 'The project was created')
}
function formatDate(date) {
const pad = (number) => number.toString().padStart(2, '0');
const year = date.getFullYear();
const month = pad(date.getMonth() + 1); // Les mois commencent à 0
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
return `${year}-${month}-${day}T${hours}:${minutes}`;
// Instanciate a new project
CreateProject().then((showInfo) => {
$showInformation = showInfo
// Remove the saved peripherals ofthe current project
unsavePeripherals()
$needProjectSave = true
generateToast('info', 'bxs-folder-plus', 'The project was created')
})
}
</script>
<!-- Project buttons -->
<RoundedButton on:click={initializeNewProject} text={$_("newProjectString")} icon='bxs-plus-square' tooltip={$_("newProjectTooltip")}/>
<DropdownList icon='bxs-folder-open' text={$_("openProjectString")} choices={choices} tooltip={$_("openProjectTooltip")} on:click={loadProjectsList} on:selected={openSelectedProject}/>
<!-- Project tabcontrol -->
<Tab { tabs } maxHeight='73vh'/>

View File

@@ -28,5 +28,11 @@
"projectAvatarTooltip": "Load a new show avatar",
"projectCommentsLabel": "Comments",
"projectCommentsPlaceholder": "Leave your comments here",
"projectLoadAvatarButton": "Load a new avatar"
"projectLoadAvatarButton": "Load a new avatar",
"projectHardwareShowLabel" : "My Show",
"projectHardwareInputsLabel": "INPUTS",
"projectHardwareOutputsLabel": "OUTPUTS",
"projectHardwareDeleteTooltip": "Delete this peripheral",
"projectHardwareAddTooltip": "Add this peripheral to project"
}

View File

@@ -2,7 +2,7 @@ import App from './App.svelte';
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
import {currentProject, needProjectSave} from './stores.js';
import {showInformation, needProjectSave} from './stores.js';
// Load dictionaries
import { addMessages, init } from 'svelte-i18n';
@@ -24,28 +24,4 @@ const app = new App({
target: document.body,
});
// Set the initial title
WindowSetTitle("DMXConnect")
// When the current project data is modified, pass it to unsaved and change the title
let title;
currentProject.subscribe(value => {
needProjectSave.set(true)
title = value.Name
});
// If the project need to be saved, show the information in the title
needProjectSave.subscribe(value => {
if (value) {
console.log(`<!> The current project need to be save`);
WindowSetTitle("DMXConnect - " + title + " (unsaved)")
} else {
WindowSetTitle("DMXConnect - " + title)
}
})
document.addEventListener("DOMContentLoaded", function() {
});
export default app;

View File

@@ -2,10 +2,10 @@ import { writable } from 'svelte/store';
// Projects management
export let projectsList = writable([])
export let needProjectSave = writable(true)
// Show settings
export let currentProject = writable({});
export let needProjectSave = writable(false)
export let showInformation = writable({})
// Toasts notifications
export let messages = writable([])
@@ -33,3 +33,6 @@ export const colors = writable({
export const firstSize = writable("10px")
export const secondSize = writable("14px")
export const thirdSize = writable("20px")
// List of current hardware
export let peripherals = writable({})

View File

@@ -16,6 +16,8 @@ html, body {
position: relative;
width: 100%;
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
}
body {

7
go.mod
View File

@@ -1,11 +1,15 @@
module changeme
module dmxconnect
go 1.21
toolchain go1.21.3
require (
github.com/lxn/win v0.0.0-20210218163916-a377121e959e
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79
github.com/rs/zerolog v1.33.0
github.com/wailsapp/wails/v2 v2.9.1
golang.org/x/sys v0.20.0
gopkg.in/yaml.v2 v2.4.0
)
@@ -35,7 +39,6 @@ require (
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)

11
go.sum
View File

@@ -1,10 +1,12 @@
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -26,6 +28,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -35,6 +39,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79 h1:CA1UHN3RuY70DlC0RlvgtB1e8h3kYzmvK7s8CFe+Ohw=
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79/go.mod h1:oBuZjmjlKSj9CZKrNhcx/adNhHiiE0hZknECjIP8Z0Q=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -44,6 +50,9 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -72,6 +81,7 @@ golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -80,6 +90,7 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

234
hardware/FTDIFinder.go Normal file
View File

@@ -0,0 +1,234 @@
package hardware
import (
"bufio"
"context"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
goRuntime "runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/rs/zerolog/log"
)
const (
ftdiFinderExecutableName = "FTDI_finder.exe"
ftdiSenderExecutableName = "FTDI_sender.exe"
)
// FTDIFinder represents how the protocol is defined
type FTDIFinder struct {
findTicker time.Ticker // Peripherals find ticker
peripherals map[string]Peripheral // The list of peripherals handled by this finder
scanChannel chan struct{} // The channel to trigger a scan event
goWait sync.WaitGroup // Check goroutines execution
}
// NewFTDIFinder creates a new FTDI finder
func NewFTDIFinder(findPeriod time.Duration) *FTDIFinder {
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder created")
return &FTDIFinder{
findTicker: *time.NewTicker(findPeriod),
peripherals: make(map[string]Peripheral),
scanChannel: make(chan struct{}),
}
}
//go:embed third-party/ftdi/detectFTDI.exe
var finderExe []byte
//go:embed third-party/ftdi/dmxSender.exe
var senderExe []byte
// Initialize initializes the FTDI finder
func (f *FTDIFinder) Initialize() error {
// Check platform
if goRuntime.GOOS != "windows" {
log.Error().Str("file", "FTDIFinder").Str("platform", goRuntime.GOOS).Msg("FTDI finder not compatible with your platform")
return fmt.Errorf("the FTDI finder is not compatible with your platform yet (%s)", goRuntime.GOOS)
}
// Create the FTDI executables
err := createExecutable(ftdiFinderExecutableName, finderExe)
if err != nil {
return err
}
createExecutable(ftdiSenderExecutableName, senderExe)
if err != nil {
return err
}
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder initialized")
return nil
}
// createExecutable creates and writes an executable to the temporary directory of the system
func createExecutable(fileName string, storedFile []byte) error {
tempFile, err := os.Create(filepath.Join(os.TempDir(), fileName))
if err != nil {
log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to create an FTDI executable")
return err
}
log.Trace().Str("file", "FTDIFinder").Str("filePath", tempFile.Name()).Msg("FTDI executable created")
// Write the embedded executable to the temp file
if _, err := tempFile.Write(storedFile); err != nil {
log.Err(err).Str("file", "FTDIFinder").Str("fileName", fileName).Msg("unable to write the content to an FTDI executable")
return err
}
tempFile.Close()
log.Trace().Str("file", "FTDIPeripheral").Str("fileName", fileName).Msg("FTDI executable written")
return nil
}
// Start starts the finder and search for peripherals
func (f *FTDIFinder) Start(ctx context.Context) error {
f.goWait.Add(1)
go func() {
defer f.goWait.Done()
for {
select {
case <-ctx.Done():
return
case <-f.findTicker.C:
// Scan the peripherals
err := f.scanPeripherals(ctx)
if err != nil {
log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals")
}
case <-f.scanChannel:
// Scan the peripherals
err := f.scanPeripherals(ctx)
if err != nil {
log.Err(err).Str("file", "FTDIFinder").Msg("unable to scan FTDI peripherals")
}
}
}
}()
return nil
}
// ForceScan explicily asks for scanning peripherals
func (f *FTDIFinder) ForceScan() {
f.scanChannel <- struct{}{}
}
// Stop stops the finder
func (f *FTDIFinder) Stop() error {
log.Trace().Str("file", "FTDIFinder").Msg("stopping the FTDI finder...")
// Wait for goroutines to stop
f.goWait.Wait()
// Stop the ticker
f.findTicker.Stop()
// Delete the FTDI executable files
fileToDelete := filepath.Join(os.TempDir(), ftdiFinderExecutableName)
err := os.Remove(fileToDelete)
if err != nil {
log.Warn().Str("file", "FTDIFinder").Str("fileName", fileToDelete).AnErr("error", err).Msg("unable to remove the executable file")
}
fileToDelete = filepath.Join(os.TempDir(), ftdiSenderExecutableName)
err = os.Remove(fileToDelete)
if err != nil {
log.Warn().Str("file", "FTDIFinder").Str("fileName", fileToDelete).AnErr("error", err).Msg("unable to remove the executable file")
}
log.Trace().Str("file", "FTDIFinder").Msg("FTDI finder stopped")
return nil
}
// GetName returns the name of the driver
func (f *FTDIFinder) GetName() string {
return "FTDI"
}
// GetPeripheral gets the peripheral that correspond to the specified ID
func (f *FTDIFinder) GetPeripheral(peripheralID string) (Peripheral, bool) {
// Return the specified peripheral
peripheral := f.peripherals[peripheralID]
if peripheral == nil {
log.Error().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral from the FTDI finder")
return nil, false
}
log.Debug().Str("file", "FTDIFinder").Str("peripheralID", peripheralID).Msg("peripheral found by the FTDI finder")
return peripheral, true
}
// scanPeripherals scans the FTDI peripherals
func (f *FTDIFinder) scanPeripherals(ctx context.Context) error {
detectionCtx, cancel := context.WithCancel(ctx)
defer cancel()
log.Trace().Str("file", "FTDIFinder").Msg("FTDI scan triggered")
ftdiPeripherals := make(map[string]Peripheral)
finder := exec.CommandContext(detectionCtx, filepath.Join(os.TempDir(), ftdiFinderExecutableName))
log.Trace().Str("file", "FTDIFinder").Msg("has executed the FIND executable")
stdout, err := finder.StdoutPipe()
if err != nil {
return fmt.Errorf("unable to create the stdout pipe: %s", err)
}
defer stdout.Close()
stderr, err := finder.StderrPipe()
if err != nil {
return fmt.Errorf("unable to create the stderr pipe: %s", err)
}
defer stderr.Close()
err = finder.Start()
if err != nil {
return fmt.Errorf("unable to find FTDI peripherals: %s", err)
}
scannerErr := bufio.NewScanner(stderr)
for scannerErr.Scan() {
return fmt.Errorf("unable to find FTDI peripherals: %s", scannerErr.Text())
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
peripheralString := scanner.Text()
// The program output is like '0:1:2' where 0 is the location, 1 is the S/N and 2 is the name
peripheralInfo := strings.Split(peripheralString, ":")
log.Debug().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Str("peripheralSN", peripheralInfo[1]).Msg("new FTDI peripheral detected")
// Convert the location to an integer
location, err := strconv.Atoi(peripheralInfo[0])
if err != nil {
log.Warn().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("no location provided for this FTDI peripheral")
location = -1
}
// Add the peripheral to the temporary list
peripheral, err := NewFTDIPeripheral(peripheralInfo[2], peripheralInfo[1], location)
if err != nil {
return fmt.Errorf("unable to create the FTDI peripheral: %v", err)
}
ftdiPeripherals[peripheralInfo[1]] = peripheral
log.Trace().Str("file", "FTDIFinder").Str("peripheralName", peripheralInfo[2]).Msg("successfully added the FTDI peripheral to the finder")
}
// Compare with the current peripherals to detect arrivals/removals
removedList, addedList := comparePeripherals(f.peripherals, ftdiPeripherals)
// Emit the events
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
log.Info().Str("file", "FTDIFinder").Msg("FTDI remove list emitted to the front")
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
log.Info().Str("file", "FTDIFinder").Msg("FTDI add list emitted to the front")
// Store the new peripherals list
f.peripherals = ftdiPeripherals
return nil
}
// CreatePeripheral is not implemented here
func (f *FTDIFinder) CreatePeripheral(context.Context) (Peripheral, error) {
return nil, nil
}
// DeletePeripheral is not implemented here
func (f *FTDIFinder) DeletePeripheral(serialNumber string) error {
return nil
}

196
hardware/FTDIPeripheral.go Normal file
View File

@@ -0,0 +1,196 @@
package hardware
import (
"bufio"
"context"
_ "embed"
"fmt"
"io"
"github.com/rs/zerolog/log"
"os"
"os/exec"
)
const (
activateCommandString = 0x01
deactivateCommandString = 0x02
setCommandString = 0x03
)
// FTDIPeripheral contains the data of an FTDI peripheral
type FTDIPeripheral struct {
name string // The name of the peripheral
serialNumber string // The S/N of the FTDI peripheral
location int // The location of the peripheral
universesNumber int // The number of DMX universes handled by this peripheral
programName string // The temp file name of the executable
dmxSender *exec.Cmd // The command to pilot the DMX sender program
stdin io.WriteCloser // For writing in the DMX sender
stdout io.ReadCloser // For reading from the DMX sender
stderr io.ReadCloser // For reading the errors
disconnectChan chan struct{} // Channel to cancel the connection
errorsChan chan error // Channel to get the errors
}
// NewFTDIPeripheral creates a new FTDI peripheral
func NewFTDIPeripheral(name string, serialNumber string, location int) (*FTDIPeripheral, error) {
log.Info().Str("file", "FTDIPeripheral").Str("name", name).Str("s/n", serialNumber).Int("location", location).Msg("FTDI peripheral created")
return &FTDIPeripheral{
name: name,
dmxSender: nil,
serialNumber: serialNumber,
location: location,
universesNumber: 1,
disconnectChan: make(chan struct{}),
errorsChan: make(chan error, 1),
}, nil
}
// Connect connects the FTDI peripheral
func (p *FTDIPeripheral) Connect(ctx context.Context) error {
// Connect if no connection is already running
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("connecting FTDI peripheral...")
// Check if the connection has already been established
if p.dmxSender != nil {
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender already initialized")
return nil
}
// Initialize the exec.Command for running the process
p.dmxSender = exec.Command(p.programName, fmt.Sprintf("%d", p.location))
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender instance created")
// Create the pipes for stdin, stdout, and stderr asynchronously without blocking
var err error
if p.stdout, err = p.dmxSender.StdoutPipe(); err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stdout pipe")
return fmt.Errorf("unable to create stdout pipe: %v", err)
}
if p.stdin, err = p.dmxSender.StdinPipe(); err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stdin pipe")
return fmt.Errorf("unable to create stdin pipe: %v", err)
}
if p.stderr, err = p.dmxSender.StderrPipe(); err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to create stderr pipe")
return fmt.Errorf("unable to create stderr pipe: %v", err)
}
// Launch a goroutine to read stderr asynchronously
go func() {
scanner := bufio.NewScanner(p.stderr)
for scanner.Scan() {
// Process each line read from stderr
log.Err(fmt.Errorf(scanner.Text())).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error detected in dmx sender")
}
if err := scanner.Err(); err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error reading from stderr")
}
}()
// Launch the command asynchronously in another goroutine
go func() {
// Run the command, respecting the context cancellation
err := p.dmxSender.Run()
select {
case <-ctx.Done():
// If the context is canceled, handle it gracefully
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender was canceled by context")
return
default:
// Handle command exit normally
if err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while execution of dmx sender")
if exitError, ok := err.(*exec.ExitError); ok {
log.Warn().Str("file", "FTDIPeripheral").Int("exitCode", exitError.ExitCode()).Str("s/n", p.serialNumber).Msg("dmx sender exited with code")
}
} else {
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmx sender exited successfully")
}
}
}()
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxSender process started successfully")
return nil
}
// Disconnect disconnects the FTDI peripheral
func (p *FTDIPeripheral) Disconnect(ctx context.Context) error {
log.Trace().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("disconnecting FTDI peripheral...")
if p.dmxSender != nil {
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
_, err := io.WriteString(p.stdin, string([]byte{0x04, 0x00, 0x00, 0x00}))
if err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
return fmt.Errorf("unable to disconnect: %v", err)
}
p.stdin.Close()
p.stdout.Close()
p.dmxSender = nil
err = os.Remove(p.programName)
if err != nil {
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Str("senderPath", p.programName).Msg("unable to delete the dmx sender temporary file")
return fmt.Errorf("unable to delete the temporary file: %v", err)
}
return nil
}
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while disconnecting: not connected")
return fmt.Errorf("unable to disconnect: not connected")
}
// Activate activates the FTDI peripheral
func (p *FTDIPeripheral) Activate(ctx context.Context) error {
if p.dmxSender != nil {
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
_, err := io.WriteString(p.stdin, string([]byte{0x01, 0x00, 0x00, 0x00}))
if err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
return fmt.Errorf("unable to activate: %v", err)
}
return nil
}
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while activating: not connected")
return fmt.Errorf("unable to activate: not connected")
}
// Deactivate deactivates the FTDI peripheral
func (p *FTDIPeripheral) Deactivate(ctx context.Context) error {
if p.dmxSender != nil {
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
_, err := io.WriteString(p.stdin, string([]byte{0x02, 0x00, 0x00, 0x00}))
if err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
return fmt.Errorf("unable to deactivate: %v", err)
}
return nil
}
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while deactivating: not connected")
return fmt.Errorf("unable to deactivate: not connected")
}
// SetDeviceProperty sends a command to the specified device
func (p *FTDIPeripheral) SetDeviceProperty(ctx context.Context, uint32, channelNumber uint32, channelValue byte) error {
if p.dmxSender != nil {
log.Debug().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("dmxsender is defined for this FTDI")
commandString := []byte{0x03, 0x01, 0x00, 0xff, 0x03, 0x02, 0x00, channelValue}
_, err := io.WriteString(p.stdin, string(commandString))
if err != nil {
log.Err(err).Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("unable to write command to sender")
return fmt.Errorf("unable to set device property: %v", err)
}
return nil
}
log.Warn().Str("file", "FTDIPeripheral").Str("s/n", p.serialNumber).Msg("error while setting device property: not connected")
return fmt.Errorf("unable to set device property: not connected")
}
// GetInfo gets all the peripheral information
func (p *FTDIPeripheral) GetInfo() PeripheralInfo {
return PeripheralInfo{
Name: p.name,
SerialNumber: p.serialNumber,
ProtocolName: "FTDI",
}
}

174
hardware/MIDIFinder.go Normal file
View File

@@ -0,0 +1,174 @@
package hardware
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/mattrtaylor/go-rtmidi"
"github.com/rs/zerolog/log"
)
// MIDIFinder represents how the protocol is defined
type MIDIFinder struct {
findTicker time.Ticker // Peripherals find ticker
peripherals map[string]Peripheral // The list of peripherals
scanChannel chan struct{} // The channel to trigger a scan event
goWait sync.WaitGroup // Check goroutines execution
}
// NewMIDIFinder creates a new DMXUSB protocol
func NewMIDIFinder(findPeriod time.Duration) *MIDIFinder {
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder created")
return &MIDIFinder{
findTicker: *time.NewTicker(findPeriod),
peripherals: make(map[string]Peripheral),
}
}
// Initialize initializes the MIDI driver
func (f *MIDIFinder) Initialize() error {
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder initialized")
return nil
}
// Start starts the finder and search for peripherals
func (f *MIDIFinder) Start(ctx context.Context) error {
f.goWait.Add(1)
go func() {
defer f.goWait.Done()
for {
select {
case <-ctx.Done():
return
case <-f.findTicker.C:
// Scan the peripherals
err := f.scanPeripherals(ctx)
if err != nil {
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals")
}
case <-f.scanChannel:
// Scan the peripherals
err := f.scanPeripherals(ctx)
if err != nil {
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals")
}
}
}
}()
return nil
}
// Stop stops the finder
func (f *MIDIFinder) Stop() error {
log.Trace().Str("file", "MIDIFinder").Msg("stopping the MIDI finder...")
// Wait for goroutines to stop
f.goWait.Wait()
// Stop the ticker
f.findTicker.Stop()
log.Trace().Str("file", "MIDIFinder").Msg("MIDI finder stopped")
return nil
}
// GetName returns the name of the driver
func (f *MIDIFinder) GetName() string {
return "MIDI"
}
// GetPeripheral gets the peripheral that correspond to the specified ID
func (f *MIDIFinder) GetPeripheral(peripheralID string) (Peripheral, bool) {
// Return the specified peripheral
peripheral, found := f.peripherals[peripheralID]
if !found {
log.Error().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("unable to get this peripheral in the MIDI finder")
return nil, false
}
log.Trace().Str("file", "MIDIFinder").Str("peripheralID", peripheralID).Msg("MIDI peripheral found in the driver")
return peripheral, true
}
func splitStringAndNumber(input string) (string, int, error) {
// Regular expression to match the text part and the number at the end
re := regexp.MustCompile(`^(.*?)(\d+)$`)
matches := re.FindStringSubmatch(input)
// Check if the regex found both a text part and a number
if len(matches) == 3 {
// matches[1]: text part (might contain trailing spaces)
// matches[2]: numeric part as a string
textPart := strings.TrimSpace(matches[1]) // Remove any trailing spaces from the text
numberPart, err := strconv.Atoi(matches[2])
if err != nil {
return "", 0, err // Return error if the number conversion fails
}
return textPart, numberPart, nil
}
// Return an error if no trailing number is found
return "", 0, fmt.Errorf("no number found at the end of the string")
}
// ForceScan explicily asks for scanning peripherals
func (f *MIDIFinder) ForceScan() {
f.scanChannel <- struct{}{}
}
// scanPeripherals scans the MIDI peripherals
func (f *MIDIFinder) scanPeripherals(ctx context.Context) error {
midiPeripherals := make(map[string]Peripheral)
log.Trace().Str("file", "MIDIFinder").Msg("opening MIDI scanner port...")
midiScanner, err := rtmidi.NewMIDIInDefault()
if err != nil {
log.Err(err).Str("file", "MIDIFinder").Msg("unable to open the MIDI scanner port...")
return fmt.Errorf("unable to open the MIDI scanner: %s", err)
}
defer midiScanner.Close()
midiScanner.SetCallback(func(m rtmidi.MIDIIn, b []byte, f float64) {})
log.Trace().Str("file", "MIDIFinder").Msg("scanning MIDI peripherals...")
devicesCount, err := midiScanner.PortCount()
if err != nil {
log.Err(err).Str("file", "MIDIFinder").Msg("unable to scan MIDI peripherals...")
return fmt.Errorf("unable to scan MIDI peripherals: %s", err)
}
for i := 0; i < devicesCount; i++ {
portName, err := midiScanner.PortName(i)
if err != nil {
log.Warn().Str("file", "MIDIPeripheral").Msg("found peripheral without a correct name, set it to unknown")
portName = "Unknown device 0"
}
// Separate data
name, location, err := splitStringAndNumber(portName)
if err != nil {
log.Err(err).Str("file", "MIDIFinder").Str("description", portName).Msg("invalid peripheral description")
return fmt.Errorf("invalid pripheral description: %s", err)
}
log.Info().Str("file", "MIDIFinder").Str("name", name).Int("location", location).Msg("MIDI peripheral found")
// Add the peripheral to the temporary list
sn := strings.ToLower(strings.Replace(name, " ", "_", -1))
midiPeripherals[sn] = NewMIDIPeripheral(name, location, sn)
}
// Compare with the current peripherals to detect arrivals/removals
removedList, addedList := comparePeripherals(f.peripherals, midiPeripherals)
// Emit the events
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
log.Info().Str("file", "MIDIFinder").Msg("MIDI remove list emitted to the front")
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
log.Info().Str("file", "MIDIFinder").Msg("MIDI add list emitted to the front")
// Store the new peripherals list
f.peripherals = midiPeripherals
return nil
}
// CreatePeripheral is not implemented here
func (f *MIDIFinder) CreatePeripheral(context.Context) (Peripheral, error) {
return nil, nil
}
// DeletePeripheral is not implemented here
func (f *MIDIFinder) DeletePeripheral(serialNumber string) error {
return nil
}

View File

@@ -0,0 +1,58 @@
package hardware
import (
"context"
"github.com/rs/zerolog/log"
)
// MIDIPeripheral contains the data of a MIDI peripheral
type MIDIPeripheral struct {
name string // The name of the peripheral
location int // The location of the peripheral
serialNumber string // The S/N of the peripheral
}
// NewMIDIPeripheral creates a new MIDI peripheral
func NewMIDIPeripheral(name string, location int, serialNumber string) *MIDIPeripheral {
log.Trace().Str("file", "MIDIPeripheral").Str("name", name).Str("s/n", serialNumber).Int("location", location).Msg("MIDI peripheral created")
return &MIDIPeripheral{
name: name,
location: location,
serialNumber: serialNumber,
}
}
// Connect connects the MIDI peripheral
func (p *MIDIPeripheral) Connect(ctx context.Context) error {
return nil
}
// Disconnect disconnects the MIDI peripheral
func (p *MIDIPeripheral) Disconnect(ctx context.Context) error {
return nil
}
// Activate activates the MIDI peripheral
func (p *MIDIPeripheral) Activate(ctx context.Context) error {
return nil
}
// Deactivate deactivates the MIDI peripheral
func (p *MIDIPeripheral) Deactivate(ctx context.Context) error {
return nil
}
// SetDeviceProperty - not implemented for this kind of peripheral
func (p *MIDIPeripheral) SetDeviceProperty(context.Context, uint32, uint32, byte) error {
return nil
}
// GetInfo gets the peripheral information
func (p *MIDIPeripheral) GetInfo() PeripheralInfo {
return PeripheralInfo{
Name: p.name,
ProtocolName: "MIDI",
SerialNumber: p.serialNumber,
}
}

69
hardware/OS2LDriver.go Normal file
View File

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

View File

@@ -0,0 +1,60 @@
package hardware
import (
"context"
"github.com/rs/zerolog/log"
)
// OS2LPeripheral contains the data of an OS2L peripheral
type OS2LPeripheral struct {
name string // The name of the peripheral
serialNumber string // The serial number of the peripheral
}
// NewOS2LPeripheral creates a new OS2L peripheral
func NewOS2LPeripheral(name string, serialNumber string) *OS2LPeripheral {
log.Trace().Str("file", "OS2LPeripheral").Str("name", name).Str("s/n", serialNumber).Msg("OS2L peripheral created")
return &OS2LPeripheral{
name: name,
serialNumber: serialNumber,
}
}
// Connect connects the MIDI peripheral
func (p *OS2LPeripheral) Connect(ctx context.Context) error {
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral connected")
return nil
}
// Disconnect disconnects the MIDI peripheral
func (p *OS2LPeripheral) Disconnect(ctx context.Context) error {
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral disconnected")
return nil
}
// Activate activates the MIDI peripheral
func (p *OS2LPeripheral) Activate(ctx context.Context) error {
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral activated")
return nil
}
// Deactivate deactivates the MIDI peripheral
func (p *OS2LPeripheral) Deactivate(ctx context.Context) error {
log.Info().Str("file", "OS2LPeripheral").Msg("OS2L peripheral deactivated")
return nil
}
// SetDeviceProperty - not implemented for this kind of peripheral
func (p *OS2LPeripheral) SetDeviceProperty(context.Context, uint32, uint32, byte) error {
return nil
}
// GetInfo gets the peripheral information
func (p *OS2LPeripheral) GetInfo() PeripheralInfo {
return PeripheralInfo{
Name: p.name,
SerialNumber: p.serialNumber,
ProtocolName: "OS2L",
}
}

190
hardware/hardware.go Normal file
View File

@@ -0,0 +1,190 @@
package hardware
import (
"context"
"fmt"
"sync"
"time"
"github.com/rs/zerolog/log"
"github.com/lxn/win"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/sys/windows"
)
// PeripheralEvent is trigger by the finders when the scan is complete
type PeripheralEvent string
const (
// PeripheralArrival is triggerd when a peripheral has been connected to the system
PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL"
// PeripheralRemoval is triggered when a peripheral has been disconnected from the system
PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL"
debounceDuration = 500 * time.Millisecond
)
var (
debounceTimer *time.Timer
)
// HardwareManager is the class who manages the hardware
type HardwareManager struct {
drivers map[string]PeripheralFinder // The map of peripherals finders
peripherals []Peripheral // The current list of peripherals
peripheralsScanTrigger chan struct{} // Trigger the peripherals scans
goWait sync.WaitGroup // Wait for goroutines to terminate
}
// NewHardwareManager creates a new HardwareManager
func NewHardwareManager() *HardwareManager {
log.Trace().Str("package", "hardware").Msg("Hardware instance created")
return &HardwareManager{
drivers: make(map[string]PeripheralFinder),
peripherals: make([]Peripheral, 0),
peripheralsScanTrigger: make(chan struct{}),
}
}
// Start starts to find new peripheral events
func (h *HardwareManager) Start(ctx context.Context) error {
for finderName, finder := range h.drivers {
err := finder.Initialize()
if err != nil {
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to initialize finder")
return err
}
err = finder.Start(ctx)
if err != nil {
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to start finder")
return err
}
}
// n, err := detector.Register()
// if err != nil {
// log.Err(err).Str("file", "hardware").Msg("error registering the usb event")
// }
// h.detector = n
// // Run the detector
// n.Run(ctx)
// h.goWait.Add(1)
// go func() {
// defer h.goWait.Done()
// for {
// select {
// case <-ctx.Done():
// return
// case <-h.detector.EventChannel:
// // Trigger hardware scans
// log.Info().Str("file", "hardware").Msg("peripheral change event")
// case <-h.peripheralsScanTrigger:
// log.Info().Str("file", "hardware").Msg("scan triggered")
// }
// }
// }()
return nil
}
// GetDriver returns a register driver
func (h *HardwareManager) GetDriver(driverName string) (PeripheralFinder, error) {
driver, exists := h.drivers[driverName]
if !exists {
log.Error().Str("file", "hardware").Str("driverName", driverName).Msg("unable to get the driver")
return nil, fmt.Errorf("Unable to locate the '%s' driver", driverName)
}
log.Debug().Str("file", "hardware").Str("driverName", driverName).Msg("got driver")
return driver, nil
}
// RegisterDriver registers a new peripherals driver
func (h *HardwareManager) RegisterDriver(driver PeripheralFinder) {
h.drivers[driver.GetName()] = driver
log.Info().Str("file", "hardware").Str("driverName", driver.GetName()).Msg("driver registered")
}
// GetPeripheral gets the peripheral object from the parent driver
func (h *HardwareManager) GetPeripheral(driverName string, peripheralID string) (Peripheral, bool) {
// Get the driver
parentDriver, found := h.drivers[driverName]
// If no driver found, return false
if !found {
log.Error().Str("file", "hardware").Str("driverName", driverName).Msg("unable to get the driver")
return nil, false
}
log.Trace().Str("file", "hardware").Str("driverName", parentDriver.GetName()).Msg("driver got")
// Contact the driver to get the device
return parentDriver.GetPeripheral(peripheralID)
}
// Scan scans all the peripherals for the registered finders
func (h *HardwareManager) Scan() error {
h.peripheralsScanTrigger <- struct{}{}
return nil
}
func (h *HardwareManager) wndProc(hwnd windows.HWND, msg uint32, wParam, lParam uintptr) uintptr {
log.Trace().Str("file", "hardware").Uint32("msg", msg).Msg("wndProc triggered")
if msg == win.WM_DEVICECHANGE {
// Trigger the devices scan when the last DEVICE_CHANGE event is received
if debounceTimer != nil {
debounceTimer.Stop()
log.Debug().Str("file", "hardware").Msg("scan debounce timer stopped")
}
debounceTimer = time.AfterFunc(debounceDuration, func() {
log.Debug().Str("file", "hardware").Msg("peripheral changed")
h.peripheralsScanTrigger <- struct{}{}
})
}
return win.DefWindowProc(win.HWND(hwnd), msg, wParam, lParam)
}
// 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.drivers {
err := finder.Stop()
if err != nil {
log.Err(err).Str("file", "hardware").Str("finderName", finderName).Msg("unable to stop the finder")
}
}
// Wait for goroutines to finish
h.goWait.Wait()
log.Info().Str("file", "hardware").Msg("hardware manager stopped")
return nil
}
// peripheralsList emits a peripheral event
func emitPeripheralsEvents(ctx context.Context, peripheralsList map[string]Peripheral, peripheralEvent PeripheralEvent) {
for _, peripheral := range peripheralsList {
runtime.EventsEmit(ctx, string(peripheralEvent), peripheral.GetInfo())
log.Trace().Str("file", "hardware").Str("event", string(peripheralEvent)).Msg("emit peripheral event")
}
}
// comparePeripherals compares the peripherals to determine which has been inserted or removed
func comparePeripherals(oldPeripherals map[string]Peripheral, newPeripherals map[string]Peripheral) (map[string]Peripheral, map[string]Peripheral) {
// Duplicate the lists
oldList := make(map[string]Peripheral)
newList := make(map[string]Peripheral)
for key, value := range oldPeripherals {
oldList[key] = value
}
log.Trace().Str("file", "hardware").Any("oldList", oldList).Msg("peripheral oldList comparison")
for key, value := range newPeripherals {
newList[key] = value
}
log.Trace().Str("file", "hardware").Any("newList", newList).Msg("peripheral newList comparison")
// Remove in these lists all the commons peripherals
for key := range newList {
if _, exists := oldList[key]; exists {
delete(oldList, key)
delete(newList, key)
}
}
// Now the old list contains the removed peripherals, and the new list contains the added peripherals
log.Trace().Str("file", "hardware").Any("oldList", oldList).Msg("peripheral oldList computed")
log.Trace().Str("file", "hardware").Any("newList", newList).Msg("peripheral newList computed")
return oldList, newList
}

34
hardware/interfaces.go Normal file
View File

@@ -0,0 +1,34 @@
package hardware
import "context"
// Peripheral represents the methods used to manage a peripheral (input or output hardware)
type Peripheral interface {
Connect(context.Context) error // Connect the peripheral
Disconnect(context.Context) error // Disconnect the peripheral
Activate(context.Context) error // Activate the peripheral
Deactivate(context.Context) error // Deactivate the peripheral
SetDeviceProperty(context.Context, uint32, uint32, byte) error // Update a device property
GetInfo() PeripheralInfo // Get the peripheral information
}
// PeripheralInfo represents a peripheral information
type PeripheralInfo struct {
Name string `yaml:"name"` // Name of the peripheral
SerialNumber string `yaml:"sn"` // S/N of the peripheral
ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral
Settings []interface{} `yaml:"settings"` // Number of DMX universes handled by the peripheral
}
// PeripheralFinder represents how compatible peripheral drivers are implemented
type PeripheralFinder interface {
Initialize() error // Initializes the protocol
Start(context.Context) error // Start the detection
Stop() error // Stop the detection
ForceScan() // Explicitly scans for peripherals
CreatePeripheral(ctx context.Context) (Peripheral, error) // Creates a new peripheral
DeletePeripheral(serialNumber string) error // Removes a peripheral
GetName() string // Get the name of the finder
GetPeripheral(string) (Peripheral, bool) // Get the peripheral
}

View File

@@ -0,0 +1,34 @@
#include <iostream>
#include <vector>
#include <thread>
#include "ftd2xx.h"
int main() {
FT_STATUS ftStatus;
FT_HANDLE ftHandle = nullptr;
FT_DEVICE_LIST_INFO_NODE *devInfo;
DWORD numDevs;
// create the device information list
ftStatus = FT_CreateDeviceInfoList(&numDevs);
if (ftStatus != FT_OK) {
std::cerr << "Unable to get the FTDI devices : create list error" << std::endl;
return 1;
}
if (numDevs > 0) {
// allocate storage for list based on numDevs
devInfo =
(FT_DEVICE_LIST_INFO_NODE*)malloc(sizeof(FT_DEVICE_LIST_INFO_NODE)*numDevs);
// get the device information list
ftStatus = FT_GetDeviceInfoList(devInfo, &numDevs);
if (ftStatus != FT_OK) {
std::cerr << "Unable to get the FTDI devices : get list error" << std::endl;
return 1;
}
for (int i = 0; i < numDevs; i++) {
if (devInfo[i].SerialNumber[0] != '\0') {
std::cout << i << ":" << devInfo[i].SerialNumber << ":" << devInfo[i].Description << std::endl;
}
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="DMXSender" type="win32"/>
<description>Detect FTDI</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

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

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

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="DMXSender" type="win32"/>
<description>DMXSender</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@@ -0,0 +1,8 @@
windres dmxSender.rc dmxSender.o
windres detectFTDI.rc detectFTDI.o
g++ -o dmxSender.exe dmxSender.cpp dmxSender.o -I"include" -L"lib" -lftd2xx -mwindows
g++ -o detectFTDI.exe detectFTDI.cpp detectFTDI.o -I"include" -L"lib" -lftd2xx -mwindows
@REM g++ -o dmxSender.exe dmxSender.cpp -I"include" -L"lib" -lftd2xx
@REM g++ -o detectFTDI.exe detectFTDI.cpp -I"include" -L"lib" -lftd2xx

1667
hardware/third-party/ftdi/include/ftd2xx.h vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
hardware/third-party/ftdi/lib/ftd2xx.lib vendored Normal file

Binary file not shown.

25
main.go
View File

@@ -2,8 +2,14 @@ package main
import (
"embed"
"time"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
@@ -12,6 +18,16 @@ import (
var assets embed.FS
func main() {
// Configure the logger
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: "2006-01-02 15:04:05",
})
zerolog.TimestampFunc = func() time.Time {
return time.Now().Local()
}
zerolog.SetGlobalLevel(zerolog.TraceLevel)
// Create an instance of the app structure
app := NewApp()
@@ -24,13 +40,16 @@ func main() {
AssetServer: &assetserver.Options{
Assets: assets,
},
OnStartup: app.startup,
OnStartup: app.onStartup,
OnDomReady: app.onReady,
OnShutdown: app.onShutdown,
Bind: []interface{}{
app,
},
LogLevel: logger.ERROR,
})
if err != nil {
println("Error:", err.Error())
log.Err(err).Str("file", "main").Msg("unable to start the application")
}
}
}

1
package.json Normal file
View File

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

123
peripherals.go Normal file
View File

@@ -0,0 +1,123 @@
package main
import (
"dmxconnect/hardware"
"fmt"
"github.com/rs/zerolog/log"
)
// AddPeripheral adds a peripheral to the project
func (a *App) AddPeripheral(protocolName string, peripheralID string) error {
// Get the device from its finder
p, found := a.hardwareManager.GetPeripheral(protocolName, peripheralID)
if !found {
log.Error().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("unable to found the specified peripheral")
return fmt.Errorf("unable to found the peripheral ID '%s'", peripheralID)
}
// Add the peripheral ID to the project
a.projectInfo.PeripheralsInfo[peripheralID] = p.GetInfo()
log.Info().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("peripheral added to project")
// TODO: Connect the peripheral
return nil
}
// RemovePeripheral adds a peripheral to the project
func (a *App) RemovePeripheral(protocolName string, peripheralID string) error {
// TODO: Disconnect the peripheral
// Remove the peripheral ID from the project
delete(a.projectInfo.PeripheralsInfo, peripheralID)
log.Info().Str("file", "peripheral").Str("protocolName", protocolName).Str("periphID", peripheralID).Msg("peripheral removed from project")
return nil
}
// AddOS2LPeripheral adds a new OS2L peripheral
func (a *App) AddOS2LPeripheral() (hardware.PeripheralInfo, error) {
// Get the OS2L driver
os2lDriver, err := a.hardwareManager.GetDriver("OS2L")
if err != nil {
log.Err(err).Str("file", "peripheral").Msg("unable to found the OS2L driver")
return hardware.PeripheralInfo{}, err
}
log.Trace().Str("file", "peripheral").Msg("OS2L driver got")
// Create a new OS2L peripheral with this driver
os2lPeripheral, err := os2lDriver.CreatePeripheral(a.ctx)
if err != nil {
log.Err(err).Str("file", "peripheral").Msg("unable to create the OS2L peripheral")
return hardware.PeripheralInfo{}, err
}
os2lInfo := os2lPeripheral.GetInfo()
log.Info().Str("file", "peripheral").Str("s/n", os2lInfo.SerialNumber).Msg("OS2L peripheral created, adding to project")
// Add this new peripheral to the project
return os2lInfo, a.AddPeripheral(os2lDriver.GetName(), os2lInfo.SerialNumber)
}
// FOR TESTING PURPOSE ONLY
func (a *App) ConnectFTDI() error {
// Connect the FTDI
driver, err := a.hardwareManager.GetDriver("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.GetDriver("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.GetDriver("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.GetDriver("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.GetDriver("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)
}

191
project.go Normal file
View File

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