Bjørn Einar strømmer data [Luke 22, 2012]


lørdag 22. desember 2012 Julekalender JavaScript Node.js

Bjørn Einar Bjartnes (@bjartnes) jobber sammen med Einar W. Høst (som dere møtte 3. desember) i Computas, og er en spennende fyr med spennende interesser. Du kan lese om hva han holder på med på bjartwolf.com.

I dagens luke, på lille, lille julaften, drar Bjørn Einar frem Node.js og sitt radiostyrte helikopter, og snakker om å strømme data.

beb

Hvem er du?
Automasjonsingeniør fra oljebransjen som har gått i land og begynt å kode

Hva er jobben din?
Overingeniør hos Computas AS, der jeg blant annet er fagnettverksleder for Samhandling og portaler. Utvikler stort sett SharePoint-løsninger for olje- og gassbransjen.

Hva kan du?
Jobber nå mest med SharePoint prosjekter, så jeg har mest fokus på å bruke SharePoint som produkt i samspill med andre typer webløsninger.

Hva liker du best med yrket ditt?
Vi er en gjeng på jobben som brenner for programmering litt utover det normale. Elsker å henge med dem og kode sære ting for moro skyld.


Omfavn strømmene!

Jeg har fått skikkelig dilla på strømmer i det siste.

Jeg har ingen helt presis definisjon på hva strømmer er, det kan bety litt forskjellige ting i forskjellige sammenhenger, alt fra IO-baserte strømmer til doven evaluering av sekvenser. Det har stort sett å gjøre med data som kommer over tid, i en strøm, som i en vannslange eller på et samlebånd. Jeg har hørt det omtalt som å jobbe med data i bevegelse, og synes det er en god beskrivelse.

Mange vil kjenne strømmer fra IO, der man leser og skriver strømmer til og fra konsollet og filer. Da jeg begynte å programmere tenkte jeg mest på strømmer som noe som kom i veien. Jeg skal egentlig bare ha innholdet i en fil, men er nødt til å forholde meg til en strøm av data. Hvorfor kan jeg ikke bare få innholdet fra filen? Etter at jeg begynte å leke med Node.js forstod jeg plutselig at strømmer egentlig er morsomme å jobbe med.

Istedenfor å bruke mye plass på å forklare forskjellige strømmer, så tenkte jeg heller å viste et eksempel som jeg liker.

Eksempelet er delt i to. I det første eksempelet vil jeg logge navigasjonsdata fra et quadcopter (eller drone på godt norsk) til en zip-fil. Det andre eksempelet leser tilbake fra zip-filen og skriver navigasjonsdata ut til konsollet. Hensikten min er å kunne logge data fra noen ekte flyturer og så kunne spille dem tilbake, så jeg kan teste ut forskjellige programmer til quadcopteret uten å faktisk fly det.

CycloneCloseup1

Helt tilfeldig valgt bilde av et Quadcopter

Noen ord om dette quadcopteret er på sin plass. Den kommer med et åpent API i C, som igjen har en Node.js modul som eksponerer APIet gjennom enkle funksjoner. Dronen setter opp sitt eget trådløse nett, som man kan koble seg til. Data fra dronen kommer som eventer i JavaScript. Disse eventene, navdata eventer, innholder data som status på dronen, verdier på måleinstrumenter osv. En kan også skrive kommandoer tilbake for å styre dronen, men det sparer jeg til en senere gang.

Strømme data til fil

Målet her er altså å gjøre om vanlige eventer til en strøm av data, slik at det kan pipes videre inn i moduler som forstår strømmer. Det artige er at dronen vil begynne å skrive til en zip-fil mens den fortsatt flyr. Det er dette jeg har illustrert i tegningen (nedenfor), der man ser eventene komme inn i den den gule roboten som symboliserer ar-drone-modulen; det serialiseres, pakkes og skrives til fil etterhvert som pakkene kommer inn.

100002010000034A00000254A369A25C 

var arDrone = require('ar-drone');
var gzip = require('zlib').createGzip();
var bacon = require('baconjs').Bacon;
var Stream = require('stream');
var fs = require('fs');
var drone = arDrone.createClient();

// Lager en strøm av eventer fra navdata
var eventStream = bacon.fromEventTarget(drone, 'navdata');

// Lager en strøm for å skrive ut JSON-data og ny linje for
// hvert navdata-event fra dronen
var jsonStream = new Stream();
jsonStream.readable = true;
eventStream.onValue(function(x) {
    jsonStream.emit('data', JSON.stringify(x) +"\n");
});

// Kobler alle strømmene sammen 
jsonStream.pipe(gzip).pipe(fs.createWriteStream('logdata2.gz'));

15 linjer kode for å snakke med et quadcopter og skrive dataene i sann tid til en komprimert fil som JSON. Noen ganger er Node.js veldig moro.

Strømme data fra en fil

Hensikten med dette lille programmet er å spille tilbake logfilene i ønsket hastighet. Dette kan være nyttig om man ønsker å teste ut noe kode som skal lese fra helikopteret uten å fly, lade, kræsje etc. Planen er å lese tilbake ett og ett datapunkt hvert 10 ms.

