Particularités librairie DmxSimple

Salut à tous,

j'utilise la librairie DmxSimple pour piloter des jeux de lumières. Actuellement mon système tourne sur un NANO, je gère 120 adresses et ça fonctionne à peu près bien.

Quand je dis à peu près, c'est parceque j'ai l'impression parfois qu'il envoie les trames en plusieurs blocs. Je m'explique, sur certaines scènes, j'allume tous mes projos en même temps (du moins c'est la théorie), sauf qu'en vrai, certains s"allument avant d'autres. Le décalage est léger, probablement deux ou trois trames d'écart, mais c'est perceptible en les regardant. avec une vraie console DMX, cela ne se produit pas, ils s'allument tous synchrones.

Alors je me dis que ça vient de la librairie et de son fonctionnement, car le phénomène est aléatoire et difficilement reproductible (ce ne sont jamais systématiquement les mêmes ou le même nombres qui sont touchés par le retard).

pour envoyer les données sur le bus, il faut appeler cette instruction :

DmxSimple.write(Adresse,Valeur);

sauf que ça c'est pour une seule adresse, donc dans le code, je me retrouve avec plusieurs fois ce genre de chose à la suite. Probablement qu'il commence à écrire la trame DMX dès le premier DmxSimple.write, et les suivants seront pris en compte au prochain passage...

pour en avoir le coeur net, malin mais incompétent que je suis, j'ai ouvert le .ccp de la librairie pour tenter de comprendre comment il marche, et j'en ai déduis les choses suivantes :

  • je ne vois pas d'appel à l'UART, donc il fabrique la trame DMX entièrement "à la main" à grand coup d'interruption, ce qui suppose que le code peut faire sa vie et d'un coup il saute dans la routine DMX, pour revenir dans le code, et ainsi de suite, d'où des éléments parfois incomplets à l'instant T.
  • la présence de l'instruction volatile uint8_t dmxBuffer[DMX_SIZE]; me conforte dans l'idée qu'il rempli un tableau qu'il va dépiler ensuite un à un pour émettre sur le bus dans dmxSendByte(dmxBuffer[dmxState-1]);
  • la présence d'une variable dmxStarted = 1; indique l'état de l'envoi, pourtant si j'appelle dmxStarted; dans mon propre code pour savoir où il en est, ça ne marche pas, comme si cette variable était propre au .cpp et que je ne pouvais pas l'utiliser dans mon propre code qui lui même utilise ce .cpp.
  • la présence d'une fonction dmxEnd(); qui stoppe l'envoie des trames DMX, accessible en mettant à 0 une des variables que je gère (nombre de canaux DMX).

Mon idée est la suivante, utiliser dmxEnd(); périodiquement, pour être certains que le tableau dmxBuffer[DMX_SIZE]; a eu le temps de se charger complétement, afin que quand je réactive dmxBegin(); il puisse envoyer la purée en une traite (ou plutôt en une trame).
Seulement je crains qu'en invoquant l’arrêt avec dmxEnd(); la fonction tableau se désactive également.

Alors voici (enfin) mes questions :

  • Est-ce que l’arrêt dmxEnd(); empêcherait l'incrémentation du tableau ?
  • Si cela est hasardeux, est-ce qu'il existe un autre moyen de procéder pour qu'il n'envoie les données sur le bus que quand le tableau est stable ?

En gros est-ce qu'il est possible de faire comme avec certains écrans ou NEOPIXELS, de charger toutes les valeurs, puis ensuite, au bon moment envoyer une instruction pour qu'il dump sur le bus tout ce qui a été accumulé précédemment ?

merci à vous pour votre aide

La bibliothèque envoie en effet la frame totale par chunks, c’est écrit dans le code

Oui ce qu'ils entendent par là, c'est qu'ils envoient des octets à chaque coup du timer. Les timers sont réglés pour péter sur les timing du bus, ce qui doit leur éviter de faire appel à un UART.
Ils construisent le baudrate à la main. Donc normal que ça soit du chunk.

le problème étant que qu'ils commencent à chunker avant que toutes les valeurs aient été chargées, d'où le fonctionnement bizarre où il faut 2 voire 3 frames pour tout passer.

dans le code d'origine j'ai trouvé ça, (ou plutôt mon code fait appel à ça) :

void dmxWrite(int channel, uint8_t value) {
  if (!dmxStarted) dmxBegin();
  if ((channel > 0) && (channel <= DMX_SIZE)) {
    if (value<0) value=0;
    if (value>255) value=255;
    dmxMax = max((unsigned)channel, dmxMax);
    dmxBuffer[channel-1] = value;
  }
}

je remarque surtout que si le protocole a été stoppé au préalable (ce que je fais de façon volontaire), dès que j'appelle dmxWrite il redémarre le protocole d'émission réseau.
Et seconde observation, lorsque le protocole d'émission réseau est stoppé, le tableau conteneur n'est pas géré.

j'ai modifié la librairie à ma façon, et ai transformé le code d'origine ci dessus en ça :

void dmxWrite(int channel, uint8_t value)
{
	if ((!dmxStarted)&&(dmxMax=128)) dmxBegin();
    if (value<0) value=0;
    if (value>255) value=255;
    dmxMax = max((unsigned)channel, dmxMax);
    dmxBuffer[channel-1] = value;
}

Cette fois ci, il peut à la fois remplir le tableau directement à l'appel de la fonction dmxWrite meme si l'émission du DMX est stoppé, et aussi redémarrer le protocole quand la variable dmxMax vaut 128, car je gère cette variable depuis mon code ( le 128 est un truc de mon choix, sans impact sur le final, il faut juste que ça soit supérieur ou égal aux nbr de canaux émis).

Là, je suis sûr que quand je vais ré-activer le protocole, le tableau sera chargé, et il va pouvoir tout envoyer dès la première frame.

Oui ils gèrent eux même le protocole d’émission parce qu’une UART ne respecterait pas le timing complètement (par exemple générer le « break » DMX - un état bas prolongé d’au moins 88 µs en début de trame).

Si vous voulez essayer avec une bibliothèque qui s’appuie sur l’USART de l’arduino, regardez DMX Library for Arduino / Wiki / Home ou un de ses forks comme GitHub - JeffersGlass/JDMX: A fork of the Conceptinetics DMX library

vous perdez bien sûr l’usage de la classe HardwareSerial pour Serial. La bibliothèque gère proprement les timings critiques comme le break et le Mark After Break.

(Je l’ai utilisée une fois il y a des années pour aider sur un petit projet ponctuel)

Il y aussi GitHub - mathertel/DMXSerial: An Arduino library for sending and receiving DMX packets. qui existe et utilise Serial il le semble - jamais testée


Attention au = au lieu du ==

Et pour value

C’est un entier non signé donc jamais négatif et il ne peut pas non plus être supérieur à 255 puisque c’est juste un octet… les tests ne servent à rien :slight_smile:

Sinon je n’ai pas compris le point de votre modif. Le code d’origine (à part les 2 tests inutiles) me paraît cohérent et vous ne faites rien de plus (enfin vous faites des trucs en plus qui seraient faits de toutes façons du moment que le Channel est correct)

Merci pour ta réponse.

Je me posais la question au sujet du = à la place du ==, mais ailleurs dans la librairie, ils n'utilisent qu'un seul = pour le test conditionnel, alors j'ai fait pareil.

le test qui vise à limiter à du positif et inférieur à 255 était déjà présent, j'ai rien touché.

Ça m’étonnerait - lien ??

Sincèrement votre modification ne sert à rien et ne change rien.

Oui - c’est viré par le compilateur de toutes façons mais ça ne sert à rien.

Dans le code original, l'appel de dmxWrite démarrait la séquence d'émission du bus DMX. En gros il se mettait à cavaler direct dès le premier dmxWrite, avant même que le tableau n'ait pu être complété (j'envoie des dizaines de dmxWrite(x,x); au même moment.
C'est l'explication du problème je pense, la première frame contenait les "nouvelles" data au début, puis la suite n'avait pas eut le temps d'etre écrite et il envoyait les vieilles data périmées de la fin du tableau qui n'était pas encore mise à jour . Le tableau n'étant complet (remplacement complet de l'ancien contenu) au bout de 2 ou 3 frames, d'où le petit décalage perceptible.

là ce que j'ai fait, c'est que le tableau est accessible durant la phase OFF.
Je peux invoquer mes dmxWrite autant que je veux, le tableau se rempli tranquille. Quand la tache est complétée (à l'issue de toutes mes écritures de variables du bus), je repositionne le maxChannel à 128, ce qui aura pour effet de redémarrer l'émission DMX. à ce moment là il dump le tableau dans le bus, la première frame contient tout le tableau, qui est à jour.

et c'est ça qui change tout.
C'est un peu comme si vous tentez d'envoyer une chaine de caractère sur le port COM, et qu'il "print" dès le premier caractère rencontré après l'ouverture des guillemets, sans attendre la fin de la chaine. Là je lui ai ajouté le retour chariot, que je contrôle via maxChannel.

Question, pourquoi je ne peut pas utiliser les variables de la librairie dans mon code (juste en consultation pour connaitre son état d'avancement par exemple) ?

Ah OK - je vois. Vous rajoutez en fait un booléen pour lui dire de démarrer ou pas .

Avec == au lieu de = , si vous gérez dmxMax

if ((!dmxStarted)&&(dmxMax==128)) dmxBegin();

Alors oui vous décidez de quand ça démarre.

Ce serait sans doute plus propre de rajouter un bool pour ne pas avoir d’effets de bords ailleurs ou la bibliothèque utilise dmxMax ou de virer le begin() de write et rajouter directement des fonctions start et stop

Ce sont les règles du C++, ses variables sont privées . Il faut les rendre publiques et les déclarer externes. Cela dit comme sa bibliothèque utilise abondamment les interruptions, il faut faire attention à ce que vous voulez toucher

étant un incompétent notoire, je touche aux codes des autres qu'avec parcimonie et quand je suis sûr de comprendre à peu près la connerie que je suis en train de faire.

j'ai utilisé la valeur de dmxMax, car j'ai vu qu'il faisait des tests dessus pour gérer dmxStop() (quand il est à 0) et begin() (quand il est positif).
Comment je peux faire pour rendre les variables de la librairie publique ? Typiquement je voudrais lire la valeur de dmxState pour savoir où il en est dans l’émission du paquet et optimiser mes opérations.

Essayez d'enlever le mot clef static devant la définition de dmxState pour le rendre accessible depuis l'extérieur

et dans DmxSimple.h, après le
extern DmxSimpleClass DmxSimple;

vous rajoutez
extern uint16_t dmxState;

ça devrait rendre dmxState visible dans votre code.

Mais attention, c'est utilisé dans l'interruption, c'est pour cela qu'il l'avait rendue invisible aux autres. Donc cette variable devrait devenir volatile et il faudrait mettre un sémaphore pour lire la variable pour vous assurer qu'elle n'est pas en cours de modification par l'ISR...

➜ ça ne me parait pas une bonne idée :slight_smile:

merci pour l'info.
Le fait de la lire peut perturber le fonctionnement des interruptions ?
je ne ferai que la tester, je ne vais pas la modifier ou agir dessus.

typiquement je vais tester si elle vaut 127 cela veut dire que tous les slots ont été envoyé, auquel cas je passe en pause sur l'émission du DMX (en écrivant dmxMax à 0), et peut passer à une autre étape de mon programme.

pour répondre de manière générale à votre question :

si elle n'est pas déclarée volatile, le compilateur peut optimiser et la conserver dans un registre et vous n'allez pas aller lire la valeur réelle en mémoire donc vous ne la verrez pas évoluer.

Si vous la déclarez volatile, comme c'est sur deux octets (uint16_t) lorsque vous faites un test sans mettre une section critique (pour empêcher qu'elle soit en cours de traitement par l'ISR) vous risquez de lire une valeur fausse et donc ne pas prendre la bonne décision.

Imaginez que l'interruption se produit pile pendant que vous faites le test et vous avez testé le premier octet, l'interruption survient et modifie la valeur, puis vous revenez tester le second octet. (pour illustrer le propos : imaginez que la valeur soit 255 avant l'interruption soit 0x00FF. Vous voulez tester la valeur haute, vous voyez le 0x00, puis l'interruption arrive et incrémente d'un le compteur, la mémoire contient maintenant 0x0100, et vous revenez à votre code et vous regardez l'octet de poids faible et vous voyez 0x00 ➜ votre code principal va conclure que la valeur vaut 0 ce qui n'est pas du tout le cas.


Pour revenir à votre cas particulier, si vous regardez la bibliothèque, ils font une boucle bloquante DANS L'ISR

  while (1) {
    ... // des trucs
    // Successfully completed that stage - move state machine forward
    dmxState++;
    if (dmxState > dmxMax) {
      dmxState = 0; // Send next frame
      break;
    }
  }

et ils n'en sortent que lorsqu'ils ont envoyé dmxMax éléments.

donc pendant que ce code s'exécute (c'est une interruption) votre code principal est bloqué et donc à la sortie de l'interruption dmxState vaudra 0. Vous ne pourrez pas lire son évolution en cours de route.

effectivement, c'est peut être pas le bon choix de variable.
En fait, j'avais pas compris ça comme il faut, merci pour l'éclairement autour de la fonction qui est dans le while.
Dans le while il appelle dmxSendByte qui est le nerf de la guerre.
Il se met en while pour envoyer la totalité de la frame sans que personne ne viennent lui péter les burnes, les timings étant critiques, ils ont surement fait ça pour être stable.

Le système étant figé, je peux voir combien de temps il met pour balancer la frame complète, ça sera parfaitement répetable et stable. Je mesurerai donc le temps d'émission à l'oscilloscope et modifierai le code pour allouer le temps à passer dans la fonction qui correspond. je peux meme utiliser delay() , ça ne devrait pas etre bloquant.

J'ai relu rapidement le code, il n'y a pas émission en "chunks" à proprement parler, toute la trame est émise.

pour moi, le problème que vous avez identifié provient effectivement de l'absence de possibilité de dire "je suis en train de remplir le buffer" et "ça y'est il est plein tu peux balancer la trame".

En DMX, l’émetteur envoie en boucle des trames composées d’un break pour signaler le début, d’un Mark After Break, d’un start code, puis des valeurs des canaux dans l’ordre. Les récepteurs attendent le break pour se synchroniser, comptent les octets jusqu’à leur adresse, lisent la valeur qui les concerne, l’appliquent, puis ignorent le reste de la trame jusqu’au prochain break.

Le standard DMX permet d'envoyer jusqu'à 512 canaux mais autorise de raccourcir la trame si on n’utilise qu'une centaine de canaux i faut le dire à la bibliothèque, par exemple avec un

  DmxSimple.maxChannel(100);

ça évite d'émettre une trame longue qui prend du temps alors qu'il n'y a pas de récepteurs au delà de 100.

Ensuite une trame de 100 éléments va prendre moins de 3ms à émettre.

Donc si vous remplissez et émettez au fur et à mesure, comme la bibliothèque émet en permanence tant qu'il y a des modifications vous aurez en début de remplissage du buffer un délai de 3ms entre chaque demande de write().

L'œil humain est sensible à des changements de lumière ou à des décalages de synchronisation dans un intervalle de 16 à 50 ms dans des conditions normales, donc effectivement au début si le projecteur 1 et le projecteur 20 doivent s'allumer ensemble et qu'il y a 20 trames entre les deux, ça prend 60ms de latence et ça commence à se voir "un peu".

il faudrait explorer les autres bibliothèques pour voir si elles proposent un fonctionnement différent ou alors modifier celle là pour avoir accès au buffer et le remplir avant de faire le déclenchement.

un workaround serait éventuellement d'augmenter au début le nombre de canaux.

Vous commencez par dire qu'il n'y a qu'un canal

  DmxSimple.maxChannel(1);

et vous faites un write. ça part en ~120µs pas 3ms

ensuite vous faites un

  DmxSimple.maxChannel(2);

et vous remplissez le second canal ce qui émet les 2 et ça part en ~150μs pas 3ms

etc..

➜ vous diminuez ainsi significativement la latence lors du début du remplissage mais vous ne couperez pas au décalage plus vous augmentez le nombre max de canaux

oui, c'est bien ça que j'ai fait en permettant l'accès à dmxBuffer[channel-1] = value; lors de l'état "arrêt" du protocole, j'assure de remplir mon tableau avec les data courantes, chose qui n'était pas possible avant.

Je limite également la longueur de la trame en envoyant que ce que j'ai besoin (environ 120).

quand mon tableau est plein (ou quand je suis sûr qu'il est up to date), je réactive le begin() et ainsi, la première trame ne contient que des infos up to date, plus besoin d'attendre plusieurs trames pour que toutes les infos à jour soient parties. (les premières trames contenaient des data nouvelles et anciennes à la fois, les anciennes étant écrasées au fil de la progression puisque le tableau n'était accessible en écriture que pendant que le process d'émission était actif)
J'ai fait un essai avec 4 projos, ça marche correctement, là où avant je voyais le petit décalage.
à présent je vais arranger mon nouveau code pour séquencer les actions :

  • début
  • arrêt du protocole DMX (maxChannel=0)
  • collecte des commandes
  • déterminisme des modes de fonctionnement et des combinaisons
  • élaboration des patterns correspondants
  • mise à jour IHM (displays et LED)
  • génération des valeurs pour chaque canaux
  • attente 2ms pour etre sûr que toutes les valeurs ont eu le temps d'etre écrite dans le tableau.
  • démarrage du protocole DMX (maxChannel=128) (qui aura pour conséquence le vidage du tableau dans le bus, et donc completude des bonnes valeurs dès la première frame).
  • attente 5ms pour etre sûr que la frame a été émise completement au moins une fois.
  • retour au début.

émettre 128 canaux prend plus de 2ms

émettre en DMX, oui, mais pas pour remplir un tableau. L'instruction doit prendre quelques µs.
je compte environ 5ms pour une trame de 128 canaux.
mais avant d'envoyer la trame, je dois remplir mon tableau, pour que le process d'envoi (while) puisse taper dans tableau qui contiendra à lors que des données up to date, puisque je le rempli avant d'initier l'envoi.

les 2 ms c'est pour faire 128 fois cette opération :

void dmxWrite(int channel, uint8_t value)
{dmxBuffer[channel-1] = value;}

ça devrait aller.

et une fois que ça c'est fait,
je fais DmxSimple.maxChannel(128);
ce qui aura pour effet de démarrer dmxBegin(); et rentrer dans le while.
et là, il va émettre la première trame de 128 slots en allant taper dans le buffer qui sera rempli avec les bonnes valeurs.

Là où avant, il ne pouvait mettre à jour le tableau que quand l'émission était débutée... il avait le temps d'en faire quelques une avant la première émission je suppose, puis les autres suivaient ensuite, à la trame 2 ou 3...
avec ma méthode, quand il débute l'émission, le buffer ne contient plus d'infos périmées, et c'est ça que je veux, et mon essai de ce matin semble confirmer que c'est la solution.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.