Téléchargement

secondscreen_setup.exe

Malgré l'effort fourni pour exploiter au mieux les capacités de la bestiole, ceci reste du bon gros hack. D'éventuelles instabilités liées aux flux de données et autres paquets mal traités peuvent occasionner déconnexions et plantages au niveau du LiveView

Source incluse


Trouvée au hasard d'un magasin d'occasions, voici un vestige de l'ère pré-smartwatch, j'ai nommé le Sony Ericsson LiveView (2011). Aucune mémoire interne, pas sous Android mais conçu pour fonctionner avec, Bluetooth non LTE. Pas vraiment tactile non plus. Bref, que du bonheur, et dans mon cas la batterie étant dead, mon champ de possibilités d'utilisations se retrouve réduit à un branchement permanent secteur (USB). Rien pour nous décourager cependant.

La recherche concernant le dev LiveView m'amène à cette documentation (pdf) ainsi qu'à la LiveViewLib (C#), amalgame aboutit de plusieurs projets dont LiveViewNet. Pas de grand reverse-engineering à faire aujourd'hui.

Second Screen, un premier projet

L'écran en lui-même fait un beau 128pixels au carré. Autant exploiter toute sa surface pour afficher des images. Voici donc la logique à intégrer:

  • Se connecter au bouzin
  • Le forcer à rester allumé (l'écran s'éteint automatiquement après quelques secondes)
  • Envoyer l'image
  • Profit
  • (Recommencer?)

Connexion

Assez simple, se fait grâce à l'indispensable librairie 32Feet de InTheHand niveau DentBleue.

blah, blah...

server = new LiveViewServer();
server.connectListener += new LiveViewServer.ConnectListener(server_connectListener);
try{
    server.Start();
...

Une fois connecté, un petit handler pour envoyer les premières commandes:

server.connectListener += (l) =>{
    l.Send(new GetTimeResponse(GetTimeResponse.GetLocalTime(), true)); //plus stable quand synchronisé avec l'heure dès le départ
    l.Send(new SetMenuSizeMessage(0)); //on ne peut envoyer du bitmap que quand il n'y a aucun item de menu
    l.SendBytes(wakeUp); //pour le maintenir allumé

j'ai été obligé d'ajouter une fonction SendBytes à la librairie pour envoyer des données brutes car la fonction screenOn, offerte par le SDK officiel, semblait ne pas avoir été implémentée. En effet, en envoyant

l.Send(new SetScreenmodeMessage(0, Screenmode.BRIGHTNESS_OFF));
l.Send(new SetScreenmodeMessage(0, Screenmode.BRIGHTNESS_DIM));
l.Send(new SetScreenmodeMessage(0, Screenmode.BRIGHTNESS_MAX));

soit

// LiveViewLib.MessageTypes.SetScreenmodeMessage
// 40-04-00-00-00-01-61
// LiveViewLib.MessageTypes.SetScreenmodeMessage
// 40-04-00-00-00-01-63
// LiveViewLib.MessageTypes.SetScreenmodeMessage
// 40-04-00-00-00-01-65

l'écran finissait toujours par s'éteindre.

j'ai alors décidé d'envoyer les paquets en Byte[], faisant varier l'octet de la luminosité (ici 0x61, 0x63 et 0x65). Quitte a jouer avec les valeurs, j'ai par hasard envoyé la 0x64 et ai découvert avec joie que cela marchait. Bug ou octet valide, nous avons désormais un paquet wake-up + always on.

readonly byte[] wakeUp = { 0x40, 0x04, 0x00, 0x00, 0x00, 0x01, 0x64 };

Envoyer une image

Comme spécifié par la doc, envoyer de petites images comme du 48x48, 36² ou 16² ne pose pas de problème, toutefois je n'ai jamais réussi à faire passer une 128² en voie normale. J'ai donc créé une fonction qui:

  • Prend l'image d'entrée
  • La centre (fit & crop) sur du 128x128 max
  • La découpe en blocs de 32x32
  • Affiche les blocs aux coordonnées correspondantes
Image img = Image.FromFile("lenna.jpg");
//resize, crop and fit to lv size thus sq^128
int w = img.Width < img.Height ? 128 : img.Width * 128 / img.Height;
int h = img.Height < img.Width ? 128 : img.Height * 128 / img.Width;
//center pic
int dW = w > 128 ? (w - 128) / 2 : 0;
int dH = h > 128 ? (h - 128) / 2 : 0;
img = img.GetThumbnailImage(w, h, null, IntPtr.Zero);

//random tile order
var mosaic = Enumerable.Range(0, 16).OrderBy(n => Guid.NewGuid());
foreach (int tile in mosaic) //split in 32*32 tiles to avoid 128*128 buffer f*ckery
{
    int y = tile / 4; //clockwise
    int x = tile % 4;
    Image o = new Bitmap(32, 32);
    using (Graphics g = Graphics.FromImage(o))
    {
        g.DrawImage(img, new Rectangle(0, 0, 32, 32), new Rectangle(x * 32 + dW, y * 32 + dH, 32, 32), GraphicsUnit.Pixel);
    }
    l.Send(new DisplayBitmapMessage((byte)(x * 32), (byte)(y * 32), imageToByteArray(o))); //sending the puzzle piece
}
img.Dispose();

L'image apparaît ainsi à la manière du bon vieil écran de veille Mes Images de Windows XP.

Maintenant que nous pouvons afficher une image convenablement nous ne sommes qu'à quelques lignes de transformer le LiveView en cadre photo numérique.

//getting all my jpg pictures as shuffled
var pics = Directory.EnumerateFiles(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "*.jpg", SearchOption.AllDirectories).OrderBy(n => Guid.NewGuid()).ToList();

Text = " Photo Frame Mode ("+pics.Count+" pictures)"; //connection ok
for (int i = 0; i < pics.Count; i++)
{
    Image img = Image.FromFile(pics[i]);
    //resize, crop and fit to lv size thus sq^128
    int w = img.Width < img.Height ? 128 : img.Width * 128 / img.Height;
    int h = img.Height < img.Width ? 128 : img.Height * 128 / img.Width;
    //center pic
    int dW = w > 128 ? (w - 128) / 2 : 0;
    int dH = h > 128 ? (h - 128) / 2 : 0;
    img = img.GetThumbnailImage(w, h, null, IntPtr.Zero);

    //random tile order
    var mosaic = Enumerable.Range(0, 16).OrderBy(n => Guid.NewGuid());
    foreach (int tile in mosaic) //split in 32*32 tiles to avoid 128*128 buffer f*ckery
    {
        int y = tile / 4; //clockwise
        int x = tile % 4;
        Image o = new Bitmap(32, 32);
        using (Graphics g = Graphics.FromImage(o))
        {
            g.DrawImage(img, new Rectangle(0, 0, 32, 32), new Rectangle(x * 32 + dW, y * 32 + dH, 32, 32), GraphicsUnit.Pixel);
        }
        l.Send(new DisplayBitmapMessage((byte)(x * 32), (byte)(y * 32), imageToByteArray(o))); //sending the puzzle piece
    }
    img.Dispose();

    Thread.Sleep(15 * 1000);
}
Environment.Exit(0);

S'en servir comme écran de projection

Avec la possibilité d'y afficher l'image de notre choix, donnons lui des ambitions de VNC en lui donnant la possibilité d'afficher une humble partie de notre écran.

Nous créons donc une fenêtre (Form) dont la zone interne (Client) sera de 128*128, y plaçons dans le coin supérieur gauche une picturebox invisible pour nous en servir de repère absolu sur l'écran. Le fond est rendu invisible à l'aide de l'astuce du magenta.

        public Image grabScreenPart()
        {
            Image img = new Bitmap(128,128);
            using(Graphics g = Graphics.FromImage(img)){
                g.CopyFromScreen(captureBox.X, captureBox.Y, 0, 0, new Size(128, 128));
            }
            return img;
        }

et, dans une boucle, nous faisons donc un snapshot de la zone de l'écran contenue dans le client de la Form, avant de l'envoyer vers le LiveView. Ceci toutes les 15 secondes (notez le framerate de compet').

            for(;;)
            {
                if (isMoving) continue; //don't refresh when form moved to avoid overflow

                Image curPic = grabScreenPart();
                var mosaic = Enumerable.Range(0, 16); //.OrderBy(n => Guid.NewGuid());
                foreach (int tile in mosaic) //split in 32*32 tiles to avoid 128*128 buffer f*ckery
                {
                    int y = tile / 4; //clockwise
                    int x = tile % 4;
                    Image o = new Bitmap(32, 32);
                    using (Graphics g = Graphics.FromImage(o))
                    {
                        g.DrawImage(curPic, new Rectangle(0, 0, 32, 32), new Rectangle(x * 32, y * 32, 32, 32), GraphicsUnit.Pixel);
                    }
                    liveView.Send(new DisplayBitmapMessage((byte)(x * 32), (byte)(y * 32), imageToByteArray(o))); //sending the puzzle piece
                }

                System.Threading.Thread.Sleep(15*1000); //resting a bit
            }

Toute la zone d'écran contenue dans la fenêtre sera donc affichée sur la montre. Pratique pour suivre quelque information capitale quand on est dans la pièce d'à côté!

Aller plus loin: intégration au lecteur MusicPeek

Afin d'exploiter au mieux l'appareil, une fois connecté à l'ordinateur avec MusicPeek en cours de lecture, celui-ci affichera sur le LiveView la pochette du morceau en cours, et on pourra passer d'une piste à l'autre en swipant l'écran tactile. Pour des raisons encore obscures nous avons une fuite de mémoire une fois l'appareil lié, mais j'espère résoudre ce problème dans une build future.

Article précédent Article suivant