Category Archives: programmeren

Het opzetten van nginx op os-x

Introductie
Nu we bezig zijn met het opzetten van een nieuwe server, vond ik het een mooi moment een oud flask/sqlalchemy weer wat nieuw leven in te blazen. Dit projectje is volgens de git-logs een jaar geleden tot stilstand gekomen, vlak voordat ik een eerste versie in productie wilde zetten. Ik had toen geen zin (want tijd maak je wel als je zin hebt) om uit te zoeken hoe dat exact moest en daarna is het in de dagelijkse drukte ondergesneeuwd.  Maar die nieuwe server was een mooie aanleiding om een maandagavond op te offeren om er eens goed voor te gaan zitten.

Flask en nginx
Flask levert een ingebouwde server mee, maar die is niet geschikt voor productiedoeleinden. Je kunt met mod_wsgi apache wel configureren om flask te serven, maar gebruikelijk is het om hiervoor de server ngnix te gebruiken. Ik had al bedacht dat als ik de applicatie via ngnix op poort 8080 (of zo) zou kunnen laten draaien, dat het dan relatief eenvoudig zou moeten zijn om dat via apache weer door te sluizen (maar dat is voor later zorg).

Voordat ik deze architectuur op de productieserver ging opzetten, wilde ik eerst eens alle noodzakelijke stappen op mijn eigen mac uitproberen en documenteren (en deze blog schrijven). Ik had al geruime tijd geleden een vrij goede tutorial gevonden, maar die is wel bedoeld voor Ubuntu en ook op details wat achterhaald. Dus ben ik zelf maar aan de slag gegaan, met die tutorial als basis.

Vreemd genoeg kon ik geen package manager voor OS-X vinden die ngnix kon downloaden. Zowel pip3 als easy_install als brew gaven aan dat ze geen distributie hiervoor konden vinden. Dus toen maar gewoon de tarball gedownload en zelf de boel installeren. De eerste configuratie ging echter mis, omdat ik iets vergeten was:

./configure --sbin-path=/usr/local/nginx/nginx  --conf-path=/usr/local/nginx/nginx.conf   --pid-path=/usr/local/nginx/nginx.pid   --with-http_ssl_module

./configure: error: the HTTP rewrite module requires the PCRE library.
You can either disable the module by using --without-http_rewrite_module
option, or install the PCRE library into the system, or build the PCRE library
statically from the source with nginx by using --with-pcre=<path> option.

Ook best logisch, want zoals de meeste webservers maakt ook nginx uitgebreid gebruik van reguliere expressies voor rewrite en dergelijke. Het is mogelijk om nginx te installeren zonder pcre, maar het is beter om ze wel mee te nemen. Dus eerst dat maar installeren, waarbij het wel van belang is dat je onthoudt wat je als prefix meegeeft, want die heb je later weer nodig bij de configuratie van nginx zelf:

$ cd pcre-8.41
$ ./configure --prefix=/usr/local
$ sudo make install

Nu kunnen we nginx goed configureren:

$ ./configure  --sbin-path=/usr/local/nginx/nginx --conf-path=/usr/local/nginx/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --with-http_ssl_module --with-pcre=../pcre-8.41
$ make
$ sudo make install

Als je de boel dan wilt opstarten, kun je een foutmelding verwachten: nginx luistert standaard naar poort 80, maar ik heb (zoals de meeste mensen) ook apache al draaien die ook naar die poort luistert:

$ sudo /usr/local/nginx/nginx
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (48: Address already in use)
nginx: [emerg] still could not bind()

Eenvoudig de configuratie aanpassen, waarbij je de poort naar 8080 zet; dan werkt het.

nginx en uWSGI
In principe kan nginx alleen maar statische bestanden serven. Om python uit te kunnen laten voeren en het resultaat daarvan te serven heb je uWSGI nodig. Vreemd genoeg kon ik die wel gewoon met pip3 installeren:

$ pip3 install uwsgi
Collecting uwsgi
Downloading uwsgi-2.0.15.tar.gz (795kB)
100% |████████████████████████████████| 798kB 712kB/s
Installing collected packages: uwsgi
Running setup.py install for uwsgi ... done
Successfully installed  uwsgi-2.0.15
$

Voor deze test had ik een eenvoudige flask applicatie geschreven in de directory /Sites/flask/. Om nginx en uWSGI aan elkaar te knopen, veranderde ik eerst opnieuw het config-bestand van de eerste om hem naar de juiste directory te verwijzen:

