PicoDuino Tilt
par , le lundi 5 septembre 2016 à 22:35

Catégorie : Général
Mots clés : Arduino

Un détecteur d'orientation d'écran de PC (avec fonction Pivot), associé à un système de notification lumineux et sonore.


Le concept

J'ai depuis pas mal de temps 2 écrans avec fonction pivot et hub USB intégré; mais la fonction pivot est juste mécanique et ne ré-oriente pas l'image du bureau.

Je suis sous Linux et je voulais un gadget amusant qui m'offrait aussi une fonction notification sonore et lumineuse.

Je veux donc utiliser un détecteur d'orientation branché (et alimenté) sur l'un des ports USB de l'écran. J'ai 2 ports USB sur le coté gauche de l'écran et 2 autres ports en dessous.

Pour le hardware mon choix s'est rapidement porté sur une carte PicoDuino. La version 'Full' comprend déjà une led RGB (c'est toujours ça de gagner) et une broche amplifiée par transistor qui peux débiter 100mA (parfait pour décoincer un petit buzzer) :

PicoDuino Front

Avec la Picoduino version Full et son AtTiny85 on est un peu serré en terme de broches :

  • broche 3 et 4 utilisées par l'USB; et comme je veux discuter avec la carte en live (récupération de l'orientation du 'Tilt, envoi de commande pour les effets lumineux et sonore) je ne pourrais rien connecter à ces broches.
  • la led RGB est connectée aux broches 0, 1 et 2.
  • la broche 5 fait office de reset.

Implémentation hardware

D'entrée de jeux je vais convertir la broche reset en E/S standard. Attention! c'est une opération difficilement réversible, mais ne présentant pas trop de risque. C'est sur cette broche que je vais brancher un détecteur de tilt qui agit comme un interrupteur.

Pour le son, un simple buzzer devrait suffire. Ceci dit avec 40mA max par broche ça risque de ne pas faire trop de bruit, mais coup de chance la version Full de PicoDuino a un transistor sur sa broche 1 et peut pousser l'intensité à 100mA, "ça va le faire". Bon au passage on va devoir se passer de la led verte connectée sur cette broche.

Après différents essais sur breadboard, j'ai réalisé un premier montage soudé sur picots que l'on enficher au bout de la PicoDuino :

PicoDuino Tilt Plugin PicoDuino Tilt Plugin back

Le switch-tilt est un mécanisme contenant une bille métallique contenu dans un petit tube dont l'un des bouts a les contacts ouvert de l'interrupteur. L'angle du tilt est donc important pour avoir un position de contact ou non-contact franche en fonction de l'orientation de l'écran :

PicoDuino Tilt horizontal PicoDuino Tilt vertical

Et une fois an place sur l'écran ça donne :

PicoDuino Tilt

Programmation du Tilt

Je veux récupérer l'orientation de l'écran, ce qui consiste à lire l'état du bouton-tilt. La clé USB va nous envoyer automatiquement les changements d'orientation.

Je veux aussi pouvoir jouer avec la led et le buzzer. J'ai défini un jeu de commandes très basique :

  • <puissance lumineuse entre 0 (éteint) et 255 (plein puissance)><couleur r,g ou b> ; ex : 0b pour éteindre la led bleu, 127g pour allumer la led verte à mi-puissance, 255r pour allumer à fond la led rouge. Mais n'oubliez pas que je vais faire l'impasse sur la led verte connectée au buzzer.
  • <fréquence>t pour emmètre un son continue avec une fréquence précise. Il faut faire un '0t' pour couper le son.

Le code du stretch Arduino est :

#include <DigiUSB.h>

//------------------------------------------------------------------------------------------------
int last             = HIGH;
int current          = LOW;
int value            = 0;
unsigned long timing = millis();

const int BLUE       = PB0;
const int RED        = PB2;
const int GREEN      = PB1;

const int HP         = PB1;
const int TILT       = PB5;

//------------------------------------------------------------------------------------------------
void setup() {
  DigiUSB.begin();

  pinMode(TILT, INPUT_PULLUP);

  pinMode(BLUE, OUTPUT); 
  analogWrite(BLUE, 255);
  pinMode(RED, OUTPUT); 
  digitalWrite(RED, HIGH);
  pinMode(GREEN, OUTPUT); 
  analogWrite(GREEN, 255);

  pinMode(HP, OUTPUT); 
  digitalWrite(HP, LOW);
  analogWrite(HP, 0);
}

