11-hardware-definition #17

Merged
thinkode merged 13 commits from 11-hardware-definition into develop 2025-01-18 14:53:30 +00:00
33 changed files with 2987 additions and 116 deletions
Showing only changes of commit 17b5d39fc4 - Show all commits

4
.gitignore vendored
View File

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

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

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

54
app.go
View File

@@ -1,6 +1,7 @@
package main
import (
"changeme/hardware"
"context"
"fmt"
"io"
@@ -8,6 +9,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/runtime"
@@ -22,18 +24,38 @@ const (
// 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
// 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.RegisterFinder(hardware.NewMIDIFinder())
hardwareManager.RegisterFinder(hardware.NewFTDIFinder())
return &App{
hardwareManager: hardwareManager,
}
}
// 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
err := a.hardwareManager.Start(ctx)
if err != nil {
log.Fatalf("Unable to start the device manager: %s", err)
return
}
}
// GetPeripherals gets all the peripherals connected
func (a *App) GetPeripherals() {
a.hardwareManager.Scan(a.ctx)
}
// GetProjects gets all the projects in the projects directory
@@ -180,3 +202,31 @@ func copy(src, dst string) (int64, error) {
nBytes, err := io.Copy(destination, source)
return nBytes, err
}
// 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()
}

View File

@@ -9,9 +9,28 @@
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 { currentProject, 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 { GetPeripherals } from "../wailsjs/go/main/App";
// When the list of hardware changed, update the store
EventsOn('PERIPHERAL_ARRIVAL', function(peripheralInfo){
console.log("Hardware has been added to the system");
console.log(peripheralInfo)
$peripherals = [...$peripherals, peripheralInfo];
})
EventsOn('PERIPHERAL_REMOVAL', function(peripheralInfo){
console.log("Hardware has been removed from the system");
console.log(peripheralInfo)
$peripherals = $peripherals.filter(item => JSON.stringify(item) !== JSON.stringify(peripheralInfo))
console.log($peripherals)
// $peripherals = devicesList
// console.log(devicesList)
})
let selectedMenu = "settings"
// When the navigation menu changed, update the selected menu
@@ -50,6 +69,9 @@
SaveFile: "",
});
// Request the list of peripherals
GetPeripherals()
</script>
<header>

View File

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

View File

@@ -1,5 +1,6 @@
<script>
import { stop_propagation } from 'svelte/internal';
import Tooltip from '../General/Tooltip.svelte';
import { createEventDispatcher } from 'svelte';
@@ -12,30 +13,38 @@
export let message = '';
export let hide = false;
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 being clicked
const dispatch = createEventDispatcher();
function click(){
function click(event){
event.stopPropagation()
dispatch('click')
}
</script>
<div class="container">
<div class="badge"
<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}
on:mouseleave={toggleTooltip}>
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}></Tooltip>
<Tooltip message={message} show={tooltipShowing} position={tooltipPosition}></Tooltip>
{/if}
</div>
@@ -50,7 +59,5 @@
align-items: center;
padding: 0;
}
</style>

View File

@@ -23,13 +23,13 @@
</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:input={handleInput}/>
<!-- 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:input={handleInput}/>
{/if}
</div>

View File

@@ -52,20 +52,28 @@
dispatch('click');
}
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: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 +82,7 @@
style="width: calc({width} / 3); height: calc({width} / 3); border-radius: calc({width}); background-color:{statusColor}; display:block;">
</div>
{/if}
<Tooltip message={tooltipMessage} show={tooltipShowing}></Tooltip>
<Tooltip message={tooltipMessage} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>

View File