Her tenkte jeg å vise noen spesielle mekanismer for strømmer. Det ene er å bremse strømmer - i Node.js gjøres dette via en mekanisme kalt backpressure. Det gjør at om en strøm ikke klarere å ta unna data fort nok, vil de strømmene som sender data til den opptatte strømmen vente til de får beskjed om at strømmen er klar til å ta imot mer. Alt som trengs er at en strøm returnerer false når den blir skrevet til for å signalisere at den ikke kan ta unna mer, og at den sender eventet drain når den er klar for mer.

Her kunne en tenke seg å bruke timestamps e.l, men for enkelhets skyld spiller jeg tilbake hvert 10 millisekund. Jeg laget den trege strømmen i en egen node-modul:

var Stream = require('stream');

// Her lager vi en stream som bremser hver gang den får data
// og venter litt med å si at den er klar igjen
// Jeg ser på det som et skikkelig trangt rør

var SlowStream =function (delayInMs) {
    var slowStream =new Stream();
    slowStream.writable =true;
    slowStream.write =function(val) {
        slowStream.emit('data', val);

        // Hvis en stream returnerer false, vil alle
        // oppstrøms vente til den emitter drain

        setTimeout(function () {
            slowStream.emit('drain');
        } , delayInMs);

        return false; 
    };

    slowStream.end =function(val) { slowStream.emit('end'); };
    return slowStream;
}

module.exports = SlowStream;

En annen mekanisme jeg bruker er fra rammeverket bacon.js. Bacon lar meg lage eventstrømmer, og gir et sett av operasjoner jeg kan gjøre på disse strømmene. Her bruker jeg map og filter. Filter fungerer som filter ellers, ypperlig forklart i kjempekjekt-bloggen tidligere i år. Den tar en predikat, og kun data som tilfredstiller predikatet får slippe videre.

Map fungerer også som ellers, tar ett og ett element, transformerer det, og sender det videre. Den eneste forskjellen fra tradisjonell map og filter her er at det brukes på data som ennå ikke finnes. Filter bruker jeg for å kun hente ut de navdata-verdiene hvor dronen er i flymodus og høyden er over 1 cm. Jeg bruker map her for å hente ut noen få verdier; for hvert nav-data element, som inneholder ganske mye data, så vil jeg ha et nytt objekt som kun innneholder høyde og retning på dronen.

Jeg bruker også en strøm, linestream, som streamer filer linje for linje basert på en filstrøm. Totalt sett så er flyten i programmet som følger:

  1. Les fra fil
  2. Pipe det til unzippedstream
  3. Pipe det igjennom en linjestrøm
  4. Pipe det igjennom en treg strøm
  5. Map en JSON parser på strømmen
  6. Filtrer ut elementene der dronen flyr over 1 cm høyt
  7. Plukk ut (ved hjelp av map) et par verdier vi er interesserte i
  8. Skriv disse dataene ut til konsollet
var linestream = require('linestream');
var SlowStream = require('./slowStream.js');
var bacon = require('baconjs').Bacon;
var unzip = require('zlib').createGunzip();
var fs = require('fs');

var unzippedStream = fs.createReadStream('logdata.txt.gz').pipe(unzip);
var slowStream =new SlowStream(10);
linestream.create(unzippedStream).pipe(slowStream);

// Lager en eventstrøm av den treige strømmen 
var navDataStream = bacon.fromEventTarget(slowStream, 'data');

// Strømmen må parses fra JSON daten i filen
// Det finnes biblioteker for å gjøre dette som en strøm, men de respekterer
// ikke backpressure riktig
var parsedStream = navDataStream.map( function (val) {
    return JSON.parse(val);
});

// Tar en strøm av navdata og returnerer kun data der dronen er i flymodus og 
// høyden er over 1 cm
var flyingStream = parsedStream.filter(function (navdata) {
    return navdata.droneState.flying ===1&& navdata.demo.altitudeMeters >0.01;
});

// Tar inn navdata og returner et objekt med høyde og retning
var heightAndAltitudeStream = flyingStream.map(function (navdata) {
    return { height: navdata.demo.altitudeMeters,
             direction: navdata.demo.clockwiseDegrees }; 
});

heightAndAltitudeStream.onValue( function (x) { console.log(x); });

Quadcopter log from Torbjørn Marø on Vimeo.

Se også video av Bjørn Einar som flyr quadcopteret sitt.

En liten avslutning

Dessverre betaler ikke kundene mine meg særlig godt for å leke med quadcopteret mitt. Om du ikke allerede har en drone i hus, bør du selvfølgelig ønske deg en til jul. Kanskje klør du i fingrene etter noe å bruke strømmer av data til som er litt mer praktisk, og da er kanskje ikke dette det mest matnyttige eksempelet. I så fall kan jeg anbefale å ta en titt på Reactive Extensions for JavaScript eller .NET, eller kanskje en litt mer konkret anvendelse, som Reactive UI.

Måtte de harde pakkene strømme til et juletre nær deg.

God jul!


Noen flere lenker: Bjørn Einar's reactive-drone prosjekt | Streams intro for Node | Gratis Rx-bok | Kjøp din egen drone | Node-modulen til AR dronen | Bra Rx-presentasjon på YouTube


comments powered by Disqus