server {
  listen       8080;
  server_name  localhost;
  charset utf-8;

  location / { try_files $uri @the_application; }
  location @the_application {
    include uwsgi_params;
    uwsgi_pass unix:/Sites/flask/demoapp_uwsgi.sock;
  }

Nu krijgen we een foutmelding; dat verwachtte ik ook, want dat .sock bestaat nog niet, omdat we uWSGI nog niet geconfigureerd:

De configuratie van uWSGI doen we via een .ini-bestand in de directory waar nginx naar kijkt, dus /Sites/flask/. Hier kopieerde ik voornamelijk de code in die in de tutorial staan, met wat voor zichzelf sprekende aanpassingen. Nu kunnen we de boel opstarten met

sudo uwsgi --ini demoapp_uwsgi.ini

Twee verschillende versies
In principe zou dit moeten werken, maar er gebeurde niks. Pas toen ik de error-logs keek, zag ik dat er bij het opstarten van uWSGI een foutmelding kwam:

ModuleNotFoundError: No module named 'encodings'

Het blijkt dat de python-versie die gelinkt is aan uWSGI dezelfde moet zijn als de versie die op het os draait. Stom genoeg draai ik nog standaard 2.7 (hoewel ik alles allang in 3.6 doe). Het makkelijkste is om even een virtuele omgeving op te zetten met de juiste python-versie en dan van daaruit uWSGI op te starten. Het enige is dat ik dan alle dependencies daar weer in moet installeren, maar dat is voorlopig alleen Flask dus dat valt wel mee.

$ source venv/bin/activate
(venv) argentina-minor:flask bart$ sudo uwsgi --ini demoapp_uwsgi.ini
Password:
[uWSGI] getting INI configuration from demoapp_uwsgi.ini

Nu hebben we eindelijk het gewenste resultaat (let op het poortnummer):

De volgende stap
De volgende stap is dit alles als achtergrondproces te laten draaien, maar dat is een volgend projectje. Ik ga eerst proberen deze zelfde stappen op de productieserver uit te voeren.

De kapotte mac-adapter

Omdat ik maar één oplader voor mijn macbook air heb, neem ik die al een paar jaar opgerold mee in mijn reis tussen Sneek en Groningen en weer terug. Hoewel dat oprollen natuurijk niet zo goed is voor die kabel, functioneerde het apparaat nog altijd prima. De kabel begon de laatste paar maanden aan de adapter-kant wel wat scheuren te vertonen, en tijdens het opladen werden zowel de adapter als de plug bijzonder warm, maar niets om me echt druk over te maken – niets wat een beetje ducttape niet op kon oplossen.

Niets wat een beetje ducttape niet op kan lossen.

Tot afgelopen weekend het opladen echt ophield. De kabelbreuk die ik al langer vermoedde aan de adapterkant was inmiddels blijkbaar zó groot, dat ik er slechts met veel kunst- en vliegwerk (en nog meer ducttape) voor kon zorgen dat mijn air nog opgeladen werd. Tijd voor een echte oplossing.


Fiary dust and unicorns
Bij normale computers en andere elektrische apparaten kun je gewoon de adapter openschroeven en er een nieuw kabeltje in solderen. Of desnoods koop je voor een klein bedrag een nieuwe adapter. Maar Apple heeft in al haar wijsheid besloten om een heel eigen plug te gebruiken voor de stroom-aansluiting van haar laptops (de zogenaamde MacSafe) en het verder volstrekt onmogelijk te maken hier zelf onderhoud aan te plegen. En een nieuwe adapter kost zo’n negentig euro – wat veel geld is om een eenvoudige kabelbreuk op te lossen.

Deze situatie is vergelijkbaar met de vijfhoekige schroeven die gebruikt worden om de laptops dicht te houden, en waar je dus een speciale schroevendraaier voor nodig hebt. Hierover heb ik op stackoverflow wel eens geschreven dat dat is ‘to protect all the unicorns and fairy dust that the machine runs on’. Het business-model van Apple is om redelijk goed spul te maken, maar om het verder onmogelijk te maken hier regulier onderhoud op te plegen: het geheim van de black box zorgt ervoor dat mensen denken dat het allemaal magie is wat er gebeurt; iets waar alleen Apple Geniuses (really?) als een soort magiërs mee om weten te gaan.

Het openen van de adapter
Online had ik een aantal blogs gelezen van mensen die erin waren geslaagd de kabel van de mac-adapter te vervangen. Het is in theorie ook niet zo moeilijk: de enige uitdaging zit hem in het open krijgen van die vermaledijde adapter. Maar ook dat was die mensen uit die blogs gelukt, dus blijkbaar moet het kunnen.

De adapter in de bankschroef en slopen maar.

Om dit voor elkaar te krijgen plaatste ik het ding in de bankschroef en ging met een punttang aan het werk. Initieel was daar geen beweging in te krijgen, maar nadat ik me een Stanley-mes de naden had bewerkt, gaf hij zich zowaar aan één kant gewonnen.

Helaas ging de andere kant minder eenvoudig. Uiteindelijk lukte het me pas na grof geweld, waarbij behalve de punttang en het mes ook een schroevendraaier en zelfs een kleine beitel aan te pas zijn gekomen. het apparaat open te krijgen. Niet alleen de naden bleken vastgelijmd, maar ook de behuizing op de elektronica. Tijdens dit werk had ik al bedacht dat een re-assemblage naderhand onmogelijk zou zijn, en dat ik de elektronica in een geseald boterhambakje zou stoppen – dat leek me ook nog wel grappig.

Eenmaal open bleek de elektronica aan de behuizing te zijn vastgelijmd.

Een eenvoudige oplossing
Het bleek evenwel dat de kabel gewoon twee aders had – niets bijzonders met twitsted pairs of iets vergelijkbaars. En het toeval wilde dat we nog een mac-adapter van een oudere computer (dus, vanzelfsprekend, met een andere MacSafe) in huis hadden. Dus in plaats van solderen kon ik gewoon mijn eigen MacSafe met behulp van een kroonsteentje aan die andere adapter monteren. Het was nog wel even spannend omdat het vermogen van beide adapters verschilde, maar na aansluiting laadde de computer op alsof er niets was gebeurd.

Een elegante en eenvoudige oplossing.

Kortom, in plaats van een nieuwe adapter (wat, behalve dat het geld kost, ook nog indruist tegen mijn principe van hergebruik) heb ik mijn probleem op kunnen lossen met een schaar, een reguliere schroevendraaier en een kroonsteentje van nog geen dubbeltje. Opnieuw blijkt dat weten hoe dingen werken je een bepaalde onafhankelijkheid en vrijheid verschaft. Nu nog eens kijken of ik die batterij in dit ding kan vervangen, want die begint ook wat kuren te vertonen…

De computer laadt weer prima op.

Statistieken uit een whatsapp-groep

Voor een project met een aantal studenten maakten we in het tweede semester van 2016/2017 gebruik van whatsapp voor de onderlinge communicatie. Gedurende dat semester werd er behoorlijk wat heen en weer gewhatsappt, maar toen het project eenmaal voltooid was (het had een behoorlijk strakke deadline in het weekend van 22 juli) droogde die stroom snel op. Omdat ik het jammer zou vinden als al die data verloren zou gaan, besloot ik de hele conversatie naar mezelf te mailen en eens aan een eenvoudige statistische analyse te onderwerpen.

Om de data aan jezelf te mailen kun je gebruik maken van de optie ‘Exporteer chat’ van whatsapp, die je te zien krijgt wanneer je op de gegevens van de whatsapp-groep zelf klikt. Helemaal onderaan, onder de lijst van de deelnemers, zie je deze optie (zie figuur).

De data die je dan opgestuurd krijgt, heeft een eenvoudig formaat: datum en tijd, afzender, en tekst – allemaal gescheiden door een dubbele punt gevolgd door een spate ': '. Op zich zou dit dus eenvoudig in mysql te laden moeten zijn. Hiervoor maakte ik even een tabel chat met drie corresponderende kolommen (genaamd wanneer, wie en wat) en probeerde de data in te laden:

load data local infile 
'/Users/bart/Desktop/wttv/wttv_chat.txt' 
into table chat fields terminated by ': ';

Dit gaf nog wel een zooi errors en warnings, maar belangrijker was dat bij inspectie bleek dat er in het datum-veld dingen als '2024-07-17 17:47:52' terecht waren gekomen. Op de corresponderende regel in de data zelf stond '24-07-17 17:47:52' en uit de context kon ik achterhalen dat dit een bericht betrof dat op 24 juli verstuurd was. Blijkbaar is het datum-formaat dus DD-MM-YY HH:MM:SS.

Het zou natuurlijk een optie zijn geweest om deze data in Excel op te ruimen, maar via deze link op stackoverflow kwam ik er achter dat het ook (eenvoudiger) via mysql zelf kon.

select str_to_date('24-07-17 17:47:52', '%d-%m-%Y %k:%i:%s');
+-------------------------------------------------------+
| str_to_date('24-07-17 17:47:52', '%d-%m-%Y %k:%i:%s') |
+-------------------------------------------------------+
| 2017-07-24 17:47:52                                   |
+-------------------------------------------------------+
1 row in set (0,00 sec)

Dus

load data local infile 
'/Users/bart/Desktop/wttv/wttv_chat.txt' 
into table chat fields terminated by ': ' 
(@var1, wie, wat) set wanneer=str_to_date(@var1, '%d-%m-%Y %k:%i:%s');

Query OK, 1619 rows affected, 271 warnings (0,03 sec)
Records: 1619  Deleted: 0  Skipped: 0  Warnings: 271

Een laatste probleem, wat ook al die foutmeldingen en warnings aan het begin had veroorzaakt, was dat er berichten in de data stonden waar een nieuwe regel in voorkwam; berichten zoals de onderstaande (ik heb de namen van de studenten in deze blog verzonnen):

11-07-17 11:52:34: Henk de Boer: I think it’s really important to see you all this Wednesday at 15:30
We’ll discuss the following:
- Decoration, fencing (Jan)
- Tents! Who has a tent, who needs one?
- What kind of tools does each group needs, how are we going to get them?
- Transportation
- Schedule at WTTV

Het mooiste is om even een scriptje te maken dat over alle regels van het bestand heengaat en dit soort regels tot één bericht samenvoegt: op die manier voorkom je vuile data en hou je de statistieken het meest accuraat. Om dit te bewerkstelligen kunnen eenvoudig kijken of de regel begint met twee cijfers (die van de datum namelijk); als dat niet het geval is, betreft het hoogstwaarschijnlijk een stuk tekst die over de regel is gelopen. Het onderstaande python-scriptje doet precies dat:

buffer = [];
previous_line = '';

with open('wttv_chat.txt') as f:
  for line in f:
    line = line.rstrip()
    if (line[:2].isdigit()):
      print(previous_line + ' '.join(c for c in buffer))
      previous_line = line
      buffer = []

    else:
      buffer.append(line) #buffering lines that belong to the same message

print(line) #otherwise the last line won't be printed.

Het enige probleem is dat er tijdens het festival op een gegeven moment een rooster werd verspreid waarin tijden staan waarop mensen aanwezig moesten zijn. Deze begonnen natuurlijk weer wel met twee cijfers aan het begin van de regel:

20-07-17 12:03:22: Karel de Graaf: VRIJDAG AANWEZIG  14.00 - 1.00
14.00 - 17.00 die-en-die studenten
17.00 - 20.00 andere studenten
20.00 - 22.00 nog een groepje
11.00 - 14.00 enzovoort

Dit gebeurde twee of drie keer, dus ik had het met de hand aan kunnen passen, maar programmatisch is natuurlijk altijd beter. Eenvoudig de check op regel 7 in het python-scriptje uitbreiden met het streepje dat na de timestamp komt:

if (line[:2].isdigit() and line[2]=='-'):

Er kwamen nu bij het laden nog wel een paar waarschuwingen, maar dat bleken opmerkingen te zijn van mensen die aan de groep waren toegevoegd – die konden we dus eenvoudig negeren. Voor de goede orde halen we die (en vergelijkbare) zooi er even uit:

delete from chat where wat is null;

Nu was de data alleszins acceptabel om de statistieken eruit te halen.

STATISTIEKEN
De meest eenvoudige query is natuurlijk om te kijken hoeveel appjes er in totaal zijn verstuurd
Hoeveel appjes zijn er in totaal verstuurd?

mysql> select count(*) from chat;
+----------+
| count(*) |
+----------+
|     3032 |
+----------+
1 row in set (0,00 sec)

Een ander interessant gegeven, dat eveneens makkelijk te achterhalen is, is wie er het meeste appt:

mysql> select count(*) as tot, wie from chat group by wie order by tot desc;

Wie heeft er hoeveel afbeeldingen en hoeveel video’s aan de groep toegevoegd?

mysql> select wie, count(*) from chat where wat like '%afbeelding%' group by wie order by wie;
mysql> select wie, count(*) from chat where wat like '%video%' group by wie order by wie;

Omdat dit een groep zeer gemotiveerde studenten betrof, was ik ook wel nieuwsgierig of je iets van die motivatie terug kon zien in de momenten waarop zoal berichten werden verstuurd. Omdat deze chat maar een half jaar heeft bestaan (ok, hij bestaat nog steeds, maar de analyse gaat over het voorbije half jaar), kon ik eenvoudig een check doen op de weeknummers:

mysql> select week(wanneer) as w, count(*) as total from chat group by w;
+------+-------+
| w    | total |
+------+-------+
|   11 |    12 |
|   13 |    32 |
|   14 |     6 |
|   15 |    24 |
|   16 |    32 |
|   18 |    58 |
|   19 |     4 |
|   20 |    28 |
|   21 |    52 |
|   22 |    12 |
|   23 |   182 |
|   24 |   236 |
|   25 |    16 |
|   26 |    76 |
|   27 |    52 |
|   28 |   776 |
|   29 |  1304 |
|   30 |   130 |
+------+-------+
18 rows in set (0,09 sec)

mysql>

Hier komt wel heel duidelijk naar voren dat week 29 de week is voorafgaand aan de strakke deadline van 22 juli: in deze week zijn verreweg de meeste berichten verstuurd, gevolgd door de week daarvoor. Het is nog interessant om te kijken wat er gebeurde in weken 23 en 24, want daar zijn ook veel meer berichten verstuurd dan gemiddeld (het gemiddelde is 3032/20 = 151 berichten per week, maar als we de uitbijters van week 28 en 29 negeren daalt dat naar zo’n vijftig berichten per week).

Minstens zo interessant is om te kijken naar de dagen waarop berichten worden verstuurd. Hier heb ik even een trucje moeten toepassen om de dagen op een normale (niet-lexicografische) manier gesorteerd te krijgen:

mysql> select dayofweek(wanneer) as d_nr, dayname(wanneer) as dag, count(*) as total from chat group by dag order by d_nr;

En de drukste uren:

mysql> select hour(wanneer) as h, count(*) from chat group by h;

Uit deze data bleek dat er geen enkel uur in het etmaal is waarin er geen bericht is verstuurd: de studenten werkten blijkbaar altijd door.

Maar interessanter is natuurlijk om te zien wie wanneer een berichtje stuurt; daarvoor maakte ik gebruik van de optie om een if-statement in een sum-clause te stoppen, zoals hier beschreven wordt:

mysql>
select hour(wanneer) as h
, sum(if(wie like '%Henk%', 1, 0)) as Henk
, sum(if(wie like '%Karel%', 1, 0)) as Karel
, sum(if(wie like '%Sjaak%', 1, 0)) as Sjaak
, sum(if(wie like '%Margriet%', 1, 0)) as Margriet
from chat
group by h;

Via deze query (althans, de echte versie ervan natuurlijk) konden we mooi achterhalen wie van de groep de nachtbrakers waren en wie de vroege vogels.

Conclusie
Het gebruiken van whatsapp voor onderlinge communicatie werkt heel goed. Het is snel, vertrouwd en makkelijk te gebruiken. Statistieken zoals deze zijn natuurlijk in eerste instantie alleen bedoeld voor de grap, maar wie weet kunnen we het ook eens gaan gebruiken om de inzet van bepaalde studenten te monitoren.

bettercodehub

Inleiding
Voor de ontwikkeling van het nieuwe tweedejaars onderwijs waren we op zoek naar een manier om eenvoudig een redelijk beargumenteerd cijfer te geven op een programmeeropdracht. Tijdens de gesprekken hierover rees het idee om de studenten de kwaliteit van hun code te laten beoordelen en hierop te laten reflecteren. Op die manier zouden ze er niet meer mee wegkomen door eenvoudig iets op te leveren dat werkt, maar zouden ze ook actief iets moeten zeggen over hun code.

Er zijn verschillende code-analysetools, maar de meeste hiervan zijn behoorlijk complex om te installeren, vereisen veel administratieve handelingen, gaan uit van heel strikte eisen die aan de code gesteld worden, of geven zoveel data dat je door de bomen het bos niet meer ziet. Nu was ik al een tijdje in gesprek met mensen van de Software Improvement Group, die met het analyseren van broncode hun brood verdienen. Enige tijd geleden heeft Joost Visser een praatje gehouden voor ons honoursprogramma naar aanleiding van een boekje dat hij vorig jaar heeft geschreven. Dit boekje heeft geresulteerd in de bettercodehub, een automatisch online analysetool die github-repositories op basis van de tien guidelines die Visser in zijn boekje beschrijft.

Analyse
Om eens te kijken hoe deze tool exact werkte, pushte ik mijn funda-harvester naar mijn github account en ging naar bettercodehub, waar ik hem onmiddellijk tussen mijn andere repositories zag staan. Om de code te analyseren hoefde ik alleen maar op het pijltje rechtsonder te klikken. Nadat ik de standaardconfiguratie had geselecteerd werd er een kloon gemaakt van de repo, die werd geanalyseerd en tenslotte weer verwijderd. Het totale proces duurde ongeveer een halve minuut (nu is die harvester niet heel groot, een kleine duizend regels code, maar evengoed is dit behoorlijk snel).

De resultaten worden in een overzichtelijk schema getoond, georganiseerd volgens de tien guidelines en de score bovenaan (Figuur 1). Je kunt op een guideline klikken om de details van de analyse te zien. Aan de linkerkant krijg je dan een beschrijving van de betreffende guideline te zien, en aan de rechterkant de resultaten van de analyse. In Figuur 2 is te zien hoe de harvester scoorde op het onderdeel ‘complexiteit’. De grafiek onderaan geeft de totale score van de code-base weer, en de teksten eronder zijn hyperlinks waarmee je de betreffende classen in het bovenste deel kunt laten zien. Deze klassen zijn op hun beurt weer hyperlinks naar de code van de refactoring candidates zelf. Zo zie je bijvoorbeeld in Figuur 3 dat hier nog een refactoring mogelijk is, omdat er een TODO in productiecode staat.

1.overzicht
Figuur 1: overzicht van de resultaten.
2.simple_units_code
Figuur 2: het detail-scherm van één van de tien guidelines.
3.codevoorbeeld
Figuur 3: een refactor candidate met de betreffende regel gearceerd.

Nadat je deze analyse hebt bekeken kun je onderdelen hiervan op je takenlijst zetten. Deze lijst komt dan weer terug op je dashboard, wat direct als input voor je retrospective kan fungeren of je nieuwe actielijst kan definiëren. Nadat je deze wijzigingen hebt ingevoerd en je een nieuwe push naar github doet kun je de analyse herhalen en zie je gelijk de verbeteringen in de analyse terugkomen. Het idee van bettercodehub is dat het op die manier een inherent onderdeel wordt van de productiecyclus.

4.tasklist
Figuur 4: de resultaten met een hoeveelheid taken die daaruit naar voren is gekomen.

Nadat ik mijn eigen code had geanalyseerd, heb ik ook nog even een studentenuitwerking van een tweedejaars project geanalyseerd (een project dat qua lines of code vergelijkbaar is met de harvester). Zoals blijkt uit Figuur 5a en b scoort deze code op een aantal onderdelen minder goed dan mijn code. Op basis van die soort vergelijkingen kun je dus redelijk gefundeerd stellen dat de ene code beter is dan de andere – wat zich weer kan laten vertalen in een cijfer.

5a.overzicht_harvester
Figuur 5a: overzicht en score van de funda-harvester.
5b.overzicht_studenten
Figuur 5a: overzicht en score van een studentenproject.

Conclusie
Deze bettercodehub heeft een aantal voordelen boven de meer traditionele analysetools. Allereerst is het online en gekoppeld aan github, dus het aan de praat krijgen hiervan kost eigenlijk geen tijd. Een ander groot voordeel is dat je niet wordt doodgegooid met allerlei details en rode lijntjes, zoals bijvoorbeeld met metrics for Eclipse het geval is. Tenslotte geeft het direct feedback met mogelijke refactoring-slagen, waarbij je op eenvoudige manier input kunt krijgen voor je retrospective. Al met al is dit de beste automatische analysetool die ik ken.

 

Stored function in reference manager

Om het werk aan mijn blogs en mijn boekje te vereenvoudigen heb ik ooit eens een programmaatje geschreven waarin ik de citaten uit de boeken die ik lees bij kan houden. Ik typ de citaten over, zodat ik daar makkelijk doorheen kan zoeken en gezichtspunten met elkaar kan vergelijken en makkelijk terug kan vinden. Een vrij eenvoudig ding, maar wel iets dat zijn meerwaarde al een aantal keren heeft bewezen.

citatenmanagerOmdat ik op verschillende manieren met deze teksten werk, maak ik soms gebruik van APA5, soms van MD5 en soms van mijn eigen systeem met siglia. Een probleem van deze werkwijze is dat ik de referenties die ik uit mijn programma haal steeds met de hand moet aanpassen aan het formaat waarin ik ze wil hebben. Om dit te vereenvoudigen wilde ik het programma met een pulldown uitbreiden waarin ik kan aangeven in welk formaat ik de referentie wil hebben.

Een probleem dat daarbij om de hoek kwam kijken, was dat ik de tekst samen met het paginanummer eenvoudig als een blob in de database heb opgeslagen. Meestal eindigen de citaten met het paginanummer tussen haakjes, maar soms ook met de referentie in kwestie in APA5. Netter zou natuurlijk zijn om het paginanummer in een separate kolom op te slaan, zodat ik die in elk gewenst formaat kan ophalen.

Allereerst begon ik met de database een beetje op te schonen. Omdat het programma toch alleen maar door mezelf wordt gebruikt, heb ik me nooit echt bekommerd over de netheid van de data, maar nu ik er toch mee bezig ging, wat het een kleine moeite dit aan te passen. Dus lege citaten uit de database, harde returns aan het eind van het citaat eruit en er voor zorgen dat alle citaten inderdaad eindigen met een haakje sluiten. Als dat allemaal gebeurd is, de tabel aanvullen met een kolom voor het paginanummer:

> delete from quotes where quote like '';
> update quotes set quote=trim(trailing '\n' from quote);
> update quotes set quote = replace(quote, ').' ,')') where quote like '%).';
> alter table quotes add column page integer unsigned default 0;

Nu leek het me zaak om uit te zoeken wat de algemene structuur was van de paginanummers aan het eind van de referenties. Hiervoor maak ik uiteraard gebruik van reguliere expressies, die in mysql echter iets anders werken dan ik gewend ben.

mysql> select '(123)' regexp '.\d\d\d.';
+---------------------------+
| '(123)' regexp '.\d\d\d.' |
+---------------------------+
|                         0 |
+---------------------------+
1 row in set (0,00 sec)

maar

mysql> select '(123)' regexp '.[0-9]+.';
+---------------------------+
| '(123)' regexp '.[0-9]+.' |
+---------------------------+
|                         1 |
+---------------------------+
1 row in set (0,00 sec)

Op zich was dit uiteraard wel afdoende om de paginanummers te identificeren, maar naar bleek bevat mysql geen mogelijkheid om op basis van reguliere expressies en backreferencing waardes te vervangen. Dus moest het maar op een pragmatischer manier.

Eerst de edge-cases maar eens identificeren, de quotes die niet eindigen met het paginanummer tussen haakjes:

select count(id) from quotes where quote not regexp '.*\([0-9]+\)';
select id, right(quote,5) from quotes where quote not regexp '.*\([0-9]+\)';

Dit bleken er op de life database slechts enige tientallen te zijn. Op een totaal van een kleine tweeduizend citaten, kunnen we dit aantal ofwel negeren ofwel later met de hand aanpassen, mocht dat nodig blijken te zijn. Moeilijker is het vinden van citaten waarin niet alleen het paginanummer in staat, maar ook al het boek waar het uitkomt, zoals

" contemporary nihilism. (Dreyfus and Keyll 2011, 214)"

Het vreemde was echter dat deze vorm ook in de selection hierboven terugkomt.

mysql> select ' contemporary nihilism. (Dreyfus and Kelly 2011, 214)' regexp '.*\([0-9]+\)' as f;
+---+
| f |
+---+
| 1 |
+---+
1 row in set (0,01 sec)

Wat natuurlijk raar is, want (Dreyfus and Kelly 2011, 214) voldoet niet aan het patroon .*\([0-9]+\). Na enig zoeken bleek je in mysql de haakjes twee keer te moeten escapen om ze in een regex op te kunnen nemen:

mysql> select 'D1' regexp '\([[:digit:]]+\)' as f; // true
mysql> select 'D1' regexp '\\([[:digit:]]+\\)' as f; //false
mysql> select '(D1)' regexp '\\(D[[:digit:]]+\\)' as f; //true

Dus:

select '(Dreyfus and Kelly 2011, 214)' regexp '.*\\([0-9]+\\)' as f; //false 

en

select '(Dreyfus and Kelly 2011, 214)' regexp '.*\\([A-Za-z0-9 ]+, [0-9]+\\)' as f; //true

en

select '(214)' regexp '.*\\([A-Za-z0-9 ]+, [0-9]+\\)' as f; //false

Dus het identificeren van die citaten lukt wel, maar wat dan? Zoals gezegd is er in mysql geen preg_replace, maar we kunnen wel uitgaan van die spatie rechts en dan links gaan tot de eerste spatie: dat is het paginanummer. Dat kunnen we dan in de nieuwe kolom zetten en vervolgens het gedoe van begin tot eind spatie weghalen. Dus we moeten een stored function maken die het getal teruggeeft op basis van dat haakje sluiten en de spatie.

We kunnen er van uitgaan dat het laatste karakter het haakje sluiten is (dat hebben we immers hiervoor al bewerkstelligd). Er zijn ook geen citaten die op een paginanummer staan met vier cijfers of meer, maar er zijn wel citaten met één cijfer. Dus de eerste spatie of het eerste haakje openen na het einde van de string is het einde van het getal dat we zoeken. Met substring kunnen we dat relatief eenvoudig terugvinden. Dit levert allemaal de volgende use-cases op:

foobar, 123) → 123
foobar, 12) → 12
.(foobar, 1) → 1
bla (as, 1) → 1