@@ -9,13 +9,21 @@
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');
@@ -24,13 +32,13 @@
</script>
<div class="container">
<button
<button bind:this={buttonRef}
on:mousedown={emitClick}
on:mouseenter={toggleTooltip}
on:mouseleave={toggleTooltip}
on:mouseenter={() => { toggleTooltip(true) }}
on:mouseleave={() => { toggleTooltip(false) }}
style='color: {$colors.white}; background-color: { active ? $colors.second : $colors.third }; {style}'><i class='bx { icon}'></i> { text }
</button>
<Tooltip message={tooltip} show={tooltipShowing}></Tooltip>
<Tooltip message={tooltip} show={tooltipShowing} position={tooltipPosition}></Tooltip>
</div>
<style>
.container{

View File

@@ -23,7 +23,7 @@
</div>
<div class="bodyContainer"
style='background-color: {$colors.third}; max-width: {maxWidth}; max-height: {maxHeight};'>
style='background-color: {$colors.first}; max-width: {maxWidth}; max-height: {maxHeight};'>
{#if tabs[activeTab]}
<svelte:component this={tabs[activeTab].component} />
{/if}

View File

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

View File

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

View File

@@ -3,48 +3,79 @@
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 subtitle = '';
export let type = '';
export let location = '';
export let line1 = '';
export let line2 = '';
export let removable = false;
export let pluggable = false;
export let plugged = 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(){
function remove(event){
dispatch('delete')
}
function add(event){
dispatch('add')
}
let showOptions = false
function click(){
showOptions = !showOptions
dispatch('click')
}
</script>
<div class="profile">
<div>
<p>{title}</p>
<h6 class="subtitle">{subtitle}</h6>
<h6>{line1}</h6>
<h6>{line2}</h6>
<div class="card">
<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 class="actions">
<InfoButton on:click={remove} style="margin: 0.2em;" icon='bx-trash' interactive message={$_("projectHardwareDeleteTooltip")} hide={!removable}/>
<InfoButton style="margin: 0.2em;" background={ signalized ? $colors.orange : $colors.first } icon='bx-pulse' hide={!signalizable}/>
<InfoButton style="margin: 0.2em;" background={ plugged ? $colors.ok : $colors.nok} icon='bxs-plug' hide={!pluggable}/>
<div class="options" style="display: {showOptions ? "block" : "none"};">
<Input label={$_("projectCommentsLabel")} type='large' width='100%'/>
</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;
@@ -52,6 +83,17 @@
.actions {
margin-left: 0.2em;
}
.options {
color: var(--first-color);
padding: 0.2em;
margin-top: 0.2em;
position: absolute;
display:block;
border-radius: 0.5em;
max-width: 50;
background-color: var(--fourth-color);
text-align: left;
}
p{
margin: 0;
}

View File

@@ -1,66 +1,125 @@
<script lang=ts>
import DeviceCard from "./DeviceCard.svelte";
import Tab from "../General/Tab.svelte";
import { _ } from 'svelte-i18n'
import { peripherals } from "../../stores";
import { ConnectFTDI, ActivateFTDI, DeactivateFTDI, DisconnectFTDI, SetDeviceFTDI } from "../../../wailsjs/go/main/App";
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)
})
}
</script>
<div class="container">
<div class="configuration">
<p>Option 1</p>
<p>Option 2</p>
<p>Option 3</p>
</div>
<div class="hardware">
<div style="padding: 0.5em;">
<p style="margin-bottom: 1em;">Available devices</p>
<div class="availableHardware">
<p style="color: var(--first-color);"><i class='bx bxs-plug'></i> Detected</p>
{#each $peripherals as peripheral}
<DeviceCard title={peripheral.Name == "" ? "Please wait..." : peripheral.Name} type={peripheral.ProtocolName} location={peripheral.Location ? peripheral.Location : ""} line1={peripheral.SerialNumber ? "S/N: " + peripheral.SerialNumber : ""} addable/>
{/each}
<p style="color: var(--first-color);"><i class='bx bxs-network-chart' ></i> Others</p>
<DeviceCard on:click={() => console.log("Edit the OS2L hardware")} title="OS2L device" type="OS2L" line1="Add to configure" addable on:delete={() => console.log("Delete the OS2L device")}/>
<DeviceCard on:click={() => console.log("Edit the OSC hardware")} title="OSC device" type="OSC" line1="Add to configure" addable on:delete={() => console.log("Delete the OSC device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<DeviceCard on:click={() => console.log("Edit the ArtNet hardware")} title="ArtNet device" type="ArtNet" line1="Add to configure" addable on:delete={() => console.log("Delete the ArtNet device")}/>
<div class="hardware">
<div class="inputHardware">
<DeviceCard title="Virtual DJ" subtitle="OS2L" line1="Listening to 192.168.1.53" line2="Please update your OS2L client" removable pluggable signalizable on:delete={() => console.log("Delete the Virtual DJ device")}/>
<DeviceCard title="Pioneer DDJ-FLX4" subtitle="MIDI" line1="Listening to COM14" line2="Version: 0-alpha-13.15" removable pluggable signalizable/>
<DeviceCard title="Platformia LM730" subtitle="OSC" line1="Listening to 192.168.1.418" line2="Please update your OSC client" removable pluggable signalizable/>
</div>
<p><i class='bx bx-down-arrow-alt'/>{$_("projectHardwareInputsLabel")}<i class='bx bx-down-arrow-alt'/></p>
<div class="show">{$_("projectHardwareShowLabel")}</div>
<p><i class='bx bx-down-arrow-alt'/>{$_("projectHardwareOutputsLabel")}<i class='bx bx-down-arrow-alt'/></p>
<div class="outputHardware">
<DeviceCard title="DMX rendering" subtitle="Rendering interface" line2="All universes associated." pluggable plugged on:delete={() => console.log("Delete the rendering interface")}/>
<DeviceCard title="BerryMix 1" subtitle="BerryMix interface" line1="Writing on ETHERNET10" line2="Universe 1 associated." removable pluggable/>
<DeviceCard title="00feadfee456" subtitle="USB - FTDI" line1="Writing on COM14" line2="Universe 2 associated." removable pluggable/>
<DeviceCard title="Server 12" subtitle="ArtNet" line1="Writing to 192.168.1.354" line2="Universe 3 & 4 associated." removable pluggable signalizable signalized/>
</div>
<div style="padding: 0.5em; flex:2; width:100%;">
<p style="margin-bottom: 1em;">Project devices</p>
<div class="configuredHardware">
<DeviceCard disconnected on:click={() => console.log("Edit the VDJ hardware")} title="Virtual DJ" type="OS2L" location="192.168.1.53" line1="Please update your OS2L client" removable signalizable on:delete={() => console.log("Delete the Virtual DJ device")}/>
<DeviceCard disconnected title="Pioneer DDJ-FLX4" type="MIDI" location="COM14" line1="Version: 0-alpha-13.15" removable signalizable/>
<DeviceCard title="Platformia LM730" type="OSC" location="192.168.1.418" line1="Please update your OSC client" removable signalizable/>
</div>
<p style="margin-bottom: 1em;">Device settings</p>
<div>
<p><i>Select a device 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>
.container {
display: flex;
}
.configuration {
flex: 1;
background-color: var(--first-color);
border-radius: 0.5em;
p {
margin: 0;
}
.hardware {
flex: 3;
margin-left: 0.5em;
text-align: center;
display: flex;
width: 100%;
}
.inputHardware {
background-color: var(--first-color);
.availableHardware {
background-color: var(--second-color);
border-radius: 0.5em;
padding: 0.2em;
display: flex;
flex-wrap: wrap;
max-height: calc(100vh - 300px);
overflow-y: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
/* overflow: visible; */
}
.show {
text-align: center;
background-color: var(--first-color);
margin: 1em;
padding: 3em;
border-radius: 0.5em;
.availableHardware::-webkit-scrollbar {
display: none;
}
.outputHardware {
background-color: var(--first-color);
.configuredHardware {
background-color: var(--second-color);
border-radius: 0.5em;
padding: 0.5em;
padding: 0.2em;

View File

@@ -33,5 +33,6 @@
"projectHardwareShowLabel" : "My Show",
"projectHardwareInputsLabel": "INPUTS",
"projectHardwareOutputsLabel": "OUTPUTS",
"projectHardwareDeleteTooltip": "Delete this device"
"projectHardwareDeleteTooltip": "Delete this device",
"projectHardwareAddTooltip": "Add this device to project"
}

View File

@@ -4,7 +4,7 @@ import { writable } from 'svelte/store';
export let projectsList = writable([])
// Show settings
export let currentProject = writable({});
export let currentProject = writable({})
export let needProjectSave = writable(false)
// Application colors
@@ -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([])

View File

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

4
go.mod
View File

@@ -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
View File

@@ -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=

117
hardware/FTDIFinder.go Normal file
View File

@@ -0,0 +1,117 @@
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
)
// FTDIFinder represents how the protocol is defined
type FTDIFinder struct {
peripherals map[string]Peripheral
}
// NewFTDIFinder creates a new FTDI finder
func NewFTDIFinder() *FTDIFinder {
return &FTDIFinder{
peripherals: make(map[string]Peripheral),
}
}
// Initialize initializes the FTDI finder
func (f *FTDIFinder) Initialize() error {
if goRuntime.GOOS != "windows" {
return fmt.Errorf("<!> The FTDI finder is not compatible with your platform yet (%s)", goRuntime.GOOS)
}
fmt.Println("FTDI finder initialized")
return nil
}
// GetName returns the name of the finder
func (f *FTDIFinder) GetName() string {
return "FTDI Finder"
}
//go:embed third-party/ftdi/detectFTDI.exe
var findFTDI []byte
// Scan scans the FTDI peripherals
func (f *FTDIFinder) 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(f.peripherals, ftdiPeripherals)
// Emit the events
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
// Store the new peripherals list
f.peripherals = ftdiPeripherals
return nil
}

202
hardware/FTDIPeripheral.go Normal file
View File

@@ -0,0 +1,202 @@
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",
UniversesNumber: p.universesNumber,
}
}

112
hardware/MIDIFinder.go Normal file
View File

@@ -0,0 +1,112 @@
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.
*/
// MIDIFinder represents how the protocol is defined
type MIDIFinder struct {
peripherals map[string]Peripheral // The list of peripherals
}
// NewMIDIFinder creates a new DMXUSB protocol
func NewMIDIFinder() *MIDIFinder {
return &MIDIFinder{
peripherals: make(map[string]Peripheral),
}
}
// Initialize initializes the DMX-USB finder
func (f *MIDIFinder) Initialize() error {
fmt.Println("MIDI finder initialized")
return nil
}
// GetName returns the name of the finder
func (f *MIDIFinder) GetName() string {
return "MIDI"
}
func splitStringAndNumber(input string) (string, int, error) {
// Regular expression to match the text part and the number at the end
re := regexp.MustCompile(`^(.*?)(\d+)$`)
matches := re.FindStringSubmatch(input)
// Check if the regex found both a text part and a number
if len(matches) == 3 {
// matches[1]: text part (might contain trailing spaces)
// matches[2]: numeric part as a string
textPart := strings.TrimSpace(matches[1]) // Remove any trailing spaces from the text
numberPart, err := strconv.Atoi(matches[2])
if err != nil {
return "", 0, err // Return error if the number conversion fails
}
return textPart, numberPart, nil
}
// Return an error if no trailing number is found
return "", 0, fmt.Errorf("no number found at the end of the string")
}
// Scan scans the interfaces compatible with the MIDI protocol
func (f *MIDIFinder) 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(f.peripherals, midiPeripherals)
// Emit the events
emitPeripheralsEvents(ctx, removedList, PeripheralRemoval)
emitPeripheralsEvents(ctx, addedList, PeripheralArrival)
// Store the new peripherals list
f.peripherals = midiPeripherals
return nil
}

View 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",
}
}