//------------------------------------------------------------------------------------------------
void loop() { 
  while (DigiUSB.available()) {
    char c = DigiUSB.read();
    switch(c) {
        case '0': case '1': case '2':
        case '3': case '4': case '5':
        case '6': case '7': case '8':
        case '9':
          value = 10*value + c-'0';
          break;
        case 't':
          if (value) {
            tone(HP, value);
          }
          else {
            noTone(HP);
          }
          value=0;
          break;
        case 'b':
          analogWrite(BLUE, 255 - value);
          value = 0;
          break;
        case 'r':
          digitalWrite(RED, value > 127 ? LOW : HIGH);
          value = 0;
          break;
        case 'g':
          analogWrite(GREEN, 255 - value);
          value = 0;
          break;
    }
  }

  if (millis() - timing > 500) {
    timing = millis();
    current = digitalRead(TILT);

    //DigiUSB.println(current == HIGH ? "HIGH" : "LOW");

    if (current != last) {
      DigiUSB.print("Position to ");
      DigiUSB.println(current ? "horizontal" : "vertical");
      last = current;
    }
  }

  DigiUSB.delay(100);
}

Pour communiquer avec le Tilt, j'utilise digiterm qui s'installe simplement avec gem install digiusb (après un apt-get install python).

Tilt Digiterm

Logiciel utilitaire

Pour la rotation automatique de l'écran il faut un petit programme qui écoute le Tilt et réagit en conséquence. Nous allons utiliser la commande digiterm du package ruby digiusb :

apt-get install ruby`ruby -e 'puts RUBY_VERSION[/\d+\.\d+/]'`-dev
gem install digiusb

Nous allons aussi utiliser 2 scripts pour gérer la rotation :

Le premier script shell permet de choisir l'écran sur lequel est connecté le Tilt.

/usr/local/bin/setup_pivot.sh

#!/bin/sh

SCREENS=`xrandr --query | grep connected | cut -d ' ' -f 1 | paste - -`

if CHOICE=`zenity --list --title "Select screen attached to the Tilt" --column "Screen name" $SCREENS`; then
   echo -n $CHOICE | tee /etc/pivot.conf
else
   echo "No choice"
fi

Le second script gère la logique de rotation :

/usr/local/bin/manage_pivot.sh

#!/bin/bash

mkfifo ~/.pivot.sock

while true; do

   unbuffer cat ~/.pivot.sock | unbuffer digiterm --raw | while read orientation; do
      echo "=== $orientation +++"
      echo $orientation
      if [[ $orientation =~ "horizontal" ]]; then
     echo "*** Rotate 0 ***"
     xrandr --output `cat /etc/pivot.conf` --rotate normal
      elif [[ $orientation =~ "vertical" ]]; then
     echo "*** Rotate 90 ***"
     xrandr --output `cat /etc/pivot.conf` --rotate left
      fi
   done

   sleep 1s

done

Attention, il faut permettre à l'utilisateur courant d’accéder au périphérique USB en créant un fichier UDEV :

/etc/udev/rules.d/49-micronucleus.rules

# UDEV Rules for Micronucleus boards including the Digispark.
# This file must be placed at:
#
# /etc/udev/rules.d/49-micronucleus.rules (preferred location)
# or
# /lib/udev/rules.d/49-micronucleus.rules (req'd on some broken systems)
#
# After this file is copied, physically unplug and reconnect the board.
#
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="0753", MODE:="0666"
KERNEL=="ttyACM*", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="0753", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05df", MODE:="0666"
KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05df", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1"
#
# If you share your linux system with other users, or just don't like the
# idea of write permission for everybody, you can replace MODE:="0666" with
# OWNER:="yourusername" to create the device owned by you, or with
# GROUP:="somegroupname" and mange access using standard unix groups.

puis udevadm control -R pour recharger les règles UDEV.

Il faut lancé ce dernier script lors de l'ouverture de la session utilisateur (panneau de configuration du bureau : Applications au démarrage).

Améliorations possible

Il serait bon de faire évoluer le produit :

  • sécuriser le système : shell escap, service en root mais commande lancée pour l'utilisateur
  • faire un boîtier en impression 3D (led visible à travers le boîtier).
  • utiliser un accéléromètre plutôt que le détecteur d'orientation, ce qui permettrai d'utiliser le Tilt dans n'importe quel sens (en fonction du port USB sur l'écran).
  • gérer le multi-écran, avec un Tilt par écran.
  • développer une vrai interface graphique avec fonction d'apprentissage de l'orientation des écrans.
  • redévelopper le service et outils en un seul programme en réduisant la dépendance aux outils externes (librandr plustôt que la commande xrandr...).
  • lier les notifications du bureau avec le Tilt (led, buzzer).
  • rendre le tilt fonctionnel sur l'écran de login.
  • packager le système (DEB, RPM...).