In de stored function die we gaan moeten is het dus zaak om eerst te kijken of de referentie überhaupt eindigt met een haakje-sluiten. Als dat zo is, dan lopen we van achteren naar voren over die string tot de eerste spatie of het eerste haakje openen: wat daartussen zit is het paginanummer. Ik moest wel weer even kijken hoe dat ook al weer zat met de if-syntax in mysql (gelukkig was ik diezelfde dag bezig Y te helpen met haar programmeeropdracht die in Pascal moest – wat een vergelijkbare syntax heeft), maar uiteindelijk werd de hele functie als volgt:

delimiter $$
create function get_page_number(p_reference char(10)) returns integer
begin
  declare x int;
  declare rv varchar(6);
  declare tmpstr char(1);

  set x = length(p_reference)-1;
  set rv = '';

  if substring(p_reference, length(p_reference),1) != ')' then return -1;
  end if;

  while x>0 do
    set tmpstr = substring(p_reference, x, 1);
    if tmpstr=' ' or tmpstr='(' then
      set x=-1;
    else
      set rv = concat(tmpstr, rv);
      set x = x-1;
    end if;
  end while;
  return cast(rv as unsigned);
end$$
delimiter ;

Nu is het zaak van alle referenties deze cijfers op te halen en die in de nieuwe kolom page te zetten. Initieel ziet het er goed uit:

mysql> select right(quote,6) as org, get_page_number(right(quote,6)) as page from quotes limit 10;
+--------+------+
| org    | page |
+--------+------+
| . (15) |   15 |
| . (17) |   17 |
| . (19) |   19 |
| . (19) |   19 |
| . (21) |   21 |
| . (46) |   46 |
| . (47) |   47 |
| . (47) |   47 |
| . (48) |   48 |
| . (49) |   49 |
+--------+------+
10 rows in set (0,00 sec)

Maar wanneer ik dat over de gehele dataset doe, krijg ik een foutmelding waar ik niet aan had gedacht:

mysql> select right(quote,6) as org, get_page_number(right(quote,6)) as page from quotes;
ERROR 1292 (22007): Truncated incorrect INTEGER value: '51-53'
mysql>

Er zitten natuurlijk ook referenties in die over de pagina heengaan, dus die dit formaat hebben. Dat betekent dat de nieuwe kolom page niet een integer maar een string moet worden, en dat de lengte van het return-type maximaal zeven kan worden (\d\d\d-\d\d\d is het uiterste wat voor kan komen).

alter table quotes drop column page;
alter table quotes add column page varchar(16);

Dit vereiste ook een paar wijzigingen in de stored function hierboven: hij moet geen integer maar een varchar(8) teruggeven, hij moet iets langere strings als input krijgen en de cast onderaan is niet nodig. Uiteindelijk heb ik de input en de output van de functie even lang gemaakt, want er bleken ook referenties in de database te zitten die het algoritme niet op tijd deden stoppen. Hiermee kon ik de hele dataset aanpassen:

mysql> update quotes set page = get_page_number(right(quote,16));
Query OK, 1183 rows affected (0,06 sec)
Rows matched: 1183  Changed: 1183  Warnings: 0

Om de referentie uit het citaat zelf te verwijderen, maakte ik een tweede functie die heel veel lijkt op de eerste – duplicatie, ik weet het, maar dit is als het goed is toch maar een one-off:

create function remove_reference(p_quote blob) returns blob
begin
  declare x int;
  declare tmpstr char(1);

  if substring(p_quote, length(p_quote),1) != ')' then return p_quote; 
  end if;

  set x = length(p_quote);

  testwhile: while x>0 do 
    set tmpstr = substring(p_quote, x, 1);
    if tmpstr='(' then leave testwhile;
    else set x = x-1;
    end if;
  end while;
  
  return substring(p_quote,1,x-1);
end

Het enige wat nog even lastig was, is dat je een kolom in mysql niet kunt updaten met waarden uit diezelfde kolom. Dat is natuurlijk eenvoudig op te lossen met een tijdelijke kolom:

> alter table quotes add column quote_new blob;
> update quotes set quote_new=remove_reference(quote);
> alter table quotes drop column quote;
> alter table quotes change column quote_new quote blob;