190
hardware/hardware.go Normal file
View File

@@ -0,0 +1,190 @@
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 {
finders []PeripheralFinder // The list 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{
finders: make([]PeripheralFinder, 0),
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
}
// RegisterFinder registers a new peripherals finder
func (h *HardwareManager) RegisterFinder(finder PeripheralFinder) {
h.finders = append(h.finders, finder)
fmt.Printf("Success registered the %s finder\n", finder.GetName())
}
// Scan scans all the peripherals for the registered finders
func (h *HardwareManager) Scan(ctx context.Context) error {
if len(h.finders) == 0 {
return fmt.Errorf("No peripherals finder registered")
}
for _, finder := range h.finders {
finder := finder
go func() {
err := finder.Scan(ctx)
if err != nil {
fmt.Printf("Unable to scan peripherals with the %s finder: %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
}

29
hardware/interfaces.go Normal file
View File

@@ -0,0 +1,29 @@
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 // Name of the peripheral
SerialNumber string // S/N of the peripheral
ProtocolName string // Protocol name of the peripheral
UniversesNumber int // Number of DMX universes handled by the peripheral
}
// PeripheralFinder represents how compatible peripheral finders are implemented
type PeripheralFinder interface {
Initialize() error // Initializes the protocol
GetName() string // Get the name of the finder
Scan(context.Context) error // Scan for peripherals
}

View File

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

View File

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

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

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

1
package.json Normal file
View File

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