Le script schell, utilisant la commande digiterm ne fonctionne pas avec la socket ~/.pivot.sock et ne permet donc pas d'envoyer des commandes (couleur, son) au Tilt. La redirection de l'entrée standard ne semble pas être prise en compte par digiterm.

J'ai donc commencé à développer une commande autonome pour tout gérer (communication avec le Tilt, gestion de la rotation avec la bibliothèque X) :

/home/antoine/workspace/tilt/digiint.cpp

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <usb.h>
#include <sys/stat.h>
#include <termios.h>
#include <X11/extensions/Xrandr.h>

char horizontal_config[256] = "";
char vertical_config[256] = "";
char command_pipe[256] = "";

void switch_position(const char *from, const char *to) {
    Display *disp;
    Window win;
    XRRScreenResources *screen;
    XRROutputInfo *output_info;
    XRRCrtcInfo *crtc_info;
    int iscres;
    int ioutput;
    int irotation;

    disp = XOpenDisplay(getenv("DISPLAY"));
    win = DefaultRootWindow(disp);
    screen = XRRGetScreenResources(disp, win);

    if (FILE *positions = fopen(from, "w")) {
    for (ioutput = 0; ioutput < screen->noutput; ioutput++) {
        output_info = XRRGetOutputInfo(disp, screen, screen->outputs[ioutput]);
        crtc_info = XRRGetCrtcInfo(disp, screen, output_info->crtc);

        fprintf(positions, "%s ==> %dx%d+%dx%d rot=%d\n", output_info->name, crtc_info->x, crtc_info->y, crtc_info->width, crtc_info->height, crtc_info->rotation);

        XRRFreeCrtcInfo(crtc_info);
        XRRFreeOutputInfo(output_info);
    }

    fclose(positions);
    }


    if (FILE *positions = fopen(to, "r")) {
    int x, y, w, h, rotation;
    char name[255];

    int width    = 4096;
    int height   = 4096;
    float dpi    = (22 * DisplayHeight(disp, 0)) / DisplayHeightMM(disp, 0);
    int widthMM  = (int) ((22 * width ) / dpi);
    int heightMM = (int) ((22 * height) / dpi);

    XRRSetScreenSize(disp, win, width, height, widthMM, heightMM);

    for (ioutput = 0; ioutput < screen->noutput; ioutput++) {
        output_info = XRRGetOutputInfo(disp, screen, screen->outputs[ioutput]);
        crtc_info = XRRGetCrtcInfo(disp, screen, output_info->crtc);

        fscanf(positions, "%s ==> %dx%d+%dx%d rot=%d\n", name, &x, &y, &w, &h, &rotation);

        XRRSetCrtcConfig(disp, screen, output_info->crtc, CurrentTime, x, y, crtc_info->mode, rotation, crtc_info->outputs, crtc_info->noutput);

        XRRFreeCrtcInfo(crtc_info);
        XRRFreeOutputInfo(output_info);
    }

    fclose(positions);
    }

    XRRFreeScreenResources(screen);

}

void action(char *command) {
   printf(" => trigger %s", command);
   if (strstr(command, "to horizontal")) {
      printf("  -> switch to horizontal position\n");
      switch_position(vertical_config, horizontal_config);
   } else if (strstr(command, "to vertical")) {
      printf("  -> switch to vertical position\n");
      switch_position(horizontal_config, vertical_config);
   } else {
      printf("  -> UNKNOWN command\n");
   }
}