Nu moet ik aan de voorkant wat dingen veranderen om die goed weer te kunnen geven, maar dat is een volgend project.

De Sinterklaassurprise

Voor de sinterklaasviering had ik (na een wat onduidelijk proces) Y toebedeeld gekregen, met de opdracht hierbij een surprise te maken waarvan de oplossing minstens een kwartier zou duren. Het cadeau, dat maximaal vijftien euro mocht kosten, kwam feitelijk op de tweede plaats – de nadruk zou op de verpakking moeten liggen.

Omdat Y momenteel overweegt om na de middelbare school informatica te gaan studeren, bedacht ik dat het leuk zou zijn iets met arduino’s en LEDs in elkaar te knutselen. Na wat nagedacht en verschillende dingen overwogen te hebben, besloot ik een ouderwetse puzzel in een nieuw jasje te gieten. Een rij willekeurige letters voorzien van een geheime sleutel om bepaalde letters hieruit te filteren, die dan gezamenlijk de boodschap zouden vormen. Het cadeau zelf zou ik in de onderkant van de piano verstoppen, dus de letters zouden INDEPIANO vormen.

De sleutel naar deze letters zou gevormd worden door vier LEDs die op binaire wijze de juiste positie zouden aanwijzen. Met vier bits kun je van 1 tot 15 tellen, dus ik maakte een A4tje met vijftien regels van vijftien letters. Om het geheel nog iets complexer te maken (en het blaadje wat voller) maakte ik twee van dergelijke kolommen. Een eenvoudig python-scriptje volstond om deze tekst te genereren, waarna ik eenvoudig de juiste letters kon uitzoeken:

str = string.letters[26::]

for i in range(15):
  for j in range(2):
    for j in range(15):
      sys.stdout.write(random.choice(str))

    sys.stdout.write("    ")
  print ""

Het idee was dat voor elke letter de LEDs drie waarden weergaven: allereerst de kolom, dan de regel binnen die kolom en dan de positie van de letter op die regel. Tussen elke letter zouden de LEDs even allemaal uit gaan, zodat duidelijk was waar een letter begon en eindigde. Deze opzet betekende wel dat alle drie de waarden verschillend moesten zijn, anders zag je de waarden niet veranderen. INDEPIANO heeft negen letters, dus ik moet een array maken negen elementen; elk element op zijn beurt weer drie arrays, namelijk van kolom, regel en nummer. En elk van deze arrays gaf aan welke LED er aan en welke er uit moest staan:

int Characters[9][3][4] = {
{ {0,0,0,1},{0,1,0,1},{0,1,1,1} }, //I
{ {0,0,1,0},{0,0,1,1},{1,1,1,1} }, //N
{ {0,0,1,0},{0,1,1,1},{0,1,0,1} }, //D
{ {0,0,0,1},{1,0,0,0},{1,0,1,0} }, //E
{ {0,0,0,1},{0,1,1,1},{1,1,1,0} }, //P
{ {0,0,1,0},{1,0,1,0},{1,0,0,0} }, //I
{ {0,0,0,1},{1,1,0,1},{1,1,1,0} }, //A
{ {0,0,1,0},{1,1,0,0},{1,0,1,0} }, //N
{ {0,0,1,0},{1,0,0,1},{1,1,1,1} }  //O
};

De algemene structuur was dan heel eenvoudig: elke cyclus moest de Arduino drie achtereenvolgende arrays gebruiken om te bepalen welke LEDs er aan en welke er uit moesten. Daarna moest -ie een paar seconden alle LEDs uit zetten om naar de volgende trits te gaan. De volledige code van het surprise zag er dan ook als volgt uit:

void setup() {
  for (int i=10; i<14; i++) {
    pinMode(i, OUTPUT);
  }
}

void loop() {
  for (int col=0; col<9; col++) {
    for (int row=0;row<3; row++) {
      blinkLeds(Characters[col][row]);
      delay(2000);
    }

    clearLeds();
    delay(4000);
  }
}

void blinkLeds(int on[4]) {
  clearLeds();
  for (int j=0; j<4; j++) {
    if (on[j]==1) digitalWrite(10+j,HIGH);
  }
}

void clearLeds() {
  for (int i=10; i<14; i++) {
    digitalWrite(i, LOW);
  }
}

Het leukste was natuurlijk om de surprise in een mooi doosje te verpakken. Een eenvoudige tuimelschakelaar, een aan/uit LED en dan de vier LEDs die het eigenlijk werk deden. Hieronder een paar foto’s van dat proces.

suprise

Een nieuw certificaat

Op het domein van deze  site heb ik een ssl-certificaat Niet dat ik heel geheime bestanden heb om weer te geven, maar vooral omdat ik dan bepaalde zaken kan uitproberen waarvoor je een https-verbinding moet hebben, en omdat het goed lijkt te zijn voor je google-rankings.

Ik heb hiervoor een goedkoop certificaat (zeven euro per jaar) en dat moest onlangs verlengd worden. Als ik niets zou doen, zou het vanzelf verlopen en dat wilde ik niet. Ik kreeg keurig een mailtje van de organisatie waar ik één en ander heb geregeld (Xolphin) en via hun site kon ik eenvoudig de geldigheid verlengen – wat feitelijk neer bleek te komen op het doorlopen van dezelfde stappen als wanneer je voor het eerst zo’n certificaat aanvraagt.

Het gaat bij ssl om asymmetrische encryptie, dus je hebt een publieke en een private sleutel nodig. De private sleutel staat, uiteraard, alleen op de server. Om een publieke sleutel te maken, moet je een Certificate Signing Request (csr) creëren en die aan de autoriserende partij (Xolphin in dit geval, of feitelijk Comodo) aanbieden. Die genereren op basis van deze csr een publieke sleutel die je vervolgens ook op de server zet. De csr kun je eenvoudig plakken in het Control Panel van Xolphin.

Om de publieke en private sleutel te genereren maak ik gebruik van openssl:

[root]# openssl genrsa -out bartbarnard.nl.2016.key 2048
[root]# chmod 400 bartbarnard.nl.2016.key
[root]# openssl req -new -key bartbarnard.nl.2016.key -out www.bartbarnard.nl.csr

Bij het aanmaken van die csr wordt je gevraagd om een aantal gegevens in te vullen. Hierbij is het van belang dat de Common Name en het domein uit de aanvraag hetzelfde zijn, anders is de sleutel niet goed. Gelukkig heeft Xolphin een directe check op de sleutel, zodat je gelijk ziet of er zaken verkeerd zijn ingevuld.

