generated from thinkode/modelRepository
Compare commits
7 Commits
develop
...
037735fb85
| Author | SHA1 | Date | |
|---|---|---|---|
| 037735fb85 | |||
| 7cf222c4f9 | |||
| 7832d744b7 | |||
| 2496d49634 | |||
| 9964ccef7e | |||
| 17b5d39fc4 | |||
| 4690f771fa |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
63
.vscode/settings.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
147
app.go
147
app.go
@@ -1,151 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"changeme/hardware"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"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
|
||||
hardwareManager *hardware.HardwareManager // For managing all the hardware
|
||||
wmiMutex sync.Mutex // Avoid some WMI operations at the same time
|
||||
projectInfo ProjectInfo // The project information structure
|
||||
projectSave string // The file name of the project
|
||||
// FOR TESTING PURPOSE ONLY
|
||||
ftdi *hardware.FTDIPeripheral
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
// Create a new hadware manager
|
||||
hardwareManager := hardware.NewHardwareManager()
|
||||
hardwareManager.RegisterDriver(hardware.NewMIDIDriver())
|
||||
hardwareManager.RegisterDriver(hardware.NewFTDIDriver())
|
||||
hardwareManager.RegisterDriver(hardware.NewOS2LDriver())
|
||||
return &App{
|
||||
hardwareManager: hardwareManager,
|
||||
projectSave: "",
|
||||
projectInfo: ProjectInfo{
|
||||
PeripheralsInfo: make(map[string]hardware.PeripheralInfo),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// GetProjects gets all the projects in the projects directory
|
||||
func (a *App) GetProjects() ([]ProjectInfo, error) {
|
||||
projects := []ProjectInfo{}
|
||||
|
||||
f, err := os.Open(projectsDirectory)
|
||||
err := a.hardwareManager.Start(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to open the projects directory: %v", err)
|
||||
return nil, err
|
||||
log.Fatalf("Unable to start the device manager: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to read the project file: %v", err)
|
||||
return ShowInfo{}, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 |
@@ -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="*"/>
|
||||
|
||||
@@ -9,9 +9,52 @@
|
||||
import Show from './components/Show/Show.svelte';
|
||||
import GeneralConsole from './components/Console/GeneralConsole.svelte';
|
||||
import RoundIconButton from './components/General/RoundIconButton.svelte';
|
||||
import { currentProject, needProjectSave } from './stores';
|
||||
import { showInformation, needProjectSave, peripherals } from './stores';
|
||||
import { SaveProject } from '../wailsjs/go/main/App.js';
|
||||
import { construct_svelte_component } from 'svelte/internal';
|
||||
import { construct_svelte_component } from 'svelte/internal';
|
||||
import { EventsOn } from '../wailsjs/runtime'
|
||||
import { CreateProject, GetPeripherals } from "../wailsjs/go/main/App";
|
||||
import { WindowSetTitle } from "../wailsjs/runtime/runtime"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
// Handle the event when a new peripheral is detected
|
||||
EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){
|
||||
console.log("Hardware has been added to the system");
|
||||
// When a new peripheral is detected, add it to the map and:
|
||||
// - Pass the isDetected key to true
|
||||
// - Set the isSaved key to the last value
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
|
||||
peripheralInfo.isDetected = true
|
||||
peripheralInfo.isSaved = (lastSavedProperty === true) ? true : false
|
||||
peripherals.update((peripherals) => {
|
||||
peripherals[peripheralInfo.SerialNumber] = peripheralInfo
|
||||
return {...peripherals}
|
||||
})
|
||||
})
|
||||
|
||||
// Handle the event when a peripheral is removed from the system
|
||||
EventsOn('PERIPHERAL_REMOVAL', function(peripheralInfo){
|
||||
console.log("Hardware has been removed from the system");
|
||||
// When a peripheral is disconnected, pass its isDetected key to false
|
||||
// If the isSaved key is set to false, we can completely remove the peripheral from the list
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastSavedProperty = peripheralsList[peripheralInfo.SerialNumber]?.isSaved
|
||||
let needToDelete = (lastSavedProperty !== true) ? true : false
|
||||
peripherals.update((storedPeripherals) => {
|
||||
if (needToDelete){
|
||||
delete storedPeripherals[peripheralInfo.SerialNumber];
|
||||
return { ...storedPeripherals };
|
||||
}
|
||||
storedPeripherals[peripheralInfo.SerialNumber].isDetected = false
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
})
|
||||
|
||||
// Set the window title
|
||||
$: {
|
||||
WindowSetTitle("DMXConnect - " + $showInformation.Name + ($needProjectSave ? " (unsaved)" : ""))
|
||||
}
|
||||
|
||||
let selectedMenu = "settings"
|
||||
// When the navigation menu changed, update the selected menu
|
||||
@@ -21,33 +64,33 @@
|
||||
|
||||
// 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)
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to save the project: ${error}`)
|
||||
})
|
||||
}
|
||||
|
||||
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: "",
|
||||
// Request the list of peripherals
|
||||
// TODO: Handle the error here
|
||||
GetPeripherals()
|
||||
|
||||
// Handle window shortcuts
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Check the CTRL+S keys
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
// Avoid the natural behaviour
|
||||
event.preventDefault();
|
||||
// Save the current project
|
||||
saveProject()
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -55,7 +98,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>
|
||||
|
||||
@@ -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]}
|
||||
|
||||
63
frontend/src/components/General/InfoButton.svelte
Normal file
63
frontend/src/components/General/InfoButton.svelte
Normal 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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{
|
||||
|
||||
91
frontend/src/components/Settings/DeviceCard.svelte
Normal file
91
frontend/src/components/Settings/DeviceCard.svelte
Normal 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>
|
||||
@@ -1,9 +1,178 @@
|
||||
<script lang=ts>
|
||||
import DeviceCard from "./DeviceCard.svelte";
|
||||
import Tab from "../General/Tab.svelte";
|
||||
import { _ } from 'svelte-i18n'
|
||||
import { needProjectSave, peripherals } from "../../stores";
|
||||
import { get } from "svelte/store"
|
||||
import { AddOS2LPeripheral, RemovePeripheral, ConnectFTDI, ActivateFTDI, DeactivateFTDI, DisconnectFTDI, SetDeviceFTDI, AddPeripheral } from "../../../wailsjs/go/main/App";
|
||||
import RoundedButton from "../General/RoundedButton.svelte";
|
||||
|
||||
function ftdiConnect(){
|
||||
ConnectFTDI().then(() =>
|
||||
console.log("FTDI connected"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to connect: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
function ftdiActivate(){
|
||||
ActivateFTDI().then(() =>
|
||||
console.log("FTDI activated"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to activate: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
function ftdiDeactivate(){
|
||||
DeactivateFTDI().then(() =>
|
||||
console.log("FTDI deactivated"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to deactivate: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
let sliderValue = 0
|
||||
function ftdiSetDevice(value){
|
||||
console.log("value is " + value)
|
||||
SetDeviceFTDI(value).then(() =>
|
||||
console.log("FTDI device set up"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to set the device: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
function ftdiDisconnect(){
|
||||
DisconnectFTDI().then(() =>
|
||||
console.log("FTDI disconnected"))
|
||||
.catch((error) => {
|
||||
console.log("Error when trying to disconnect: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
// Add the peripheral to the project
|
||||
function addPeripheral(peripheral){
|
||||
// Add the peripheral to the project (backend)
|
||||
AddPeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
|
||||
peripherals.update((value) => {
|
||||
if (value[peripheral.SerialNumber]) {
|
||||
value[peripheral.SerialNumber].isSaved = true;
|
||||
}
|
||||
return {...value}
|
||||
})
|
||||
$needProjectSave = true
|
||||
}).catch((error) => {
|
||||
console.log("Unable to add the peripheral to the project: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the peripheral from the project
|
||||
function removePeripheral(peripheral) {
|
||||
// Delete the peripheral from the project (backend)
|
||||
RemovePeripheral(peripheral.ProtocolName, peripheral.SerialNumber).then(() => {
|
||||
// If the peripheral is not detected, we can delete it form the store
|
||||
// If not, we only pass the isSaved key to false
|
||||
let peripheralsList = get(peripherals)
|
||||
let lastDetectedProperty = peripheralsList[peripheral.SerialNumber]?.isDetected
|
||||
let needToDelete = (lastDetectedProperty !== true) ? true : false
|
||||
peripherals.update((storedPeripherals) => {
|
||||
if (needToDelete){
|
||||
delete storedPeripherals[peripheral.SerialNumber];
|
||||
return { ...storedPeripherals };
|
||||
}
|
||||
storedPeripherals[peripheral.SerialNumber].isSaved = false
|
||||
return { ...storedPeripherals };
|
||||
})
|
||||
$needProjectSave = true
|
||||
}).catch((error) => {
|
||||
console.log("Unable to remove the peripheral from the project: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
// Create the OS2L peripheral
|
||||
function createOS2L(){
|
||||
AddOS2LPeripheral().then(os2lDevice => {
|
||||
peripherals.update(currentPeriph => {
|
||||
os2lDevice.isSaved = true
|
||||
os2lDevice.isDetected = true
|
||||
currentPeriph[os2lDevice.SerialNumber] = os2lDevice
|
||||
return {...currentPeriph}
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log("Unable to add the OS2L peripheral: " + error)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<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={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} addable={!peripheral.isSaved}/>
|
||||
{/if}
|
||||
{/each}
|
||||
<p style="color: var(--first-color);"><i class='bx bxs-network-chart' ></i> Others</p>
|
||||
<RoundedButton on:click={createOS2L} text="Add an OS2L peripheral" icon="bx-plus-circle" tooltip="Configure an OS2L connection"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0.5em; flex:2; width:100%;">
|
||||
<p style="margin-bottom: 1em;">Project peripherals</p>
|
||||
<div class="configuredHardware">
|
||||
{#each Object.entries($peripherals) as [serialNumber, peripheral]}
|
||||
{#if peripheral.isSaved}
|
||||
<DeviceCard on:delete={() => removePeripheral(peripheral)} on:dblclick={() => removePeripheral(peripheral)}
|
||||
disconnected={!peripheral.isDetected} title={peripheral.Name == "" ? "Please wait..." : peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} removable signalizable/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<p style="margin-bottom: 1em;">Peripheral settings</p>
|
||||
<div>
|
||||
<p><i>Select a peripheral to edit its settings</i></p>
|
||||
<button on:click={ftdiConnect}>Connect FTDI 0</button>
|
||||
<button on:click={ftdiActivate}>Activate FTDI 0</button>
|
||||
<div class="slidecontainer">
|
||||
<input type="range" min="0" max="255" class="slider" bind:value={sliderValue} on:input={() => ftdiSetDevice(sliderValue)}>
|
||||
</div>
|
||||
<button on:click={ftdiDeactivate}>Deactivate FTDI 0</button>
|
||||
<button on:click={ftdiDisconnect}>Disconnect FTDI 0</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
.hardware {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.availableHardware {
|
||||
background-color: var(--second-color);
|
||||
border-radius: 0.5em;
|
||||
padding: 0.2em;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
/* overflow: visible; */
|
||||
}
|
||||
.availableHardware::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.configuredHardware {
|
||||
background-color: var(--second-color);
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em;
|
||||
padding: 0.2em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -1,27 +1,26 @@
|
||||
<script lang=ts>
|
||||
import { projectsList, currentProject, needProjectSave } from '../../stores.js';
|
||||
import { projectsList, showInformation, needProjectSave, peripherals } from '../../stores.js';
|
||||
import RoundedButton from "../General/RoundedButton.svelte";
|
||||
import ProjectPropertiesContent from "./ProjectPropertiesContent.svelte";
|
||||
import DropdownList from "../General/DropdownList.svelte";
|
||||
import InputsOutputsContent from "./InputsOutputsContent.svelte";
|
||||
import Tab from "../General/Tab.svelte";
|
||||
import { GetProjects, GetProjectInfo } from "../../../wailsjs/go/main/App";
|
||||
import { CreateProject, GetProjects, GetProjectInfo } from "../../../wailsjs/go/main/App";
|
||||
import { _ } from 'svelte-i18n'
|
||||
import {colors} from '../../stores.js';
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const tabs = [
|
||||
{ title: $_("projectPropertiesTab"), icon: 'bxs-info-circle', tooltip: $_("projectPropertiesTooltip"), component: ProjectPropertiesContent },
|
||||
{ title: $_("projectInputOutputTab"), icon: 'bxs-plug', tooltip: $_("projectInputOutputTooltip"), component: InputsOutputsContent },
|
||||
];
|
||||
|
||||
|
||||
// Refresh the projects list
|
||||
let choices = new Map()
|
||||
function loadProjectsList(){
|
||||
GetProjects().then((projects) => {
|
||||
choices = new Map(projects.map(item => [item.ShowInfo.SaveFile, item.ShowInfo.Name]));
|
||||
$projectsList = projects
|
||||
console.log(projectsList)
|
||||
choices = new Map(projects.map(item => [item.Save, item.Name]));
|
||||
$projectsList = projects
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to get the projects list: ${error}`)
|
||||
})
|
||||
@@ -31,8 +30,26 @@
|
||||
let selectedOption = event.detail.key
|
||||
// Open the selected project
|
||||
GetProjectInfo(selectedOption).then((projectInfo) => {
|
||||
console.log("Project opened")
|
||||
$currentProject = projectInfo
|
||||
$showInformation = projectInfo.ShowInfo
|
||||
peripherals.update((storedPeripherals) => {
|
||||
// Set all the isSaved keys to false and delete the disconnected peripherals
|
||||
for (let peripheralID in storedPeripherals) {
|
||||
storedPeripherals[peripheralID].isSaved = false
|
||||
if (!storedPeripherals[peripheralID].isDetected) {
|
||||
delete storedPeripherals[peripheralID]
|
||||
}
|
||||
}
|
||||
// Add the saved peripherals of the project
|
||||
// If already exists pass the isSaved key to true, if not create the peripheral and set it to disconnected
|
||||
for (let peripheralID in projectInfo.PeripheralsInfo){
|
||||
// Add the peripheral to the list of peripherals, with the last isDetected key and the isSaved key to true
|
||||
let lastDetectedKey = storedPeripherals[peripheralID]?.isDetected
|
||||
storedPeripherals[peripheralID] = projectInfo.PeripheralsInfo[peripheralID]
|
||||
storedPeripherals[peripheralID].isDetected = (lastDetectedKey === true) ? true : false
|
||||
storedPeripherals[peripheralID].isSaved = true
|
||||
}
|
||||
return {...storedPeripherals}
|
||||
})
|
||||
needProjectSave.set(false)
|
||||
}).catch((error) => {
|
||||
console.error(`Unable to open the project: ${error}`)
|
||||
@@ -40,24 +57,11 @@
|
||||
}
|
||||
|
||||
function initializeNewProject(){
|
||||
currentProject.set({
|
||||
Name: "My new show",
|
||||
Date: formatDate(new Date()),
|
||||
Avatar: "appicon.png",
|
||||
UniversesNumber: 1,
|
||||
Comments: "Write your comments here",
|
||||
SaveFile: "",
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
$needProjectSave = true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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({})
|
||||
|
||||
// Application colors
|
||||
export const colors = writable({
|
||||
@@ -23,3 +23,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({})
|
||||
@@ -16,6 +16,8 @@ html, body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
4
go.mod
4
go.mod
@@ -5,7 +5,10 @@ go 1.21
|
||||
toolchain go1.21.3
|
||||
|
||||
require (
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e
|
||||
github.com/mattrtaylor/go-rtmidi v0.0.0-20220428034745-af795b1c1a79
|
||||
github.com/wailsapp/wails/v2 v2.9.1
|
||||
golang.org/x/sys v0.20.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@@ -35,7 +38,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
|
||||
)
|
||||
|
||||
|
||||
5
go.sum
5
go.sum
@@ -26,6 +26,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 +37,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=
|
||||
@@ -72,6 +76,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=
|
||||
|
||||
140
hardware/FTDIDriver.go
Normal file
140
hardware/FTDIDriver.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
goRuntime "runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
scanDelay = 4 * time.Second // Waiting delay before scanning the FTDI devices
|
||||
)
|
||||
|
||||
// FTDIDriver represents how the protocol is defined
|
||||
type FTDIDriver struct {
|
||||
peripherals map[string]Peripheral
|
||||
}
|
||||
|
||||
// NewFTDIDriver creates a new FTDI finder
|
||||
func NewFTDIDriver() *FTDIDriver {
|
||||
return &FTDIDriver{
|
||||
peripherals: make(map[string]Peripheral),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the FTDI driver
|
||||
func (d *FTDIDriver) Initialize() error {
|
||||
if goRuntime.GOOS != "windows" {
|
||||
return fmt.Errorf("<!> The FTDI driver is not compatible with your platform yet (%s)", goRuntime.GOOS)
|
||||
}
|
||||
fmt.Println("FTDI driver initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (d *FTDIDriver) GetName() string {
|
||||
return "FTDI"
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral that correspond to the specified ID
|
||||
func (d *FTDIDriver) GetPeripheral(peripheralID string) (Peripheral, bool) {
|
||||
// Return the specified peripheral
|
||||
peripheral := d.peripherals[peripheralID]
|
||||
if peripheral == nil {
|
||||
fmt.Println("Unable to get the peripheral with the driver")
|
||||
return nil, false
|
||||
}
|
||||
fmt.Println("Peripheral found by the FTDI driver")
|
||||
|
||||
return peripheral, true
|
||||
}
|
||||
|
||||
//go:embed third-party/ftdi/detectFTDI.exe
|
||||
var findFTDI []byte
|
||||
|
||||
// Scan scans the FTDI peripherals
|
||||
func (d *FTDIDriver) Scan(ctx context.Context) error {
|
||||
time.Sleep(scanDelay)
|
||||
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp("", "findFTDI*.exe")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write the embedded executable to the temp file
|
||||
if _, err := tempFile.Write(findFTDI); err != nil {
|
||||
return err
|
||||
}
|
||||
tempFile.Close()
|
||||
|
||||
ftdiPeripherals := make(map[string]Peripheral)
|
||||
|
||||
finder := exec.Command(tempFile.Name())
|
||||
|
||||
stdout, err := finder.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create the stdout pipe: %s", err)
|
||||
}
|
||||
|
||||
stderr, err := finder.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create the stderr pipe: %s", err)
|
||||
}
|
||||
|
||||
err = finder.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find FTDI devices: %s", err)
|
||||
}
|
||||
|
||||
scannerErr := bufio.NewScanner(stderr)
|
||||
for scannerErr.Scan() {
|
||||
return fmt.Errorf("Unable to find FTDI devices: %s", scannerErr.Text())
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
deviceString := scanner.Text()
|
||||
// The program output is like '0:1:2' where 0 is the location, 1 is the S/N and 2 is the name
|
||||
deviceInfo := strings.Split(deviceString, ":")
|
||||
|
||||
fmt.Println("FTDI device: " + deviceString)
|
||||
// Convert the location to an integer
|
||||
location, err := strconv.Atoi(deviceInfo[0])
|
||||
if err != nil {
|
||||
location = -1
|
||||
}
|
||||
// Add the peripheral to the temporary list
|
||||
peripheral, err := NewFTDIPeripheral(deviceInfo[2], deviceInfo[1], location)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create the FTDI peripheral: %v", err)
|
||||
}
|
||||
ftdiPeripherals[deviceInfo[1]] = peripheral
|
||||
}
|
||||
// Compare with the current peripherals to detect arrivals/removals
|
||||
removedList, addedList := comparePeripherals(d.peripherals, ftdiPeripherals)
|
||||
// Emit the events
|
||||
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
|
||||
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
|
||||
// Store the new peripherals list
|
||||
d.peripherals = ftdiPeripherals
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePeripheral is not implemented here
|
||||
func (d *FTDIDriver) CreatePeripheral(context.Context) (Peripheral, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RemovePeripheral is not implemented here
|
||||
func (d *FTDIDriver) RemovePeripheral(serialNumber string) error {
|
||||
return nil
|
||||
}
|
||||
201
hardware/FTDIPeripheral.go
Normal file
201
hardware/FTDIPeripheral.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
activateCommandString = 0x01
|
||||
deactivateCommandString = 0x02
|
||||
setCommandString = 0x03
|
||||
)
|
||||
|
||||
//go:embed third-party/ftdi/dmxSender.exe
|
||||
var dmxSender []byte
|
||||
|
||||
// FTDIPeripheral contains the data of an FTDI peripheral
|
||||
type FTDIPeripheral struct {
|
||||
name string // The name of the peripheral
|
||||
serialNumber string // The S/N of the FTDI peripheral
|
||||
location int // The location of the peripheral
|
||||
universesNumber int // The number of DMX universes handled by this peripheral
|
||||
programName string // The temp file name of the executable
|
||||
dmxSender *exec.Cmd // The command to pilot the DMX sender program
|
||||
stdin io.WriteCloser // For writing in the DMX sender
|
||||
stdout io.ReadCloser // For reading from the DMX sender
|
||||
stderr io.ReadCloser // For reading the errors
|
||||
disconnectChan chan struct{} // Channel to cancel the connection
|
||||
errorsChan chan error // Channel to get the errors
|
||||
wg sync.WaitGroup // Tasks management
|
||||
}
|
||||
|
||||
// NewFTDIPeripheral creates a new FTDI peripheral
|
||||
func NewFTDIPeripheral(name string, serialNumber string, location int) (*FTDIPeripheral, error) {
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp("", "dmxSender*.exe")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the embedded executable to the temp file
|
||||
if _, err := tempFile.Write(dmxSender); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tempFile.Close()
|
||||
|
||||
return &FTDIPeripheral{
|
||||
name: name,
|
||||
programName: tempFile.Name(),
|
||||
serialNumber: serialNumber,
|
||||
location: location,
|
||||
universesNumber: 1,
|
||||
disconnectChan: make(chan struct{}),
|
||||
errorsChan: make(chan error, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect connects the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Connect() error {
|
||||
// Connect if no connection is already running
|
||||
fmt.Println(p.dmxSender)
|
||||
if p.dmxSender == nil {
|
||||
// Executing the command
|
||||
p.dmxSender = exec.Command(p.programName, fmt.Sprintf("%d", p.location))
|
||||
|
||||
var err error
|
||||
p.stdout, err = p.dmxSender.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create the stdout pipe: %v", err)
|
||||
}
|
||||
|
||||
p.stdin, err = p.dmxSender.StdinPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create the stdin pipe: %v", err)
|
||||
}
|
||||
|
||||
p.stderr, err = p.dmxSender.StderrPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create the stderr pipe: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(p.stderr)
|
||||
for scanner.Scan() {
|
||||
// Traitez chaque ligne lue depuis stderr
|
||||
fmt.Printf("Erreur : %s\n", scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("Error reading from stderr: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
p.wg.Add(1)
|
||||
go func() {
|
||||
defer p.wg.Done()
|
||||
err = p.dmxSender.Run()
|
||||
if err != nil {
|
||||
// Affiche l'erreur (cela inclut également les erreurs de retour du programme)
|
||||
fmt.Printf("Erreur lors de l'exécution : %v\n", err)
|
||||
|
||||
// Vérifie si l'erreur est liée au code de retour
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
// Récupère et affiche le code de retour
|
||||
fmt.Printf("Le programme s'est terminé avec le code : %d\n", exitError.ExitCode())
|
||||
fmt.Println(p.stderr)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Le programme s'est terminé avec succès.")
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Disconnect() error {
|
||||
if p.dmxSender != nil {
|
||||
_, err := io.WriteString(p.stdin, string([]byte{0x04, 0x00, 0x00, 0x00}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to disconnect: %v", err)
|
||||
}
|
||||
p.stdin.Close()
|
||||
p.stdout.Close()
|
||||
p.dmxSender = nil
|
||||
err = os.Remove(p.programName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to delete the temporary file: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Unable to disconnect: not connected")
|
||||
// if p.dmxSender != nil {
|
||||
// err := p.dmxSender.Process.Kill()
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("Unable to disconnect: %v", err)
|
||||
// }
|
||||
// p.stdin.Close()
|
||||
// p.stdout.Close()
|
||||
// p.dmxSender = nil
|
||||
// err = os.Remove(p.programName)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("Unable to delete the temporary file: %v", err)
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
// return fmt.Errorf("Unable to disconnect: not connected")
|
||||
}
|
||||
|
||||
// Activate activates the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Activate() error {
|
||||
if p.dmxSender != nil {
|
||||
_, err := io.WriteString(p.stdin, string([]byte{0x01, 0x00, 0x00, 0x00}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to activate: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Unable to activate: not connected")
|
||||
}
|
||||
|
||||
// Deactivate deactivates the FTDI peripheral
|
||||
func (p *FTDIPeripheral) Deactivate() error {
|
||||
if p.dmxSender != nil {
|
||||
_, err := io.WriteString(p.stdin, string([]byte{0x02, 0x00, 0x00, 0x00}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to deactivate: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Unable to deactivate: not connected")
|
||||
}
|
||||
|
||||
// SetDeviceProperty sends a command to the specified device
|
||||
func (p *FTDIPeripheral) SetDeviceProperty(uint32, channelNumber uint32, channelValue byte) error {
|
||||
if p.dmxSender != nil {
|
||||
fmt.Println(channelValue)
|
||||
commandString := []byte{0x03, 0x01, 0x00, 0xff, 0x03, 0x02, 0x00, channelValue}
|
||||
_, err := io.WriteString(p.stdin, string(commandString))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to set device property: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Unable to set device property: not connected")
|
||||
}
|
||||
|
||||
// GetInfo gets all the peripheral information
|
||||
func (p *FTDIPeripheral) GetInfo() PeripheralInfo {
|
||||
return PeripheralInfo{
|
||||
Name: p.name,
|
||||
SerialNumber: p.serialNumber,
|
||||
ProtocolName: "FTDI",
|
||||
}
|
||||
}
|
||||
132
hardware/MIDIDriver.go
Normal file
132
hardware/MIDIDriver.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattrtaylor/go-rtmidi"
|
||||
)
|
||||
|
||||
/*
|
||||
DMXConnect
|
||||
DMXUSBProtocol.go
|
||||
|
||||
Copyright (c) Valentin Boulanger
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// MIDIDriver represents how the protocol is defined
|
||||
type MIDIDriver struct {
|
||||
peripherals map[string]Peripheral // The list of peripherals
|
||||
}
|
||||
|
||||
// NewMIDIDriver creates a new DMXUSB protocol
|
||||
func NewMIDIDriver() *MIDIDriver {
|
||||
return &MIDIDriver{
|
||||
peripherals: make(map[string]Peripheral),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the MIDI driver
|
||||
func (d *MIDIDriver) Initialize() error {
|
||||
fmt.Println("MIDI driver initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (d *MIDIDriver) GetName() string {
|
||||
return "MIDI"
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral that correspond to the specified ID
|
||||
func (d *MIDIDriver) GetPeripheral(peripheralID string) (Peripheral, bool) {
|
||||
// Return the specified peripheral
|
||||
peripheral := d.peripherals[peripheralID]
|
||||
if peripheral == nil {
|
||||
return nil, false
|
||||
}
|
||||
return peripheral, true
|
||||
}
|
||||
|
||||
func splitStringAndNumber(input string) (string, int, error) {
|
||||
// Regular expression to match the text part and the number at the end
|
||||
re := regexp.MustCompile(`^(.*?)(\d+)$`)
|
||||
matches := re.FindStringSubmatch(input)
|
||||
|
||||
// Check if the regex found both a text part and a number
|
||||
if len(matches) == 3 {
|
||||
// matches[1]: text part (might contain trailing spaces)
|
||||
// matches[2]: numeric part as a string
|
||||
textPart := strings.TrimSpace(matches[1]) // Remove any trailing spaces from the text
|
||||
numberPart, err := strconv.Atoi(matches[2])
|
||||
if err != nil {
|
||||
return "", 0, err // Return error if the number conversion fails
|
||||
}
|
||||
return textPart, numberPart, nil
|
||||
}
|
||||
|
||||
// Return an error if no trailing number is found
|
||||
return "", 0, fmt.Errorf("no number found at the end of the string")
|
||||
}
|
||||
|
||||
// Scan scans the interfaces compatible with the MIDI protocol
|
||||
func (d *MIDIDriver) Scan(ctx context.Context) error {
|
||||
midiPeripherals := make(map[string]Peripheral)
|
||||
fmt.Println("Opening MIDI scanner port...")
|
||||
midiScanner, err := rtmidi.NewMIDIInDefault()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error when opening the MIDI scanner: %s", err)
|
||||
}
|
||||
defer midiScanner.Close()
|
||||
fmt.Println("Scanning MIDI devices...")
|
||||
devicesCount, err := midiScanner.PortCount()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error when scanning MIDI devices: %s", err)
|
||||
}
|
||||
for i := 0; i < devicesCount; i++ {
|
||||
portName, err := midiScanner.PortName(i)
|
||||
if err != nil {
|
||||
portName = "Unknown device 0"
|
||||
}
|
||||
// Separate data
|
||||
name, location, err := splitStringAndNumber(portName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to get information from the string: %s", err)
|
||||
}
|
||||
fmt.Printf("New MIDI device found: %s on %i\n", name, location)
|
||||
// Add the peripheral to the temporary list
|
||||
midiPeripherals[portName] = NewMIDIPeripheral(name, location)
|
||||
}
|
||||
// Compare with the current peripherals to detect arrivals/removals
|
||||
removedList, addedList := comparePeripherals(d.peripherals, midiPeripherals)
|
||||
// Emit the events
|
||||
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
|
||||
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
|
||||
// Store the new peripherals list
|
||||
d.peripherals = midiPeripherals
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePeripheral is not implemented here
|
||||
func (d *MIDIDriver) CreatePeripheral(context.Context) (Peripheral, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RemovePeripheral is not implemented here
|
||||
func (d *MIDIDriver) RemovePeripheral(serialNumber string) error {
|
||||
return nil
|
||||
}
|
||||
48
hardware/MIDIPeripheral.go
Normal file
48
hardware/MIDIPeripheral.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package hardware
|
||||
|
||||
// MIDIPeripheral contains the data of a MIDI peripheral
|
||||
type MIDIPeripheral struct {
|
||||
name string // The name of the peripheral
|
||||
location int // The location of the peripheral
|
||||
}
|
||||
|
||||
// NewMIDIPeripheral creates a new MIDI peripheral
|
||||
func NewMIDIPeripheral(name string, location int) *MIDIPeripheral {
|
||||
return &MIDIPeripheral{
|
||||
name: name,
|
||||
location: location,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Connect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate activates the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Activate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the MIDI peripheral
|
||||
func (p *MIDIPeripheral) Deactivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty - not implemented for this kind of peripheral
|
||||
func (p *MIDIPeripheral) SetDeviceProperty(uint32, uint32, byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInfo gets the peripheral information
|
||||
func (p *MIDIPeripheral) GetInfo() PeripheralInfo {
|
||||
return PeripheralInfo{
|
||||
Name: p.name,
|
||||
ProtocolName: "MIDI",
|
||||
}
|
||||
}
|
||||
60
hardware/OS2LDriver.go
Normal file
60
hardware/OS2LDriver.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// OS2LDriver represents how the protocol is defined
|
||||
type OS2LDriver struct {
|
||||
peripherals map[string]Peripheral // The list of peripherals
|
||||
}
|
||||
|
||||
// NewOS2LDriver creates a new OS2L driver
|
||||
func NewOS2LDriver() *OS2LDriver {
|
||||
return &OS2LDriver{
|
||||
peripherals: make(map[string]Peripheral),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the MIDI driver
|
||||
func (d *OS2LDriver) Initialize() error {
|
||||
fmt.Println("OS2L driver initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePeripheral creates a new OS2L peripheral
|
||||
func (d *OS2LDriver) CreatePeripheral(ctx context.Context) (Peripheral, error) {
|
||||
// Create a random serial number for this peripheral
|
||||
randomSerialNumber := fmt.Sprintf("%08x", rand.Intn(1<<32))
|
||||
peripheral := NewOS2LPeripheral("OS2L", randomSerialNumber)
|
||||
d.peripherals[randomSerialNumber] = peripheral
|
||||
return peripheral, nil
|
||||
}
|
||||
|
||||
// RemovePeripheral removes an OS2L dev
|
||||
func (d *OS2LDriver) RemovePeripheral(serialNumber string) error {
|
||||
delete(d.peripherals, serialNumber)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the driver
|
||||
func (d *OS2LDriver) GetName() string {
|
||||
return "OS2L"
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral that correspond to the specified ID
|
||||
func (d *OS2LDriver) GetPeripheral(peripheralID string) (Peripheral, bool) {
|
||||
// Return the specified peripheral
|
||||
peripheral := d.peripherals[peripheralID]
|
||||
if peripheral == nil {
|
||||
return nil, false
|
||||
}
|
||||
return peripheral, true
|
||||
}
|
||||
|
||||
// Scan scans the interfaces compatible with the MIDI protocol
|
||||
func (d *OS2LDriver) Scan(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
55
hardware/OS2LPeripheral.go
Normal file
55
hardware/OS2LPeripheral.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package hardware
|
||||
|
||||
import "fmt"
|
||||
|
||||
// OS2LPeripheral contains the data of an OS2L peripheral
|
||||
type OS2LPeripheral struct {
|
||||
name string // The name of the peripheral
|
||||
serialNumber string // The serial number of the peripheral
|
||||
}
|
||||
|
||||
// NewOS2LPeripheral creates a new OS2L peripheral
|
||||
func NewOS2LPeripheral(name string, serialNumber string) *OS2LPeripheral {
|
||||
return &OS2LPeripheral{
|
||||
name: name,
|
||||
serialNumber: serialNumber,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect connects the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Connect() error {
|
||||
fmt.Println("OS2L peripheral connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Disconnect() error {
|
||||
fmt.Println("OS2L peripheral disconnected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate activates the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Activate() error {
|
||||
fmt.Println("OS2L peripheral activated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the MIDI peripheral
|
||||
func (p *OS2LPeripheral) Deactivate() error {
|
||||
fmt.Println("OS2L peripheral deactivated")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceProperty - not implemented for this kind of peripheral
|
||||
func (p *OS2LPeripheral) SetDeviceProperty(uint32, uint32, byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInfo gets the peripheral information
|
||||
func (p *OS2LPeripheral) GetInfo() PeripheralInfo {
|
||||
return PeripheralInfo{
|
||||
Name: p.name,
|
||||
SerialNumber: p.serialNumber,
|
||||
ProtocolName: "OS2L",
|
||||
}
|
||||
}
|
||||
212
hardware/hardware.go
Normal file
212
hardware/hardware.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/lxn/win"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// PeripheralEvent is trigger by the finders when the scan is complete
|
||||
type PeripheralEvent string
|
||||
|
||||
const (
|
||||
// PeripheralArrival is triggerd when a peripheral has been connected to the system
|
||||
PeripheralArrival PeripheralEvent = "PERIPHERAL_ARRIVAL"
|
||||
// PeripheralRemoval is triggered when a peripheral has been disconnected from the system
|
||||
PeripheralRemoval PeripheralEvent = "PERIPHERAL_REMOVAL"
|
||||
debounceDuration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
debounceTimer *time.Timer
|
||||
)
|
||||
|
||||
// HardwareManager is the class who manages the hardware
|
||||
type HardwareManager struct {
|
||||
drivers map[string]PeripheralDriver // The map of peripherals finders
|
||||
peripherals []Peripheral // The current list of peripherals
|
||||
deviceChangedEvent chan struct{} // The event when the devices list changed
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewHardwareManager creates a new HardwareManager
|
||||
func NewHardwareManager() *HardwareManager {
|
||||
return &HardwareManager{
|
||||
drivers: make(map[string]PeripheralDriver),
|
||||
peripherals: make([]Peripheral, 0),
|
||||
deviceChangedEvent: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts to finding new device events
|
||||
func (h *HardwareManager) Start(ctx context.Context) error {
|
||||
cb := windows.NewCallback(h.wndProc)
|
||||
|
||||
inst := win.GetModuleHandle(nil)
|
||||
cn, err := syscall.UTF16PtrFromString("DMXConnect device watcher")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert window class name to UTF16: %w", err)
|
||||
}
|
||||
wc := win.WNDCLASSEX{
|
||||
HInstance: inst,
|
||||
LpfnWndProc: cb,
|
||||
LpszClassName: cn,
|
||||
}
|
||||
wc.CbSize = uint32(unsafe.Sizeof(wc))
|
||||
if win.RegisterClassEx(&wc) == 0 {
|
||||
return fmt.Errorf("failed to register window class: %w", syscall.GetLastError())
|
||||
}
|
||||
wName, err := syscall.UTF16PtrFromString("usbevent.exe")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert window name to UTF16: %w", err)
|
||||
}
|
||||
wdw := win.CreateWindowEx(
|
||||
0,
|
||||
wc.LpszClassName,
|
||||
wName,
|
||||
win.WS_MINIMIZE|win.WS_OVERLAPPEDWINDOW,
|
||||
win.CW_USEDEFAULT,
|
||||
win.CW_USEDEFAULT,
|
||||
100,
|
||||
100,
|
||||
0,
|
||||
0,
|
||||
wc.HInstance,
|
||||
nil)
|
||||
if wdw == 0 {
|
||||
return fmt.Errorf("failed to create window: %w", syscall.GetLastError())
|
||||
}
|
||||
|
||||
_ = win.ShowWindow(wdw, win.SW_HIDE)
|
||||
win.UpdateWindow(wdw)
|
||||
|
||||
// To continuously get the devices events from Windows
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
var msg win.MSG
|
||||
got := win.GetMessage(&msg, win.HWND(windows.HWND(wdw)), 0, 0)
|
||||
if got == 0 {
|
||||
win.TranslateMessage(&msg)
|
||||
win.DispatchMessage(&msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// To handle the peripheral changed
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-h.deviceChangedEvent:
|
||||
fmt.Println("This is the list of devices")
|
||||
h.Scan(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDriver returns a register driver
|
||||
func (h *HardwareManager) GetDriver(driverName string) (PeripheralDriver, error) {
|
||||
driver, exists := h.drivers[driverName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Unable to locate the '%s' driver", driverName)
|
||||
}
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
// RegisterDriver registers a new peripherals driver
|
||||
func (h *HardwareManager) RegisterDriver(driver PeripheralDriver) {
|
||||
h.drivers[driver.GetName()] = driver
|
||||
fmt.Printf("Success registered the %s driver\n", driver.GetName())
|
||||
}
|
||||
|
||||
// GetPeripheral gets the peripheral object from the parent driver
|
||||
func (h *HardwareManager) GetPeripheral(driverName string, peripheralID string) (Peripheral, bool) {
|
||||
// Get the driver
|
||||
parentDriver := h.drivers[driverName]
|
||||
// If no driver found, return false
|
||||
if parentDriver == nil {
|
||||
fmt.Println("Unable to get the driver")
|
||||
return nil, false
|
||||
}
|
||||
// Contact the driver to get the device
|
||||
return parentDriver.GetPeripheral(peripheralID)
|
||||
}
|
||||
|
||||
// Scan scans all the peripherals for the registered finders
|
||||
func (h *HardwareManager) Scan(ctx context.Context) error {
|
||||
if len(h.drivers) == 0 {
|
||||
return fmt.Errorf("No peripherals driver registered")
|
||||
}
|
||||
for _, driver := range h.drivers {
|
||||
finder := driver
|
||||
go func() {
|
||||
err := driver.Scan(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to scan peripherals with the %s driver: %s\n", finder.GetName(), err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HardwareManager) wndProc(hwnd windows.HWND, msg uint32, wParam, lParam uintptr) uintptr {
|
||||
switch msg {
|
||||
case win.WM_DEVICECHANGE:
|
||||
// Trigger the devices scan when the last DEVICE_CHANGE event is received
|
||||
if debounceTimer != nil {
|
||||
debounceTimer.Stop()
|
||||
}
|
||||
debounceTimer = time.AfterFunc(debounceDuration, func() {
|
||||
fmt.Printf("Devices list has changed, refresh the devices list\n")
|
||||
h.deviceChangedEvent <- struct{}{}
|
||||
})
|
||||
}
|
||||
return win.DefWindowProc(win.HWND(hwnd), msg, wParam, lParam)
|
||||
}
|
||||
|
||||
// peripheralsList emits a peripheral event
|
||||
func emitPeripheralsEvents(ctx context.Context, peripheralsList map[string]Peripheral, peripheralEvent PeripheralEvent) {
|
||||
for _, peripheral := range peripheralsList {
|
||||
runtime.EventsEmit(ctx, string(peripheralEvent), peripheral.GetInfo())
|
||||
}
|
||||
}
|
||||
|
||||
// comparePeripherals compares the peripherals to determine which has been inserted or removed
|
||||
func comparePeripherals(oldPeripherals map[string]Peripheral, newPeripherals map[string]Peripheral) (map[string]Peripheral, map[string]Peripheral) {
|
||||
// Duplicate the lists
|
||||
oldList := make(map[string]Peripheral)
|
||||
newList := make(map[string]Peripheral)
|
||||
|
||||
for key, value := range oldPeripherals {
|
||||
oldList[key] = value
|
||||
}
|
||||
for key, value := range newPeripherals {
|
||||
newList[key] = value
|
||||
}
|
||||
// Remove in these lists all the commons peripherals
|
||||
for key := range newList {
|
||||
if _, exists := oldList[key]; exists {
|
||||
delete(oldList, key)
|
||||
delete(newList, key)
|
||||
}
|
||||
}
|
||||
// Now the old list contains the removed peripherals, and the new list contains the added peripherals
|
||||
fmt.Printf("%s\n", oldList)
|
||||
fmt.Printf("%s\n", newList)
|
||||
return oldList, newList
|
||||
}
|
||||
32
hardware/interfaces.go
Normal file
32
hardware/interfaces.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package hardware
|
||||
|
||||
import "context"
|
||||
|
||||
// Peripheral represents the methods used to manage a peripheral (input or output hardware)
|
||||
type Peripheral interface {
|
||||
Connect() error // Connect the peripheral
|
||||
Disconnect() error // Disconnect the peripheral
|
||||
Activate() error // Activate the peripheral
|
||||
Deactivate() error // Deactivate the peripheral
|
||||
SetDeviceProperty(uint32, uint32, byte) error // Update a device property
|
||||
|
||||
GetInfo() PeripheralInfo // Get the peripheral information
|
||||
}
|
||||
|
||||
// PeripheralInfo represents a peripheral information
|
||||
type PeripheralInfo struct {
|
||||
Name string `yaml:"name"` // Name of the peripheral
|
||||
SerialNumber string `yaml:"sn"` // S/N of the peripheral
|
||||
ProtocolName string `yaml:"protocol"` // Protocol name of the peripheral
|
||||
Settings []interface{} `yaml:"settings"` // Number of DMX universes handled by the peripheral
|
||||
}
|
||||
|
||||
// PeripheralDriver represents how compatible peripheral drivers are implemented
|
||||
type PeripheralDriver interface {
|
||||
Initialize() error // Initializes the protocol
|
||||
GetName() string // Get the name of the finder
|
||||
GetPeripheral(string) (Peripheral, bool) // Get the peripheral
|
||||
Scan(context.Context) error // Scan for peripherals
|
||||
CreatePeripheral(ctx context.Context) (Peripheral, error) // Creates a new peripheral
|
||||
RemovePeripheral(serialNumber string) error // Removes a peripheral
|
||||
}
|
||||
34
hardware/third-party/ftdi/detectFTDI.cpp
vendored
Normal file
34
hardware/third-party/ftdi/detectFTDI.cpp
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
hardware/third-party/ftdi/detectFTDI.manifest
vendored
Normal file
12
hardware/third-party/ftdi/detectFTDI.manifest
vendored
Normal 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
142
hardware/third-party/ftdi/dmxSender.cpp
vendored
Normal 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;
|
||||
}
|
||||
12
hardware/third-party/ftdi/dmxSender.manifest
vendored
Normal file
12
hardware/third-party/ftdi/dmxSender.manifest
vendored
Normal 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>
|
||||
8
hardware/third-party/ftdi/generate.bat
vendored
Normal file
8
hardware/third-party/ftdi/generate.bat
vendored
Normal 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
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
BIN
hardware/third-party/ftdi/lib/ftd2xx.lib
vendored
Normal file
Binary file not shown.
1
package.json
Normal file
1
package.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
77
peripherals.go
Normal file
77
peripherals.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"changeme/hardware"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GetPeripherals gets all the peripherals connected
|
||||
func (a *App) GetPeripherals() error {
|
||||
return a.hardwareManager.Scan(a.ctx)
|
||||
}
|
||||
|
||||
// AddPeripheral adds a peripheral to the project
|
||||
func (a *App) AddPeripheral(protocolName string, peripheralID string) error {
|
||||
// Get the device from its finder
|
||||
p, found := a.hardwareManager.GetPeripheral(protocolName, peripheralID)
|
||||
if !found {
|
||||
return fmt.Errorf("Unable to localize the peripheral %s", peripheralID)
|
||||
}
|
||||
// Add the peripheral ID to the project
|
||||
a.projectInfo.PeripheralsInfo[peripheralID] = p.GetInfo()
|
||||
// TODO: Connect the peripheral
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePeripheral adds a peripheral to the project
|
||||
func (a *App) RemovePeripheral(protocolName string, peripheralID string) error {
|
||||
// TODO: Disconnect the peripheral
|
||||
// Remove the peripheral ID from the project
|
||||
delete(a.projectInfo.PeripheralsInfo, peripheralID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddOS2LPeripheral adds a new OS2L peripheral
|
||||
func (a *App) AddOS2LPeripheral() (hardware.PeripheralInfo, error) {
|
||||
// Get the OS2L driver
|
||||
os2lDriver, err := a.hardwareManager.GetDriver("OS2L")
|
||||
if err != nil {
|
||||
return hardware.PeripheralInfo{}, err
|
||||
}
|
||||
// Create a new OS2L peripheral with this driver
|
||||
os2lPeripheral, err := os2lDriver.CreatePeripheral(a.ctx)
|
||||
if err != nil {
|
||||
return hardware.PeripheralInfo{}, nil
|
||||
}
|
||||
os2lInfo := os2lPeripheral.GetInfo()
|
||||
// Add this new peripheral to the project
|
||||
return os2lInfo, a.AddPeripheral(os2lDriver.GetName(), os2lInfo.SerialNumber)
|
||||
}
|
||||
|
||||
// FOR TESTING PURPOSE ONLY
|
||||
|
||||
func (a *App) ConnectFTDI() error {
|
||||
// Create a new FTDI object
|
||||
var err error
|
||||
a.ftdi, err = hardware.NewFTDIPeripheral("FTDI TEST INTERFACE", "A50825I", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.ftdi.Connect()
|
||||
}
|
||||
|
||||
func (a *App) ActivateFTDI() error {
|
||||
return a.ftdi.Activate()
|
||||
}
|
||||
|
||||
func (a *App) SetDeviceFTDI(channelValue byte) error {
|
||||
return a.ftdi.SetDeviceProperty(0, 0, channelValue)
|
||||
}
|
||||
|
||||
func (a *App) DeactivateFTDI() error {
|
||||
return a.ftdi.Deactivate()
|
||||
}
|
||||
|
||||
func (a *App) DisconnectFTDI() error {
|
||||
return a.ftdi.Disconnect()
|
||||
}
|
||||
161
project.go
Normal file
161
project.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"changeme/hardware"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
projectsDirectory = "projects" // The directory were are stored all the projects
|
||||
avatarsDirectory = "frontend/public" // The directory were are stored all the avatars
|
||||
projectExtension = ".dmxproj" // The extension of a DMX Connect project
|
||||
)
|
||||
|
||||
// GetProjects gets all the projects in the projects directory
|
||||
func (a *App) GetProjects() ([]ProjectMetaData, error) {
|
||||
projects := []ProjectMetaData{}
|
||||
|
||||
f, err := os.Open(projectsDirectory)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to open the projects directory: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := f.Readdir(0)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to read the projects directory: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, fileInfo := range files {
|
||||
// Open the file and get the show name
|
||||
fileData, err := os.ReadFile(filepath.Join(projectsDirectory, fileInfo.Name()))
|
||||
if err == nil {
|
||||
projectObject := ProjectInfo{}
|
||||
err = yaml.Unmarshal(fileData, &projectObject)
|
||||
if err == nil {
|
||||
// Add the SaveFile property
|
||||
projects = append(projects, ProjectMetaData{
|
||||
Name: projectObject.ShowInfo.Name,
|
||||
Save: fileInfo.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
// CreateProject creates a new blank project
|
||||
func (a *App) CreateProject() ShowInfo {
|
||||
date := time.Now()
|
||||
a.projectSave = ""
|
||||
a.projectInfo.ShowInfo = ShowInfo{
|
||||
Name: "My new show",
|
||||
Date: fmt.Sprintf("%04d-%02d-%02dT%02d:%02d", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute()),
|
||||
Avatar: "appicon.png",
|
||||
Comments: "Write your comments here",
|
||||
}
|
||||
return a.projectInfo.ShowInfo
|
||||
}
|
||||
|
||||
// GetProjectInfo returns the information of the saved project
|
||||
func (a *App) GetProjectInfo(projectFile string) (ProjectInfo, error) {
|
||||
// Open the project file
|
||||
projectPath := filepath.Join(projectsDirectory, projectFile)
|
||||
content, err := os.ReadFile(projectPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to read the project file: %v", err)
|
||||
return ProjectInfo{}, err
|
||||
}
|
||||
a.projectInfo = ProjectInfo{}
|
||||
err = yaml.Unmarshal(content, &a.projectInfo)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get the project information: %v", err)
|
||||
return ProjectInfo{}, err
|
||||
}
|
||||
// Load it into the app
|
||||
a.projectSave = projectFile
|
||||
// Return the show information
|
||||
return a.projectInfo, nil
|
||||
}
|
||||
|
||||
// ChooseAvatarPath opens a filedialog to choose the show avatar
|
||||
func (a *App) ChooseAvatarPath() (string, error) {
|
||||
// Open the file dialog box
|
||||
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Choose your show avatar",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "Images",
|
||||
Pattern: "*.png;*.jpg;*.jpeg",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Copy the avatar to the application avatars path
|
||||
avatarPath := filepath.Join(avatarsDirectory, filepath.Base(filePath))
|
||||
_, err = copy(filePath, avatarPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Base(filePath), nil
|
||||
}
|
||||
|
||||
// UpdateShowInfo updates the show information
|
||||
func (a *App) UpdateShowInfo(showInfo ShowInfo) {
|
||||
fmt.Printf("%s\n", showInfo)
|
||||
a.projectInfo.ShowInfo = showInfo
|
||||
}
|
||||
|
||||
// SaveProject saves the project
|
||||
func (a *App) SaveProject() (string, error) {
|
||||
log.Printf("Saving the project %s to %s", a.projectInfo.ShowInfo.Name, a.projectSave)
|
||||
// If there is no save file, create a new one with the show name
|
||||
if a.projectSave == "" {
|
||||
date := time.Now()
|
||||
a.projectSave = fmt.Sprintf("%04d%02d%02d%02d%02d%02d%s", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), projectExtension)
|
||||
}
|
||||
data, err := yaml.Marshal(a.projectInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Create the project directory if not exists
|
||||
err = os.MkdirAll(projectsDirectory, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(projectsDirectory, a.projectSave), data, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return a.projectSave, nil
|
||||
}
|
||||
|
||||
// ShowInfo defines the information of the show
|
||||
type ShowInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Date string `yaml:"date"`
|
||||
Avatar string `yaml:"avatar"`
|
||||
Comments string `yaml:"comments"`
|
||||
}
|
||||
|
||||
// ProjectMetaData defines all the minimum information for a lighting project
|
||||
type ProjectMetaData struct {
|
||||
Name string // Show name
|
||||
Save string // The save file of the project
|
||||
}
|
||||
|
||||
// ProjectInfo defines all the information for a lighting project
|
||||
type ProjectInfo struct {
|
||||
ShowInfo ShowInfo `yaml:"show"` // Show information
|
||||
PeripheralsInfo map[string]hardware.PeripheralInfo `yaml:"peripherals"` // Peripherals information
|
||||
}
|
||||
Reference in New Issue
Block a user