int main (int argc, char **argv)
{
  bool debug = argc > 1 && (strcmp(argv[1], "-d") == 0); 
  bool sendLine = false;
  int arg_pointer = 1;

  struct usb_bus *bus = NULL;
  struct usb_device *digiSpark = NULL;
  struct usb_device *device = NULL;

  static struct termios old, nouveau;

  char message[256] = "";
  int message_pos = 0;

  strcat(strcpy(horizontal_config, getenv("HOME")), "/.tilt_horizontal.conf");
  strcat(strcpy(vertical_config, getenv("HOME")), "/.tilt_vertical.conf");
  strcat(strcpy(command_pipe, getenv("HOME")), "/tilt_command.pipe");

  mkfifo(command_pipe, 0600);

  tcgetattr(0, &old);
  nouveau = old;
  nouveau.c_lflag &= ~ICANON;
  nouveau.c_lflag &= ~ECHO;
  nouveau.c_cc[VTIME] = nouveau.c_cc[VMIN] = 0;
  tcsetattr(0, TCSANOW, &nouveau);

  //-------------------------------------------------------------
  // Search the DigiSpark card on all USB port
  //-------------------------------------------------------------
  usb_init();

  usb_find_busses();
  usb_find_devices();
  bus = usb_get_busses();

  while (bus != NULL) {

    device = bus->devices;

    while (device != NULL) {
      // Check to see if each USB device matches the DigiSpark Vendor and Product IDs
      if (((device->descriptor.idVendor == 0x16c0) && (device->descriptor.idProduct == 0x05df)) || ((device->descriptor.idVendor == 0x16d0) && (device->descriptor.idProduct == 0x0753)))
    digiSpark = device;

      device = device->next;
    }

    bus = bus->next;
  }

  if (digiSpark == NULL) {
    printf("No Digispark Found\n");
      return 1;
  } else {
    printf("Digispark found : idVendor=%x, idProduct=%x\n", digiSpark->descriptor.idVendor, digiSpark->descriptor.idProduct);
  }


  //-------------------------------------------------------------
  // Get the data stream interface
  //-------------------------------------------------------------
  int result = 0;

  int numInterfaces = 0;
  struct usb_dev_handle *devHandle = NULL;
  struct usb_interface_descriptor *interface = NULL;

  devHandle = usb_open(digiSpark);

  if (devHandle != NULL) {
    /*result = usb_set_configuration(devHandle, digiSpark->config->bConfigurationValue);
    if(result < 0) {printf("Error %i setting configuration to %i\n", result, digiSpark->config->bConfigurationValue); return 1;}*/

    numInterfaces = digiSpark->config->bNumInterfaces;
    interface = &(digiSpark->config->interface[0].altsetting[0]);

    //if(debug) printf("Found %i interfaces, using interface %i\n", numInterfaces, interface->bInterfaceNumber);

    /*result = usb_claim_interface(devHandle, interface->bInterfaceNumber);
    if(result < 0) { printf("Error %i claiming Interface %i\n", result, interface->bInterfaceNumber); return 1;}*/
    }

  //-------------------------------------------------------------
  // Communicate with the DigiSpark card in both directions
  //-------------------------------------------------------------
  char c = 0, car;
  char thechar = ' ';

  printf("Open FIFO '%s'\n", command_pipe);

  if (int fifo = open(command_pipe, O_RDONLY | O_NONBLOCK)) {
    printf("Reading FIFO...\n");

    while (c != 3) {
      if (debug) printf("=\n");
      car = read(fifo, &c, 1);
      if (debug) putchar('.');

      while (car != EOF && car > 0) {
    putchar(c);
    result = usb_control_msg(devHandle, (0x01 << 5), 0x09, 0, c, 0, 0, 1000);

    if (result < 0) {
      printf("Error %i writing to USB device\n", result);
      return 1;
    }
    car = read(fifo, &c, 1);
    if (debug) putchar('_');
      }

    if (debug) putchar('-');
    thechar = ' ';

    while (thechar != 0) {
      thechar = 0;

      result = usb_control_msg(devHandle, (0x01 << 5) | 0x80, 0x01, 0, 0, &thechar, 1, 1000);
      if (result >= 0) {
        if (thechar) {
          putchar(thechar);
          message[message_pos++ % 255] = thechar;
          if (thechar == '\n') {
             message[message_pos] = 0;
             action(message);
             message[message_pos = 0] = 0;
          }
        } else
           if (debug) putchar('~');
      } else
         if (debug) putchar('?');
    }
    if (debug) putchar('\n');

      usleep(100000);
    }

    close(fifo);
  } else {
     printf("Can't open the %s fifo file !\n", command_pipe);
  }

  //-------------------------------------------------------------
  // Close the card interface
  //-------------------------------------------------------------
  result = usb_release_interface(devHandle, interface->bInterfaceNumber);

  if (result < 0) {
    printf("Error %i releasing Interface 0\n", result);
    return 1;
    }

  usb_close(devHandle);

  printf("Bye !\n");

  return 0;
}

A débugger.

A bientôt

Antoine

Ecrire à l'auteur