[root]# openssl req -new -key bartbarnard.nl.2016.key -out www.bartbarnard.nl.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:NL
State or Province Name (full name) []:.
Locality Name (eg, city) [Default City]:Sneek
Organization Name (eg, company) [Default Company Ltd]:bartbarnard.nl
Organizational Unit Name (eg, section) []:.
Common Name (eg, your name or your server's hostname) []:www.bartbarnard.nl
Email Address []:admin@bartbarnard.nl

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Als het csr eenmaal goed is en doorgestuurd naar Comodo, krijg je een validatie op het admin-adres van het domein in kwestie, admin@domeinnaam.nl. In dit mail staat een controlecode, die je weer in de site van Comodo moet plakken (standaar mail-validatie, kortom). Als dat gedaan is, krijg je weer een mail met het certificaat (of je kunt het via het control-panel downloaden).

Dat certificaat moet dan op de server geplaatst worden en de httpd.conf aangepast worden:

SSLCertificateFile crt/bartbarnard.nl.2016.crt
SSLCertificateKeyFile key/bartbarnard.nl.2016.key
DocumentRoot /bartbarnard.nl/www

Na deze aanpassingen nog even apache herstarten en controleren of het allemaal in orde is. Als dat het geval is, kun je weer wachten op volgend jaar.

certificaat slotje

Randomizeren van arrays in JavaScript

Voor een project waar ik mee bezig ben wil ik, zonder gebruik te maken van een framework als jQuery of Lodash, een array randomiseren en daar vervolgens overheen itereren. Nu zijn er verschillende manieren om dit randomiseren te bewerkstelligen, maar ik vond online al vrij snel een algoritme dat een fatsoenlijke performance had, en daarbij de naam droeg van een oude computerheld: dus dat maar gebruikt:

// knuth array shuffle
// from https://bost.ocks.org/mike/shuffle/
function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {
    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}

Hoewel een string ook in Javascript feitelijk een array van karakters is, werkte de aanroep naar de bovenstaande functie met een String als actuele parameter niet in één keer. Dat verbaasde me, want

$ node -e 'foo="ABC"; console.log(foo[2]);'
C
$

Maar gelukkig heeft JavaScript een goeie methode om een string om een specifieke array te maken van een array: split(). Zo geeft "A B C".split(' ') de array ['A','B','C'] terug. Het probleem hiermee was evenwel dat de string in kwestie niet gescheiden zou worden door een specifiek karakter: alle letters stonden eenvoudig achter elkaar. Nu zou je verwachten dan split() dan zou werken, maar dat bleek niet het geval te zijn. Uiteindelijk bleek split('') (dus met een lege string als parameter) het werk te doen:

[test.js]
var foo = "BSDASCDASDFDAS".split('');
console.log(foo)
console.log(shuffle(foo));

$ node test.js
[ 'B', 'S', 'D', 'A', 'S', 'C', 'D', 'A', 'S', 'D', 'F', 'D', 'A', 'S' ]
[ 'D', 'A', 'A', 'F', 'S', 'S', 'B', 'S', 'D', 'D', 'A', 'S', 'D', 'C' ]
$

Met getallen en strings werkte deze methode prima. Maar toen ik het wilde gebruiken om een array van html-elementen door elkaar te gooien, ging het mis. Omdat ik geen JavaScript-framework kon gebruiken, moest terugvallen op de bekende ouderwetse methode van het selecteren van DOM-elementen. In dit specifieke geval had ik een tabel met zesendertig feitelijk identieke td’s: het enige verschil tussen deze elementen was hun positie in de tabel. Hierdoor kon de JavaScript-engine geen onderscheid maken tussen de gewone en de geshuffelde array:

var foo = document.getElementsByTagName('td');
var bar = shuffle(foo);
console.log(foo==bar); //true

Tijd om iets anders te bedenken. Omdat ik een situatie had waarin ik gegarandeerd elk element uit de array exact één keer moest langslopen, kon ik niet eenvoudig een ‘gerandomizede’ pointer naar de array gebruiken. Een mogelijke oplossing zou zijn om een willekeurige pointer te maken, dat element uit de array te halen en een nieuwe array te maken uit de elementen voor en de elementen na deze pointer – dus dat de nieuwe array telkens één element korter is dan de oorspronkelijke. In node leek dit een hoopvolle route, maar de browser bleek de methode slice() niet te ondersteunen – die je nodig hebt om de oorspronkelijke array in stukjes te knippen.

Wat nu wanneer we het element dat door de gerandomizede pointer wordt aangewezen null maken en hierna over de array heen itereren en elk element dat niet null is aan een nieuwe array toevoegen? Dit leek in ieder geval wel te werken voor eenvoudige strings:

var b = "BSDASCDASDFDAS";
var foo = b.split('');

foo[4] = null;

var bar=[];
var j=0;

for (var i=0; i<foo.length; i++) {
  if (foo[i]!=null) {
    bar[j]=foo[i];
    j++;
  }
}

console.log(foo); //[ 'B', 'S', 'D', 'A', null, 'C', 'D', 'A', 'S', 'D', 'F', 'D', 'A', 'S' ]
console.log(bar); //[ 'B', 'S', 'D', 'A', 'C', 'D', 'A', 'S', 'D', 'F', 'D', 'A', 'S' ]

Op basis hiervan maakte ik een functie removeFromArray() die een array als parameter krijgt een dezelfde array zonder null-values teruggeeft. Om dit goed te testen maakte ik een tabelletje met zes keer zes cellen die ik in eerste instantie oplopende kleuren gaf:

var colors = ["#f291fb", "#e279eb", "#d064da", "#db50c7", "#a93db3", "#932a9c", "#7a1c83", "#650e6d", "#4f0656", "#37023c", "#c06df4", "#aa56e0", "#9542c9", "#7f2eb2", "#661a96", "#4e0e76"];

function init() {
  var els = document.getElementsByTagName("td");
  var e;
  var j=0;

  for (var i=0; i<els.length; i++) { e = els[i]; e.classList.value=""; e.bgColor=colors[j]; j>colors.length-1 ? j++ : j=0;
  }
}
Eerste test: opeenlopende kleuren.
Eerste test: opeenlopende kleuren.

Nu is de uitdaging om niet lineair over de els-array te itereren, maar om dat te doen met een willekeurige pointer bij elke iteratie, en dat element via de hierboven beschreven methode uit de array te halen.

function init() {
  var els = document.getElementsByTagName("td");
  var e;
  var j=0;
  var idx;

  while (els.length) {
    idx = Math.floor(Math.random() * els.length);

    e = els[idx];
    e.classList.value="";
    e.bgColor=colors[j];

    els[idx] = null;
    els = removeFromArray(els);

    j>colors.length-1 ? j++ : j=0;
  }
}

En dat levert inderdaad het gewenste resultaat op:

Het gewenste resultaat. Het blijkt dat Javascript random door alle td's heenloopt.
Het gewenste resultaat. Het blijkt dat Javascript random door alle td’s heenloopt.

Wat nog wel een beetje gedoe was, was dat document.getElementById() geen array, maar een htmlCollection oplevert. Hierdoor werd dit pas na de eerste call naar removeFromArray() een echte array. Dus ik moest die htmlCollection even expliciet omzetten naar een Array:

var els = [].slice.call(tdElements);

Wel een beetje raar dat ik daar pas achter kwam toen ik verder met het project ging – met die kleurtjes ging het goed. Soit.

Arduino en de GPS-Shield

1. Introductie
Voor ons ballon-project maken we gebruik van een mini GPS tracker. Dit ding maakt gebruikt van een simkaart om sms’jes te ontvangen en te versturen. Door het versturen van een sms kun je de coördinaten van de tracker opvragen, hem opdracht geven om elke zoveel seconden zijn positie door te geven of hem zijn gegevens aan meerdere mobiele nummers laten sturen. De tracker stuurt altijd een sms terug met daarin zijn lengte- en breedtegraden, zijn snelheid en een link waar je op kunt klikken om de positie in Google Maps weer te geven.

De sms'jes die je van de tracker krijgt.
De sms’jes die je van de tracker krijgt.

Hoewel dit klikken op een link op de smartphone op zich prima werkt, wilde ik per se de posities in een database opslaan zodat ik deze real-time op een grote kaart kon tonen. Het ophalen van die posities uit de database en deze op een kaart plotten was me al eerder gelukt. De uitdaging van deze fase bestond uit het ontvangen van de sms’jes, hier de coördinaten uit te destilleren en deze in de database te zetten.

2. De GSM Shield
Zoals voor zo veel dingen is er een shield voor Arduino waar je een simkaart in kunt stoppen. En net als altijd is het even pielen om die boel goed aan de praat te krijgen, maar wanneer je de shield goed op de Arduino heb gezet en je hebt de juiste seriële poort op je computer gevonden, werkt het prima. Ik had even wat moeite met een simkaart waar ik blijkbaar nog een pincode op had staan, maar dankzij de mobiele telefoon die we van hde Hanze hebben (en die we verder toch niet gebruiken) kon ik die er makkelijk afhalen. Sowieso is het wel praktisch om toch een telefoon bij de hand te hebben, ook om beltegoed op te waarderen en dingen te checken. Met behulp van de voorbeeldcode op arduino.cc kon ik zien dat de simkaart het deed.

GSM networks scanner
 Modem IMEI: 
 8621700184399
 Current carrier: vodafone
 Signal Strength: 22 [0-31]
 Scanning available networks. May take some seconds.
 > vodafone
 > T-Mobile NL
 > KPN MOBILE

De tracker heeft ook de mogelijkheid om de coördinaten naar extra telefoonnummers te sturen. Tijdens het testen had ik ook het nummer van een collega, met wie ik het project samen begeleidt, aan de tracker toegevoegd; vreemd genoeg kreeg ik daarna zelf helemaal geen sms’jes meer. Uiteindelijk hoorde ik van die college dat hij wel wat berichten had gekregen – zij het alleen maar noodberichten die de tracker had verstuurd toen ik ten einde raad de sos-knop had ingedrukt – dus er werden wel berichten verstuurd. Blijkbaar onthoudt de tracker zelf welke nummers er geautoriseerd zijn en stuurt hij alleen naar die nummers een sms. Staat, bleek achteraf, ook in de docs:

Aber: sobald eine Rufnummer im Tracker autorisiert wurde, gibt der Tracker nur Antwort an die autorisierten Rufnummern, fremde Rufnummern werden ignoriert.

Verder lezen in de online-documentatie leverde het inzicht op dat de tracker ook de optie heeft om zichzelf helemaal te resetten. In eerste instantie leek ook dat niets uit te halen, maar uiteindelijk kreeg ik in de trein zomaar een sms met als tekst ‘Reset ok!’. En toen werkte het eindelijk. Blijkbaar kun je het ding ook behoorlijk van slag brengen door niet een bepaalde volgorde in commando’s te geven.

Een kort rondje door Sneek leverde inderdaad tien positie-bepalingen op. Met deze ervaring had ik voldoende fiducie in de tracker en mijn begrip daarvan om de volgende stap in het proces te zetten.

3. Processing en Arduino
Nu ik de tracker eindelijk onder controle had, was het tijd om na te denken over de manier waarop ik de coördinaten in de database kon krijgen. Het meest eenvoudige was natuurlijk om in Processing de eerste regel van de sms te parseren, de juiste dat daaruit te halen en uiteindelijk in de database te zetten. Die kon dan weer door nodejs opgehaald worden en op Google maps geplot worden. Berichten die in dit systeem binnenkwamen zouden moeten worden gecheckt op inhoud en eventueel opgeslagen worden in de database. Processing is in feite gewoon Java, dus een jdbc opzetten moest zonder problemen kunnen – en er bleek ook een speciale Processing-library daarvoor te bestaan.

Om alle een beetje te vereenvoudigen en de communicatie twee kanten op via hetzelfde systeem te laten lopen, wilde ik mooie (of eigenlijk lelijke) interface met buttons maken waar je op kon klikken om berichten naar de Arduino te sturen, die dan weer door hem naar de tracker gestuurd konden worden. Er zijn verschillende libraries voor GUI’s in Processing. Ik probeerde eerste controlP5, wat ik te onduidelijk vond. Ook het Italiaanse Interfascia was mij te complex (en werkte niet met Processing onder versie 2 en de mysql-library die ik gebruik werkt niet met 3…). Uiteindelijk kwam ik uit op G4P. Wel even zoeken naar de juiste download. v3.5.4 is de laatste voor P2. Ziet er op zich wel ok uit, is redelijk gedocumenteerd en werkt tenminste out of the box; maar een schoonheidsprijs krijgt het ding niet.

interface tisagEerst maar eens de basale communicatie opzetten. Dat is vrij triviaal: Arduino stuurt tekst naar een seriële poort waar Processing naar luistert. Het enige probleem probleem is dat die gps-tracker de data doorstuurt over meerdere regels en de code in principe ophoudt bij de eerste ‘\n’. Dus ik moest iets doen met checken of de regel ik kwestie latlng-data bevat:

if (val != null) {
  if (val.indexOf("lat:")&amp;gt;-1) {
    String lat = parse(val, "lat");
    String lng = parse(val, "lng");
    database.query("insert into latlng(lat, lng) 
                    values (" +lat+ ", " +lng+ ")");
  }
}

String parse(String input, String latOrLng) {
  String[] foo = input.split(" ");
  String checkstr = (latOrLng.equals("lat")) ? foo[0] : foo[1];
  String[] res = checkstr.split(":");

  return res[1];
}

De check op regel 1 hierboven kijkt of er in de string die binnenkomt ergens ‘lat:’ staat (dit geldt alleen voor de regels die van belang zijn). Als dat het geval is, haal ik (met parse(String, String)) eerst de lengtegraden en vervolgens de breedtegraden uit de string in kwestie. Een paar testjes, waarbij de Arduino gewoon direct tekst naar de seriële poort stuurt, lieten zien dat dit op zich prima werkt.

De volgende stap was, uiteraard, de andere kant op: opdrachten (strings) vanuit Processing naar de Arduino sturen. De tracker heeft een flink aantal opties en functies, die we tijdens het project niet allemaal nodig hebben. Alle functies zijn beschreven op deze site; de volgende functies wil ik implementeren:

herstel fabrieksinstellingen           begin123456
resetten tracker                       reset123456
extra telefoonnummers autoriseren      admin123456 <<telnr>>
geautoriseerd nummer verwijderen       noadmin123456 <<telnr>>
instellen update rate automatische tracking
starten automatische tracking          t030s005n123456
stoppen automatische tracking          notn123456

De communicatie van Processing naar Arduino leek nog niet heel triviaal. Het is vrij eenvoudig om één karakter (char) over de lijn te sturen, maar omdat ik hele opdrachten wilde sturen (die aan de Processing-kant met behulp van de interface zouden worden samengesteld) moest ik op de één of andere manier een hele string versturen en door de Arduino opvangen. Na wat proberen kwam ik op een blog die en goed en werkend voorbeeld had. Die maar even gekopieerd en van daaruit verder.

Met behulp van dit voorbeeld lukte het uiteindelijk om aan de Arduino-kant een string op te vangen en afhankelijk daarvan iets te doen. Om duidelijk te maken wat de Arduino allemaal aan het doen was, koppelde ik een aantal LEDs aan de digitale poorten hiervan, zodat ik die als status-indicatoren kon gebruiken.

if (chomp("START")==0) {
  respons = "Received START";
  statusLed = 10;
} else&nbsp; if (chomp("STOP")==0) {
  respons = "Received STOP";
  statusLed = 11;
} else&nbsp; if (chomp("ADD")==0) {
  respons = "Received ADD";
  statusLed = 12;
}

if (currentStatus != statusLed) {
  currentStatus = statusLed;
  clearLEDS();
  Serial.println(respons);
  digitalWrite(statusLed, HIGH);
}

ledsOmdat de string die de Arduino binnenkreeg uiteindelijk als sms door de shield verstuurd zou moeten worden, moest deze aan de methode sms.print(char[]) worden meegegeven. Nu is een string in C (zoals in alle programmeertalen) een char[], dus ik dacht dat ik die wel gewoon zo kon aanroepen. Maar vreemd genoeg compileerde Listing 1 hieronder wel, en Listing 2 niet:

// Listing 1
String SMSManager::sendSMS(String msg, String number) {
  sms.beginSMS("+31612345678");
  sms.print("Hallo daar!");
  sms.endSMS();

  return "";
}

// Listing 2
String SMSManager::sendSMS(String msg, String number) {
  sms.beginSMS(number);
  sms.print(msg);
  sms.endSMS();

  return "";
}

Na het goed doorlezen van de documentatie bleek dat die sms-library uit van een cons char* uitgaat, terwijl string-variabelen mutable zijn. Dus moesten we die variabelen omzetten in een immutable op het moment dat die methode wordt aangeroepen. Gelukkig heeft C daar een methode voor:

String SMSManager::sendSMS(String msg, String number) {
  const char* s_msg = msg.c_str();
  const char* s_number = number.c_str();

  sms.beginSMS(s_msg);
  sms.print(s_number);
  sms.endSMS();

  return "";
}

Hoewel dit het probleem op scheen te lossen, wat het allemaal niet heel stabiel – er kwamen soms wel en soms niet berichten terug in Processing. In eerste instantie dacht ik dat dat met locking te maken had, dus paste ik ReceiveSMS() zo aan dat -ie een boolean teruggaf en liet het hele systeem hangen zolang er een bericht verwerkt werd. Maar dat hielp niet.

Maar de berichten komen wel door in de serial controller van Arduino zelf, hoewel ik ze niet zag in Processing. Maar wat valt er op wanneer we goed naar die serial controller kijken? De berichten komen na elkaar, en in processing staat een readStringUntil(‘\n’)…NAAST_ELKAARDus eenvoudig na het versturen van het bericht Serial.print(‘\n’) toevoegen en het werkte. Irritant hoe je een paar uur kunt verspillen met zo’n stomme fout.

Object Oriented
Nu de communicatie twee kanten op goed leek te gaan, werd het tijd om de code wat op te ruimen. De Arduino doet nu feitelijk een paar verschillende dingen:

– ontvangen van opdrachten van Processing
– versturen van sms’jes
– veranderen van de status LED
– informatie terugsturen naar Processing

Het is netter om de code die met deze verschillende processen te maken heeft in separate tabs te zetten. Op de arduino-site is aangegeven hoe dat moet. Ook aan de Processing-kant maakte ik verschillende objecten voor verschillende zaken. Hoewel de editor van zowel Processing als van Arduino behoorlijk waardeloos is, kun je hier wel gewoon een nieuwe tab aanmaken met een nieuwe (C++ of Java)klasse.

Logging
Als laatste wilde ik dat de processing-app een logfile bij zou gaan houden, waarin ik alle opdrachten en communicatie over en weer zou kunnen bijhouden. Samen met de indicator-leds zou ik dan het hele proces goed moeten kunnen monitor. Omdat Processing uiteindelijk gewoon Java is, had ik ook een framework als log4j in kunnen zetten, maar dat leek me wat overdreven. Uiteindelijk heb ik een separate klasse gemaakt die het loggen voor z’n rekening nam. Feitelijk met maar één methode, info(String), die de string die -ie meekrijgt naar een bestand wegschrijft. Die kunnen we dan met tail -f mooi volgen.

public void info(String message) {
  try {
    BufferedWriter output = new BufferedWriter(
                         new FileWriter(logFile, true));
    output.append (message);
    output.close();
    println (message);
  } catch (IOException e) {
    println("panic");
    e.printStackTrace();
  }
}

Voor die log moest ik natuurlijk wat strings met elkaar concaterneren. Nu weet ik dat Strings in Java immutable zijn. Dus String.format(), of concatenatie? Na lezen op stackoverflow toch maar voor de eerste optie gekozen.

String lat = parse(respons, "lat");
String lng = parse(respons, "lng");
logger.info(String.format("Got GPS coordinates: %1$s, %2$s ", lat, lng));

logsDe hele cirkel leek nu wel te werken. Ik kon via Processing de Tracker aanzetten en als ik een sms terugstuurde met de telefoon, kwam die keurig in de database terecht. Via het eerdere project met nodejs werd de locatie dan keuring op Google Maps getoond. Tijd voor de laatste schakel: de tracker zelf naar de Arduino laten sturen. Dat bleek echter nog niet mee te vallen…

Real-time markers plotten op Google maps

Inleiding
Voor ons ballonproject maken we gebruik van een gps-tracker die met behulp van een SIM-kaart zijn GPS-coördinaten via SMS naar specifieke telefoonnummers stuurt. Dit werkt op zich prima: je geeft op de tracker aan dat deze bijvoorbeeld elke minuut de coördinaten naar je iPhone stuurt, in die SMS staan de lengte- en breedtegraden en een linkje naar Google maps. Als je daar op klikt, krijg je de positie van de tracker te zien.

De sms'jes die je van de tracker krijgt.
De sms’jes die je van de tracker krijgt.

Hoewel dit op zich prima is, is het natuurlijk veel cooler om het pad dat de tracker aflegt real time op een kaart te plotten. Omdat dit met de SMS’jes zelf niet kan (en we het sowieso op een groot scherm willen zien) leek het me beter om de berichten te versturen naar een SIM-kaart die verbonden is met een Arduino, die op zijn beurt de berichten doorstuurt naar Processing die de coördinaten uit het bericht filtert en deze in een database zet. Deze architectuur is onderwerp van een andere blog (die later komt); nu ging het me er om hoe ik markers op een kaart kon plotten op het moment dat deze in de database terechtkomen.

Google Maps Markers

Een collega van me had me onlangs geattendeerd op Open Street Map; dit scheen mooier en beter te zijn dan Google maps. Maar na een (toegegeven korte) blik op de site en de API die hierbij geleverd wordt vond ik dit niet heel transparant, dus besloot ik het geheel toch maar eenvoudig in Google maps te maken.

Ik heb voor een ander projectje wel eens iets vergelijkbaars gedaan, maar die code bleek oud en had ook een live update: de data wordt hier gewoon uit de database gehaald op het moment dat de pagina geladen wordt en daar gebeurt verder niet zo veel mee. Voor een live verbinding tussen maps en de database heb ik even gezocht naar een bridge tussen Angular en maps, maar wat ik vond bleek niet heel vruchtbaar.

Omdat ik feitelijk een stateful verbinding tussen de client en de server wilde hebben, viel de keuze uiteindelijk op websockets en node.js. Zoals gebruikelijk bij dit soort projecten maakte ik gebruik van een blog van iemand anders die een minimal working example leverde. Dit voorbeeld was vrij redelijk, hoewel de code niet heel transparant is (niet in de laatste plaats omdat het onderscheid tussen users en clients hier niet duidelijk genoeg ingezet wordt). Na het bestuderen van een blog over de communicatie tussen node en mysql kreeg ik de database-verbinding uiteindelijk aan de praat.

Uiteindelijk moest ik de voorbeeldcode van Gianluca Guarini helemaal uitkleden tot een bare minimum om te ontdekken wat er bij mij mis ging. Het opzetten van de node server is eenvoudig genoeg, maar de client had wat meer voeten in de aarde. Het fascinerende van de opzet is dat je de browser een request laat doen naar een specifieke poort van de server waar node op staat te draaien (in dit geval http://localhost:8000/); node stuurt vervolgens de hele client naar de browser, die vervolgens een socket opent met de server. De server stuurt vervolgens op gezette tijden een message naar de clients, met een string (‘notification’ in het voorbeeld hieronder) om de verschillende berichten van elkaar te kunnen scheiden:

var updateClients = function(data) {
  data.time = new Date();
  connectionsArray.forEach(function(tmpSocket){
    tmpSocket.emit('notification' , data);
  });
};

Deze wordt dan in de client opgevangen:

var socket = io.connect('http://localhost:8000');
var html = $('#container').html();
var tmp = '';
socket.on('notification', function (data) {
  $.each(data.latlng, function(index, latlng) {
  tmp += "<br/>Latitude: " +latlng.lat+ ", 
      longitude:" +latlng.lng;

});

$("#container").html(tmp);
});

Met deze code wordt de div ‘container’ uiteraard gevuld met steeds dezelfde data. Om dat te voorkomen kunnen we beter aan de server-kant alleen de meest recente coördinaten versturen, anders gaat er steeds veel te veel over de lijn en moet de browser veel te veel werk doen. Om deze reden heb ik de tabel in mysql voorzien van een timestamp: we kunnen dan eenvoudig alleen de coördinaten versturen waarvan de timestamp voorbij de laatste broadcast ligt.

Testsite’je
Om de ontwikkeling wat te vereenvoudigen maakte ik even een testscriptje die om data in de database te zetten. Ik heb hele stapel coördinaten uit mijn grafvelden-onderzoekje die ik daar wel voor kon gebruiken. Dit betreft feitelijk statische test-data, dus die zette ik direct in javascript (geen database-connectie nodig). Een eenvoudig server-side scriptje dat kan worden aangeroepen vanuit een webpagina, zodat ik in eerste instantie controle houd over de input in de database, maar dat niet de hele tijd met de hand hoef in te voeren. Dus twee knoppen: één om een nieuwe locatie in de database in te voeren en één om de tabel weer te legen.

Site'je om testdata in de database te stoppen.
Site’je om testdata in de database te stoppen.

Ik moest even nadenken over de juiste volgorde: een nieuwe client meldt zich aan en moet dan alle punten tot dusver krijgen. Daarna krijgen alle clients alleen de nieuwe punten. Dus aan de serverkant moeten we bijhouden wanneer de laatste update is geweest en een separate methode maken voor wanneer clients zich aanmelden. Die methode is er al (io.sockets.on( ‘connection’,)), ik moet alleen dan alle coördinaten naar die nieuwe client pushen.

Timestamp
Een timestamp in mysql is in ISO8601; gelukkig is er een javascript-functie die een datum om kan zetten in zo’n string. Maar die bleek wel heel secuur:

bart$ node -e 'var d = new Date();console.log(d.toISOString());'
2016-05-10T16:12:04.626Z
bart$

Het grootste probleem van deze functie bleek dat deze timestamps omzet naar UTC en voorziet van een tijdzone. Dit doet mysql niet (die neemt gewoon de server-tijd over), dus als ik die timestamp zou willen gebruiken zou ik hier een hoop gedoe mee krijgen. Dus maar pragmatisch zijn en gewoon een auto_increment id aan de tabel toevoegen. Uiteindelijk wordt het maar één proces dat hierin data gaat invoeren (namelijk die tracker) dus er zullen geen race conditions ontstaan. Het bijhouden van de laatste id aan de server-kant wordt dan redelijk triviaal (regel 2 in de code hieronder):

.on('result', function(data) {
  if (data.id > lastID) lastID = data.id;
  rv.push(data);
})

.on('end', function() {
  if (connectionsArray.length) {
    console.log('time: ' +pollingTimer);
    pollingTimer = setTimeout(getLatLng, POLLING_INTERVAL, 1);
    if (rv.length) updateClients({latlng:rv});
  }
});
Nu komen de coördinaten binnen (onderkant scherm)
Nu komen de coördinaten binnen (onderkant scherm)

Het laatste punt dat nog geadresseerd moest worden is dat een nieuwe client alle coördinaten krijgt en alle clients alleen de meest recente. Op zich was dit niet heel lastig: ik kon gewoon een parameter aan die methode meegeven aan de hand waarvan hij alle of alleen de nieuwste uit de database haalde:

function getLatLng (onlynew) {
  var where = onlynew ? " where id >;" +lastID : " ";
  var query=database.query("select * from latlng" 
            +where+ " order by id desc");
//
}

Alleen bleek er toen ineens van alles niet meer te werken. Toen ik hiermee aan de slag ging, bedacht ik me dat de hele architectuur van de blog van Guarini helemaal verkeerd was: alle clients krijgen een update wanneer een nieuwe client zich aanmeldt en hij maakt gebruik van een setTimeOut die wordt aangemaakt wanneer de sql-query afgerond is. Dit kan beter veranderd worden in een setInterval die wordt aangemaakt wanneer de server is opgestart:

app.listen(8000);
setInterval(getLatLng, POLLING_INTERVAL, 1);

Volatile
Opvallend was dat de client nu alleen nog maar de nieuwe coördinaten binnenkreeg, terwijl tests op alle mogelijke posities uitwezen dat alles wel degelijk werd verstuurd. Nadat ik een hele zondag hieraan had verspild was het enige dat ik kon bedenken dat het versturen zelf (dus vanuit node) niet goed werd uitgevoerd. Guarini maakt in zijn blog gebruik van socket.volatile.emit() om de data naar de client te pushen. Op stackoverflow zei iemand hierover : ‘Essentially, if you don’t care if the client receives the data, then send it as volatile’. Voor mij is het echter wel van belang dat de clients de data ontvangen. Toen ik dat volatile had weggehaald werkte alles eindelijk naar behoren.

De hele cirkel compleet.
De hele cirkel compleet.

Nu is het zaak om de coördinaten niet via het test-script, maar via Processing in de database te krijgen.